Timesheet tools based on GitLab objects. GitLab is used as the timelog backend. https://alchemic.studio
  • TypeScript 82.7%
  • Vue 14.1%
  • HTML 1.6%
  • CSS 1.2%
  • JavaScript 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Sébastien Denooz c2ff15d0c7 feat(brand): wire the charte graphique through the interface
The delivered tokens land as app/assets/css/brand.css, kept as the source of
truth so a regenerated charte can overwrite it. Nuxt UI's own tokens are
bound to the charte's roles at the end of main.css, and the ramps are aliased
in app/app.config.ts — so color="primary" becomes the logo's indigo
everywhere without a single component changing.

Three deviations from the charte, all for contrast and all measured:

- Nuxt UI points each role at its 500 shade in light mode, which fails AA on
  white (the reason ENF-6 already pinned darker steps). The roles take the
  charte's 600s: 8.60 / 6.51 / 5.42.
- --brand-info is aqua 700, which the charte's OWN table measures at 2.75:1
  and marks insuffisant. Info text takes aqua 950; aqua 500 keeps the job the
  charte gives it, the focus ring.
- success/warning/danger are single hexes with no ramp measuring 3.39, 3.19
  and 4.83 on white. Status colour is not part of the logo, so those keep the
  AA-proven shades; the charte's values stay under their --brand-* names.

A fourth came out of the dark-mode screenshot: Nuxt UI spends one token on
the text of a solid button and of an inverted surface, and the charte's
text-inverse only reaches 4.20:1 on the pale indigo a dark solid button
becomes. Its own on-primary clears both, at 6.12 and 14.22.

--spacing-* is NOT adopted — the one edit inside brand.css, commented in
place with its reason. In Tailwind v4 those names are the spacing scale, and
the charte's is geometric where Tailwind's is linear: w-10 would go from
2.5rem to 8rem and top-7, which positions the row's delete confirmation, from
1.75rem to 3rem. Type scale, radii, shadows and easings are adopted.

The three faces are self-hosted from @fontsource-variable — the charte links
fonts.googleapis.com, which CA-12 forbids and font-src 'self' would refuse.
Latin + latin-ext only, one variable file per family, added to the PWA
precache so the offline shell keeps them.

Gates: prettier, eslint, typecheck ×2, 1336 unit (coverage thresholds),
214 e2e including axe on four flows, 3 PWA, build + CSP.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqkRZdkVr7dyx1v3WtDonu
2026-08-26 17:13:19 +02:00
app feat(brand): wire the charte graphique through the interface 2026-08-26 17:13:19 +02:00
docs feat(brand): wire the charte graphique through the interface 2026-08-26 17:13:19 +02:00
i18n/locales feat(row): three-line activity row with the forge reaction on the line 2026-08-26 12:40:53 +02:00
lib fix(storage,csp): an uninitialized adapter, a meta-ignored directive, a wrong diagnosis 2026-08-26 16:11:26 +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(brand): wire the charte graphique through the interface 2026-08-26 17:13:19 +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)