- TypeScript 81.9%
- Vue 15.2%
- HTML 1.5%
- CSS 1.1%
- JavaScript 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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 |
||
| app | ||
| docs | ||
| i18n/locales | ||
| lib | ||
| public | ||
| scripts | ||
| server | ||
| tests | ||
| tools | ||
| .editorconfig | ||
| .env.example | ||
| .gitignore | ||
| .nuxtrc | ||
| .prettierignore | ||
| .prettierrc | ||
| eslint.config.mjs | ||
| nuxt.config.ts | ||
| package.json | ||
| playwright.config.ts | ||
| playwright.pwa.config.ts | ||
| playwright.smoke.config.ts | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| RALPH.md | ||
| README.md | ||
| tsconfig.json | ||
| tsconfig.tests.json | ||
| vitest.config.ts | ||
| vitest.smoke.config.ts | ||
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 = trueinapp.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
- On your instance: User settings → Access tokens → Add new token.
- Select the
apiscope (the app needs to read tickets, post notes and query timelogs). Set an expiry — the app warns 14 days before it lapses. - 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
apiscope; 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 withNUXT_PUBLIC_GTAG_IDoffers 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 auditsdocs/DEPLOY-NUXTHUB.md— Cloudflare D1 deploymentdocs/ACCEPTANCE-MAP.md— CA-1..CA-26 → automated spec mappingdocs/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)