Timesheet budget, Left and warnings

Task 4592 · Done · Coder · created · updated

# C2 — Timesheet: Budget & Left columns, warnings, narrow layout ## Original requirement (verbatim) > "Let people set a budget in hours for each project, see how much is left on the timesheet, get a warning when a project is close to its budget, and compare budget with actual hours on the Reports page." This child covers asks **A1** "Let people set a budget in hours for each project", **A2** "see how much is left on the timesheet" and **A3** "get a warning when a project is close to its budget" on the Timesheet page, building on C1's model (`parseBudgetHours`, `setProjectBudget`, `budgetStatus`, `budgetWarning`, `byProjectRows(..., budgets)`, `BUDGETS_KEY`, `let budgets`, and the `--warn-*`/`--danger-*` tokens — all already on develop). ## Requirements (PRD §5, verbatim) R1 (A1) Each project — active, archived, or added with no time yet — can be given a budget in hours on the Timesheet's By project card, changed, or cleared; it persists across reloads and tabs in this browser. R2 (A2) The By project card shows, for each project with a budget, its budget and how much is left (or how far over), live while a timer runs. Projects without a budget show no left figure (an unobtrusive "—"). R3 (A3) A project ≥ 80 % used is visibly and accessibly marked Near budget; at/over 100 % it is marked Budget used up / Over budget; the running timer on such a project shows a warning message. ## Settled decisions (PRD §4, verbatim) - **D3 — Where a budget is set: inline in the By project card**, one "Budget (h)" number field per project row, active and archived rows alike — same interaction as Rate (save on change, toast confirming, empty or 0 clears, invalid/negative refused with a toast and the saved value restored). Assumed because it is the established per-project-attribute pattern; no new page or dialog. - **D6 — Where the warning appears:** (a) on the project's row in By project (a status that reads in words, not colour alone), and (b) in the Running panel on the Timesheet while a timer runs or is paused on a project that is Near / used up / Over budget — a live `role="status"` message written only on change (like `#long-run`), so it is announced once when the threshold is crossed, not every second. No browser notifications, no sound, no email (out of scope — the app has no server, D9). - **D10 — Format:** hours read as hours and minutes, matching the week figures (`formatHoursMinutes`, e.g. "12h 30m"); budget field shows the number as typed ("37.5"). - **D12 — Docs:** the Help page (public/help.html, "By project" and Reports sections) and README "What it does" describe budgets — part of making the feature discoverable, not optional polish. ## Pinned contracts (Architecture, verbatim) ### 2.3 Status semantics and the exact strings `budgetMs = round(hours × 3 600 000)`. `percent = floor(usedMs / budgetMs × 100)` (uncapped); `barPercent = min(100, percent)`. | state | condition | `label` | `leftText` | |---|---|---|---| | `under` | `usedMs × 100 < budgetMs × 80` | `""` | `formatHoursMinutes(leftMs)` | | `near` | ≥ 80 % and `usedMs < budgetMs` | `"Near budget"` | `formatHoursMinutes(leftMs)` | | `used` | `budgetMs ≤ usedMs < budgetMs + 60 000` | `"Budget used up"` | `"0h 00m"` | | `over` | `usedMs ≥ budgetMs + 60 000` | `"Over budget"` | `formatHoursMinutes(overMs) + " over"` | `leftMs = max(0, budgetMs − usedMs)`, `overMs = max(0, usedMs − budgetMs)`. **Decision — "over" starts one whole minute past the budget.** Chose a one-minute grace over a strict `>` because figures read in whole minutes (`formatHoursMinutes` floors): a strict comparison would show "Over budget · 0h 00m over" for the first 59 s, a contradiction on screen. AC3's cases (10h00m → used up, 12h30m → over) hold either way. With Rounding ≥ 5 min the used figure moves in whole steps, so the grace never shows. `budgetWarning` sentences (H = `formatHours(hours)`, e.g. "10h", "37.5h"): - near → `"Website is near its 10h budget."` - used → `"Website has used all of its 10h budget."` - over → `"Website is over its 10h budget."` **Decision — the live-region sentence carries no ticking figure.** AC9's examples embed "1h 30m left", which would change every minute and make a `role="status"` region re-announce every minute while the timer runs. Chose: the status sentence changes only when the **state** changes (announced exactly at each crossing — AC9's real requirement), and the ticking figure (`status.leftText`, e.g. "1h 30m left" / "0h 20m over") sits in a sibling, non-live span next to it, so the person still sees how much is left. Rejected: per-minute announcements (noisy for screen-reader users). ### 2.5 DOM ids (pinned so docs/tests/later work can refer to them) - Timesheet: `#budget-warning` (`<p class="budget-warning" hidden>`) inside `#running`, directly after `#long-run`, containing `<span id="budget-warning-text" role="status"></span> <span id="budget-warning-left" class="budget-warning-left"></span>`. ## HOW — Timesheet surface design (Architecture §3.1, verbatim) ### 3.1 Timesheet — By project card (C2) - **Columns (desktop, > 900 px viewport — see the stacked layout below):** Project | Total | Budget (h) | Left | Rate (£/h) | Value. Budget and Left sit next to Total because Left is read *against* Total (Budget − Total = Left). Update `index.html` thead, the archived group header `th.colSpan` 4 → 6 (public/app.js:353), and the tfoot `colspan="3"` → `5` so "Total value" still sits under Value. - **Budget field:** built in `byProjectRow` exactly like the rate field (public/app.js:387–425): `input.type="number"`, `className="budget"`, `min="0"`, `step="0.25"`, `inputMode="decimal"`, placeholder "—", `aria-label "Hour budget for <project>"`, `change` handler: `validity.badInput` or non-empty-but-`parseBudgetHours`=0 → restore saved value + toast "A budget must be a positive number of hours"; else `setProjectBudget`, `writeJson(BUDGETS_KEY)`, re-render, toast `Budget for “X” set to <formatHours>` / `Budget for “X” cleared`. In `renderProjectRows`' in-place update, the field is only written when it is not `document.activeElement` (same guard as rate) — this is what keeps focus while the timer ticks. - **Left cell:** `td.num.left` holding `<span class="left-figure">` (`status.leftText`, or "—" with no budget) and, when `status.label` is non-empty, `<span class="budget-status near|used|over">label</span>` beneath it (block). Row gets class `budget-near` / `budget-used` / `budget-over` (cleared otherwise) so the Left figure takes `--warn-text` / `--danger-strong` (2.4). All writes via the existing `setText` / `classList.toggle` (no rebuild, no flicker). The label text is what satisfies "not colour alone". - **Running panel:** the warning is updated **inside `renderByProject(now)`**, right after it computes the rows (public/app.js:331–341) — not in `render()`. Reason (gate finding): the budget field's change handler, like the rate's (app.js:419), re-renders only By project, and with a paused timer there is no tick, so a warning updated in `render()` would stay stale after a budget is set or cleared; every caller of `renderByProject` (render, budget change, rate change, cancelRename) now refreshes it. With no live entry it hides the warning. For a live entry: find the row whose `project` equals `live.project` among `groups.active.concat(groups.archived)`, `T.budgetWarning(live.project, row.budgetStatus)`; hide `#budget-warning` when null; write `#budget-warning-text` **only on change** (`setText`); write `#budget-warning-left` with `leftText + (state === "over" ? "" : " left")` (e.g. "1h 30m left", "0h 00m left", "0h 20m over"). Shown for running **and** paused timers. Never disables Start/Resume. - **Stacked layout below desktop width — the quality-critical part.** Measured: the card's content box is viewport − 82 px (main padding 2×20, card padding 2×20, borders), capped at ~798 px from a 880 px `main`; at 375 px it is ~293 px. The six figure/field columns need ~530 px at their minimum (Total `123:45:67`, Budget field 5.5em, Left `12h 30m over` + chip, Rate field 6.5em, Value `£12,345.00`, plus 8 px cell padding each side), and the name cell also holds Rename/Archive (~150 px). So the six-column table is only comfortable at the capped width. **Decision: the six-column table applies only above 900 px viewport (`@media (max-width: 900px)` switches to the stacked layout)** — chosen by this measurement, not by reusing the existing 640 px breakpoint (gate finding: at 700–900 px the name would be squeezed to a sliver). C2 must confirm by measurement at 901 px that "Client onboarding portal" wraps only at spaces (never mid-word) with Rename/Archive beside or under it; if it does not, raise the breakpoint and record the measured value. **Below the breakpoint each project row becomes a two-line block** — CSS grid on `.by-project tbody tr[data-project]`: line 1 the project name cell spanning the full width (name stays whole, wraps; Rename/Archive keep their place); line 2 a wrapping row of the five figure cells, each preceded by its column name from a `data-label` attribute (`td::before { content: attr(data-label) }`, muted, small). `thead` is visually hidden at that width (kept for AT). tfoot "Total value" stays a single right-aligned line. Because changing a table cell's `display` strips table semantics in Chromium/WebKit, `byProjectRow` sets explicit `role="row"`/`role="cell"` (`role="rowheader"` not needed — the name is a td today) and `index.html` sets `role="table"`, `role="rowgroup"`, `role="columnheader"` on the static parts — the established fix (explicit ARIA roles restore the semantics). **Definite C2 deliverable:** a new token `--field-min-height: 32px` applied as `min-height` to both `input.budget` and `input.rate` in the By project card at every width (the rate field's `padding: 4px 8px` alone is below 32 px), verified by measuring the rendered field height in devtools. Rate keeps `--rate-width`; the budget field uses a new `--budget-width: 5.5em`. - Rejected: horizontal scroll inside the card (AC10 asks legible without scrolling; hides Value off-screen); hiding columns on phones (silent narrowing, R18); a separate budget sub-row under every project row at all widths (breaks the desktop "one tidy table aligned with Rate/Value" of AC10). - Desktop width is unchanged in layout model; 880 px main width fits six columns (name ≥ 24 chars readable — verify with "Client onboarding portal"). - **Help text** under the card (`#by-project-help`) gains one sentence on Budget/Left and the 80 % warning. `--field-min-height` and `--budget-width` are spacing tokens: add them to `:root` only (not the dark blocks), so dark-block parity is untouched. Help: `public/help.html` "By project" list gains Budget (h), Left, the Near budget (80 %) / Budget used up / Over budget wording, and the Running-panel message (Architecture §3.3). README is C3's, not this child's. ## Acceptance criteria (PRD §6, verbatim) States every criterion must hold in: **no budget · budget, 0 h used · under 80 % · near (80–<100 %) · exactly 100 % · over · running timer on the project · paused timer · archived project · project renamed · Rounding = 0/5/15/30 · light and dark theme · narrow (375 px) and desktop (≥1024 px) width · page opened from `file://` and from the dev URL.** - **AC6 — set a budget.** In By project, each row (active and archived) has a labelled Budget (h) number field (accessible name e.g. "Hour budget for Website"). Typing 40 and leaving the field saves it, toasts e.g. "Budget for “Website” set to 40h", and survives a reload. Clearing it toasts "… cleared" and removes left/warning. Typing -5 or 0x refuses with a toast and restores the saved value. Focus is never lost while the timer ticks (field not rebuilt). *Verify:* dev site in a browser: start a timer, type a budget while it runs, reload; observe each behaviour. - **AC7 — how much is left.** A "Left" column shows "Xh YYm" (under/near), "0h 00m" (used up) or "Xh YYm over" (over), updating every second while a timer runs on that project; "—" with no budget. The table footer stays correct (Total value still spans correctly). *Verify:* dev site: budget 1h on a project with 0:50 tracked, run its timer; watch Left count down to 0h 00m then read "… over". - **AC8 — warning on the row.** Near / used up / over rows carry a status that is readable as text (e.g. "Near budget", "Over budget"), not colour alone, and is distinguishable at a glance from normal rows in both themes (contrast ≥ 4.5:1 for text). *Verify:* dev site, light and dark mode: set budgets to produce each state; observe labels; check contrast with the browser devtools. - **AC9 — warning while tracking.** With the timer running or paused on a near/used-up/over project, the Running panel shows a message such as "Website: 85% of its 10h budget used — 1h 30m left" / "Website is 0h 20m over its 10h budget". It is a `role="status"` region updated only when its text changes (announced at the crossing, not every second), hidden for under-budget or no-budget projects. *Verify:* dev site: budget slightly above current total, run the timer across 80 % and 100 %; observe the message appear and change; in devtools confirm the node's text is not rewritten every tick while unchanged. - **AC10 — legibility (quality bar).** With the extra column(s), the By project card at 375 px width has no horizontal page scroll; project names stay readable (never truncated to a few characters — a full name of up to ~24 characters remains readable, longer names wrap or truncate with the full name available); the number fields are usable (tap target ≥ 32 px high) and figures are right-aligned and not cramped. At desktop width the table reads as one tidy table aligned with the existing Rate/Value columns. *Verify:* dev site at 375 px and 1280 px (devtools device mode) with a project named "Client onboarding portal" budgeted; screenshot; observe no overflow and readable name. A build that fits only by shredding names or overlapping fields FAILS. - **AC15 — available to the operator, ON by default.** After merge to `develop` and deploy, a person opening https://app.dev.timesavr.org can set a budget on the Timesheet, see Left and warnings, and see the Reports comparison — no flag, setting, or hidden toggle. *Verify:* open https://app.dev.timesavr.org in a browser after the deploy run; perform AC6, AC7, AC9, AC11 end to end. - **AC16 — works from disk.** The same flow works opening public/index.html from `file://`. *Verify:* open the file directly in a browser; repeat AC6 and AC11. - **AC17 — no regression.** Existing data with no budget key loads exactly as before (no errors; By project otherwise unchanged); rates, values, archive, rename, tags, Today page, weekly report and CSV behave as before. *Verify:* `bash tools/check.sh` → `gate: ok`; dev site with pre-existing data, observe no console errors. - **AC18 — docs.** Help page "By project" and "Reports" sections and README "What it does" describe the budget field, Left, the 80 %/100 % warnings and the Budget vs actual card. *Verify:* read the rendered help.html on the dev site. - **AC19 — gate.** `bash tools/check.sh` green; new CSS colours are `:root` variables with dark-theme values (static-site sanity checks the dark blocks). *Verify:* run it, observe `gate: ok`. Quality bar (Architecture §5 risk 1, binding): screenshots at 375, 768, 901 and 1280 px with a project named "Client onboarding portal" budgeted, light and dark — no horizontal page scroll, name wrapping only at spaces (never mid-word, never ellipsised), Budget/Rate fields ≥ 32 px high, figures right-aligned and not cramped, and at desktop one tidy six-column table aligned with Rate/Value. A build that "fits" by `overflow:hidden`/ellipsis on the name FAILS. Verify the accessibility tree still exposes a table with rows/cells at 375 px, and that `#budget-warning-text` is not rewritten per tick. Out of scope: Do NOT touch public/reports.html, `initReports`, the Reports help section or README.md (C3). Do NOT change the weekly report rows/totals, the "Last four weeks" chart, the week navigator or the CSV export. Do NOT change how Total, Rate, Value, Total value, Rounding, Daily goal, This/Last week or the Today page compute or display (budgets read the Total; they never alter it). Do NOT change existing localStorage key shapes; do NOT redeclare `BUDGETS_KEY` (C1 owns it — a second const is a runtime SyntaxError). Do NOT add model logic to public/app.js — use C1's `T.*` functions; if one is missing or wrong, fix it in src/timesheet.js with a test. No server, network calls, notifications API, email or sound; no new dependency, build step or ES modules; no new page and no Settings option; no hard-coded colours/spacing (`:root` variables with dark values only). Never block starting/continuing a timer on an over-budget project — warn, never prevent. Deferred (do not build): recurring/period budgets, configurable threshold, CSV budget columns, money budgets, Today-page warnings, cross-tab live refresh. Done means: - AC6 — dev site (or a local static serve of public/): start a timer, type a budget while it runs, leave the field, reload; observe toast "Budget for “Website” set to 40h", persistence, "… cleared" on clear, refusal + restore for -5 / 0x, and focus kept while the timer ticks. - AC7 — budget 1h on a project with 0:50 tracked, run its timer; watch Left count down to 0h 00m then read "… over"; "—" with no budget; tfoot "Total value" still sits under Value. - AC8 — light and dark: produce near / used up / over; observe text labels; devtools contrast ≥ 4.5:1. - AC9 — budget slightly above current total, run the timer across 80 % and 100 %; observe the message appear/change (running and paused); devtools confirms `#budget-warning-text` is not rewritten each tick; hidden when under/no budget; setting/clearing a budget with a paused timer updates it immediately. - AC10 — 375, 768, 901, 1280 px screenshots per the quality bar above; observe no overflow, readable name, ≥ 32 px fields; record the measured breakpoint. - AC15/AC16 (Timesheet half) — after deploy, https://app.dev.timesavr.org performs AC6/AC7/AC9 with no flag; same from `file://` public/index.html. - AC17 — pre-existing data with no budgets key loads with no console errors; rates/values/archive/rename/tags/Today unchanged. - AC18 (By project half) — rendered help.html "By project" describes the budget field, Left, the 80 %/100 % warnings and the Running-panel message. - AC19 — `bash tools/check.sh` → `gate: ok`. Required checks: gate

Outcome

Budget (h) + Left columns, Near/Used up/Over labels, live Running-panel warning, stacked By project <900px (measured), Help. Gate green, visual check 375–1280 light/dark OK; PR #49 merged. Follow-up: Entries card overflows at 375px (pre-existing).

Pull request

Artifacts

Activity

  • Draft
  • Ready
  • Runner started
  • Runner ended (done)