LLLog

Keeps the dated record of the important user actions of a running linelist on the very hidden __log worksheet. Each event is one Checking bundle: the bundle title is the SECTION the action belongs to, the entry label carries the timestamp, the action code and a short detail, and the scope carries the outcome. Every bundle is flushed through CheckingOutput.PrintOutput, which appends across renders, so the sheet grows one line per event and the built-in status and title dropdowns of the output sheet filter the log by outcome and by section.

WHAT ONE LINE SAYS

An entry line reads as the timestamp and the platform, then the procedure that raised the event, then the action code and the detail:

2026-08-15 13:55:45 mac-64 excel-16.112 ClickShowHideSection > showhide-section: Demographics hidden on sheet1

The procedure is named by the caller. VBA carries no call stack to read a name from, and one button can raise the same refusal from four places, so a line that names only the action leaves the reader guessing which press wrote it. The name is optional: a caller that passes none writes the same line as before, opening at the action code.

THE THREE SECTIONS

The log is read by three sections, and the title of an entry is the section the action belongs to. The writer runs in compact mode, so a section title is written the first time the sheet carries it and never again: a session of a hundred actions reads as a hundred lines under at most three titles.

open/close the workbook open, the workbook close, a log restart data input/output every import, every export, and clearing the data linelist lifecycle everything else the user does inside the workbook

An action code no case names lands in linelist lifecycle, so a new logged action reaches the sheet without touching this class. The action code itself moves down to the entry line, beside the timestamp, where it stays readable without heading a block of its own.

A title heads a block of its own kind. An action coming back to a section written earlier lands at the foot of THAT block, not at the foot of the sheet, so the three blocks stay whole however far apart in time their actions were taken, and a blank row stands between them. Inside a block the lines keep the order they happened. Each line also carries its section in the hidden title column, and that is what the title dropdown of the output sheet filters on, so reading the log one section at a time is a dropdown away.

The last thing that happened is therefore no longer the last line of the sheet: a close lands under the open it belongs with, near the top, while the lifecycle block below carries the bulk of the session.

PROVISIONING

Create(wkb) binds to the __log worksheet of the given workbook and builds it very hidden when the workbook has none. An old linelist grows the sheet on its first logged event, so the generation tree stays untouched. Building a sheet wants the workbook structure unprotected, which is the caller''s duty.

ROTATION

LOG_MAX_ENTRIES is read as a row cap. A flush that finds the last written row past the cap clears the sheet and removes the row marker CheckingOutput keeps, so the next render re-initialises the sheet, and the fresh log opens with one info line ("restarted").

FILTER DROPDOWNS

The interactive row hiding of the output sheet wants a Worksheet_Change handler injected through the VBE, and injecting code wants VBE trust, which field machines lack. LLLog leaves the worksheet code module alone; the dropdowns stay plain validation cells and the filtering is reachable through CheckingOutput.FilterWorksheet.

Version: 1.1 (2026-08-15)

Factory

Create #

create

Create a log store bound to the __log worksheet of a workbook

Signature:

Public Function Create(ByVal wkb As Workbook) As LLLog

Entry point for creating LLLog instances.

Binds to the __log worksheet of the given workbook. When the workbook has none, the sheet is appended after the last worksheet and made very hidden, so an old linelist grows its log on first use.

Parameters:

  • wkb: Workbook. The linelist workbook carrying the log.

Returns: LLLog. Fully initialised instance.

Throws:

  • ProjectError.ObjectNotInitialized When wkb is Nothing.

PublicAccessors

SheetName #

sheet-name

Name of the log worksheet

Signature:

Public Property Get SheetName() As String

Answers the internal sheet name so the viewing surface and the close guard reach the sheet through the class.

Returns: String. The log worksheet name.


SectionOf #

section-of

The section an action code is logged under

Signature:

Public Function SectionOf(ByVal action As String) As String

The three sections are the whole title vocabulary of the log sheet. The workbook open, the workbook close and a log restart are the session boundaries and read as open/close; everything that moves data in or out of the workbook reads as data input/output; every other action the user takes inside the workbook reads as linelist lifecycle, which is also where an action code named by no case below lands.

Parameters:

  • action: String. The action code of the event.

Returns: String. One of the three section titles.


MaxEntries #

max-entries

Row cap of the log sheet

Signature:

Public Property Get MaxEntries() As Long

Answers the cap the rotation reads, so a test can seed a row past it without writing ten thousand lines.

Returns: Long. The row cap.


Logging

LogSuccess #

log-success

Log an action that ended well

Signature:

Public Sub LogSuccess(ByVal action As String, _
                      Optional ByVal detail As String = vbNullString, _
                      Optional ByVal source As String = vbNullString)

One method per outcome, each flushing one Checking bundle.

Parameters:

  • action: String. The action code, used as the bundle title.
  • detail: Optional String. Short free text beside the timestamp.
  • source: Optional String. The procedure that raised the event.

LogWarning #

log-warning

Log a refusal or a degraded run

Signature:

Public Sub LogWarning(ByVal action As String, _
                      Optional ByVal detail As String = vbNullString, _
                      Optional ByVal source As String = vbNullString)

Parameters:

  • action: String. The action code, used as the bundle title.
  • detail: Optional String. Short free text beside the timestamp.
  • source: Optional String. The procedure that raised the event.

LogFailure #

log-failure

Log an action that failed

Signature:

Public Sub LogFailure(ByVal action As String, _
                      Optional ByVal detail As String = vbNullString, _
                      Optional ByVal source As String = vbNullString)

Parameters:

  • action: String. The action code, used as the bundle title.
  • detail: Optional String. Short free text beside the timestamp.
  • source: Optional String. The procedure that raised the event.

LogInfo #

log-info

Log a lifecycle line

Signature:

Public Sub LogInfo(ByVal action As String, _
                   Optional ByVal detail As String = vbNullString, _
                   Optional ByVal source As String = vbNullString)

Parameters:

  • action: String. The action code, used as the bundle title.
  • detail: Optional String. Short free text beside the timestamp.
  • source: Optional String. The procedure that raised the event.

Export

ReportLines #

report-lines

The whole log as plain text, one line per element

Signature:

Public Function ReportLines() As BetterArray

Writing the log out as a file a user can send on.

The text a user sends on when something goes wrong, so it stands on its own: the workbook it came from and the platform it is written on, then every row of the Metadata worksheet, then the log itself in the order it was written. The log SHEET is the record here, unlike GenerationLog which keeps its run in memory, so the rows are read back off the sheet. The text is built here rather than inside ExportText so what a file will say can be read without a file being written. A log that cannot reach its metadata still answers its entries.

Returns: BetterArray. The lines, lower bound 1.

Throws:

  • ProjectError.ObjectNotInitialized When the log has no worksheet.

ExportText #

export-text

Write the whole log out as a plain text file

Signature:

Public Function ExportText(ByVal folderPath As String, _
                           Optional ByVal baseName As String = vbNullString) As String

The file lands in the given folder as -log.txt. A second export of the same linelist overwrites the first, which is what a person sending "the log" wants. What goes into it is ReportLines. This routine only opens the file, writes the lines and closes it.

Parameters:

  • folderPath: String. The folder taking the file.
  • baseName: String. The file name without its suffix. The workbook name when empty.

Returns: String. The full path of the written file.

Throws:

  • ProjectError.ObjectNotInitialized When the log has no worksheet.
  • ProjectError.InvalidArgument When no folder is given.

Internal members (not exported)

PublicAccessors

Wksh #

wksh

Retrieve the log worksheet backing this store

Signature:

Public Property Get Wksh() As Worksheet

Properties that expose internal state.

Returns: Worksheet. Reference to the log worksheet.


Wksh #

wksh-set

Define the worksheet that will receive the log

Signature:

Public Property Set Wksh(ByVal sh As Worksheet)

Parameters:


Internals

ResolveLogSheet #

resolve-log-sheet

Resolve the log worksheet, building it very hidden when absent

Signature:

Private Function ResolveLogSheet(ByVal wkb As Workbook) As Worksheet

Sheet resolution, entry building, and rotation.

The sheet is appended after the last worksheet, because a bare Add puts a new sheet in front of whatever is active. An existing sheet keeps its visibility, so the viewing surface can hold it open across a bind.

Parameters:

Returns: Worksheet. The resolved or newly created worksheet.


AppendHeadingTo #

write-heading-to

The first block of the report: which workbook, and on what

Signature:

Private Sub AppendHeadingTo(ByVal lines As BetterArray, ByVal sh As Worksheet)

The platform of the export and the platform of an entry can differ, since a linelist travels between machines, and both are worth having.

Parameters:


AppendMetadataTo #

append-metadata-to

Every row of the Metadata worksheet, into the report

Signature:

Private Sub AppendMetadataTo(ByVal lines As BetterArray, ByVal wkb As Workbook)

The whole variable and value block goes out, not a chosen few rows, so a linelist built by a later designer carries whatever that designer wrote without this routine being touched again. The block below the creation rows is the geobase metadata, and it goes out with the rest. A linelist with no Metadata worksheet says so and the report goes on: a log is worth having even when the metadata is missing.

Parameters:


AppendEntriesTo #

append-entries-to

Every row of the log sheet, into the report

Signature:

Private Sub AppendEntriesTo(ByVal lines As BetterArray, ByVal sh As Worksheet)

A row carrying the title alone opens a section and reads as a heading in brackets; every other written row is one entry, and it reads as the timestamp, the outcome in brackets, then the action and the detail. Blank rows are dropped, because CheckingOutput lays them out for the eye and they say nothing.

Parameters:


PlainOutcome #

plain-outcome

The outcome word of a row, without the picture in front of it

Signature:

Private Function PlainOutcome(ByVal text As String) As String

Checking paints an outcome with a symbol before the word -- a cross for an error, a tick for a success, an i in a circle for an info line -- and that symbol is what the sheet shows. The text export cannot carry it. Print # writes one byte per character, so every one of those symbols reaches the file as a single stray mark, and a log a user sends on reads "[_ Error]" and "[_ Info]" throughout. The word alone is what the file wants. A section title carries no symbol and comes back untouched: only a first character outside plain text is taken off.

Parameters:

Returns: String. The word alone.


CellText #

cell-text

One cell as text, with an error value reading as empty

Signature:

Private Function CellText(ByVal cellValue As Variant) As String

A cell holding #N/A or #REF! raises 13 on CStr, and one such cell in a log or on a metadata sheet must not take down the export of everything else.

Parameters:

Returns: String. The value as text, or an empty string when it is an error.


WriteEntry #

write-entry

Rotate when the cap is passed, then flush one dated entry

Signature:

Private Sub WriteEntry(ByVal action As String, ByVal detail As String, _
                       ByVal scope As Byte, _
                       Optional ByVal source As String = vbNullString)

Parameters:

Throws:


FlushEntry #

flush-entry

Build one Checking bundle and flush it through the writer

Signature:

Private Sub FlushEntry(ByVal action As String, ByVal detail As String, _
                       ByVal scope As Byte, _
                       Optional ByVal source As String = vbNullString)

The bundle title is the section of the action, not the action itself, so the sheet carries three titles rather than one per action code. The bundle carries one key, so the key uniqueness of Checking binds inside the bundle and every event stays a plain append.

Parameters:


EntryLabel #

entry-label

Compose the dated label of one entry

Signature:

Private Function EntryLabel(ByVal action As String, ByVal detail As String, _
                            Optional ByVal source As String = vbNullString) As String

The label opens with the timestamp and the platform; the procedure, the action code and the detail follow behind the separator, so the two halves land in their own columns of the log sheet. The action code moved here when the title became the section: the sheet has three titles now, and the line has to say which action it records. A separator inside the free text would push it past the last written column, so it is softened to a single dash. The procedure sits in front of the action because that is the order a reader wants: who acted, what they did, and on what. The platform rides with the timestamp rather than with the detail, because the detail column is what a reader searches and what several tests read whole. It is on every line on purpose: a log rotation clears the sheet, so a platform written once at the top of a session would not survive one.

Parameters:

Returns: String. The composed label.


Soften #

soften

Take the column separator out of a piece of free text

Signature:

Private Function Soften(ByVal text As String) As String

CheckingOutput splits the label on the separator and lays the pieces across three columns, so a separator inside free text would push the rest of the line past the last written column and out of the sheet.

Parameters:

Returns: String. The text with every separator turned into a single dash.


PlatformTag #

platform-tag

The operating system, the bitness and the Excel version, on one tag

Signature:

Private Function PlatformTag() As String

A log read months later says nothing about where it was written, and the same code does not behave the same on the two platforms: a geobase export that Windows accepts has refused on this Mac. Every entry therefore names its platform. The name and the bitness come from the compile constants, so they are the build that is running and cannot be misread. The Excel version comes from the application and is read once, because it is the same for every line of a session and the log writes a line per user action.

Returns: String. Something of the shape "mac-64 excel-16.90".


RotateWhenPastCap #

rotate-when-past-cap

Clear the sheet and restart the log once the cap is passed

Signature:

Private Sub RotateWhenPastCap()

Reads the last written row of the output column. Past the cap, the sheet is cleared and the row marker removed, so CheckingOutput re-initialises on the next render, and the fresh log opens with one info line.


Writer #

writer

Lazily create and return the CheckingOutput writer

Signature:

Private Function Writer() As CheckingOutput

The writer is asked for compact appending, which is the mode a log wants: a section title is written the first time the sheet carries it and never again, and no blank rows stand between the entries.

Returns: CheckingOutput. Writer bound to the log worksheet.


HiddenStore #

hidden-store

Lazily create and return the HiddenNames store of the log sheet

Signature:

Private Function HiddenStore() As HiddenNames

Returns: HiddenNames. Hidden name store bound to the log worksheet.


ErrorHandling

ThrowError #

throw-error

Raise a ProjectError-coded runtime error

Signature:

Private Sub ThrowError(ByVal errNumb As Long, ByVal errorMessage As String)

Centralised error-raising pattern and the set-once seal.

Parameters:

Throws:


Seal #

seal

Seal the instance so setup setters can no longer be written

Signature:

Public Sub Seal()

GuardNotSealed #

guard-not-sealed

Guard a setup setter against writes after sealing

Signature:

Private Sub GuardNotSealed(ByVal propName As String)

Parameters:


Used in (10 file(s))