@startinblox/timesheet-validator
v1.0.5
Published
Startin'blox components for the GitLab timesheet consistency checker
Keywords
Readme
@startinblox/timesheet-validator
Startin'blox components for the GitLab timesheet consistency checker: they display, for a period, which declared timesheet entries have related GitLab activity and which need a human to look at them.
Generated from solid-boilerplate. Backend and functional specification live in the timesheet-validator experiment.
What these components must not do
The data on screen is activity data about named colleagues. Three rules constrain the implementation, not just the copy:
- No score, ratio or ranking. Counts describe what was found; nothing sums or compares them.
- A flagged entry says what the check did not find, never what a person did not do. Every
REVIEW_NEEDEDcarries the disclaimer, rendered as static text with no dependency on the explanation model. UNAVAILABLEis a technical failure, not a verdict. It is styled and worded so it cannot be mistaken for a milder warning, and it shows no evidence counts — nothing was checked, so implying otherwise would be a lie.
The stories are where these are checked. timesheet-analysis/ReviewNeeded and timesheet-analysis/Unavailable exist specifically to make them visible.
Components
| Element | Base class | Purpose |
|---|---|---|
| <solid-timesheet-validator> | OrbitComponent | Fetches an analyses container, renders summary + list, or a detail view |
| <timesheet-summary> | ComponentObjectHandler | Period aggregates. Its object is the LDP container |
| <timesheet-analyses> | ComponentObjectsHandler | The list. Emits analysis-selected |
| <timesheet-analysis> | ComponentObjectHandler | One analysis in full. Emits explanation-requested |
| <timesheet-members> | ComponentObjectsHandler | Weekly/monthly situation per freelancer. Alphabetical, hours only, no sort control. Emits member-selected |
| <timesheet-member-summary> | ComponentObjectHandler | One freelancer's period, as a card |
| <timesheet-status> | ComponentObjectHandler | Status as glyph + word + colour, never colour alone |
| <timesheet-explanation> | ComponentObjectHandler | A generated explanation, or its absence |
Everything below <solid-timesheet-validator> is presentational: data in as properties, intent out as events. Only the Orbit component talks to the store.
<solid-timesheet-validator
data-src="https://api.example.com/analyses/?week=2026-W32">
</solid-timesheet-validator>Why the summary reads the container
The backend carries the period aggregates on the container itself, so <solid-timesheet-validator> reads the same URI twice: once expanded into members, once as a resource for the totals. The store serves both from one request.
That is also why the project view needs no endpoint of its own — ?project=… returns a container whose aggregates are already scoped. Summing the members client-side would work and would quietly move a backend responsibility into the UI, so it is deliberately not done.
Pointing at a real backend
Add ?api=<base> to the page URL — in the demo or in Storybook — and the page talks to that API instead of the fixtures. Without it, everything runs offline.
http://localhost:5173/ fixtures
http://localhost:5173/?api=http://localhost:8000 liveThe backend must allow the origin (CORS_ORIGINS, preset for 6006/5173/4173). Full instructions, including running everything from one compose file, are in the API repository's docs/wiring.md.
The demo SPA
npm run demo # http://localhost:5173
npm run build-demodemo/ rather than a root index.html: this package is a library, and the boilerplate's one-entry-point build stays untouched. The demo aggregates every component into the UI they are meant to form — period switcher (week/month), entries and per-freelancer views, project navigation, detail drawer, Why?.
Running with no backend at all
src/mocks/backend.mock.ts replaces both layers the app uses — window.sibStore.getData and fetch — with fixtures. No server, no GitLab token, no spreadsheet.
npm run storybook # every story, including the Orbit component, runs offline
npm run cy:run # component tests install the mock per specIt is installed globally in .storybook/preview.ts and per-test in cypress/support/component.ts. To use it anywhere else:
import { installMockBackend, MOCK_DATA_SRC } from "@mocks/backend.mock";
installMockBackend();The fixtures apply the same filters as the real API (week, status, project, member) and recompute the aggregates for the filtered set, so changing data-src offline behaves as it does in production. breakExplanations() makes the explanation endpoint fail, which is how the degraded path is demonstrated.
| File | Contents |
|---|---|
| src/mocks/analyses.mock.ts | Analyses in store shape — one per outcome, including the all-zero flagged entry and the unavailable one |
| src/mocks/ldp.mock.ts | Containers, projects, filtering, aggregate computation, deterministic explanations |
| src/mocks/backend.mock.ts | installMockBackend() / uninstallMockBackend() / breakExplanations() |
The mocks are store-shaped rather than wire-shaped: property names are what the store yields once the JSON-LD context has been applied (app:status → status), because that is the layer the components actually see.
Stories
Storybook is the development environment — this is a library, and there is no index.html.
Stories here lean on one story per state rather than one story with controls. States are the interesting thing: a status set has four of them, an analysis has five outcomes, an explanation has four. Controls remain available for exploring, but the enumerated stories are what someone reviews, and what shows a regression.
npm run storybookDevelopment
npm install
npm run storybook
npm run cy:run
npx biome check . --writeSee AGENTS.md for conventions: base-class decision tree, cherryPickedProperties, i18n, cache invalidation.
Status
Progress, findings and open decisions for the whole POC — backend included — are in the API repository's docs/progress.md.
Verified in a real browser (Node 24 via nvm; Node 18 cannot run Vite 7):
- 11 Cypress component specs — the safeguards hold in the DOM
- 11 end-to-end specs on the built demo, on fixtures
- 8 more on the same page against a running API, asserting invariants rather than fixture counts so they hold against live data too
- TypeScript clean across
src,stories,demoandcypress
Still unverified: <solid-timesheet-validator> against the real sib store. The demo drives the presentational components directly, so the store path — _getProxyValue expanding the analyses container — remains WI-13's open risk. The mock is written to the store's contract, which makes that spike cheap.
Two findings worth knowing, both caught by running things:
src/initializer.tsblocks forever onorbit-readyoutside Storybook and Cypress, so no component registers in a plain page unless Orbit or its mock loads first.- Container members are summaries — no evidence counts, no disclaimer, no explanations link. Opening an entry must dereference the member's
@id. The fixtures used to serve whole analyses and hid this; they now serve summaries.
New strings are not yet extracted: run npm run locale:extract before building for production.
