Global

Members

(constant) AUTO_PAUSE_ON_TAB_SWITCH :boolean

When true, pauses a running timer automatically when the browser tab becomes hidden (visibilitychange → document.hidden). The pause is silent; the event is recorded in the console log via wlLog.info but no UI notification appears. Set to false to disable. Override in 00-config.local.js.

Type:
  • boolean
Default Value:
  • false
Source:

(constant) BACKUP_RETENTION_DAYS

How many days of entries to include in each backup file.

Source:

(constant) CAL_ACCOUNT_LABELS :Object.<string, string>

Maps raw Outlook account keys (lowercase) to human-readable labels shown in the calendar strip. The PowerShell server sends the DisplayName field; this map handles display-name variants, email domains, and substring matches.

Add an entry for each calendar account you want labelled; unknown accounts are shown without a label badge.

Type:
  • Object.<string, string>
Source:

(constant) COLLAPSE_PREFIX

localStorage key prefix for section open/collapsed state.

Source:

(constant) CUSTOM_PALETTE

Fallback colour palette nextDistinctColor() (01-state.js) draws from when auto-generating a new category's colour, in preference order. Every value here is a plain hex string and does not itself need sanitising — but any user-supplied colour (e.g. from the category colour picker) must still pass through safeCssColor() before use, same as everywhere else in the app.

Source:

(constant) DATE_LABEL_DAYS

Day-of-week abbreviations used by fmtDateLabel — module-level to avoid per-call allocation.

Source:

(constant) DATE_LABEL_MONTHS

Month abbreviations used by fmtDateLabel — module-level to avoid per-call allocation.

Source:

(constant) DEFAULT_CATS

Built-in categories seeded on first run.

Source:

(constant) DEFAULT_WORK_LOCATION

Location id used for any day that has no stored value.

Source:

(constant) EOD_SUBMIT_ENABLED :boolean

When true, the End of Day hours form shows a "submit" button that posts the entry through the local server (Claude in Chrome, then Playwright). When false (default) the form is a draft to copy from: hours and description are shown with a copy button and nothing is sent. Override in 00-config.local.js.

Type:
  • boolean
Default Value:
  • false
Source:

(constant) EPIC_STALE_DAYS

Days of inactivity after which an epic is offered up for archiving.

Source:

(constant) GAP_REPORT_UTILITY_TEXTS :Set.<string>

Texts of utility entries logged via logUtilEntry() in 03-timer.js. They never carry documentation, so they would just be noise in the gap report. Exported so pure-fns-weeklyreport.js's buildWeeklyTicketSummary() can reuse the same exclusion rather than declaring a second copy.

Type:
  • Set.<string>
Source:

(constant) HIDDEN_MEETINGS_PREFIX

localStorage key prefix for the meetings a user has hidden on a given day.

Source:

(constant) ICON_ACTIVITY

Activity-line SVG icon for the section header (Lucide-style, 24×24 viewBox, 1.5px stroke).

Source:

JIRA_BASE :string

Base URL for Jira ticket links. Ticket keys found in task names are converted to <a href="${JIRA_BASE}/${key}"> anchors. Set to '' to disable link generation. Override via setJiraBase() in src/js/00-config.local.js (gitignored) — copy from src/js/00-config.local.example.js and set your real instance URL.

Type:
  • string
Source:

(constant) LONG_DAY_SPAN_MS

Day span, in ms, above which a workday is expected to contain a real break.

Source:

(constant) MIGRATIONS :Array.<Migration>

Type:
Source:

(constant) MIN_EXPECTED_BREAK_MS

Untracked time, in ms, below which a long day is flagged as missing a break.

Source:

(constant) MS_PER_HOUR

Milliseconds in one hour, for converting tracked time to timesheet hours.

Source:

(constant) PROTECTED_CAT_IDS :Array.<string>

Built-in epic IDs that must never be archived. They are seeded on first run (DEFAULT_CATS in app-constants.js) and 'other' doubles as the fallback that getCat() resolves unknown tags to, so removing it from the pickers would leave orphaned entries with nothing to point at.

Type:
  • Array.<string>
Source:

(constant) RAPID_SIG_SHORTCUTS :Object.<string, string>

Signifier shortcode map for quick-capture inline tokens. Maps !<shortcode> tokens to signifier values used on entry objects. Unmapped tokens are left in the text unchanged.

Type:
  • Object.<string, string>
Source:

(constant) RETIRED_KEYS :Array.<RetiredKey>

localStorage keys belonging to features that have since been removed. Entries are never deleted from this list — a user upgrading across several versions still needs the key swept out of their browser.

Type:
Source:

(constant) SIG_SYMBOL

Signifier id → display symbol. Entries with no signifier show '●' (billable, the default).

Source:

(constant) SIG_TITLE

Signifier id → accessible title. Entries with no signifier show 'Billable'.

Source:

(constant) STORE_BLOCKS :string

localStorage key for the time-block array.

Type:
  • string
Source:

(constant) STORE_CATS

localStorage key for custom work categories.

Source:

(constant) STORE_ENTRIES

localStorage key for logged work entries.

Source:

(constant) STORE_LOCATION

localStorage key for the per-day work-location record.

Source:

(constant) STORE_LOGNOTES

localStorage key for ad-hoc log notes.

Source:

(constant) STORE_MIGRATION

localStorage key for the end-of-month migration record.

Source:

(constant) STORE_POMO_LOG

localStorage key for the pomodoro session log.

Source:

(constant) STORE_QP_HIDDEN

localStorage key for quick-pick items the user has dismissed.

Source:

(constant) STORE_TIMER

localStorage key for the active timer state.

Source:

(constant) STORE_TRACKERS

localStorage key for tracker progress grids.

Source:

(constant) TF_PANE_IDS

Maps each view to the DOM id of the pane that hosts it.

Source:

(constant) TF_STRIP_END

21:00 in minutes from midnight — right edge of the day-overview strip.

Source:

(constant) TF_STRIP_START

07:00 in minutes from midnight — left edge of the day-overview strip.

Source:

(constant) TF_VIEWS

Ordered list of view ids — drives the segmented control and keyboard nav.

Source:

(constant) TF_VIEW_LABELS

Display labels for the segmented control. Static so we avoid recomputing on every render.

Source:

(constant) TIMESHEET_ROUTE_NOTE

Status-line suffix per submission route the server reports.

Source:

WEATHER_LAT :number

Latitude of the work location (decimal degrees). Default used when the server is not running; normally set via config.local.ps1.

Type:
  • number
Source:

WEATHER_LON :number

Longitude of the work location (decimal degrees). Default used when the server is not running; normally set via config.local.ps1.

Type:
  • number
Source:

WEATHER_NAME :string

Display name for the work location shown next to the weather widget. Default used when the server is not running; normally set via config.local.ps1.

Type:
  • string
Source:

(constant) WEEKLY_REPORT_NO_TICKET_KEY

Sentinel ticket key for entries whose text carries no parseable Jira prefix.

Source:

(constant) WORK_LOCATIONS :Readonly.<Record.<string, {emoji: string, label: string}>>

Work-location presets keyed by their stored id. Each entry carries the emoji and human-readable label shown in the date-nav header. The first key is the default applied to any day with no stored location.

Type:
  • Readonly.<Record.<string, {emoji: string, label: string}>>
Source:

_boardDragTaskId

Shared drag-state for board column DnD (set by dragstart, read by drop).

Source:

_exportBackupFn

Registered exportBackup function, injected at startup to avoid a non-leaf dep.

Source:

_flowNoteEditId

Task ID whose note is currently being edited inline in the Flow view.

Source:

_heroStopped :boolean

Type:
  • boolean
Source:

_heroStoppedEntry :Object|null

The log entry that was just stopped. Kept so the Undo action can recover it and the stopped panel can display the correct task name / session range.

Type:
  • Object | null
Source:

_heroStoppedTimer :number|null

Type:
  • number | null
Source:

(constant) _invalidListenerPending :WeakSet.<HTMLInputElement>

Tracks inputs that already have a pending clear-on-edit listener, so that calling markInputInvalid() repeatedly before the user edits does not stack multiple listeners on the same element.

Type:
  • WeakSet.<HTMLInputElement>
Source:

_qcFilterCat :string|null

Active category chip selection. Serves two purposes:

  • filters the task list to show only that category, and
  • tags any ad-hoc log entry created via "Log without tracking". null means "All" (no filter; entry falls back to selectedTag).
Type:
  • string | null
Source:

_qcSearch :string

Type:
  • string
Source:

_qcTickInterval :number|null

Type:
  • number | null
Source:

_rapidOpen :boolean

Type:
  • boolean
Source:

_renderFn

Registered render function, injected at startup to avoid a non-leaf dep.

Source:

catManageOpen

Controls whether the epic manage row (rename/delete/add) is expanded.

Source:

(constant) copyButtonOriginalLabels

Label markup each copy button had before its first "Copied!" flash.

Source:

doneHistoryOpen

Whether the Done column's older-history expander is open.

Source:

emergencyMode :boolean

True while the focus/emergency overlay is visible.

Type:
  • boolean
Source:

wipWarnDismissed

Whether the WIP-over warning banner has been dismissed this render cycle.

Source:

Methods

_heroBindCatPicker(wrap)

Binds open/close/select keyboard and pointer events on a .hero-cat-wrap element.

Parameters:
Name Type Description
wrap HTMLElement

The .hero-cat-wrap container.

Source:

_heroCancelStoppedTimer()

Cancels the auto-dismiss timer (called by Undo / Done buttons).

Source:

_heroCatSelect(newTag)

Updates the active entry's category, persists, and re-renders.

Parameters:
Name Type Description
newTag string

Category ID to apply.

Source:

_heroFillIdle()

Updates the idle panel: logged-today total and last-session time.

Source:

_heroFillPaused()

Updates the paused panel with the frozen clock and task details.

Source:

_heroFillRunning()

Updates the running panel: category dot + task title + started-at sub-line.

Source:

_heroFillStopped()

Updates the stopped panel with the session summary.

Source:

_heroHandleDone()

Dismiss the stopped confirmation panel immediately (same as auto-dismiss).

Source:

_heroHandleStart()

Handles the "▶ Start tracking" button. Uses the composer input if it has text; otherwise focuses the plan input.

Source:

_heroHandleUndo()

Undo the just-stopped entry: remove it from entries and optionally restart the timer if the entry had been running (i.e. it had no prior tsEnd).

Source:

_heroLastNoteText(entryId) → {string}

Returns "↳ last note X ago" text for the most recent session-note on an entry, or an empty string when no session notes have been added yet.

Parameters:
Name Type Description
entryId string

ID of the active log entry.

Source:
Returns:
Type
string

_heroRenderRecentChips()

Builds the recent-task chip strip in the idle panel. Shows up to 3 distinct recent entries with their category dot.

Source:

_heroSessionCount(entry) → {number}

Returns the number of distinct time entries today for the same task text.

Parameters:
Name Type Description
entry Object
Source:
Returns:
Type
number

_heroSetCategory(elId, tag, interactiveopt)

Fills a category display cell with the dot + label (and optional caret/picker).

Parameters:
Name Type Attributes Default Description
elId string

ID of the .hero-task-category element.

tag string

Category ID.

interactive boolean <optional>
false

When true, renders a caret button and picker panel.

Source:

_heroShowPanel(id, visible)

Parameters:
Name Type Description
id string

Element ID.

visible boolean

Whether to show the element.

Source:

_heroStartFromChip(text, tag)

Starts tracking from a recent-chip click. Re-uses the existing entry (no duplicate) and starts the timer.

Parameters:
Name Type Description
text string

Task description.

tag string

Category ID.

Source:

_qcActivateRow(rowId, text, tag, isActive)

Starts or switches to a task from the quick-capture list. If the row is already the active timer, just closes the modal. For plan-task rows a fresh time entry is created.

Parameters:
Name Type Description
rowId string

Row ID from the data attribute.

text string

Task description.

tag string

Category ID.

isActive boolean

Whether this is the currently-running task.

Source:

_qcBindTaskListEvents(el)

Attaches click listeners to task-row action buttons and their parent rows. Separated from rendering so each has a single responsibility.

Parameters:
Name Type Description
el HTMLElement

The #qcTaskList container element.

Source:

_qcBuildTaskGroups(searchLower, todayKey) → {Object}

Collects and deduplicates the three task groups for the current filter state. Pure data function — performs no DOM operations.

Parameters:
Name Type Description
searchLower string

Lower-cased search string; empty string means no filter.

todayKey string

Date key for today in YYYY-MM-DD format.

Source:
Returns:
Type
Object

_qcLogOnly()

Handles the "Log without tracking" footer button and Enter keypress.

Parses inline shorthand tokens from the input before creating the entry: #<cat> overrides category, !<sig> sets the signifier, ><date> sets the entry date (today / tomorrow / YYYY-MM-DD / weekday).

  • Non-empty text after token stripping → creates a log entry (no timer) and closes.
  • Only tokens, no text → refocuses the input so the user adds a description.
  • Completely empty input → closes and focuses the ad-hoc row in the time log.
Source:

_qcRenderAll()

Re-renders all dynamic regions of the quick-capture modal.

Source:

_qcRenderCatChips()

Renders the "All" chip plus one chip per category. The active chip is highlighted; clicking a chip filters the task list.

Source:

_qcRenderRunningStrip()

Shows or hides the running strip. When a timer is active the strip displays the current task and elapsed time. When idle the strip is hidden and the pulsing-dot class is removed.

Source:

_qcRenderTaskList()

Orchestrates data collection, rendering, and event binding for the task list. Delegates each concern to a single-purpose helper.

Source:

_qcRenderTokenPreview(parsed)

Renders colored pill badges below the search input for any recognised inline tokens in the current input value. Hides the preview container when no tokens are active (e.g. on modal open or after the field is cleared).

Parameters:
Name Type Description
parsed Object

Result of parseRapidTokens() for the current raw input value.

Source:

_qcStartTick()

Starts a 1-second interval to update the elapsed-time label in the running strip.

Source:

_qcStopTick()

Clears the running-strip interval.

Source:

_qcTaskListHtml(groups, search) → {string}

Renders the task list HTML string from pre-built group data. Receives all inputs as parameters — performs no DOM or module-state reads.

Parameters:
Name Type Description
groups Object
search string

Original (un-lowercased) search string for the empty-state message.

Source:
Returns:

HTML string ready for assignment to el.innerHTML.

Type
string

_qcTaskRowHtml(rowId, text, cat, isActive) → {string}

Returns the HTML for one task row in the task list. The action button is opacity-0 and revealed on row hover via CSS.

Parameters:
Name Type Description
rowId string

Unique row key: entry ID or 'plan:' + plan-task ID.

text string

Task/entry description text.

cat Object

Resolved category.

isActive boolean

True when this row matches the currently-running entry.

Source:
Returns:

HTML string.

Type
string

_qcUpdateElapsed()

Updates only the elapsed-time label; called every second by the ticker.

Source:

activeTimerDurationMs(entry) → {number}

Returns the effective tracked duration (ms) of an active-timer entry, honouring pause state — matches the formula used by renderFlowHeader so the strip bar and Flow-view duration freeze while paused.

Parameters:
Name Type Description
entry object
Source:
Returns:

0 if no timer is active for this entry.

Type
number

addEntry(withTimer)

Creates a new log entry from the capture input's current value. Optionally starts the timer on the new entry (withTimer = true), in which case any running timer is stopped first and the matching plan task is auto-promoted to "in progress".

Parameters:
Name Type Description
withTimer boolean

If true, start the timer on the new entry.

Source:

addLogNote()

Reads the note input, appends a note to logNotes, persists, and re-renders.

Source:

addPlanTask() → {void}

Reads the plan-input field and adds a new "todo" task to today's plan. Inherits the currently selected tag and that category's billable default. No-ops if the input is empty.

Source:
Returns:
Type
void

(async) addTaskToNotion(task) → {Promise.<string>}

Adds a work-log task to Notion as a child page under the matching project. The project is looked up server-side by matching the task's category label to a Notion project's Epic field. Uses /api/notion-add-task (Notion REST API, no AI).

Parameters:
Name Type Description
task Object

Plan task object with at least text and tag.

Source:
Throws:

If the API call fails or no URL is returned.

Type
Error
Returns:

The URL of the newly created Notion page.

Type
Promise.<string>

annotateBillableStatus(entries) → {Array.<Object>}

Returns copies of entries each carrying its resolved billable status as a _billable flag. Pure helpers such as findGapReportEntries() have no access to the category/task lookups isEntryBillable() needs, so callers resolve the status up front and hand it over on the entry. The input entries are not mutated.

Parameters:
Name Type Description
entries Array.<Object>

Log entries to annotate.

Source:
Returns:

New entry objects with _billable set.

Type
Array.<Object>

applyBackupRetention(entries, retentionDays, nowMs) → {Object}

Filters an entries array to those whose date field falls within the retention window (today minus retentionDays, inclusive). Entries with a missing or unparseable date are excluded to keep backups clean.

The cutoff is computed from nowMs so the function stays pure and testable.

Parameters:
Name Type Description
entries Array.<{date: (string|undefined)}>

Raw entries array from localStorage.

retentionDays number

How many days back to keep (the app uses 21).

nowMs number

Current time as a Unix timestamp in milliseconds.

Source:
Returns:

The filtered entries and count of dropped ones.

Type
Object
Example
applyBackupRetention(entries, 21, Date.now())
// → { retainedEntries: [...], dropped: 12 }

applyEpicArchive(categories, staleIds) → {Array.<EpicCategory>}

Returns a copy of categories with archived: true stamped on every epic whose ID is in staleIds. Non-destructive: no record is dropped and every other field is preserved untouched.

Parameters:
Name Type Description
categories Array.<EpicCategory>

All known epics.

staleIds Array.<string>

Epic IDs to archive.

Source:
Returns:

A new array with the archived epics flagged.

Type
Array.<EpicCategory>

applyTime(baseTsMs, timeStr) → {number}

Replaces the hours/minutes of a base timestamp with values parsed from a "HH:MM" string, returning the resulting timestamp in milliseconds.

Parameters:
Name Type Description
baseTsMs number

Base Unix timestamp (ms) that supplies the date.

timeStr string

Time string in "HH:MM" format.

Source:
Returns:

New Unix timestamp (ms) with the updated time.

Type
number

autoCarryTasks() → {number|undefined}

Carries unfinished plan tasks from past days into today — runs once per day (guarded by a localStorage flag). Deduplicates by text, preserving the most recent past status and status-comment history. Checkpoints are carried forward with done reset to false for a fresh day.

Source:
Returns:

Number of tasks newly carried, or undefined if carry has already run today.

Type
number | undefined

billBtnHtml(t, status) → {string}

Builds the billable toggle button HTML for a task row. Returns empty string for pending/blocked/upcoming tasks where billing is irrelevant.

Parameters:
Name Type Description
t Object

The plan task.

status string

The task's current status.

Source:
Returns:

HTML button element, or ''.

Type
string

bindAdHocRow() → {void}

Binds click/Enter handlers for the ad-hoc inline log row (#tlAdHocBtn / #tlAdHocInput). Called from renderTimelineSection() on both the empty-state branch and the normal entry-list branch, since #timeline's innerHTML — and the row inside it — is fully replaced on every render() call. Both branches must call this, or the row exists in the DOM but silently does nothing.

Source:
Returns:
Type
void

bindBoardColumnDnD() → {void}

Makes each rendered board card draggable and wires its dragstart/dragend. Called once per renderPlan() cycle after columns are populated. Static column drop-zone listeners are set up once in initBoardColumnDnD().

Source:
Returns:
Type
void

bindEntryMetaEvents(timelineEl)

Binds open/save/clear/cancel events for each entry's proof-link/note editor, plus the yes/no handlers for its restart note-confirmation banner (see createRestartedEntry in 05-entries.js). Re-attached after every render() call since #timeline's innerHTML is fully replaced each time, same pattern as the other entry-row bindings in render() (time editor, category picker, billable toggle).

Parameters:
Name Type Description
timelineEl HTMLElement

The #timeline element.

Source:

bindEpicsManager() → {void}

Wires the epics modal: the toolbar button that opens it, the tidy action, and the close button. Called once at startup (12c-startup.js).

These controls used to live in the epic manage strip inside #tagRow, which is display:none in work-log.html — so archiving could be triggered from five pickers but never undone. The modal is the reachable home for them.

Source:
Returns:
Type
void

bindFlowNoteEvents(pane)

Binds note edit/save/remove/cancel events on the Flow pane after each render. Listeners are attached to freshly-rendered child elements; prior children are discarded by the innerHTML replacement in renderFlowView, so there are no orphan listeners. Both renderFlowView and renderPlan are called after mutations so the Flow pane and the kanban board stay in sync.

Parameters:
Name Type Description
pane HTMLElement

The #tfFlowPane element.

Source:

bindMonthlyCalendarEvents(calEl) → {void}

Binds the calendar's interactions inside calEl: clicking a day cell navigates to that day's Log view, and the prev/next buttons move the viewed month.

Implicit dependency: the prev/next handlers mutate module-level _mlYear / _mlMonth and then call renderMonthlyLog() to re-render the whole view. Both globals must therefore be in scope when this function is called.

Parameters:
Name Type Description
calEl HTMLElement

The #mlCalendar container whose markup was just rendered.

Source:
See:
Returns:
Type
void

bindPlanCheckpointEvents(qa) → {void}

Binds the per-card checkpoint editor handlers (open/close badge, three-state done toggles, inline edit, delete, add-on-Enter, and drag-to-reorder). Called from bindPlanEvents() after each render.

Parameters:
Name Type Description
qa function

Selector helper closed over the rendered column lists.

Source:
Returns:
Type
void

bindPlanCommentEvents(qa) → {void}

Binds the per-card status-comment editor handlers (accept/skip/edit input, plus the comment-history expander). Called from bindPlanEvents() after each render.

Parameters:
Name Type Description
qa function

Selector helper closed over the rendered column lists.

Source:
Returns:
Type
void

bindPlanEvents(lists)

Binds all plan list event handlers after each render.

Parameters:
Name Type Description
lists Array.<HTMLElement>

Column list elements (To Do, In Progress, Done).

Source:

bindPlanNoteEvents(qa) → {void}

Binds the per-card note editor handlers (toggle, display-to-edit, save, delete, cancel, and textarea keyboard shortcuts). Called from bindPlanEvents() after each render.

Parameters:
Name Type Description
qa function

Selector helper closed over the rendered column lists.

Source:
Returns:
Type
void

bindSignifierClicks()

Attaches click and keyboard listeners to all .esig elements after a render.

Source:

bindTagRowEvents() → {void}

Wires every listener for the markup buildTagRowHtml() just produced.

Each lookup past the always-present dropdown controls is null-guarded: only one inline mode is in the DOM at a time, so most of these elements are absent on any given render.

Source:
Returns:
Type
void

bindTimelineEntryEvents(timelineEl) → {void}

Binds every per-entry event handler in the timeline: the time editor, the category picker (including its inline "+ new epic" creator), the billable toggle, delete, restart, and inline text rename. Called once per renderTimelineSection() call on the non-empty branch, since #timeline's innerHTML is fully replaced on every render() call.

Parameters:
Name Type Description
timelineEl HTMLElement

The #timeline element.

Source:
Returns:
Type
void

blockAriaLabel(block) → {string}

Accessible name for a planned block. A bare focusable div is dropped from the accessibility tree unless it has a role, so the block is also given role="group"; the name carries the time range and says how to move it, which screen-reader users cannot otherwise discover.

Parameters:
Name Type Description
block Object

The planned block.

Source:
Returns:

Text such as "Write report — planned 11:00–12:00, press arrow keys to move".

Type
string

blockMoveDelta(key) → {number}

Slot change a key press asks for.

Parameters:
Name Type Description
key string

A KeyboardEvent.key value.

Source:
Returns:

-1 for ArrowUp (earlier), 1 for ArrowDown (later), 0 for any other key.

Type
number

buildBackupPayload(state, retentionDays, nowMs) → {Object}

Builds the JSON-backup payload with the retention window applied to every time-series array, so the backup file size stays bounded over time.

Previously only entries was trimmed while planTasks, blocks, devLog, and distractions were bundled in full — those grow every day (carried tasks, per-day time blocks, appended distraction/dev-log records) and were the reason "21-day" backups kept getting bigger. Each of those arrays carries a YYYY-MM-DD date field, so they prune through the same window as entries.

Non-time-series data is intentionally kept whole: categories and qpHidden are small reference sets with no date, and pomoLog is already capped at source (100 records) so it needs no further trimming here.

Kept pure (no localStorage, no ambient clock) so the bounded-size guarantee is unit-testable — the caller supplies the current state and the clock.

Parameters:
Name Type Description
state object

Current application state arrays.

Properties
Name Type Attributes Description
entries Array <optional>

Log entries (dated; trimmed).

categories Array <optional>

Category definitions (undated; kept whole).

planTasks Array <optional>

Plan/board tasks (dated; trimmed).

blocks Array <optional>

Time blocks (dated; trimmed).

pomoLog Array <optional>

Pomodoro log (capped at source; kept whole).

devLog Array <optional>

Dev-changelog entries (dated; trimmed).

distractions Array <optional>

Distraction records (dated; trimmed).

qpHidden Array <optional>

Hidden quick-pick ids (undated; kept whole).

retentionDays number

How many days back to keep for dated arrays.

nowMs number

Current time as a Unix timestamp in milliseconds.

Source:
Returns:

The backup payload object and a per-array count of records excluded from it (only arrays that dropped at least one record are included). The count folds together records that aged out of the window and records dropped for a missing/malformed date — both are legitimately excluded, so they are not distinguished here.

Type
Object
Example
buildBackupPayload({ entries, planTasks, blocks }, 21, Date.now())
// → { payload: { version: '1', entries: [...], planTasks: [...], ... }, dropped: { blocks: 4 } }

buildBlockActionButtonsHtml(block) → {string}

Builds the action buttons shown on a planned block: start (task blocks only), add/change emoji, and delete. Each button's accessible name includes the block's text because a glyph or "▶ start" alone is announced identically on every block, so screen-reader users cannot tell which one they are on (WCAG 4.1.2 / 2.4.6).

Parameters:
Name Type Description
block Object

The planned block.

Source:
Returns:

HTML for the buttons, ready to append inside the block element.

Type
string

buildDailyLogItems(dateKey) → {Array.<{ts: number, type: string, entryId: (string|undefined), parentEntryId: (string|undefined), color: string, text: string, sub: string}>}

Builds a chronologically sorted array of feed items for the Log view. Merges time entries, log notes, and task status comments for the given day.

Item types:

  • 'entry' — a time-tracked entry; includes entryId.
  • 'note' — a freeform log note; text is wrapped in <em>.
  • 'session-note' — a note attached to a running/completed entry; includes parentEntryId. Renderers nest these inside their parent entry row rather than as standalone timeline rows.
  • 'task' — a plan-task status comment.
Parameters:
Name Type Description
dateKey string

YYYY-MM-DD date string.

Source:
Returns:
Type
Array.<{ts: number, type: string, entryId: (string|undefined), parentEntryId: (string|undefined), color: string, text: string, sub: string}>

buildEntryCatPickerHtml(entry, categoryList) → {string}

Builds the category picker contents for a single log entry: one button per existing category, a cancel button, and an inline "+ new epic" creator so a brand-new epic can be added without leaving the entry — mirrors the board's task-card picker (catOpts in 10a-tasks-row.js's renderRow), which previously was the only place epics could be created.

Parameters:
Name Type Description
entry Object

The log entry.

categoryList Array.<{id: string, label: string, color: string}>

Available categories.

Source:
Returns:

HTML string for the picker's contents.

Type
string

buildEntryLinkMap(dayEntries) → {Object.<string, string>}

Builds a lookup of proof links for a single day, keyed by lowercased task text so it lines up with buildEntryNoteMap and the task keys produced by groupEntriesByCategory. Unlike notes (free text, one line per entry), links are short reference codes — Zephyr keys, Confluence pages, ticket URLs — so multiple links for the same task are deduplicated and joined onto a single comma-separated line rather than one per entry.

Parameters:
Name Type Description
dayEntries Array.<Object>

Entries for the exported day.

Source:
Returns:

Map of lowercased task text to a comma-separated, deduplicated list of that task's proof links.

Type
Object.<string, string>
Example
buildEntryLinkMap([
  { text: 'Update test steps', link: 'T197797' },
  { text: 'Update test steps', link: 'T197805' },
])
// → { 'update test steps': 'T197797, T197805' }

buildEntryMetaHtml(entry, isEditing) → {string}

Builds the proof-link / note indicator and inline editor for a log entry. Read-only state shows a 🔗 and/or 📝 indicator (title = full value) plus a toggle button; editing state additionally shows the link/note inputs. Both fields are optional and independent — either can be filled in on its own. When this entry has a pending restart confirmation (_pendingNoteConfirm, set by createRestartedEntry in 05-entries.js), the editor also shows a "same note as last time?" banner above the inputs.

Parameters:
Name Type Description
entry Object

The log entry.

isEditing boolean

Whether this entry's link/note panel is open.

Source:
Returns:

HTML string.

Type
string

buildEntryNoteMap(dayEntries) → {Object.<string, string>}

Builds a lookup of entry-level notes for a single day, keyed by lowercased task text so it lines up with buildTaskNoteMap and the task keys produced by groupEntriesByCategory. Entry notes are written at the time the work happens (unlike a plan task's note, which describes the task in general), so multiple entries sharing a task text each contribute their own note line rather than overwriting one another.

Parameters:
Name Type Description
dayEntries Array.<Object>

Entries for the exported day.

Source:
Returns:

Map of lowercased task text to newline-joined notes, one line per entry that carries a note.

Type
Object.<string, string>
Example
buildEntryNoteMap([{ text: 'Fix login', note: 'reproduced in staging' }])
// → { 'fix login': 'reproduced in staging' }

buildFlowTaskNoteHtml(task, isEditing) → {string}

Builds the note display / edit widget for a task-type flow row. Shows read-only text when not editing; shows a textarea when editing.

Parameters:
Name Type Description
task Object

The plan task object.

isEditing boolean

Whether this task's note is being edited.

Source:
Returns:

HTML string.

Type
string

buildGapReportRowHtml(entry) → {string}

Builds one gap-report row: date label, task text (with any leading Jira key linkified), and a "+ fix" button that jumps to the entry.

Parameters:
Name Type Description
entry Object

A flagged log entry (from findGapReportEntries()).

Source:
Returns:

HTML string.

Type
string

buildManageRowHtml(selCat) → {string}

Builds the inner markup of the epic manage row for whichever inline mode is active. The modes are mutually exclusive and tested in priority order: rename, add-new, then the idle button strip.

Reads the module-level mode flags (editingCatId, addingNewCat) rather than taking them as parameters, matching how the rest of this concatenated file owns that state. Archive and restore are not handled here - #385 moved them into the epics manager modal (renderEpicsManager/bindEpicsManager above).

Parameters:
Name Type Description
selCat EpicCategory

The currently selected epic, already colour-sanitised by getCat().

Source:
Returns:

HTML for the inside of #catManageRow.

Type
string

buildMeetingButtonsHtml(meeting, index) → {Object}

Builds the two per-meeting buttons ("▶ start" and "✕ hide"). Both carry an aria-label naming the meeting: the visible glyph text is identical on every row, so without it a screen-reader user cannot tell which meeting a button acts on (WCAG 4.1.2 / 2.4.6).

Parameters:
Name Type Description
meeting Object

The meeting the buttons act on.

index number

Position of the meeting in the rendered list; the hide handler reads it back.

Source:
Returns:

HTML for each button.

Type
Object

buildMonthlyCalendarHtml(year, month) → {string}

Builds the heatmap calendar markup: navigation header, day labels, day cells and the colour legend. Pure string building (reads hours per day through mlHoursForDay); touches no DOM.

Day cells are rendered as <button> elements so keyboard users can reach them with Tab and activate them with Enter or Space (WCAG 2.1.1 Keyboard). The prev/next buttons carry aria-label values that include the target month name, satisfying WCAG 4.1.2 Name, Role, Value.

Parameters:
Name Type Description
year number

Full year to render.

month number

Month index, 0-based.

Source:
Returns:

HTML for the calendar container's contents.

Type
string

buildPlanReviewRowHtml(task) → {string}

Builds one checklist row: date, ticket key (via the existing parseJiraLabel()) or plain text, and "done"/"drop" reconcile actions.

Parameters:
Name Type Description
task Object

A plan task from findWeeklyPlanReviewTasks().

Source:
Returns:

HTML string.

Type
string

buildRollingSummary(dateKeys, opts) → {Array.<{dateKey: string, locationEmoji: string, sodTs: (number|null), eodTs: (number|null), totalMs: number, topTasks: Array.<{text: string, totalMs: number}>}>}

Computes a rolling per-day summary for a set of date keys.

All data dependencies are injected so the function is pure and unit-testable without any browser globals or localStorage access.

Parameters:
Name Type Description
dateKeys Array.<string>

Date keys (YYYY-MM-DD) to summarise, most recent first.

opts object
Properties
Name Type Description
entries Array.<object>

All work-log entries.

getDayStartTs function

(dateKey: string) => number|null — SOD timestamp.

getDayEodTs function

(dateKey: string) => number|null — EOD timestamp.

getLocationEmoji function

(dateKey: string) => string — location emoji char.

Source:
Returns:
Type
Array.<{dateKey: string, locationEmoji: string, sodTs: (number|null), eodTs: (number|null), totalMs: number, topTasks: Array.<{text: string, totalMs: number}>}>
Example
buildRollingSummary(['2026-06-04'], {
  entries: [{ date: '2026-06-04', text: 'Write code', ts: 1000, tsEnd: 4600000, signifier: '' }],
  getDayStartTs: () => 1000,
  getDayEodTs: () => null,
  getLocationEmoji: () => '🏠',
})
// → [{ dateKey: '2026-06-04', locationEmoji: '🏠', sodTs: 1000, eodTs: null,
//      totalMs: 4599000, topTasks: [{ text: 'Write code', totalMs: 4599000 }] }]

buildSessionNotesHtml(notes) → {string}

Builds the HTML fragment for a list of session-notes nested under a parent entry row. Returns an empty string when there are no notes.

Parameters:
Name Type Description
notes Array.<object>

Session-note items for one parent entry.

Source:
Returns:

HTML string, or '' if notes is empty.

Type
string

buildStatSubHtml(label, ms) → {string}

Builds the HTML for a sub-stat tile's task line: a Jira ticket link (when the label parses as one) plus the task name, or the raw label when it doesn't, followed by the formatted duration.

Parameters:
Name Type Description
label string

Task text, possibly Jira-prefixed.

ms number

Tracked duration in milliseconds.

Source:
Returns:

HTML string for the tile body.

Type
string

buildSummaryText(rows, weekTotalMs, locationMap) → {string}

Builds the plain-text standup block from computed rows. Pure function — no DOM or clipboard access.

Parameters:
Name Type Description
rows Array.<object>

Output of buildRollingSummary.

weekTotalMs number

Sum of all days' tracked time in ms.

locationMap Record.<string, string>

Pre-read location map.

Source:
Returns:
Type
string

buildTagRowHtml() → {string}

Builds the complete #tagRow markup: the epic dropdown row plus the manage row. Touches no DOM of its own - it reads module state and returns a string, so the markup can be produced and asserted on independently of the listener wiring in bindTagRowEvents().

Source:
Returns:

HTML to assign to #tagRow.

Type
string

buildTaskNoteMap(planTasks, dateKey) → {Object.<string, string>}

Builds a lookup of task notes for a single day, keyed by lowercased task text so it lines up with the task keys produced by groupEntriesByCategory. Only tasks dated dateKey with a non-blank note are included.

Task/entry linkage is by date + case-insensitive text match — the same convention addEntry and flatSort already use to find a task's plan row — because plan tasks and log entries share no taskId field.

Parameters:
Name Type Description
planTasks Array.<Object>

All plan/board tasks.

dateKey string

The exported day's date key (YYYY-MM-DD).

Source:
Returns:

Map of lowercased task text to trimmed note.

Type
Object.<string, string>
Example
buildTaskNoteMap(
  [{ text: 'Fix login', date: '2026-06-04', note: 'waiting on staging creds' }],
  '2026-06-04'
)
// → { 'fix login': 'waiting on staging creds' }

buildTimesheetDayPayload(dateKey, timedEntries, categoryLabel) → {Object|null}

Builds the one whole-day timesheet entry: the date, total tracked hours (rounded to two decimals, as timesheet forms take decimal hours) and the description from buildTimesheetDescription. All tracked time counts, billable or internal, because the timesheet records the full working day.

Parameters:
Name Type Description
dateKey string

Day being submitted, YYYY-MM-DD.

timedEntries Array.<Object>

The day's entries (text, optional tag, ts, tsEnd) with a positive tracked duration.

categoryLabel function

Category id → display label.

Source:
Returns:

The entry, or null when nothing was tracked (so the caller can skip submitting).

Type
Object | null

buildTimesheetDescription(timedEntries, categoryLabel) → {string}

Builds the draft timesheet description in the house style: one activity (task, task) group per category in first-seen order, groups comma-separated, no durations — e.g. ticket review (AITO-1, AITO-2), meeting (FUAT bi-daily). The activity wording a person writes by hand ("UAT preparation and test design") is not in the log, so the category label stands in for it and the caller offers the draft for editing. Task labels keep their Jira key because that is what a reader checks against the ticket; they are de-duplicated case-insensitively within a group so a task worked in two blocks appears once.

Parameters:
Name Type Description
timedEntries Array.<Object>

The day's tracked entries (text, optional tag); a missing tag is treated as other.

categoryLabel function

Maps a category id to its display label (e.g. getCatLabel). Injected to keep this module state-free.

Source:
Returns:

The draft description, or '' when there are no entries.

Type
string
Example
buildTimesheetDescription(
  [{ text: 'AITO-1', tag: 'review' }, { text: 'AITO-2', tag: 'review' }, { text: 'FUAT', tag: 'meeting' }],
  (id) => id
)
// → 'review (AITO-1, AITO-2), meeting (FUAT)'

buildTimesheetSummaryLine(timedEntries, fmtDuration) → {string}

Builds the pasteable end-of-day summary line: one semicolon-separated Label (duration) item per distinct task, each carrying its full-day total — not per-session — so a task worked in two separate blocks (e.g. 09:30–13:00 and 13:30–16:00) still collapses to one line with one total, matching how a Jira worklog is checked against a ticket. The per-session breakdown lives in the report body instead (see formatGroupedLines's sessions rendering) — this line's job is the checkable total, not the timeline. Distinct tasks are kept in first-seen order; entries are grouped by task text, category (tag, missing normalised to other), and _billable status combined, so a task worked once billable and once internal renders as two line items rather than one status silently overwriting the other.

Non-billable items are suffixed , internal so the billable/internal split — already visible in the header totals — is also checkable at the level of an individual line item.

Unlike the grouped report body (whose task labels feed a client-facing read), this line keeps the entry's raw text, Jira key included: the line's job is proving logged hours against ticket worklogs, so the ticket key is exactly the part that must stay visible.

Parameters:
Name Type Description
timedEntries Array.<Object>

Timed entries (ts, tsEnd, text, optional tag), each optionally carrying a _billable flag (false renders as internal; anything else, including absent, renders as billable).

fmtDuration function

Formats a duration in ms (e.g. fmtDurLong). Injected so this function stays free of global state.

Source:
Returns:

The summary line, or '' when there are no entries.

Type
string
Example
buildTimesheetSummaryLine(
  [
    { text: 'AITO-183656', ts: 0, tsEnd: 4 * 3600000 },
    { text: '📅 Meeting', ts: 4 * 3600000, tsEnd: 4.5 * 3600000, _billable: false },
    { text: 'AITO-183656', ts: 4.5 * 3600000, tsEnd: 7.5 * 3600000 },
  ],
  fmtDurLong
)
// → 'AITO-183656 (7h); 📅 Meeting (30min, internal)'

buildWeeklyTicketSummary(entries, weekStart, weekEnd) → {Object}

Groups a week's finished, non-cancelled, non-utility entries by Jira ticket key (via parseJiraLabel), then by task name within each ticket — the data layer behind the weekly report draft. Entries with no parseable ticket prefix fall into the WEEKLY_REPORT_NO_TICKET_KEY bucket, sub-grouped by their own task text so untracked work stays legible rather than collapsing into one opaque blob.

Uses the same "finished, not cancelled, in-window" filter as findGapReportEntries (whose GAP_REPORT_UTILITY_TEXTS exclusion it also reuses — break/lunch/meeting entries never belong in a status report any more than they belong in the gap report).

Parameters:
Name Type Description
entries Array.<Object>

All log entries.

weekStart number

Inclusive week-start timestamp in ms (e.g. Monday 00:00).

weekEnd number

Exclusive week-end timestamp in ms (e.g. the following Monday 00:00).

Source:
Returns:

ticketOrder lists real ticket keys sorted by total time descending, with the no-ticket bucket (if present) always last regardless of its total — it is a catch-all, not a reportable line item.

Type
Object
Example
buildWeeklyTicketSummary(
  [{ id: '1', text: 'PROJ-1: Fix login', ts: 100, tsEnd: 3700, date: '2026-06-01' }],
  0, 604800000
)
// → { ticketOrder: ['PROJ-1'], grouped: { 'PROJ-1': { totalMs: 3600, ... } } }

calAccountLabel(account) → {string|null}

Resolves a human-readable account label for an Outlook calendar account. The PowerShell server sends the raw DisplayName which may be an email address, a display name, or free-form company text. Tries three strategies in order: exact match, email domain extraction, and substring match.

Parameters:
Name Type Description
account string | null

Raw Outlook account identifier.

Source:
Returns:

Display label (e.g. "Example Corp"), or null if unknown.

Type
string | null

calcMonthSummaryStats(allEntries, monthPrefix, isBillable) → {Object}

Computes monthly summary statistics from a flat entries array. Pure: takes its own data dependency, no DOM, no module globals. Excludes entries with no tsEnd (still running) or signifier === 'cancelled'.

Parameters:
Name Type Description
allEntries Array.<Object>

The full entries array to filter.

monthPrefix string

Date prefix YYYY-MM used to select entries.

isBillable function

Predicate identifying billable entries; the caller supplies the project's isEntryBillable.

Source:
Returns:
Type
Object

calcMonthTaskCounts(allTasks, monthPrefix) → {Object}

Computes open / done / migrated task counts for a given month. Pure. _migrated (programmatic) and signifier === 'migrated' (BuJo marker) both count toward the migrated total.

Parameters:
Name Type Description
allTasks Array.<Object>

The full plan-tasks array.

monthPrefix string

Date prefix YYYY-MM used to select tasks.

Source:
Returns:
Type
Object

calcStreak(allEntries) → {number}

Counts consecutive days with at least one logged entry, looking backwards from yesterday. Today is excluded so the streak only increments once the day has been completed.

Parameters:
Name Type Description
allEntries Array.<{date: string}>

The full entries array.

Source:
Returns:

The current streak length (0 if no streak).

Type
number

calendarMeetingKey(meeting) → {string}

Returns the stable per-occurrence key for a meeting.

Subject alone is not an identity: a recurring meeting has the same subject at every occurrence, so anything keyed on it treats them all as one. Pairing the subject with the start time identifies the single occurrence on screen.

Parameters:
Name Type Description
meeting *

Meeting object from the calendar API response.

Source:
Returns:

Key of the form subject|start, or '' for a non-object.

Type
string
Example
calendarMeetingKey({ subject: 'Standup', start: '2026-05-28T09:00' })
  // → 'Standup|2026-05-28T09:00'

(async) callClaudeWithNotion(prompt, optsopt) → {Promise.<string>}

Calls the Claude API with a Notion MCP server attached, via the local proxy at /api/notion-ai (API keys never exposed to the browser). Used for the URL-bookmarking form — task imports use addTaskToNotion instead.

Parameters:
Name Type Attributes Description
prompt string

User prompt text.

opts Object <optional>

Optional overrides (model string, maxTokens number).

Source:
Throws:

If the API returns a non-OK status.

Type
Error
Returns:

The concatenated text content of the response.

Type
Promise.<string>

carryMigTask(task)

Duplicates the task into next month (first day) and marks original as migrated.

Parameters:
Name Type Description
task Object
Source:

checkBlockNotifications()

Checks all of today's time blocks and acts on ones that have just become active:

  • Meeting blocks: auto-starts a log entry and timer at the scheduled start time.
  • Task blocks: prompts the user to switch/start within a 3-minute window. Each block is only acted on once (tracked in notifiedBlocks). No-ops when not viewing today.
Source:

checkChime(elapsedMs)

Fires an audible chime if the elapsed time has crossed a configured interval boundary since the last chime. No-ops when the timer is paused.

Parameters:
Name Type Description
elapsedMs number

Elapsed time in milliseconds.

Source:

checkPomoWeeklyClear()

Clears the pomodoro log when a new ISO week begins. Compares the stored week key against the current ISO week; if they differ, the log is removed and the new week key is saved.

Source:

clampBlockSlot(slot, duration) → {number}

Keeps a block inside the day grid: one of duration slots can start no earlier than slot 0 and no later than TB_SLOTS - duration.

Parameters:
Name Type Description
slot number

Requested start slot.

duration number

Block length in half-hour slots.

Source:
Returns:

The nearest valid start slot.

Type
number

clearDirHandle() → {Promise.<void>}

Clears the persisted FSA directory handle from both IndexedDB and the in-memory cache so future exports fall back to browser downloads.

Source:
Returns:
Type
Promise.<void>

clearTimerInterval() → {void}

Stops the running timer's tick, if one is held, and resets the stored ID to null. A no-op when nothing is ticking. Callers used to repeat the read/clear/reset sequence by hand; keeping it here means the interval can never be cleared without also being forgotten.

Source:
Returns:
Type
void

closeAllEditors()

Closes every open inline time-editor panel and restores the display spans.

Source:

closeGapReportOverlay()

Hides the gap-report overlay and restores focus to its trigger.

Source:

closeMigration()

Closes the migration overlay.

Source:

closePlanReviewOverlay()

Hides the checklist overlay, restores focus, and marks the week reviewed.

Source:

closeRapid()

Hides the quick-capture overlay and stops the elapsed ticker.

Source:

closeTrackerForm() → {void}

Hides the new-tracker form and resets the open-state flag.

Source:
Returns:
Type
void

closeWeeklyReportOverlay() → {void}

Hides the weekly-report overlay and restores focus to its trigger.

Source:
Returns:
Type
void

collectRecentlyUsedCatIds(sources, cutoffIso) → {Set.<string>}

Collects the IDs of every epic referenced by a log entry or a board task dated on or after cutoffIso. Records with no date are ignored rather than assumed recent — an undated record cannot vouch for an epic's freshness, and treating it as recent would keep stale epics alive forever.

Parameters:
Name Type Description
sources EpicUsageSources

Log entries and board tasks to scan.

cutoffIso string

Inclusive start of the window, as YYYY-MM-DD.

Source:
Returns:

Epic IDs used within the window.

Type
Set.<string>

commitBannerNote()

Saves a timestamped note attached to the currently-active timer entry. The note is stored in logNotes (type: 'session-note') and rendered nested under its parent entry in the flow/log views, rather than as a standalone time-tracked entry. No-ops when there is no active timer or the note is empty.

Source:

computeDayBounds(dayEntries, timedEntries, opts) → {Object}

Computes the day's start and end timestamps for the plaintext export header.

Start: the supplied day start (today only) or, failing that, the earliest entry start. End: the latest tracked end among timed entries, extended by the active timer's effective end so "Ended:" reflects work still in progress.

Pure: all environmental inputs (day start, the active timer, the current time) are injected via opts so the function can be unit-tested without globals.

Parameters:
Name Type Description
dayEntries Array.<Object>

All entries for the viewed day.

timedEntries Array.<Object>

Entries with a real tracked duration (tsEnd).

opts Object

Injected environment.

Properties
Name Type Description
isViewingToday boolean

Whether the viewed day is today.

dayStart number | null

Configured day-start ts, or null if not today.

activeTimer Object | null

The running/paused timer, or null.

now number

Current time in ms (Date.now()).

Source:
Returns:

Day bounds in ms.

Type
Object

config(cfg)

Logs a snapshot of runtime configuration at startup in a collapsed group. Called once by 07-lifecycle.js after state is loaded.

Parameters:
Name Type Description
cfg Object

Key/value pairs to display (e.g. version, entry count).

Source:

(async) copyEodTimesheetDescription() → {Promise.<void>}

Copies the description as currently edited in the form to the clipboard, so it can be pasted into the timesheet by hand.

Source:
Returns:

Resolves once the status message is set.

Type
Promise.<void>

copySummaryText(rows, weekTotalMs, locationMap) → {void}

Copies the standup text to the clipboard. Shows a brief "Copied!" confirmation on success, or logs a warning on failure.

Parameters:
Name Type Description
rows Array.<object>

Output of buildRollingSummary.

weekTotalMs number

Sum of all days' tracked time in ms.

locationMap Record.<string, string>

Pre-read location map.

Source:
Returns:
Type
void

copyWeeklyReportText() → {void}

Copies the rendered report text to the clipboard. Reads directly from the visible <pre> block rather than rebuilding a parallel string, so the copied text can never drift from what's on screen. Shows a brief "Copied!" confirmation on success, or logs a warning on failure.

Source:
Returns:
Type
void

createCategory(rawLabel) → {Object|null}

Creates and appends a new category from a raw label, unless a category with the same label already exists (case-insensitive). Shared by the task board's and the log entry's "+ new epic" pickers so both stay in sync (10b-tasks-events.js, 04-render.js).

Parameters:
Name Type Description
rawLabel string

User-entered label text, not yet trimmed.

Source:
Returns:

The new category, or null when the label is empty or already taken.

Type
Object | null

createEmojiOptionButton(emoji, onSelect) → {HTMLButtonElement}

Creates one selectable emoji button for the picker grid. The emoji is the visible label, but an explicit aria-label ("Select 😀") keeps the name consistent across screen readers, which otherwise announce emoji differently or skip them (WCAG 4.1.2).

Parameters:
Name Type Description
emoji string

The emoji character this button selects.

onSelect function

Called when the button is clicked.

Source:
Returns:

The button, not yet attached to the DOM.

Type
HTMLButtonElement

createRestartedEntry(text, tag) → {Object}

Builds a fresh log entry that continues previously logged work on text, used by every "restart with timer" entry point (the log's ▶ restart button, the kanban board's ▸ track button, and the "+ track recent" chips) so they behave consistently.

If a prior entry with the same text exists, its proof link is carried over automatically. Its note is deliberately not copied onto the new entry directly — a note written for an earlier session may no longer describe this one — instead _entryMetaEditId and _pendingNoteConfirm (both in 04a-render-entry-meta.js) are set so the new entry's proof-link/note editor opens with a "same note as last time?" prompt for the user to confirm or clear.

Parameters:
Name Type Description
text string

Entry text.

tag string

Category id.

Source:
Returns:

A new entry object, not yet pushed to entries.

Type
Object

currentPlanReviewWeekKey() → {string}

Returns the current ISO-week key (e.g. '2026-W32'), matching the format checkPomoWeeklyClear() already uses in 07-lifecycle.js.

Source:
Returns:
Type
string

cycleSignifier(entryId)

Advances an entry's signifier one step through SIG_CYCLE and persists the change. Wraps from the last value back to null (no signifier).

Parameters:
Name Type Description
entryId string

ID of the entry to update.

Source:

debug(msg, dataopt)

Emits a debug-level message, visible only when DevTools verbose level is on.

Parameters:
Name Type Attributes Description
msg string

Human-readable message.

data * <optional>

Optional value to attach to the log line.

Source:

deleteSelectedEpic() → {boolean}

Deletes the currently selected epic after confirming with the user, unless it is a built-in (PROTECTED_CAT_IDS: work, meeting, focus, break, other) — those are never offered for deletion at all, since 'work' is the hardcoded startup/reset fallback and 'other' is getCat()'s own fallback for unknown tags. A selectedTag that matches no stored epic (stale after an import or a delete elsewhere) is also refused, because getCat() would silently substitute 'other' and the function would save and log a deletion that never happened.

Unlike tidyStaleEpics()/archiving, this is a hard delete: the category record itself is removed, not flagged. Any log entry or board task still tagged with this epic keeps that tag string, but getCat() can no longer resolve it and falls back to 'other' — permanently, since there is nothing left to restore. The confirm text says so explicitly and names how many entries/tasks would be affected, mirroring tidyStaleEpics()'s pattern of showing the user what is about to happen before it happens.

Source:
Returns:

True if the epic was deleted, false if blocked, unknown or the user declined the confirm.

Type
boolean

dk(d) → {string}

Formats a Date as YYYY-MM-DD using local calendar date. Uses local date components (getFullYear / getMonth / getDate) so the returned string always matches the date the user sees on their clock, regardless of timezone.

Parameters:
Name Type Description
d Date

Date to format.

Source:
Returns:

e.g. '2026-05-26'

Type
string
Example
dk(new Date(2026, 4, 26, 12, 0, 0))  // → '2026-05-26'  (noon local)
dk(new Date(2026, 11, 31, 23, 59, 0)) // → '2026-12-31' (11 PM local)
dk(new Date(2026, 0, 1, 0, 0, 0))    // → '2026-01-01'  (midnight local)

drawPomoSegments()

Redraws all pomodoro SVG segments to reflect the current remaining time. Each minute is a sector; filled sectors fade as time elapses.

Source:

dropMigTask(task)

Archives (drops) the task by marking it done and migrated.

Parameters:
Name Type Description
task Object
Source:

durLabel(tsStart, tsEnd) → {string}

Builds an HTML <span class="etime-dur"> containing the human-readable duration between two timestamps. Returns an empty string if the duration is zero or negative.

Parameters:
Name Type Description
tsStart number

Start Unix timestamp (ms).

tsEnd number

End Unix timestamp (ms).

Source:
Returns:

HTML string, or '' if duration ≤ 0.

Type
string

ensureDayStarted() → {void}

Records start-of-day silently (no backup-restore dialog) if the day has not already been started. Called automatically when the first task timer begins.

Source:
Returns:
Type
void

enterEmergency()

Activates focus/emergency mode: hides the main UI, shows the overlay, moves the pomodoro section into the overlay, and focuses the next-action input. The previously saved next-action note is restored if one exists for the active entry.

Source:

eodKey(dayopt) → {string}

Returns the localStorage key for a given day's end-of-day timestamp.

Parameters:
Name Type Attributes Default Description
day Date <optional>
viewDate

Day to key by; defaults to the day in view.

Source:
Returns:

Key in the format wl_eod_YYYY-MM-DD.

Type
string

epicCutoffDate(todayIso, windowDays) → {string}

Returns the ISO date (YYYY-MM-DD) that starts an inactivity window ending today. The window is inclusive of both ends: with windowDays of 21 and a todayIso of 2026-08-21, the cutoff is 2026-08-01 and an epic last used on exactly that date still counts as active.

Parameters:
Name Type Description
todayIso string

Today's date as YYYY-MM-DD.

windowDays number

Length of the activity window in days.

Source:
Returns:

The inclusive cutoff date as YYYY-MM-DD.

Type
string
Example
epicCutoffDate('2026-08-21', 21) // → '2026-08-01'

error(msg, dataopt)

Emits an error — something that broke or produced an incorrect result.

Parameters:
Name Type Attributes Description
msg string

Human-readable message.

data * <optional>

Optional value to attach to the log line.

Source:

escHtml(s) → {string}

Escapes a string for safe insertion as HTML text content.

Parameters:
Name Type Description
s string

Raw string to escape.

Source:
Returns:

HTML-escaped string safe for insertion into the DOM.

Type
string
Example
escHtml('<b>bold</b>')   // → '&lt;b&gt;bold&lt;/b&gt;'
escHtml('a & b')         // → 'a &amp; b'
escHtml('"quoted"')      // → '&quot;quoted&quot;'
escHtml(42)              // → '42'

exitEmergency()

Deactivates focus/emergency mode: restores the main UI, saves the next-action note, moves the pomodoro section back to its normal position, and auto-expands the active task's checkpoint list so the user can pick up where they left off.

Source:

exportBackup()

Exports a JSON backup of recent application state: entries, plan tasks, time blocks, dev log, and distractions from the last BACKUP_RETENTION_DAYS days, plus the (undated) categories and hidden quick-pick items and the source-capped pomodoro log. Every time-series array is trimmed to the retention window so the file size stays bounded; older records remain in earlier backup files. Triggers a file download or writes to the save folder.

Source:

exportTxt()

Exports the currently viewed day's log as a plaintext file. Groups entries by category and task, includes a header with day start/end times, tracked/billable/internal totals, and the implied (untracked) break time; lists each task's individual tracked sessions as time ranges plus any plan-task/entry notes and proof links as indented lines; finishes with a pasteable per-item summary line and, when anything looks off (undocumented work, an unbroken >4h block, a full day with no break-sized gap), a warnings section. Writes to the user's chosen save folder via the File System Access API, or falls back to a browser download.

Source:

(async) fetchAndRenderCalendar() → {Promise.<void>}

Fetches today's meetings from the local PowerShell proxy (/api/calendar), caches the result, filters out user-hidden meetings, and calls renderCalStrip. Logs a warning and shows a fallback message on error.

Source:
Returns:
Type
Promise.<void>

fetchCalendarEvents()

Fetches today's Finnish flag days, public holidays, and notable days from the Nimipäivärajapinta Typesense API, displaying the first match in #liveFlagDay. If today has no event, shows the next upcoming one. Falls back to the hardcoded FLAG_DAYS_FIXED list if the API is unreachable.

Source:

fetchNameday()

Fetches today's Finnish and Swedish name days from the Nimipäivärajapinta API and renders them in #liveNameday. Falls back to an "API unavailable" note on error.

Source:

fetchWeather()

Fetches current weather, hourly precipitation probability, and sunrise/sunset data from the Open-Meteo API for the configured location (WEATHER_LAT, WEATHER_LON) and populates #liveWeather, #liveRain, and #liveSunrise. No-ops gracefully on file: protocol or network error.

Source:

filterNewBackupEntries(currentEntries, backupEntries, isValid) → {Array.<Object>}

Returns entries from backupEntries that are both schema-valid and absent from currentEntries (compared by id). Pure: no side-effects, no browser APIs.

Separated from the import flow so the deduplication logic can be unit-tested independently of localStorage and the File API. mergeBackupEntries() calls this before writing to localStorage.

Parameters:
Name Type Description
currentEntries Array.<Object>

Entries already in the local store.

backupEntries Array.<Object>

Entries from the backup file.

isValid function

Entry-schema predicate (e.g. validEntry).

Source:
Returns:

Valid entries from backupEntries whose id does not appear in currentEntries.

Type
Array.<Object>

findExportWarnings(dayEntries, opts) → {Array.<string>}

Flags anomalies in a day's entries that would otherwise only surface when someone questions the timesheet later: work logged with no way to check it, suspiciously long unbroken stretches, and a full workday with no break-sized gap in it. Surfaced as a warnings section at the end of the plaintext export so the person doing the logging finds the gap before anyone asking about it does. Non-billable entries are never flagged for a missing note/link — see findGapReportEntries for why the _billable flag must already be resolved on each entry before it reaches this function.

Pure: the duration formatter is injected (same convention as formatGroupedLines) so this has no dependency on global state.

Parameters:
Name Type Description
dayEntries Array.<Object>

All entries for the exported day, each optionally carrying a resolved _billable flag (false excludes it from the missing-note/link check).

opts Object
Properties
Name Type Attributes Default Description
workdaySpanMs number <optional>
0

Ended-minus-started span for the day.

untrackedMs number <optional>
0

workdaySpanMs minus total tracked time — i.e. the implied, unlabelled break/gap time within the day.

fmtDuration function

Formats a duration in ms.

Source:
Returns:

Human-readable warning lines; empty when nothing stands out.

Type
Array.<string>
Example
findExportWarnings(
  [{ text: 'Fix login', ts: 0, tsEnd: 3600000, signifier: '' }],
  { workdaySpanMs: 3600000, untrackedMs: 0, fmtDuration: (ms) => `${ms}ms` }
)
// → ['No note or link: Fix login']

findGapReportEntries(entries, weekStart, weekEnd) → {Array.<Object>}

Finds finished, non-cancelled, billable work entries within [weekStart, weekEnd) that have neither a proof link nor a note — candidates for the end-of-week gap report. Uses the same "finished and not cancelled" filter as every other report/aggregation in this codebase (buildRollingSummary, findLargestGap in 11-timeflow.js, exportTxt's billable summary), plus excludes break/lunch/meeting utility entries which never need documentation.

Non-billable entries are never flagged: this is a pure module with no access to the category/task lookups isEntryBillable needs, so callers resolve billable status themselves and annotate each entry with _billable before calling this (see exportTxt's entriesWithBillingStatus for the same convention). An entry with no _billable property is treated as billable, matching isEntryBillable's own "undefined means billable" default.

Parameters:
Name Type Description
entries Array.<Object>

All log entries, each optionally carrying a resolved _billable flag (false excludes it from the report).

weekStart number

Inclusive week-start timestamp in ms (e.g. Monday 00:00).

weekEnd number

Exclusive week-end timestamp in ms (e.g. the following Monday 00:00).

Source:
Returns:

Matching entries, sorted by ts ascending.

Type
Array.<Object>
Example
findGapReportEntries(
  [{ id: '1', text: 'Fix login', ts: 100, tsEnd: 200, date: '2026-06-01' }],
  0, 1000
)
// → [{ id: '1', text: 'Fix login', ts: 100, tsEnd: 200, date: '2026-06-01' }]

findLargestGap(dateKey) → {Object|null}

Finds the largest untracked gap (≥ 15 min) today, including gaps between consecutive completed entries AND the trailing gap from the most recent entry's end to now (which is often the most actionable). Returns null for past days — only actionable today.

Parameters:
Name Type Description
dateKey string

YYYY-MM-DD.

Source:
Returns:
Type
Object | null

findMostRecentEntryForText(text) → {Object|undefined}

Finds the most recently created log entry with the same text (case- insensitive, trimmed) as text. Used to carry proof-link/note context forward when a task or entry is restarted with a fresh timer.

Parameters:
Name Type Description
text string

The entry/task text to match against.

Source:
Returns:

The most recent matching entry, or undefined if none exists.

Type
Object | undefined

findPromotableTask(planTasks, text, todayKey) → {Object|null}

Finds today's plan task matching text (case-insensitive), for promotion to "in progress" when a timer starts on that text — regardless of which UI control (hero composer, recent chip, capture input, board's own "track" button) started it. Returns null when there's no match, or the match isn't in a promotable state (only todo/upcoming tasks are promoted — an already-inprogress, pending, blocked, or done task is left alone). Doesn't mutate planTasks; callers apply the status change.

Task/entry linkage is by date + case-insensitive text match — the same convention buildTaskNoteMap uses, because plan tasks and log entries share no taskId field.

Parameters:
Name Type Description
planTasks Array.<Object>

All plan/board tasks.

text string

The entry text a timer was just started on.

todayKey string

Today's date key (YYYY-MM-DD).

Source:
Returns:

The matching plan task to promote, or null.

Type
Object | null
Examples
findPromotableTask(
  [{ id: 't1', text: 'Fix login', date: '2026-06-04', status: 'todo' }],
  'Fix login',
  '2026-06-04'
) // → { id: 't1', text: 'Fix login', date: '2026-06-04', status: 'todo' }
findPromotableTask(
  [{ id: 't1', text: 'Fix login', date: '2026-06-04', status: 'inprogress' }],
  'Fix login',
  '2026-06-04'
) // → null — already in progress, nothing to promote

findStaleCategories(params) → {Object}

Works out which epics have had no recorded activity inside the window and are therefore safe to archive out of the pickers.

An epic is kept when any of these hold: it is a built-in (PROTECTED_CAT_IDS), it is the epic currently selected in the tag row, it is already archived, or a log entry or board task dated within the window references it.

Parameters:
Name Type Description
params Object

Inputs for the staleness decision.

Properties
Name Type Attributes Description
categories Array.<EpicCategory>

All known epics.

entries Array.<EpicUsageRecord> <optional>

Log entries to scan for usage.

planTasks Array.<EpicUsageRecord> <optional>

Board tasks to scan for usage.

todayIso string

Today's date as YYYY-MM-DD.

windowDays number <optional>

Inactivity window in days.

selectedTag string <optional>

Epic currently selected in the tag row; never archived.

Source:
Returns:

IDs to archive (in categories order) and the cutoff date used.

Type
Object

findTimesheetEntryProblem(entry) → {string|null}

Checks a timesheet entry before it is sent anywhere. Shared by the browser form and scripts/lib/eod-automation.mjs so both reject the same inputs.

Parameters:
Name Type Description
entry Object

Candidate entry with date (YYYY-MM-DD), hours (number, > 0 and ≤ 24) and description (non-blank string).

Source:
Returns:

A message naming the first problem, or null when valid.

Type
string | null

findWeeklyPlanReviewTasks(planTasks, weekStartKey, weekEndKey) → {Array.<Object>}

Finds plan tasks explicitly scheduled as 'upcoming' whose date falls within [weekStartKey, weekEndKey) — candidates for the weekly plan review checklist, run once a new ISO week begins so stale "upcoming" tasks (ones already finished elsewhere before their target week arrived) get caught and reconciled instead of silently resurfacing on their date.

Compares date as strings, not timestamps: YYYY-MM-DD keys already sort and compare correctly lexicographically, so no Date conversion is needed.

Parameters:
Name Type Description
planTasks Array.<Object>

All plan/board tasks.

weekStartKey string

Inclusive YYYY-MM-DD (e.g. this week's Monday).

weekEndKey string

Exclusive YYYY-MM-DD (e.g. next week's Monday).

Source:
Returns:

Matching tasks, sorted by date ascending.

Type
Array.<Object>
Example
findWeeklyPlanReviewTasks(
  [{ id: '1', text: 'PROJ-1: Fix login', status: 'upcoming', date: '2026-06-03' }],
  '2026-06-01', '2026-06-08'
)
// → [{ id: '1', text: 'PROJ-1: Fix login', status: 'upcoming', date: '2026-06-03' }]

flashCopiedLabel(button, durationMsopt) → {void}

Briefly replaces a copy button's label with "Copied!", then restores the original markup (emoji span included). The original is remembered on first use, not read at each click: a second click inside the flash would otherwise capture "Copied!" as the label to restore, leaving the button stuck.

Parameters:
Name Type Attributes Default Description
button HTMLElement

The copy button to flash.

durationMs number <optional>
2000

How long "Copied!" stays visible.

Source:
Returns:
Type
void

flatSort(tasks) → {Array.<Object>}

Sorts a flat list of plan tasks by status order and priority, inserting child tasks immediately after their parent. Tasks matching the currently running timer text are sorted first. Orphaned children (whose parent has been deleted or moved) are appended at the end.

Parameters:
Name Type Description
tasks Array.<Object>

Array of plan task objects to sort.

Source:
Returns:

New sorted array with children interleaved after parents.

Type
Array.<Object>

fmtAgo(ts, nowopt) → {string}

Formats a past timestamp as a relative "ago" string for the last-note reference line. Uses an injected now parameter so it is fully unit-testable without mocking Date.now.

Parameters:
Name Type Attributes Description
ts number

Unix timestamp in milliseconds of the past event.

now number <optional>

Current time in ms; defaults to Date.now().

Source:
Returns:

'just now' | 'X min ago' | 'Xh ago'

Type
string
Example
fmtAgo(Date.now() - 30000)          // → 'just now'
fmtAgo(Date.now() - 2 * 60000)      // → '2 min ago'
fmtAgo(Date.now() - 90 * 60000)     // → '1h ago'

fmtDateLabel(dateKey) → {string}

Formats a YYYY-MM-DD date key as a short day-of-week + date label. e.g. '2026-06-04' → 'Wed 04 Jun'

Parameters:
Name Type Description
dateKey string
Source:
Returns:
Type
string

fmtDur(ms) → {string}

Formats a duration in milliseconds as a compact human-readable string ("Xh Ym"). Used throughout the UI for tracked-time display in the timeline, chart, and plan.

Parameters:
Name Type Description
ms number

Duration in milliseconds.

Source:
Returns:

e.g. '1h 30m', '45m', '2h'

Type
string
Example
fmtDur(0)                  // → '0m'
fmtDur(45 * 60 * 1000)     // → '45m'
fmtDur(90 * 60 * 1000)     // → '1h 30m'
fmtDur(120 * 60 * 1000)    // → '2h'

fmtDurLong(ms) → {string}

Formats a duration in milliseconds as a human-readable string using the long "min" suffix. Used in plaintext exports where readability matters more than compactness.

Parameters:
Name Type Description
ms number

Duration in milliseconds.

Source:
Returns:

e.g. '1h 30min', '45min', '2h'

Type
string
Example
fmtDurLong(0)                 // → '0min'
fmtDurLong(45 * 60 * 1000)    // → '45min'
fmtDurLong(90 * 60 * 1000)    // → '1h 30min'
fmtDurLong(120 * 60 * 1000)   // → '2h'

fmtElapsed(ms) → {string}

Formats a duration in milliseconds as a compact time string.

Parameters:
Name Type Description
ms number

Duration in milliseconds.

Source:
Returns:

'MM:SS' for durations under an hour; 'HH:MM:SS' otherwise.

Type
string
Example
fmtElapsed(0)              // → '00:00'
fmtElapsed(90 * 1000)      // → '01:30'
fmtElapsed(3600 * 1000)    // → '01:00:00'
fmtElapsed(5461 * 1000)    // → '01:31:01'

fmtHm(ts) → {string}

Formats a Unix timestamp (ms) as HH:MM in local time.

Parameters:
Name Type Description
ts number
Source:
Returns:
Type
string

fmtLabel(d) → {string}

Returns a human-readable day label: 'today', 'yesterday', or a short locale date string.

Parameters:
Name Type Description
d Date

The date to label.

Source:
Returns:

'today', 'yesterday', or a short locale date such as 'Mon, Jan 5'.

Type
string

fmtMD(d) → {string}

Formats a Date as a zero-padded "MM-DD" string used as flag-day map keys.

Parameters:
Name Type Description
d Date

The date to format.

Source:
Returns:

e.g. "06-04" for the 4th of June.

Type
string

fmtTime(ts) → {string}

Formats a Unix timestamp as HH:MM in 24-hour local time.

Parameters:
Name Type Description
ts number

Unix timestamp in milliseconds.

Source:
Returns:

e.g. '09:30'

Type
string
Example
fmtTime(new Date('2026-05-25T09:05:00').getTime()) // → '09:05'
fmtTime(new Date('2026-05-25T14:30:00').getTime()) // → '14:30'

focusBlockEmojiButton(bid) → {void}

Focuses the emoji trigger button for a planned block after the grid is re-rendered by renderTimeblock(). Searches by data-bid rather than building a CSS selector from untrusted data (avoids injection risk).

Parameters:
Name Type Description
bid string

Block ID.

Source:
Returns:
Type
void

focusPlannedBlock(blockId) → {void}

Moves keyboard focus to a planned block's element, found by ID after the grid has been redrawn. Compares IDs in code instead of building a selector, because block IDs come from stored data.

Parameters:
Name Type Description
blockId string

ID of the block to focus.

Source:
Returns:
Type
void

focusTabAt(nextIndex)

Selects the view at index nextIndex in TF_VIEWS, focuses its tab button, and re-renders. Shared by the keyboard handler in initTodayFlow().

Parameters:
Name Type Description
nextIndex number
Source:

formatGroupedLines(catOrder, catGrouped, fmtDuration, getCatLabel, taskNotesopt, taskLinksopt, fmtSessionRangeopt) → {Array.<string>}

Renders the grouped-by-category structure into indented text lines: one line per category (with its total), each followed by its indented task lines and, when the task carries tracked sessions, a note, and/or a proof link, further indented sub-lines for each — session time ranges first, then note lines, then a link line — so a task worked in two separate sessions still shows both time ranges instead of only their collapsed total.

Pure: the duration/time formatters and category-label resolver are injected so this function has no dependency on global state and can be unit-tested directly.

Parameters:
Name Type Attributes Description
catOrder Array.<string>

Category keys in display order.

catGrouped Object

Grouping produced by groupEntriesByCategory.

fmtDuration function

Formats a duration in ms (e.g. fmtDurLong).

getCatLabel function

Resolves a category key to its label.

taskNotes Object.<string, string> <optional>

Map of lowercased task text to note, as produced by buildTaskNoteMap. Omitted tasks render with no note line.

taskLinks Object.<string, string> <optional>

Map of lowercased task text to a comma-separated link list, as produced by buildEntryLinkMap. Omitted tasks render with no link line.

fmtSessionRange function <optional>

Formats one tracked session as a time-range string (e.g. '09:30–13:00'). When omitted, session lines are skipped entirely (e.g. for callers that never populated sessions on their task objects).

Source:
Returns:

The body lines for the export file.

Type
Array.<string>

formatWeeklyTicketSummaryText(ticketOrder, grouped, fmtDuration) → {Array.<string>}

Renders the structure produced by buildWeeklyTicketSummary into text lines: one header line per ticket (with its total), each followed by an indented bullet per distinct task name (with its own subtotal) and any collected note:/link: lines — same indentation convention as formatGroupedLines in pure-fns-gapreport.js. Ticket blocks are separated by a blank line.

Pure: the duration formatter is injected so this has no dependency on global state and can be unit-tested directly.

Parameters:
Name Type Description
ticketOrder Array.<string>

Ticket keys in display order.

grouped Object

Grouping produced by buildWeeklyTicketSummary.

fmtDuration function

Formats a duration in ms (e.g. fmtDurLong).

Source:
Returns:

The report body lines; empty when ticketOrder is empty.

Type
Array.<string>

getActiveTimer() → {object|null}

Returns the current active timer object, or null when no timer is running.

Source:
Returns:

The active timer state object, or null.

Type
object | null

getBlocks() → {Array}

Returns the live time-blocks array.

Source:
Returns:

The blocks.

Type
Array

getCat(id) → {Object}

Returns the category object for id, falling back to the built-in 'other' category or, when that is also absent, a hardcoded stub. The returned colour always passes through safeCssColor() to strip any unsafe CSS value.

Parameters:
Name Type Description
id string

Category ID.

Source:
Returns:
Type
Object

getCatColor(id) → {string}

Returns the CSS colour string for the category with the given ID.

Parameters:
Name Type Description
id string

Category ID.

Source:
Returns:

CSS colour value, always a safe CSS string.

Type
string

getCatLabel(id) → {string}

Returns the display label for the category with the given ID.

Parameters:
Name Type Description
id string

Category ID.

Source:
Returns:

Category label.

Type
string

getCategories() → {Array.<{id: string, label: string, color: string}>}

Returns the live categories array.

Source:
Returns:

Categories array.

Type
Array.<{id: string, label: string, color: string}>

getDayStart(dayopt) → {number|null}

Retrieves a given day's recorded start-of-day timestamp from localStorage.

Parameters:
Name Type Attributes Default Description
day Date <optional>
viewDate

Day to read; defaults to the day in view.

Source:
Returns:

Unix timestamp (ms), or null if not yet set.

Type
number | null

getElapsedMs() → {number}

Returns the total elapsed milliseconds for the active timer. Accounts for accumulated time from previous pause/resume cycles. Returns 0 if no timer is active.

Source:
Returns:

Elapsed time in milliseconds.

Type
number

getEntries() → {Array}

Returns the current array of work-log entries.

Source:
Returns:

The live entries array.

Type
Array

getEodTs(dayopt) → {number|null}

Retrieves a given day's recorded end-of-day timestamp from localStorage.

Parameters:
Name Type Attributes Default Description
day Date <optional>
viewDate

Day to read; defaults to the day in view.

Source:
Returns:

Unix timestamp (ms), or null if not yet set.

Type
number | null

getFlagDays(year) → {Object.<string, string>}

Returns an object mapping "MM-DD" strings to Finnish flag-day names for the given year. Combines fixed dates with computed moveable feasts.

Parameters:
Name Type Description
year number

The full 4-digit year.

Source:
Returns:

Map of "MM-DD" → event name.

Type
Object.<string, string>

getFlowView() → {'flow'|'log'|'blocks'|'month'|'summary'}

Returns the persisted view preference, defaulting to 'flow'.

Source:
Returns:
Type
'flow' | 'log' | 'blocks' | 'month' | 'summary'

getISOWeek(d) → {number}

Returns the ISO 8601 week number for a given date.

Parameters:
Name Type Description
d Date

The date to evaluate.

Source:
Returns:

ISO week number (1–53).

Type
number

getIterationExpiry(completedDay) → {string|null}

Returns the first iteration expiry date that is strictly later than completedDay, or null if none is configured beyond that date.

Parameters:
Name Type Description
completedDay string

Completion date in "YYYY-MM-DD" format.

Source:
Returns:

The next expiry date, or null.

Type
string | null

getLogNotes() → {Array}

Returns the live log-notes array.

Source:
Returns:

The log notes.

Type
Array

getMigrationRecord() → {Object}

Returns the migration completion record (map of YYYY-MM → boolean).

Source:
Returns:
Type
Object

getMoonData(date) → {Object}

Calculates moon phase, illumination percentage, and zodiac sign for a date using a simplified version of the Meeus algorithm.

Parameters:
Name Type Description
date Date

The date to evaluate.

Source:
Returns:

emoji = phase emoji, phase = phase name, illum = illumination (%), sign = [symbol, name] of the current zodiac sign.

Type
Object

getPlanTasks() → {Array}

Returns the live plan-tasks array.

Source:
Returns:

The plan tasks.

Type
Array

getSavedDir() → {Promise.<(FileSystemDirectoryHandle|null)>}

Retrieves the previously granted File System Access directory handle from IndexedDB (with an in-memory cache).

Source:
Returns:

The handle, or null if none saved.

Type
Promise.<(FileSystemDirectoryHandle|null)>

getSelectedTag() → {string}

Returns the currently selected tag (e.g. 'work', 'personal').

Source:
Returns:

The selected tag.

Type
string

getTimerInterval() → {number|null}

Returns the current setInterval ID for the running timer, or null.

Source:
Returns:

The interval ID, or null when not ticking.

Type
number | null

getTrackers() → {Array}

Returns the live trackers array.

Source:
Returns:

The trackers.

Type
Array

getViewDate() → {Date}

Returns the Date the timeline is currently showing.

Source:
Returns:

The view date.

Type
Date

getViewLocation() → {string}

Resolves the work location for the currently viewed day.

Source:
Returns:

A location id present in WORK_LOCATIONS.

Type
string

groupEntriesByCategory(dayEntries) → {Object}

Groups a day's log entries by category and, within each category, by task (case-insensitively), preserving first-seen order. Accumulates tracked time (where tsEnd > ts) per task and per category.

Pure data transform — reads only entry fields and performs no formatting, so the caller decides how to render the durations and labels.

Parameters:
Name Type Description
dayEntries Array.<Object>

Entries for the viewed day.

Source:
Returns:

catOrder is the list of category keys in first-seen order; catGrouped[catKey] is { totalMs, tasks: { [taskKey]: { label, totalMs, hasTime, sessions } }, taskOrder }. sessions is the task's individual tracked {ts, tsEnd} pairs, in encounter order — kept separate (not merged) so a task worked in two sessions still shows both time ranges rather than one collapsed total.

Type
Object

groupTasksByColumn(viewKey) → {Object}

Partitions today's plan tasks into the three kanban column groups.

Parameters:
Name Type Description
viewKey string

Date key (YYYY-MM-DD) for the current view.

Source:
Returns:
Type
Object

handleBlockKeydown(event, block, dateKey) → {void}

Keydown handler for a planned block: ArrowUp / ArrowDown move it one half-hour slot through moveBlockToSlot(), the same path drag-and-drop uses, and focus follows the block across the redraw. Keys pressed on the block's own buttons and any Ctrl/Alt/Meta/Shift combination are left alone so those controls and assistive-technology shortcuts keep working.

Parameters:
Name Type Description
event KeyboardEvent

The keydown event, listened for on the block element.

block Object

The block the listener belongs to.

dateKey string

YYYY-MM-DD of the day in view.

Source:
Returns:
Type
void

handleDistractClick()

Handles the "distracted" button: pauses a running timer, asks for an optional note, and logs the distraction. Cancelling the prompt logs nothing and resumes the timer — but only if this click paused it, so a timer the user had already paused stays paused (#607).

Source:

handleMoodSelect(mood, icon)

Handles a mood selection from the banner dropdown. Records a distraction or parked thought as an entry (for distracted/parked moods), triggers the hook panel for 'interesting', and sets focus mode for 'focus'. Closes the panel afterwards.

Parameters:
Name Type Description
mood string

One of 'distracted' | 'parked' | 'focus' | 'interesting'.

icon string

Emoji icon for the mood.

Source:

heroEnterStopped(entry)

Called by stopTimer() just before activeTimer is cleared. Switches the hero to the stopped state and arms the 6s auto-dismiss. It does not render: stopTimer() calls render() right afterwards, which re-renders the hero card, so rendering here as well would draw the same state twice.

Parameters:
Name Type Description
entry Object

The log entry that was just stopped.

Source:

heroGetState() → {'idle'|'running'|'paused'|'stopped'}

Derives the current hero state from module-level timer variables.

Source:
Returns:
Type
'idle' | 'running' | 'paused' | 'stopped'

heroUpdateClock()

Updates the running-state elapsed clock without a full render. Only touches the clock element so the DOM churn stays minimal.

Source:

hideHandoffInput()

Hides and clears the handoff-note text field.

Source:

hideMeetingForDay(dayKey, meeting) → {void}

Hides one meeting for the rest of the day.

Stores the occurrence key rather than the subject, so hiding one instance of a recurring meeting leaves that day's other instances on the strip.

Parameters:
Name Type Description
dayKey string

Day key from dk().

meeting Object

The meeting to hide.

Source:
Returns:
Type
void

hideSaveFailureBanner()

Removes the save-failure banner, if one is currently shown.

Source:

(async) importBackup(file) → {Promise.<void>}

Restores application state from a JSON backup file previously created by exportBackup. Reads the file, validates its structure via validateBackupFile, asks the user to confirm, writes all arrays to their localStorage keys, then reloads the page so state is re-initialised cleanly from the restored data.

Assumption: import is a full replace, not a merge. All data currently in the affected localStorage keys is overwritten after the user confirms. If selective-date merging is ever needed, add a merge mode option here and update the confirmation dialog accordingly.

Parameters:
Name Type Description
file File

The .json backup file selected by the user.

Source:
Returns:
Type
Promise.<void>

info(msg, dataopt)

Emits an informational message.

Parameters:
Name Type Attributes Description
msg string

Human-readable message.

data * <optional>

Optional value to attach to the log line.

Source:

initBannerControls()

Binds all interactive controls on the V5 timer banner: mood dropdown, quick-note input, and Break / Lunch / Meeting utility pills. Called once from 07-lifecycle.js after DOMContentLoaded is guaranteed.

Source:

initBoardColumnDnD() → {void}

Registers dragover, dragleave, and drop listeners on the three static board column lists. Called exactly once on DOMContentLoaded from 07-lifecycle.js. Card draggable wiring (re-rendered each cycle) stays in bindBoardColumnDnD().

Source:
Returns:
Type
void

initBoardTabs() → {void}

Initialises the tabbed board: wires tab-click handlers, restores the last active tab from localStorage, and sets up drag-over-tab lane switching so users can drag a card onto a tab label to reveal that column before dropping. Called once on DOMContentLoaded from 07-lifecycle.js.

Source:
Returns:
Type
void

initHero()

Binds all Hero Card button events. Called once from DOMContentLoaded in 07-lifecycle.js.

Source:

initLocation() → {void}

Binds the location toggle button. Called exactly once from the boot sequence in 07-lifecycle.js; there is no re-invocation path, so the click listener is attached without a guard. If a future caller invokes this more than once, add a removeEventListener (or an "already bound" flag) first to avoid duplicate handlers. Safe to call when the button is missing.

Source:
Returns:
Type
void

initMigration()

Initialises the migration overlay: wires close button and optionally shows a last-day-of-month banner once per month.

Source:

initPomo(mins)

Initialises (or re-initialises) the pomodoro timer for the given duration. Clears any running interval, resets state, and highlights the matching duration button.

Parameters:
Name Type Description
mins number

Session duration in minutes.

Source:

initRapid()

Registers all event listeners for the quick-capture overlay. Called once on DOMContentLoaded.

Source:

initSprints() → {void}

Registers all sprint UI event listeners: the sprint mode button, start, cancel, and Enter-key shortcut on the intention input. Called once on DOMContentLoaded.

Source:
Returns:
Type
void

initTodayFlow()

Binds the static listeners exactly once on DOMContentLoaded:

  • Log-note input/button (static HTML, never recreated)
  • Segmented-control click + keyboard nav, delegated off the stable #tfHeader container so we survive its innerHTML being rewritten by every renderFlowHeader() call. Avoids the listener-accumulation bug that occurred when binding happened inside render functions.
Source:

initTrackers() → {void}

Bootstraps the Trackers feature: performs the initial render and wires up the "+ New" button to toggle the tracker form open/closed. Called once on DOMContentLoaded.

Source:
Returns:
Type
void

initWeeklyReport() → {void}

Binds the weekly-report modal buttons. Called exactly once from the boot sequence in 07-lifecycle.js. Safe to call when any button is missing.

Source:
Returns:
Type
void

isEntryBillable(entry) → {boolean}

Determines whether a log entry is billable, using a three-tier lookup:

  1. The entry's own billable flag (if explicitly set).
  2. The matching plan task's billable flag (matched case-insensitively by trimmed text).
  3. The category default.

Assumption: entries and tasks where billable is undefined are treated as billable by default. This preserves backward compatibility with data created before the billable flag was introduced — older entries must not silently disappear from billing reports after an upgrade. If the default should change to non-billable, a migration of existing localStorage data is required (see DATA.md § wl_entries).

Parameters:
Name Type Description
entry Object

Log entry object.

Source:
Returns:

True if the entry should be counted as billable.

Type
boolean
Example
isEntryBillable({ text: 'Write report', tag: 'work' })                   // → true (category default)
isEntryBillable({ text: 'Write report', tag: 'work', billable: false })  // → false (explicit flag)
isEntryBillable({ text: 'Write report', signifier: 'cancelled' })        // → false (cancelled)

isLiveMeetingBlock(block, liveEntry) → {boolean}

Whether a planned meeting block is the one the running timer is tracking. Meeting entries are logged with a leading "📅 ", so the entry's text is compared both with and without that prefix.

Parameters:
Name Type Description
block Object

The planned block.

liveEntry Object | null | undefined

The log entry the timer runs on, if any.

Source:
Returns:

True when block is a meeting that matches liveEntry.

Type
boolean

isLongRunningTimer(elapsedMs, thresholdMinsopt) → {boolean}

Returns true if a timer's elapsed time has crossed a long-running safety threshold. Pure boundary check, extracted so the threshold logic is unit-testable without mocking the DOM or the timer interval.

Parameters:
Name Type Attributes Default Description
elapsedMs number

Elapsed timer time in milliseconds.

thresholdMins number <optional>
240

Threshold in minutes (default 4h).

Source:
Returns:

True once elapsedMs exceeds the threshold.

Type
boolean
Example
isLongRunningTimer(3 * 60 * 60 * 1000)          // → false (3h)
isLongRunningTimer(4 * 60 * 60 * 1000)           // → false (exactly 4h)
isLongRunningTimer(4 * 60 * 60 * 1000 + 1)       // → true  (just past 4h)
isLongRunningTimer(31 * 60 * 1000, 30)           // → true  (custom 30 min threshold)

isMeetingHidden(meeting, hiddenKeys) → {boolean}

Returns true if the user has hidden this meeting for the day.

Hidden meetings are stored per day as occurrence keys. Entries saved before that were bare subjects, which hid every meeting sharing a title — the whole day's standups would vanish on hiding one. Those legacy entries are still honoured so a meeting hidden earlier today stays hidden; the store is keyed by date, so they clear on their own overnight.

Parameters:
Name Type Description
meeting *

Meeting object from the calendar API response.

hiddenKeys Array.<string>

Stored keys for the day.

Source:
Returns:

True if this meeting should stay off the strip.

Type
boolean
Example
isMeetingHidden({ subject: 'Standup', start: '09:00' }, ['Standup|09:00'])  // → true
isMeetingHidden({ subject: 'Standup', start: '14:00' }, ['Standup|09:00'])  // → false

isToday(d) → {boolean}

Returns true if d falls on today's calendar date.

Parameters:
Name Type Description
d Date

The date to test; only its local calendar day is compared.

Source:
Returns:

True when d is on the same calendar day as now.

Type
boolean

isWithinRetentionWindow(date, cutoffDate) → {boolean}

Checks whether a YYYY-MM-DD date string is on or after the cutoff date — the predicate applyBackupRetention() filters entries by. Extracted as its own function so the well-formed-date check and the chronological comparison are named and testable independently of the filtering loop.

Parameters:
Name Type Description
date string | undefined

Candidate entry's date field.

cutoffDate string

Cutoff as YYYY-MM-DD (lexicographic compare).

Source:
Returns:

True when date is well-formed and not before cutoffDate.

Type
boolean

isWorkdayLikelyOver(opts) → {boolean}

Returns true if today's workday looks like it may be over and hasn't been exported yet: the day was started, has at least one entry, hasn't already been ended, and workdayHours have passed since it started. Used to show a reminder nudging the user toward "end the day" (which triggers the real export) — this function only decides whether to nudge, it never exports or ends the day itself.

Pure: all environmental inputs (day-start/day-end timestamps, whether today has entries, the current time) are injected via opts so the function can be unit-tested without touching localStorage or Date.now(), matching computeDayBounds's style.

Parameters:
Name Type Description
opts Object
Properties
Name Type Attributes Default Description
sodTs number | null

Today's day-start timestamp (ms), or null if not started.

eodTs number | null

Today's day-end timestamp (ms), or null if not yet ended.

hasEntriesToday boolean

Whether at least one entry exists for today.

now number

Current time in ms.

workdayHours number <optional>
8

Hours after day-start to consider the day likely over.

Source:
Returns:

True if the reminder should be shown.

Type
boolean
Example
isWorkdayLikelyOver({ sodTs: 1000, eodTs: null, hasEntriesToday: true, now: 1000 + 8 * 3600000 })
// → true (exactly 8h after day-start)
isWorkdayLikelyOver({ sodTs: 1000, eodTs: null, hasEntriesToday: true, now: 1000 + 7 * 3600000 })
// → false (only 7h in)
isWorkdayLikelyOver({ sodTs: null, eodTs: null, hasEntriesToday: true, now: 999999 })
// → false (day never started)

jiraKeyLinkHtml(baseUrl, key) → {string}

Builds the anchor HTML for a Jira ticket key. The link opens in a new tab, so a visually hidden suffix tells screen-reader users that before they activate it (WCAG 3.2.2 / 2.4.4); sighted users see no change.

Parameters:
Name Type Description
baseUrl string

Jira browse URL, without a trailing slash.

key string

Ticket key, e.g. AITO-1234.

Source:
Returns:

HTML string for the <a> element.

Type
string
Example
jiraKeyLinkHtml('https://x.atlassian.net/browse', 'AB-1')
// → '<a class="jira-key-link" href="https://x.atlassian.net/browse/AB-1" …>AB-1<span class="sr-only"> (opens in new tab)</span></a>'

jiraTicketHtml(text) → {string}

Returns HTML for a task text string, converting any leading Jira ticket key (e.g. AITO-1234) into a clickable link. The remainder of the text is HTML-escaped and appended.

Parameters:
Name Type Description
text string

Raw task text, possibly starting with a Jira key.

Source:
Returns:

HTML string.

Type
string

jumpToGapReportEntry(entryId, entryDate)

Jumps from the gap report straight to a flagged entry: closes the report, switches the viewed day and the Today's Flow tab to that entry's Log view, opens its proof-link/note editor, and scrolls the row into view. This is the "find it yourself, fix it yourself" action the report exists for.

Parameters:
Name Type Description
entryId string

ID of the entry to jump to.

entryDate string

The entry's date (YYYY-MM-DD).

Source:

lastWeekday(year, month, weekday) → {Date}

Returns the date of the last occurrence of a weekday in the given month.

Parameters:
Name Type Description
year number

Full 4-digit year.

month number

Month (1 = January … 12 = December).

weekday number

Day of week (0 = Sunday … 6 = Saturday).

Source:
Returns:
Type
Date

load()

Loads all persistent state from localStorage into module-level variables. Invalid records are dropped per-item (rather than rejecting entire arrays) and any drops are reported via wlLog.warn so data-quality issues are visible in DevTools rather than silently disappearing. Falls back to the last snapshot if entries are missing from primary storage.

Source:

loadBlocks()

Loads time blocks from localStorage into blocks, filtering invalid entries. Drops are reported via wlLog.warn so data-quality issues are visible in DevTools. Applies a one-time migration to shift existing block slots by +2 when the time-block grid start time changed from 08:00 to 07:00.

Source:

loadExpiryDates()

Loads iteration expiry dates from localStorage into _expiryDates. Seeds localStorage with EXPIRY_SEED on first run.

Source:

loadLocationMap() → {Record.<string, string>}

Reads the stored location map from localStorage. Returns an empty map (and logs a warning) if the value is missing or corrupt, so a single bad write can never break the header render.

Source:
Returns:

Date-key → location-id map.

Type
Record.<string, string>

loadPlan() → {void}

Loads plan tasks from localStorage into the shared plan-task list, filtering out invalid entries via validPlanTask. Drops are reported via wlLog.warn so data-quality issues are visible in DevTools. Resets to empty array on parse error. Each task is { id, text, status, tag, date, [billable], [notionUrl], [emoji], [checkpoints], [parentId], [priority], [note] }.

Source:
Returns:
Type
void

loadSprintLog() → {void}

Loads sprint history from localStorage into the module-level sprintLog array. Falls back to an empty array and logs a warning on parse failure.

Source:
Returns:
Type
void

loadTrackers() → {void}

Loads the trackers array from localStorage into module state. Falls back to an empty array and logs a warning on parse failure.

Source:
Returns:
Type
void

locationFor(map, dateKey) → {string}

Resolves the stored work location for a given day, falling back to the default when the day is unset or holds an unknown value.

Parameters:
Name Type Description
map Record.<string, string>

Date-key → location-id map.

dateKey string

Day key in YYYY-MM-DD form (see dk).

Source:
Returns:

A valid location id present in WORK_LOCATIONS.

Type
string
Example
locationFor({ '2026-06-03': 'office' }, '2026-06-03') // → 'office'
locationFor({}, '2026-06-03')                         // → 'remote'
locationFor({ '2026-06-03': 'bogus' }, '2026-06-03')  // → 'remote'

logUtilEntry(kind)

Logs a short well-known activity (break / lunch / meeting) as a new entry while keeping the active timer running. The active timer is NOT stopped so the user can resume without friction.

Parameters:
Name Type Description
kind 'break' | 'lunch' | 'meeting'

Activity type.

Source:

markInputInvalid(input) → {void}

Flags a text input as rejected: red border via .input--invalid, exposed to assistive tech through aria-invalid, and focused. The mark clears itself on the next edit so a corrected value is not left looking wrong.

Safe to call multiple times without an intervening edit: the clear-on-edit listener is added only once per input.

Parameters:
Name Type Description
input HTMLInputElement

The input whose value was rejected.

Source:
Returns:
Type
void

markPlanReviewedThisWeek()

Marks the current ISO week as reviewed so the reminder banner won't reappear this week, then re-renders the banner (hides it). Called both by the banner's dismiss button and by closing the checklist — either counts as "reviewed," matching the dismiss semantics already used for the long-running-timer and end-of-day-export reminders.

Source:

(async) mergeBackupEntries(file) → {Promise.<void>}

Merges entries from a JSON backup file into the current data set without replacing any existing records. Only entries whose id is not already present in localStorage are added. Categories, tasks, and all other backup fields are ignored — this is an entries-only operation.

Intended for the "worked on another machine" case: the backup from the remote session contains new entries that are absent from the local store.

Parameters:
Name Type Description
file File

The .json backup file selected by the user.

Source:
Returns:
Type
Promise.<void>

mergeDevLog()

Merges the hardcoded DEV_CHANGES array into the persisted dev log in localStorage, adding only entries whose id is not already stored. Maintains chronological sort order by id.

Source:

mergeNoteMaps(a, b) → {Object.<string, string>}

Combines two task-keyed note maps — e.g. plan-task notes and entry notes — into one, concatenating notes for the same key with a newline so both survive as separate note: lines in the exported text (see formatGroupedLines, which splits each map value on \n).

Parameters:
Name Type Description
a Object.<string, string>

First note map (rendered first).

b Object.<string, string>

Second note map, appended after a.

Source:
Returns:

Combined map.

Type
Object.<string, string>
Example
mergeNoteMaps({ x: 'from task' }, { x: 'from entry' })
// → { x: 'from task\nfrom entry' }

migrateEntryDatesToLocal()

Fixes entry date fields that were stored using UTC midnight instead of the user's local calendar date.

Background: dk() previously called toISOString() (UTC). For UTC+ users (e.g., Finland, UTC+2/+3), entries logged between local midnight and 02:00–03:00 were stored under the previous day's UTC date. This migration re-derives date from the entry's ts timestamp using the now-correct local dk().

Safe to run multiple times — entries already on the correct local date are untouched.

Source:

migrateStorage()

Runs all pending schema migrations. Safe to call multiple times — migrations are skipped if the source key is absent or the destination key already has data.

Source:

mlDaysInMonth(y, m) → {number}

Returns the number of days in a given month.

Parameters:
Name Type Description
y number

Full year (e.g. 2026).

m number

Month index, 0-based (0 = January).

Source:
Returns:

Day count (28–31).

Type
number

mlHeatColor(hours) → {string}

Maps a logged-hours value to a CSS colour for the heatmap grid. Thresholds: 0h → bg3 (empty), <2h → faint blue, <5h → mid blue, <7h → strong blue, ≥7h → solid blue.

Parameters:
Name Type Description
hours number

Total logged hours for a single day.

Source:
Returns:

A CSS colour value (variable or rgba/hex string).

Type
string

mlHoursForDay(dateKey) → {number}

Sums tracked milliseconds for all non-cancelled entries on a given day.

Parameters:
Name Type Description
dateKey string
Source:
Returns:

Total hours (as a float).

Type
number

mlMonthPrefix(year, month) → {string}

Builds the YYYY-MM prefix that entries and tasks are matched against.

Parameters:
Name Type Description
year number

Full year (e.g. 2026).

month number

Month index, 0-based (0 = January).

Source:
Returns:

Zero-padded prefix, e.g. 2026-05.

Type
string

mondayOfWeek(tsopt) → {number}

Returns the Unix timestamp (ms) of Monday 00:00 local time for the week containing ts — the app's calendar-week boundary, matching the "Week N/52" header label (getISOWeek() in 09-clock-weather.js).

Parameters:
Name Type Attributes Description
ts number <optional>

Reference time in ms; defaults to Date.now().

Source:
Returns:

Monday 00:00 local time, in ms.

Type
number
Example
// 2026-06-03 is a Wednesday → Monday of that week is 2026-06-01
new Date(mondayOfWeek(new Date(2026, 5, 3, 14, 30).getTime())).toDateString()
// → 'Mon Jun 01 2026'
// 2026-06-01 is already a Monday → returns that same day at 00:00
new Date(mondayOfWeek(new Date(2026, 5, 1, 9, 0).getTime())).getHours() // → 0

moveBlockToSlot(blockId, requestedSlot, dateKey) → {'moved'|'unchanged'|'declined'|'missing'}

Moves a planned block to another start slot. This is the one place that changes a block's time, so drag-and-drop and the keyboard handler clamp, warn about overlaps, save and redraw identically.

Parameters:
Name Type Description
blockId string

ID of the block to move.

requestedSlot number

Wanted start slot; clamped into the grid.

dateKey string

YYYY-MM-DD of the day in view, for the overlap check.

Source:
Returns:

moved after saving and redrawing; unchanged when the clamped slot is the current one (nothing is saved or redrawn); declined when the user refused the overlap prompt; missing when no block has that ID.

Type
'moved' | 'unchanged' | 'declined' | 'missing'

moveTaskToColumn(taskId, newStatus) → {void}

Moves a task to a new board column, updating its status and timer state. Dropping into In Progress stops any running timer, creates a new time entry, and starts tracking. Dropping into Done or To Do stops the active timer.

Parameters:
Name Type Description
taskId string

The plan task ID to move.

newStatus string

Target status: 'todo' | 'inprogress' | 'done'.

Source:
Returns:
Type
void

nearestWeekday(from, weekday) → {Date}

Returns the first date on or after from that falls on the given weekday.

Parameters:
Name Type Description
from Date

Start date (inclusive).

weekday number

Day of week (0 = Sunday … 6 = Saturday).

Source:
Returns:
Type
Date

nextDistinctColor() → {string}

Returns the next visually distinct colour from the palette for a new category. Falls back to golden-angle HSL generation once all palette colours are in use.

Source:
Returns:

A CSS colour string (hex or hsl).

Type
string

nextLocation(loc) → {string}

Returns the next location id when toggling, cycling through the keys of WORK_LOCATIONS. With the two presets this flips Remote ↔ Office. An unrecognised loc (indexOf → -1) is treated as "before the first" and wraps to the first location, so the toggle always recovers to a valid state.

Parameters:
Name Type Description
loc string

Current location id.

Source:
Returns:

The following location id (wraps around).

Type
string
Example
nextLocation('remote') // → 'office'
nextLocation('office') // → 'remote'
nextLocation('bogus')  // → 'remote'  (unknown input recovers to the first)

normalizeCalendarMeeting(meeting) → {*}

Returns a copy of meeting with a subject the rest of the app can rely on.

Outlook returns no subject at all for an appointment saved without a title, so the meeting arrives with subject: null and validCalendarMeeting rejects it — an untitled meeting used to disappear from the strip entirely rather than showing up unnamed. Normalising first keeps it, since its times are what the strip actually needs.

The input is not modified; a non-object is returned unchanged so the caller's validator still gets to reject it.

Parameters:
Name Type Description
meeting *

Candidate meeting object from the calendar API response.

Source:
Returns:

A copy with a non-empty string subject, or the input unchanged.

Type
*
Example
normalizeCalendarMeeting({ subject: null, start: 'a', end: 'b' })
  // → { subject: '(no title)', start: 'a', end: 'b' }
normalizeCalendarMeeting({ subject: '  Standup  ', start: 'a', end: 'b' })
  // → { subject: 'Standup', start: 'a', end: 'b' }

noteAreaHtml(t) → {string}

Builds the inline note display / edit area for a task row. Renders a read-only line when the task has a note and the area is closed; renders a textarea with save/remove/cancel when open.

Parameters:
Name Type Description
t Object

The plan task.

Source:
Returns:

HTML string, or '' when no note and area is closed.

Type
string

noteBtnHtml(t) → {string}

Builds the note toggle button HTML for a task row. Visually distinct when the task already has a note.

Parameters:
Name Type Description
t Object

The plan task.

Source:
Returns:

HTML button element.

Type
string

notifyPomodoroEnd() → {void}

Called from pomoDone() when the Pomodoro ring reaches zero. Fires the registered _onSprintEnd callback if one is set, then clears it so it cannot fire twice.

Source:
Returns:
Type
void

notionBtnHtml(t) → {string}

Builds the Notion send/link button HTML for a task row. Shows a link icon if already sent; send icon otherwise.

Parameters:
Name Type Description
t Object

The plan task.

Source:
Returns:

HTML button element.

Type
string

nthWeekday(year, month, weekday, n) → {Date}

Returns the date of the nth occurrence of a weekday in the given month.

Parameters:
Name Type Description
year number

Full 4-digit year.

month number

Month (1 = January … 12 = December).

weekday number

Day of week (0 = Sunday … 6 = Saturday).

n number

1-based occurrence index (1 = first, 2 = second, …).

Source:
Returns:
Type
Date

openBlockEmojiPicker(bid, anchor) → {void}

Opens a floating emoji picker anchored below anchor for a time block. The picker is exposed as an ARIA dialog (WCAG 4.1.2 / 2.1.2): it carries role="dialog", aria-modal, and aria-label; Tab navigation wraps inside via trapFocusInOverlay(); Escape closes the picker and returns focus to the trigger. Calling again for the same block ID closes the picker (toggle).

Parameters:
Name Type Description
bid string

Block ID.

anchor HTMLElement

Element to position the picker below.

Source:
Returns:
Type
void

openEodModal()

Opens the end-of-day modal for the day in view: auto-exports the time log and JSON backup, saves that day's EOD timestamp, populates handoff notes for its unfinished tasks, renders its dev changelog entries, and lists the test areas to review. Viewing an earlier day ends that day, not the current one.

Source:

openExpiryModal()

Opens the iteration-expiry editor modal, pre-filling the textarea with the current expiry dates (one per line).

Source:

openGapReportOverlay()

Opens the gap report: this calendar week's billable entries missing both a proof link and a note. Builds the list fresh from current entries state every time it's opened, so it always reflects the latest data.

Source:

openIDB() → {Promise.<IDBDatabase>}

Opens (or creates) the IndexedDB database used to persist the FSA directory handle.

Source:
Returns:

Resolves with the opened database instance.

Type
Promise.<IDBDatabase>

openMigration()

Opens the migration modal and populates it with open (unresolved) tasks for the current month. Alerts if there is nothing to migrate.

Source:

openPlanReviewOverlay()

Opens the weekly plan review checklist, rebuilding it fresh from current data.

Source:

openRapid()

Opens the quick-capture overlay and focuses the search input.

Source:

openSprintSetup() → {void}

Shows the sprint setup panel, clears the intention input, resets the duration selection to 25 minutes, and focuses the intention field.

Source:
Returns:
Type
void

openTrackerForm() → {void}

Shows and populates the new-tracker form inside #trackerNewForm. Pre-fills the colour picker with the first category's colour. Attaches Save / Cancel / keyboard listeners.

Source:
Returns:
Type
void

openWeeklyReportOverlay() → {void}

Opens the weekly report draft: this calendar week's entries grouped by ticket. Builds the text fresh from current entries state every time it's opened, so it always reflects the latest data.

Source:
Returns:
Type
void

parseHandoffNotes(raw) → {Object.<string, string>}

Parses the stored wl_handoff value into a prototype-less map of task key to note. A prototype-less object keeps keys such as __proto__ and constructor as ordinary own properties, so task names cannot reach Object.prototype.

Parameters:
Name Type Description
raw string | null

Raw localStorage value (may be null or malformed JSON).

Source:
Throws:

If raw is non-empty and not valid JSON.

Type
SyntaxError
Returns:

Map of task key to note; empty if the value is missing or is not a plain JSON object (e.g. an array, string or null).

Type
Object.<string, string>

parseJiraLabel(label) → {Object}

Parses a task label into its Jira issue key and human-readable name. Returns ticket: null when the label carries no leading Jira key.

Parameters:
Name Type Description
label string

Raw task label, e.g. 'PROJ-42: Fix login' or 'Write tests'.

Source:
Returns:

Parsed parts.

Type
Object
Example
parseJiraLabel('PROJ-42: Fix login') // → { ticket: 'PROJ-42', name: 'Fix login' }
parseJiraLabel('Write tests')        // → { ticket: null, name: 'Write tests' }

parseRapidTokens(raw, cats, nowopt) → {Object}

Parses inline shorthand tokens from a raw quick-capture input string and returns a clean text plus structured token values.

Supported tokens (each stripped from the returned text):

  • #<cat> — Category: matched against category ids and labels (case-insensitive; prefix match as fallback). Last occurrence wins.
  • !<sig> — Signifier shortcode (see RAPID_SIG_SHORTCUTS). Last occurrence wins.
  • ><date> — Date pointer: today, tomorrow, YYYY-MM-DD, or weekday mon–sun (next occurrence). Last occurrence wins.

Unrecognised tokens are left in the text unchanged so the user sees them and can correct them without data being silently discarded.

Parameters:
Name Type Attributes Description
raw string

Raw input text that may contain inline shorthand tokens.

cats Array.<{id: string, label: string}>

Available categories for # resolution.

now Date <optional>

Reference date for relative date resolution; defaults to new Date().

Source:
Returns:
Type
Object

partitionSessionNotes(allItems) → {Object}

Partitions a flat item list from buildDailyLogItems into two structures: non-session-note items (the main timeline rows) and a lookup of session-note items keyed by their parentEntryId. Session-notes render nested inside their parent entry row rather than as standalone timeline entries.

Parameters:
Name Type Description
allItems Array.<object>

Items returned by buildDailyLogItems.

Source:
Returns:
Type
Object

patchCarriedTasks()

Applies data migrations and status patches to today's plan tasks after carry-over:

  • Stamps billable: true on tasks/categories that predate the feature.
  • Stamps completedAt on done tasks missing a timestamp.
  • Promotes today's task status to match the most recent past version when that version was pending/blocked/upcoming or in-progress.
Source:

pausePomo()

Pauses the pomodoro timer without resetting the remaining time.

Source:

pauseTimer()

Pauses the running timer, accumulating elapsed time so it can be resumed. No-ops if no timer is active or it is already paused.

Source:

pickSaveFolder() → {Promise.<void>}

Prompts the user to select a save folder via the File System Access API and persists the resulting directory handle. Shows a fallback alert in browsers that do not support the API.

Source:
Returns:
Type
Promise.<void>

pickableCategories(categories, keepId) → {Array.<EpicCategory>}

Filters a category list down to the epics a picker should offer. Archived epics are hidden, except for keepId — the currently selected epic stays visible even once archived so an entry already tagged with it still renders its own selection instead of appearing untagged.

Parameters:
Name Type Description
categories Array.<EpicCategory>

All known epics.

keepId string | null

Epic ID to keep regardless of archived state; omit or pass null to hide every archived epic.

Source:
Returns:

The epics to offer, in input order.

Type
Array.<EpicCategory>

planReviewWeekBounds() → {Object}

Returns this ISO week's [startKey, endKey) as YYYY-MM-DD date-string bounds, using the existing mondayOfWeek()/dk() helpers.

Source:
Returns:
Type
Object

playChime()

Plays two soft sine-wave tones (528 Hz then 660 Hz) using the Web Audio API to signal an elapsed-time milestone. Silently skips if Web Audio is unavailable (e.g. browser privacy settings).

Source:

playPomoBeep()

Plays three short 660 Hz beeps spaced 350 ms apart using the Web Audio API to signal pomodoro completion. Silently skips if Web Audio is unavailable.

Source:

pomoAddTime()

Adds 120 seconds to the current running session without resetting segments. No-op if the timer is not running.

Source:

pomoAffirmation(totalopt, leftopt) → {string}

Returns a progress-aware affirmation string based on elapsed time. Default parameters let callers pass explicit values (useful for testing).

Parameters:
Name Type Attributes Description
total number <optional>

Total session seconds; defaults to pomoTotal.

left number <optional>

Remaining seconds; defaults to pomoLeft.

Source:
Returns:

Short motivational phrase.

Type
string

pomoBuildDateRange() → {Array.<string>}

Builds an array of POMO_SPARKLINE_DAYS date strings ending today, oldest first.

Source:
Returns:

Array of YYYY-MM-DD strings.

Type
Array.<string>

pomoDone()

Called when the pomodoro timer reaches zero. Plays the completion beep, briefly animates the time display, and appends a session record to the pomodoro log in localStorage.

Source:

pomoGap(n) → {number}

Returns the angular gap (in radians) between pomodoro timer segments. Smaller gaps are used for higher segment counts to keep them visually distinct.

Parameters:
Name Type Description
n number

Number of segments (minutes).

Source:
Returns:

Gap in radians.

Type
number

pomoGetLog() → {Array.<PomoSessionEntry>}

Reads and validates the pomodoro session log from localStorage. Invalid records are dropped and reported via wlLog.warn.

Source:
Returns:

Validated session log entries, newest first.

Type
Array.<PomoSessionEntry>

pomoSaveSession(session)

Prepends a new session entry to the pomodoro log and persists it. The log is capped at 100 entries (oldest are dropped).

Parameters:
Name Type Description
session PomoSessionEntry

The session entry to record.

Source:

pomoSessionsOnDate(log, dateKey) → {number}

Returns the number of completed pomodoro sessions on the given date.

Parameters:
Name Type Description
log Array.<PomoSessionEntry>

Session log.

dateKey string

Date in YYYY-MM-DD format.

Source:
Returns:
Type
number

pomoSparkColours() → {Object}

Reads the --pomo-spark-fill and --pomo-spark-empty CSS variables for the active colour scheme. Falls back to hardcoded values when CSS vars are unavailable (e.g. headless test environments).

Source:
Returns:
Type
Object

pomoTapOut()

Ends the session early, logs a partial session, and transitions to done state. Records the elapsed minutes (minimum 1) so the session appears in the log.

Source:

positionNowLine()

Positions the "now" indicator line in the time-block grid to reflect the current time. Hides the line when outside the grid's time range (TB_START–TB_END).

Source:

prioBtnHtml(t) → {string}

Builds the priority toggle button HTML for a task row. Click cycles: normal (0) → high (1) → low (-1) → normal.

Parameters:
Name Type Description
t Object

The plan task.

Source:
Returns:

HTML button element.

Type
string

promoteMatchingTaskToInProgress(text) → {void}

Promotes today's plan task matching text to "in progress", if one exists and is still in a promotable state. Called whenever a timer starts on text that might correspond to an existing board card — the hero composer, a recent chip, the log capture input, and the board's own "track" button all funnel through this so the Kanban board stays in sync no matter which control was used to start tracking.

Parameters:
Name Type Description
text string

The entry text a timer was just started on.

Source:
Returns:
Type
void

purgeLegacyAnthropicKey() → {void}

Removes an Anthropic API key left in localStorage by an older version, which kept it in the browser. The key now lives server-side only, so any copy still in the browser is a leftover to delete rather than honour.

Source:
Returns:
Type
void

readCollapseState(sectionId, defaultCollapsed) → {boolean}

Reads the stored collapsed state for a section from localStorage.

Parameters:
Name Type Description
sectionId string

The section element's id.

defaultCollapsed boolean

Value to use when nothing is stored.

Source:
Returns:

Whether the section should be collapsed.

Type
boolean

readHiddenMeetingKeys(dayKey) → {Array.<string>}

Reads the occurrence keys of the meetings hidden for one day.

Parameters:
Name Type Description
dayKey string

Day key from dk(), e.g. '2026-09-01'.

Source:
Returns:

Stored keys, or an empty array if none are stored.

Type
Array.<string>

readOptionalLogForBackup(storeKey, label) → {Array}

Reads and parses an optional JSON-array log from localStorage for inclusion in a backup. If the stored value is present but not valid JSON, the failure is logged via wlLog (so the data loss is diagnosable rather than silent) and an empty array is returned so the rest of the backup still succeeds. Absent keys legitimately yield an empty array.

Parameters:
Name Type Description
storeKey string

The localStorage key to read.

label string

Human-readable log name for the warning message.

Source:
Returns:

The parsed array, or [] if absent or unparseable.

Type
Array

refreshEpicPickers() → {void}

Re-renders every surface that offers an epic picker, so an archive or restore is reflected everywhere at once rather than only in the modal.

Source:
Returns:
Type
void

refreshPomoDashboard()

Full Pomodoro dashboard refresh: redraws the 28-day sparkline, ribbon footer, and composer task label. Called on module load and after every session completion.

Source:

removeBlockById(blockId) → {void}

Removes one time block, persists the remaining blocks and redraws the grid. Shared by the block's delete button and the start prompt's "no" branches so all three drop a block the same way.

Parameters:
Name Type Description
blockId string

ID of the block to remove.

Source:
Returns:
Type
void

removeRetiredKeys() → {number}

Deletes localStorage keys left behind by removed features. Safe to call multiple times — absent keys are skipped. A storage failure on one key is logged and the sweep continues: this is best-effort cleanup, and an orphaned key is never worth aborting page load over.

Source:
Returns:

How many keys were actually removed.

Type
number

render()

Full application re-render: updates the date label, timer bar, stat counters, sub-stats, time-log list, chart, quick-pick, plan, completed section, and time-block view. Call whenever persistent state changes.

Design trade-off: full DOM re-render on every change rather than targeted updates. Keeps state reasoning simple for a single-user personal tool where the entry list is small (typically < 50 items per day). If performance becomes a concern, the innermost timelineEl.querySelectorAll event-binding loop (bindTimelineEntryEvents in 04c-render-timeline.js) is the first candidate for optimisation.

Source:

renderBoardDoneHistory(doneListEl, viewKey)

Appends the collapsible "older history" expander to the Done column list. Shows completed tasks from prior dates, grouped by day, within the active iteration.

Parameters:
Name Type Description
doneListEl HTMLElement

The #doneList container element.

viewKey string

The current view date key (YYYY-MM-DD).

Source:

renderCalStrip(meetings)

Renders the calendar meetings strip for today. Sorts meetings by start time, marks past meetings grey/italic, pulses ongoing meetings, and provides ▶ start and ✕ hide buttons per meeting.

Parameters:
Name Type Description
meetings Array.<Object>

Array of meeting objects from the PS server.

Source:

renderCompleted()

Renders the completed-tasks section for the currently viewed date. Shows tasks that were completed on or before the view date and whose iteration expiry date has not yet passed. Deduplicates by task text, keeping only the most recently completed version. Hides the section when there are no matching tasks.

Source:

renderDayStrip(dateKey)

Renders the compact day-overview strip: hour-tick labels, entry footprints, and (today only) a live "now" cursor.

Parameters:
Name Type Description
dateKey string

YYYY-MM-DD.

Source:

renderEmergencyCps()

Renders the checkpoint list inside the focus-mode overlay for the currently active task. Hides the wrapper if the task has no checkpoints. Each checkpoint cycles through three states on click: false → 'partial' → true.

Source:

renderEodBtn()

Updates the "end the day" button to show the end time recorded for the day in view (if any) or its default label, and dims the button once a time is set.

Source:

renderEodReminder()

Shows or hides the end-of-day export reminder banner. A nudge, not an automatic export — the app never writes a file without an explicit click, so this only ever asks. Always evaluated against today, regardless of which day is currently in view (viewDate), since the reminder is about the actual current workday.

Source:

renderEodTimesheet(dateKey)

Fills the End of Day timesheet form with today's draft entry. With nothing tracked the form is disabled rather than submitting an empty day.

Parameters:
Name Type Description
dateKey string

Day being ended, YYYY-MM-DD.

Source:

renderEpicsManager() → {void}

Fills the epics modal with the archived-epic list and binds one restore button per row.

Rows are keyed by list index (epicRestore-0, epicRestore-1, …) rather than by epic ID: IDs come from persisted data and would need escaping to be safe in an attribute and matching unescaped for the lookup, so an index sidesteps that mismatch entirely.

Source:
Returns:
Type
void

renderFlowHeader(dateKey, activeView)

Renders the section header: icon, title, tracked/billable totals, and the Flow / Log / Blocks segmented control.

Parameters:
Name Type Description
dateKey string
activeView 'flow' | 'log' | 'blocks'
Source:

renderFlowView(dateKey)

Renders the Flow view: a vertical list where each entry's accent strip height is proportional to its duration (height = max(64, 0.6 × minutes) px), giving longer tasks more visual weight.

Parameters:
Name Type Description
dateKey string
Source:

renderFolderStatus() → {void}

Updates the #folderStatus element to show the currently selected save folder name (green) or a "pick save folder" prompt (default colour).

Source:
Returns:
Type
void

renderGapReminder(dateKey)

Renders or hides the gap-reminder banner.

Parameters:
Name Type Description
dateKey string
Source:

renderHeaderAndTimerSection()

Updates the date label and prev/next-day navigation, the location chip, the start/end-of-day controls and reminder, and the timer bar/button — everything render() needs to refresh purely because the viewed date or timer state may have changed, before it gets to stats or the timeline.

Source:

renderHeaderStatTiles() → {void}

Renders the three header stat tiles (distinct tasks today / distinct epics this week / current streak) and the collapsed one-line summary that mirrors them.

Source:
Returns:
Type
void

renderHeroCard()

Switches the root card's state-modifier class and makes the matching inner panel visible. Updates all dynamic content for the current state.

Source:

renderLocation() → {void}

Updates the date-nav location button to reflect the viewed day's location. No-ops when the button is absent (e.g. in a reduced test DOM).

Source:
Returns:
Type
void

renderLongRunningWarning(elapsedMs)

Shows or hides the long-running-timer warning banner in the Hero Card's running panel. Purely visual — never stops or modifies the timer itself; the user must click "stop timer" for that. Stays hidden for the rest of this timer session once dismissed (reset by the next startTimer() call).

Called every second from tickTimer(); rebuilds the banner's innerHTML only when the displayed "Xh Ym" label actually changes (once a minute), not on every tick. The container is role="alert", so a per-second DOM rewrite would make screen readers re-announce it every second — this keeps announcements to once per minute, matching the visible text change.

Parameters:
Name Type Description
elapsedMs number

Elapsed timer time in milliseconds.

Source:

renderMigrationStep()

Renders the current migration step (or the "done" screen when all resolved).

Source:

renderMonthlyCalendar(calEl, year, month) → {void}

Renders the heatmap calendar into calEl: builds its markup, then binds its events. Kept as the single entry point so callers never render without binding.

Parameters:
Name Type Description
calEl HTMLElement

The #mlCalendar container.

year number

Full year to render.

month number

Month index, 0-based.

Source:
Returns:
Type
void

renderMonthlyLog() → {void}

Orchestrates the Monthly Log view: resolves DOM targets and delegates each panel to a single-purpose renderer.

Source:
Returns:
Type
void

renderMonthlySummary(sumEl, monthPrefix) → {void}

Renders the time-totals summary panel: total logged, billable, top category. Writes its full HTML to sumEl. No event binding. Early-returns if sumEl is absent so a partial DOM doesn't throw on innerHTML assignment.

Parameters:
Name Type Description
sumEl HTMLElement

The #mlSummary container.

monthPrefix string

Date prefix YYYY-MM used to filter entries.

Source:
Returns:
Type
void

renderMonthlyTasks(taskEl, monthPrefix) → {void}

Renders the task-inventory panel: open / done / migrated counts plus a "Run Migration" button. Writes its full HTML to taskEl and binds the button to openMigration() if that helper is loaded. Early-returns if taskEl is absent so a partial DOM doesn't throw on innerHTML.

Parameters:
Name Type Description
taskEl HTMLElement

The #mlTasks container.

monthPrefix string

Date prefix YYYY-MM used to filter plan tasks.

Source:
Returns:
Type
void

renderMoon()

Renders today's moon phase, illumination, and zodiac sign into #liveMoon. No-ops if the element is absent.

Source:

renderPlan()

Re-renders the entire plan UI as a 3-column kanban board (To Do / In Progress / Done). Pending and blocked tasks absorb into the To Do column with their existing badge treatment. The Done column shows today's completed tasks and a collapsible history expander for older ones.

Design trade-off: full DOM re-render on every state change rather than targeted updates. Acceptable for a personal tool where the task list is small (< 20 items).

Source:

renderPlanReviewList()

Rebuilds the checklist list + subtitle from current planTasks state. Shared by the initial open and by each row action, since an action removes exactly one task from the underlying data and the list should reflect that immediately rather than requiring a re-open.

Source:

renderPlanReviewReminder()

Shows or hides the weekly plan review reminder banner. A nudge, not an automatic reconciliation — this app has no live Jira connection, so only the user can confirm a planned task is still accurate. Hidden once the current ISO week has already been marked reviewed, or when there's nothing to review.

Source:

renderPomoLog()

Renders the pomodoro session history list inside #pomoLog. Shows date/time, duration, and the task that was active during each session.

Source:

renderPomoRibbon()

Renders the ribbon footer below the 4-column grid:

  • #pomoRibbonDots: last-5-session dot sequence (● filled, · empty slot)
  • #pomoRibbonPill: "Peak Focus" pill when today matches the all-time high, otherwise "N sessions today", hidden when no sessions today
  • #pomoRibbonLink: "View all N sessions ↓" button that scrolls #pomoLog

Each element is individually guarded — no-op when absent from the DOM.

Source:

renderPomoSparkline()

Draws a 28-day focus density bar chart on the #pomoSparkline canvas. Each bar represents one calendar day; bar height is proportional to the number of completed pomodoro sessions on that day. No-op when the canvas element is absent or the 2D context is unavailable.

Source:

renderQuickPick()

Renders the "recent tasks" quick-pick bar below the capture input. Deduplicates entries by text, hides manually-dismissed tasks and tasks past their iteration expiry, and caps the list at 16 items.

Source:

renderRollingSummary() → {void}

Renders the 7-day rolling summary into #tfSummaryPane. No-ops when the pane element is absent (e.g. reduced test DOM).

Source:
Returns:
Type
void

renderRow(t) → {string}

Builds the HTML string for a single plan task row. Handles two layout branches: pending/blocked (compact) and normal (full). Reads module-level state variables for edit mode, checkpoint open state, and pending comment state so re-renders are always consistent.

Parameters:
Name Type Description
t Object

The plan task object to render.

Source:
Returns:

HTML string for one .plan-item element (and optional split row).

Type
string

renderSodBtn() → {void}

Updates the session chip to show the start time recorded for the day in view (with a green dot), or the default "start the day" label when that day has no recorded start. The button's text content is its accessible name; no static aria-label is used so screen readers announce the current state ("started HH:MM" or "start the day").

Source:
Returns:
Type
void

renderSprintDurations() → {void}

Renders the duration chip row inside #sprintDurations, marking the currently selected duration active. Attaches click listeners to each chip.

Source:
Returns:
Type
void

renderSubStatTiles() → {void}

Renders the three sub-stat tiles beneath the header stats: today's most-tracked task, this week's most-tracked task, and the streak day with the longest tracked time. Hides each tile when there's no data for it.

Source:
Returns:
Type
void

renderTagRow() → {void}

Renders the epic tag row and rebinds its listeners. Every state change in the handlers below funnels back through here, so the row is always rebuilt wholesale rather than patched in place.

Source:
Returns:
Type
void

renderTimeblock()

Renders the full time-block grid for the currently viewed date: time labels, grid rows, planned blocks (with drag-to-move), live timer block, a "now" line, and the plan-task drag targets. Also handles drag-and-drop wiring for moving existing blocks and dropping tasks from the plan list.

Source:

renderTimelineSection(list) → {void}

Builds and mounts the #timeline entry list (or its empty state) plus the ad-hoc log row, then binds every entry-row event handler and renders the sections that depend on the timeline. Mirrors render()'s old two branches: an empty list bails out early after rendering its own sub-components, exactly as before.

Parameters:
Name Type Description
list Array.<Object>

viewEntries() result for the currently viewed date.

Source:
Returns:
Type
void

renderTodayFlow()

Renders the Today's Flow section: header, day-overview strip, gap reminder, and the active view pane (Flow / Log / Blocks / Month). Called from render() on every state change and when the view toggle fires.

Source:

renderTrackRecent()

Renders the "+ TRACK RECENT" strip inside #planTrackRecent. Shows chips for the most recent unique time-log entries from today so the user can restart a timer with a single click without re-typing. Limits to 5 entries; hidden when no entries exist for today.

Source:

renderTrackers() → {void}

Renders all tracker cards into #trackerList. Each card shows a 28-day hit/partial/miss grid, current streak, and total hit count. Attaches delete-button listeners after render.

Source:
Returns:
Type
void

resetPomo()

Resets the pomodoro timer to the full configured duration without starting it.

Source:

resolveCarryStatus(todayTask, prev) → {string|null}

Determines the carry-forward status a today-task should adopt based on its most recent past peer, implementing the following rules:

  • pending or blocked prev overrides todo or inprogress today — the task is still blocked so the blocking state wins.
  • upcoming prev overrides only a todo today — a todo placeholder created from an even-older copy should reflect the more recent intent to defer. It must NOT override inprogress, because the user explicitly started the task and a reload must not undo that.
  • inprogress prev promotes a todo today — the task was already being worked on and the carry placeholder should show that.

Returns null when no change is needed.

Parameters:
Name Type Description
todayTask Object

Today's task object.

prev Object

Most recent past task with the same text (case-insensitive).

Source:
Returns:

The new status to apply, or null for no change.

Type
string | null
Example
resolveCarryStatus({ status: 'todo' }, { status: 'upcoming' }) // → 'upcoming'
resolveCarryStatus({ status: 'inprogress' }, { status: 'upcoming' }) // → null
resolveCarryStatus({ status: 'todo' }, { status: 'pending' })  // → 'pending'

resolveEodTimestamp(viewDay, timedEntries, now) → {number}

Picks the timestamp recorded when a day is ended. Ending today stamps "now". Ending an earlier day stamps the end of its last timed entry, or the end of that calendar day when it has none — "now" would fall on a different day and misreport when the viewed day finished.

Parameters:
Name Type Description
viewDay Date

The day being ended.

timedEntries Array.<Object>

The ended day's entries, each with tsEnd.

now number

Current time as a Unix timestamp (ms).

Source:
Returns:

Unix timestamp (ms) to store as the day's end.

Type
number

resolveRapidDate(token, nowopt) → {string|null}

Resolves a >date shorthand token to a YYYY-MM-DD date key. Supported tokens: 'today', 'tomorrow', exact 'YYYY-MM-DD', and weekday abbreviations 'mon'–'sun' (resolves to the next occurrence, never today).

Parameters:
Name Type Attributes Description
token string

Raw date token (without the leading >).

now Date <optional>

Reference date for relative resolution; defaults to new Date().

Source:
Returns:

Resolved date key, or null if the token is unrecognised.

Type
string | null

restoreArchivedCategory(categories, catId) → {Array.<EpicCategory>}

Returns a copy of categories with the archived flag cleared from one epic, bringing it back into the pickers.

Parameters:
Name Type Description
categories Array.<EpicCategory>

All known epics.

catId string

Epic ID to restore.

Source:
Returns:

A new array with the epic un-archived.

Type
Array.<EpicCategory>

restoreGapReportFocus()

Restores focus to whatever triggered the report, then clears it.

Source:

restorePlanReviewFocus()

Restores focus to whatever triggered the checklist, then clears it.

Source:

restoreWeeklyReportFocus() → {void}

Restores focus to whatever triggered the report, then clears the trigger.

Source:
Returns:
Type
void

resumeTimer()

Resumes a paused timer from where it left off. No-ops if no timer is active or it is not paused.

Source:

resumeTimerIfActive()

Called at startup to reconnect the tick interval when the app is reloaded with an active timer persisted in localStorage. If the entry the timer was tracking no longer exists the timer is cleared. No-ops if no timer is active.

Source:

rollingDateKeys(nopt) → {Array.<string>}

Returns the last n YYYY-MM-DD date keys ending at (and including) today, ordered most-recent-first.

Parameters:
Name Type Attributes Default Description
n number <optional>
7
Source:
Returns:
Type
Array.<string>

roundToNearest30(ts) → {number}

Rounds a timestamp to the nearest 30-minute clock mark.

Tie-breaking rule (at exactly 15 min into a 30-min slot): rounds DOWN. Rationale: conservative for billing — a task must exceed the midpoint of the slot before the next slot is claimed.

Assumption: rounding ties (exactly 15 min past a slot start) are resolved in favour of the earlier slot to avoid inflating billable time. This matches the intent documented in DATA.md under wl_entries.ts.

Parameters:
Name Type Description
ts number

Unix timestamp in milliseconds.

Source:
Returns:

Rounded Unix timestamp in milliseconds.

Type
number

roundToNearest30IfBillable(ts, entry) → {number}

Rounds ts to the nearest 30-minute mark only when entry is billable. Non-billable entries keep their exact timestamps for accurate reporting.

Parameters:
Name Type Description
ts number

Unix timestamp in milliseconds.

entry Object | null

Work-log entry; if null or undefined, always rounds.

Source:
Returns:

Timestamp, conditionally rounded.

Type
number
Example
roundToNearest30IfBillable(1000 * 60 * 47, null)                      // → rounded (null always rounds)
roundToNearest30IfBillable(1000 * 60 * 47, { billable: false })       // → exact ts (non-billable)
roundToNearest30IfBillable(1000 * 60 * 47, { billable: true })        // → rounded (billable)

safeCssColor(c) → {string}

Returns c if it is a safe CSS colour (hex or hsl()), otherwise returns a neutral fallback. Prevents malformed user-supplied colour values from breaking layout or injecting CSS.

Parameters:
Name Type Description
c string

CSS colour string to validate.

Source:
Returns:

A safe CSS colour string.

Type
string
Example
safeCssColor('#7B61FF')      // → '#7B61FF'
safeCssColor('hsl(200,60%,50%)') // → 'hsl(200,60%,50%)'
safeCssColor('red')          // → '#888780'  (name blocked)
safeCssColor('')             // → '#888780'

safeRoundedStart() → {number}

Returns a rounded start timestamp that does not overlap any existing entry for today. Rounds Date.now() to the nearest 30-minute boundary, then takes the maximum of that value and the latest tsEnd among today's completed entries, preventing new entries from appearing to start before a prior entry's end time.

Source:
Returns:

Unix timestamp in milliseconds.

Type
number

save()

Persists entries, active timer, and categories to localStorage. Refuses to overwrite existing non-empty data with an empty array to guard against accidental data loss if save() is called before load() completes.

Assumption: an empty entries array in memory while localStorage still holds data means save() was called before load() finished (e.g. a race during init), not that the user intentionally deleted everything. Intentional clearing goes through a dedicated wipe path that bypasses this guard.

If localStorage.setItem throws (e.g. QuotaExceededError), the failure is caught so it never propagates to save()'s many callers — silently losing data with no signal to the user is worse than a caught, logged, and surfaced failure. A persistent banner tells the user to export a backup; it clears itself automatically the next time a save() call succeeds.

Source:

saveBlocks()

Persists the current blocks array to localStorage.

Source:

saveEodHandoffNotes()

Reads all handoff-note inputs in the EOD modal and persists their values to the wl_handoff localStorage key. Empty values are removed from the map.

Source:

saveExpiryDates()

Reads the expiry-date textarea, validates each line against YYYY-MM-DD format, deduplicates and sorts the valid dates, persists them to localStorage, and closes the modal. Invalid lines are surfaced in the feedback element but not saved.

Source:

saveHandoffNote(entryText, note)

Persists a handoff note for a given entry text. Pass an empty string or falsy value to delete the stored note. The key is stored case-insensitively.

Parameters:
Name Type Description
entryText string

The entry text to associate the note with.

note string

The note to save; falsy removes the stored note.

Source:

saveLocationMap(map) → {void}

Persists the location map to localStorage.

Parameters:
Name Type Description
map Record.<string, string>

Date-key → location-id map.

Source:
Returns:
Type
void

saveMigrationRecord(rec)

Persists the migration completion record to localStorage.

Parameters:
Name Type Description
rec Object

Map of YYYY-MM → boolean.

Source:

savePlan() → {void}

Persists the current planTasks array to localStorage.

Source:
Returns:
Type
void

saveSnapshot()

Writes a recoverable snapshot of today's log entries to localStorage every 30 minutes. The snapshot contains both a human-readable plaintext summary and the raw entry/category arrays so data can be recovered after accidental clearing. No-ops when there are no entries for today.

Assumption: 30 minutes is an acceptable data-loss window for a personal work log used in a single browser tab. Browser crashes, accidental page reloads, and mis-clicks on "clear data" are the main risks; all are adequately covered by a 30-minute recovery point. If higher durability is needed, reduce the interval in the setInterval call in 07-lifecycle.js.

Source:

saveSprintLog() → {void}

Persists the current sprintLog array to localStorage.

Source:
Returns:
Type
void

saveTaskNotionUrl(taskId, url)

Persists the Notion page URL on a plan task so the per-task button changes to an "open in Notion" link on next render.

Parameters:
Name Type Description
taskId string

Plan task ID.

url string

Notion page URL returned by the API.

Source:

saveTrackerForm() → {void}

Reads the new-tracker form, validates inputs, pushes the tracker to the trackers array, persists it, and re-renders. Shows an alert if no category is selected; focuses the name field if the name is empty.

Source:
Returns:
Type
void

saveTrackers() → {void}

Persists the current trackers array to localStorage.

Source:
Returns:
Type
void

scheduleMigTask(task, dateStr)

Reschedules the task to the given date and marks it as migrated.

Parameters:
Name Type Description
task Object
dateStr string

YYYY-MM-DD

Source:

sectorPath(cx, cy, r, a1, a2) → {string}

Builds an SVG path string for a pie-chart sector (filled segment).

Parameters:
Name Type Description
cx number

Centre x coordinate.

cy number

Centre y coordinate.

r number

Radius.

a1 number

Start angle in radians (0 = 12 o'clock).

a2 number

End angle in radians.

Source:
Returns:

SVG path d attribute value.

Type
string

selectDevChangesForDay(devLog, dayKey) → {Array.<Object>}

Selects the dev-changelog entries recorded on a given day.

Parameters:
Name Type Description
devLog Array.<Object>

All dev-changelog entries (date).

dayKey string

The ended day, YYYY-MM-DD.

Source:
Returns:

Entries dated that day.

Type
Array.<Object>

selectUnfinishedWorkedTasks(dayKey, entries, planTasks) → {Array.<Object>}

Selects the plan tasks to hand over at the end of a day: those dated that day, not done, and actually worked on (a log entry for the same text exists that day).

Parameters:
Name Type Description
dayKey string

The ended day, YYYY-MM-DD.

entries Array.<Object>

All log entries (date, text).

planTasks Array.<Object>

All plan tasks (date, status, text).

Source:
Returns:

The unfinished tasks worked on that day.

Type
Array.<Object>

setActiveTimer(next)

Replaces the active timer state.

Parameters:
Name Type Description
next object | null

New timer object, or null to clear it.

Source:

setBlockEmoji(bid, emoji)

Saves an emoji to a time block and closes the picker. Pass null or an empty string to remove the block's emoji.

Parameters:
Name Type Description
bid string

Block ID.

emoji string | null

Emoji character to assign, or null to remove.

Source:

setBlocks(next)

Replaces the time-blocks array.

Parameters:
Name Type Description
next Array

New blocks array.

Source:

setCategories(next)

Replaces the categories array.

Parameters:
Name Type Description
next Array.<{id: string, label: string, color: string}>

New categories.

Source:

setEntries(next)

Replaces the entries array.

Parameters:
Name Type Description
next Array

New entries array.

Source:

setExportBackupCallback(fn)

Registers the exportBackup function so the save-failure banner's export button can invoke it. Must be called once during app startup, before any save() call that could fail.

Parameters:
Name Type Description
fn function

The exportBackup function from 05a-export.js.

Source:

setFavicon(state)

Updates the browser favicon to a coloured dot reflecting the timer state. Skips redundant redraws by tracking the last rendered state.

Parameters:
Name Type Description
state 'active' | 'paused' | 'hyperfocus' | 'idle'

Current timer state.

Source:

setFlowView(view)

Persists the active view selection.

Parameters:
Name Type Description
view 'flow' | 'log' | 'blocks'
Source:

setJiraBase(url) → {void}

Overrides the Jira base URL at runtime. Call this from 00-config.local.js.

Parameters:
Name Type Description
url string

Jira base URL, e.g. 'https://your-instance.atlassian.net/browse'.

Source:
Returns:
Type
void

setLogNotes(next)

Replaces the log-notes array.

Parameters:
Name Type Description
next Array

New log-notes array.

Source:

setPlanTasks(next)

Replaces the plan-tasks array.

Parameters:
Name Type Description
next Array

New plan-tasks array.

Source:

setPomoFavicon()

Redraws the browser favicon as a depleting wedge mirroring the remaining pomodoro time. Silently skips when the canvas API is unavailable.

Source:

setSelectedTag(next)

Sets the currently selected tag.

Parameters:
Name Type Description
next string

The new tag value.

Source:

setSignifierRenderCallback(fn)

Registers the render function so cycleSignifier can re-render the UI after mutating an entry. Must be called once during app startup.

Parameters:
Name Type Description
fn function

The render() function from 04-render.js.

Source:

setTimerInterval(next)

Stores a new timer interval ID (or null to clear it).

Parameters:
Name Type Description
next number | null

The new interval ID.

Source:

setTimesheetStatus(message, isErroropt)

Shows a status message under the timesheet form. Plain text only: the element is an aria-live region, so emoji would be read aloud to screen-reader users; the error modifier class carries the visual distinction instead.

Parameters:
Name Type Attributes Default Description
message string

Text to display.

isError boolean <optional>
false

Adds the error modifier class when true.

Source:

setTrackers(next)

Replaces the trackers array.

Parameters:
Name Type Description
next Array

New trackers array.

Source:

setViewDate(next)

Replaces the view date.

Parameters:
Name Type Description
next Date

The new view date.

Source:

setWeatherConfig(lat, lon, name) → {void}

Updates the weather location from the /api/config response. Each parameter is applied only when the server sends a valid value; missing or invalid values leave the corresponding default unchanged.

Parameters:
Name Type Description
lat number | undefined

Latitude (decimal degrees).

lon number | undefined

Longitude (decimal degrees).

name string | undefined

Display name for the location.

Source:
Returns:
Type
void

shiftViewDay(dayDelta) → {void}

Moves the viewed day by a whole number of days. Swaps in a fresh Date rather than mutating the current one, so anything still holding the previous viewDate keeps seeing the day it was given.

Parameters:
Name Type Description
dayDelta number

Days to move; negative goes back.

Source:
Returns:
Type
void

showHandoffInput()

Shows the handoff-note text field in the timer bar so the user can record what a future self (or colleague) should know before picking this task up again.

Source:

showSaveFailureBanner()

Shows a persistent, dismissible banner warning that saving to localStorage is failing (e.g. quota exceeded), with a button to export a backup on the spot. Idempotent — a second failed save while the banner is already up does nothing, so repeat failures don't spam duplicate banners.

Source:

showSprintReview() → {void}

Stops the main timer and renders the sprint review panel inside #sprintReview. Populates the intention label, wires outcome buttons (yes / partly / no), and on save: appends the sprint to sprintLog, tags the entry with the outcome, persists both, hides the panel, and triggers a full render.

Source:
Returns:
Type
void

sigHtml(entry) → {string}

Returns the HTML string for the clickable signifier widget on one entry row.

Parameters:
Name Type Description
entry Object

Log entry object.

Source:
Returns:

HTML string for a <span> button.

Type
string

sigSymbol(entry) → {string}

Returns the display symbol for an entry's signifier.

Parameters:
Name Type Description
entry Object

Log entry object.

Properties
Name Type Attributes Description
signifier string | null <optional>

One of 'event'|'flagged'|'migrated'|'cancelled'|'overtime', or null/undefined for the billable default.

Source:
Returns:

Unicode BuJo symbol (○ ★ → ✗ !) or '●' for the billable default.

Type
string

sigTitle(entry) → {string}

Returns the accessible title string for an entry's signifier.

Parameters:
Name Type Description
entry Object

Log entry object.

Properties
Name Type Attributes Description
signifier string | null <optional>

One of 'event'|'flagged'|'migrated'|'cancelled'|'overtime', or null/undefined for the billable default.

Source:
Returns:

Accessible title (e.g. 'Meeting / event') or 'Billable' for the billable default.

Type
string

slotToTime(slot) → {string}

Converts a 0-based half-hour slot index to an "HH:MM" label. Slot 0 = TB_START:00, slot 2 = TB_START+1:00, etc.

Parameters:
Name Type Description
slot number

0-based slot index.

Source:
Returns:

"HH:MM" formatted time string.

Type
string

sodKey(dayopt) → {string}

Returns the localStorage key for a given day's start-of-day timestamp.

Parameters:
Name Type Attributes Default Description
day Date <optional>
viewDate

Day to key by; defaults to the day in view so the session chip reflects whichever day the user has navigated to.

Source:
Returns:

Key in the format wl_sod_YYYY-MM-DD.

Type
string

startPomo()

Starts the pomodoro countdown. Resets to full duration if already at zero. Fires pomoDone and clears the interval when time runs out.

Source:

startSprint() → {void}

Commits a new sprint: reads the intention from the input, creates a time-log entry, starts the main timer, starts the Pomodoro for _sprintDuration minutes, and sets _onSprintEnd so the review panel is shown when the ring reaches zero. No-ops with a focus call if the intention field is empty.

Source:
Returns:
Type
void

startTimer(entryId)

Starts (or restarts) the timer for the given entry. Clears any existing interval, resets the chime state, and begins a 1-second tick. Persists state and updates the UI immediately.

Parameters:
Name Type Description
entryId string

ID of the log entry to time.

Source:

statusOpts(cur) → {string}

Builds the