Timesheet tools based on GitLab objects. GitLab is used as the timelog backend. https://alchemic.studio
  • TypeScript 81.9%
  • Vue 15.2%
  • HTML 1.5%
  • CSS 1.1%
  • JavaScript 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Sébastien Denooz 0e53e6cda5 feat(warnings,header): silence a banner without losing the problem
Six things, one of them structural.

The warnings centre is the structural one. Every inline banner gains an ×,
and a badge beside the logo counts what is currently true. Dismissing a
banner is a statement about the interruption, never about the problem — so
the banner disappears and the badge does not, and the modal lists what was
dismissed together with the way to bring it back. A badge that went quiet
when you silenced a banner would be worth nothing, because the only moment
you would consult it is the moment it had stopped counting.

A dismissal records a VALUE, not a boolean: the expiry date and its instance,
the set of late session ids, the sorted recap ids. A renewed token that later
expires warns again; a different late session warns again though the count is
unchanged; switching forge cannot inherit the other one's silence. It is
synced rather than device-local, because a token expiry is a fact about the
account — which takes the settings schema to v4.

Three overlays stay outside all of this: the stop-first prompt, the
foreign-running dialog and the sync arbitration dialog. Each arm of each
mutates real state and none has a safe default, so an × would be a decision
made on the user's behalf.

The day list now runs in calendar order, which reverses a choice made in each
of the two preceding commits: with the rows inside a day already oldest-first,
a week running the other way made the page read in two directions at once.
Today's is the only expanded section and can now sit under four collapsed
days, so the page scrolls it into view — guarded to the current week, week
scope, once after the first paint.

The grid's detail moved into a modal, still rendering the ordinary row. That
exposed a latent bug: SessionEditModal and PostSyncCorrectModal handled
Escape without stopping it and never took focus, so the key was delivered to
the button that opened them — which in the grid lives inside another dialog.
One Escape closed both. They now take focus on open and sit above at z-[60].

The banner drops the ticket path from the line and makes the name a link,
with the path as its tooltip. It goes sr-only rather than away, so a screen
reader and a text search still find it — the same trade the row made.

Plus: "Start working" and "Add a timesheet" as visible labels whose text and
accessible name are one string, and a language switcher in the header and in
the onboarding modal, where it matters most — someone who cannot read the
setup screen cannot reach the settings that would fix it.

Also corrects the previous entry: it claimed the charter had been amended for
EF-30 when the file was never edited. It is now, in all three places.

Gates: lint, typecheck x2, 1393 unit with coverage, 235 e2e (axe extended to
the warnings modal), 3 PWA, build + CSP.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqkRZdkVr7dyx1v3WtDonu
2026-09-08 17:28:21 +02:00
app feat(warnings,header): silence a banner without losing the problem 2026-09-08 17:28:21 +02:00
docs feat(warnings,header): silence a banner without losing the problem 2026-09-08 17:28:21 +02:00
i18n/locales feat(warnings,header): silence a banner without losing the problem 2026-09-08 17:28:21 +02:00
lib feat(warnings,header): silence a banner without losing the problem 2026-09-08 17:28:21 +02:00
public feat(pwa): T38 installable PWA shell (optional) 2026-08-17 14:44:34 +02:00
scripts fix(storage,csp): an uninitialized adapter, a meta-ignored directive, a wrong diagnosis 2026-08-26 16:11:26 +02:00
server feat(vault): Phase C — credentials in the storage backend, new-device bootstrap 2026-08-18 16:56:34 +02:00
tests feat(warnings,header): silence a banner without losing the problem 2026-09-08 17:28:21 +02:00
tools feat(scaffold): T00 repository scaffold 2026-08-17 09:45:04 +02:00
.editorconfig feat(scaffold): T00 repository scaffold 2026-08-17 09:45:04 +02:00
.env.example fix(build): apply-csp reads .env; migrate pnpm settings to pnpm-workspace.yaml 2026-08-18 11:31:38 +02:00
.gitignore feat(scaffold): T00 repository scaffold 2026-08-17 09:45:04 +02:00
.nuxtrc feat(scaffold): T00 repository scaffold 2026-08-17 09:45:04 +02:00
.prettierignore feat(scaffold): T00 repository scaffold 2026-08-17 09:45:04 +02:00
.prettierrc feat(scaffold): T00 repository scaffold 2026-08-17 09:45:04 +02:00
eslint.config.mjs feat(scaffold): T00 repository scaffold 2026-08-17 09:45:04 +02:00
nuxt.config.ts feat(brand): wire the charte graphique through the interface 2026-08-26 17:13:19 +02:00
package.json feat(brand): wire the charte graphique through the interface 2026-08-26 17:13:19 +02:00
playwright.config.ts feat(platform): F2 — platform-aware registry, dynamic login, GitSheets rebrand 2026-08-19 14:52:00 +02:00
playwright.pwa.config.ts feat(deploy): emit .output/server/index.mjs — static server for PM2-style hosts 2026-08-18 11:48:43 +02:00
playwright.smoke.config.ts feat(scaffold): T00 repository scaffold 2026-08-17 09:45:04 +02:00
pnpm-lock.yaml feat(brand): wire the charte graphique through the interface 2026-08-26 17:13:19 +02:00
pnpm-workspace.yaml fix(build): apply-csp reads .env; migrate pnpm settings to pnpm-workspace.yaml 2026-08-18 11:31:38 +02:00
RALPH.md feat(sync): T39 offline issue creation (optional) 2026-08-17 14:50:51 +02:00
README.md fix(storage): diagnose a CSP-blocked request instead of reporting "Failed to fetch" 2026-08-19 23:02:38 +02:00
tsconfig.json feat(scaffold): T00 repository scaffold 2026-08-17 09:45:04 +02:00
tsconfig.tests.json feat(forgejo): Phase F6 — hardening, security review, docs 2026-08-19 19:07:41 +02:00
vitest.config.ts refactor(platform): F1 — GitLab behind the platform provider layer 2026-08-19 14:06:15 +02:00
vitest.smoke.config.ts feat(storage): T29 Supabase adapter 2026-08-17 13:21:23 +02:00

GitSheets

A static single-page app for forge-based timesheet tracking: track work sessions against issues and merge/pull requests, then sync them as timelogs on your instance — the forge is the single source of billing truth. Multiple instances can be registered side by side (Settings → Connections): sessions, labels/projects settings and the monthly quota are strictly scoped per instance, and a header switcher flips between them. Remember to include every instance's origin in GLTS_CONNECT_SRC at build time — and your storage origin too. A missing origin is not a build error: the browser silently refuses those requests at runtime and the app reports which origin its own policy blocked (see docs/SECURITY.md).

Platforms: GitLab and Forgejo. Pick the platform when you add an instance; a fleet may mix the two freely, and each instance keeps its own projects, labels and sessions. Two Forgejo caveats worth knowing before you connect one, because neither is something the app can work around:

  • CORS is off by default. A stock Forgejo refuses browser API calls until an administrator sets [cors] ENABLED = true in app.ini.
  • Time entries cannot be backdated. Forgejo stamps its own clock at sync time and discards any date you send, so GitSheets posts a dated comment alongside each entry — that comment is the record of which day the work belongs to. See docs/FORGEJO-PLAN.md §6 for the full picture.

Forgejo tokens need read:user, read:organization, write:issue and read:repository; read:organization is not optional, or org labels and the picker's drill-down come back empty.

Historical note: the project was called gl-timesheet before it grew beyond GitLab. Storage keys, backup/export kind strings, the IndexedDB database name and the GLTS_CONNECT_SRC build variable keep their original names on purpose — renaming them would orphan existing data and break cross-device replication with older builds.

Setup

pnpm install
pnpm dev          # http://localhost:3000
pnpm build        # static bundle in .output/public (CSP injected post-build)

Quality gates: pnpm lint && pnpm typecheck && pnpm test && pnpm test:e2e (unit + mocked-forge e2e; see tests/e2e/smoke/ and tests/smoke/ for the env-gated suites against real backends).

Deploy .output/public to any static host. Hosts that insist on a Node process (PM2-style panels) can instead run .output/server/index.mjs — the build emits a zero-dependency static server for the same bundle (SPA fallback, immutable asset caching, CSP as a real header; PORT/HOST env, port 3000 default). GLTS_CONNECT_SRC is REQUIRED at build time (ES-6): set it to your forge + storage origins to pin the CSP connect-src, or build a dev/test bundle with the explicit opt-out GLTS_CONNECT_SRC='*' / pnpm build:dev (docs/SECURITY.md). The NuxtHub/Cloudflare D1 storage service needs a server deployment instead — see docs/DEPLOY-NUXTHUB.md.

Connecting: token creation

  1. On your instance: User settings → Access tokens → Add new token.
  2. Select the api scope (the app needs to read tickets, post notes and query timelogs). Set an expiry — the app warns 14 days before it lapses.
  3. Paste the token in the first-run wizard.

Security model (honest version)

  • The token lives in sessionStorage for the browser session — never in app state, never persisted by storage adapters, never in URLs. Closing the browser forgets it.
  • Remember me is opt-in: AES-256-GCM sealed under a PBKDF2 passphrase. Anyone with your device AND passphrase can recover the token, and a weak passphrase can be brute-forced offline. A forgotten passphrase cannot be recovered — you re-enter the token.
  • A forge token can do anything your account can (GitLab api scope; Forgejo's four scopes above). The app only ever talks to the forge origins you configured and your chosen storage origin (enforced by an automated zero-egress test and the build CSP), but you are trusting this codebase with that power — read it, pin your deployment. Exception: a deployment built with NUXT_PUBLIC_GTAG_ID offers opt-in Google Analytics — gtag.js loads only after you accept the consent banner, and never carries the token (docs/SECURITY.md, docs/DECISIONS.md).
  • Session comments are published to the forge at sync. Reminders sit next to every field that ends up public; still, keep secrets out of comments. Forgejo has no confidential issues at all — repository visibility is the only privacy axis there, and it is set on the forge.
  • Cloud storage services have their own honest limits: the Supabase anon key and JSONBin access key act as bearer credentials for your timesheet data (the in-app setup blocks explain each model); the JSONBin Master Key is refused outright.

Catch-all ticket advice (§2.2)

Not every minute maps to a ticket. Create one catch-all issue per client or per internal bucket (e.g. acme/admin#1 "Misc & meetings"), pin it in the picker, and give it a billing label like any other ticket. Time you cannot attribute precisely still lands in the right bucket instead of evaporating — and stays correctable later (post-sync correction).

Documentation

  • docs/SECURITY.md — CSP, token handling, leak audits
  • docs/DEPLOY-NUXTHUB.md — Cloudflare D1 deployment
  • docs/ACCEPTANCE-MAP.md — CA-1..CA-26 → automated spec mapping
  • docs/DECISIONS.md, docs/BLOCKERS.md — engineering record (the v2.8 source spec is still unavailable; RALPH.md served as interim reference)
  • In-app help: /help (five languages)