phase-7: motion, an installable app, and a capture that survives no signal

GSAP carries the counter roll-ups, the bandeja card physics, the dialog
transitions and the three success moments FLOWS.md allows. Every one of
them checks prefers-reduced-motion first and does nothing when it is set.

boneyard and canvas-ui are not what SPEC.md's stack table says they are:
on npm the names belong to two abandoned projects that do neither job.
The skeletons were already ours; the two canvas spots are now sixty lines
each with no dependency. DECISIONS.md records the substitution.

The app installs, keeps a scan taken with no network in IndexedDB and
sends it when there is one, falls back to a page that explains itself,
and can push a deadline notice. Reading the log of what is queued is the
source of truth, so the notice clears when the capture actually lands.

The CSP now allows scripts by per-request nonce rather than by
'unsafe-inline'. That forced /offline to render per request: a
prerendered page carries a build-time nonce no live policy matches, so
its scripts were blocked and it never hydrated.

Two crashes fixed on the way. web-push throws on a VAPID subject that is
not https: or mailto:, and the code handed it APP_PUBLIC_URL, so any
machine with push keys died at boot; a misconfigured optional channel now
switches itself off and says why. And a subscription the push service
answers 410 for is deleted rather than retried forever.

Lighthouse on the production build: accessibility 100, best practices 96,
SEO 100, performance 73. The performance number is not trustworthy on
this machine and DECISIONS.md says why; total blocking time did fall from
17.6s to 1.7s once the hero canvas stopped drawing at full resolution
every frame and the landing page stopped importing GSAP.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Michilis
2026-09-05 20:48:48 +00:00
co-authored by Claude Opus 5
parent 4c39926483
commit e4eb1617d1
59 changed files with 2148 additions and 67 deletions
+28 -3
View File
@@ -7,11 +7,12 @@ and ready to file yourself.
Working name. See `docs/` for the specifications, `DECISIONS.md` for choices made along the
way and the gaps that still need answers.
> **Status: phase 6 of 8.** The whole taxpayer path works: scan a comprobante, confirm it,
> **Status: phase 7 of 8.** The whole taxpayer path works: scan a comprobante, confirm it,
> watch the position move, and take the resulting Formulario 120 or 515 from review to
> approved to a PDF you file yourself in Marangatu. Staff have a console: look an account
> up, work the ingestion error queue, read and export the audit log. The PWA polish and the
> scale-out work are still ahead.
> up, work the ingestion error queue, read and export the audit log. It installs to a home
> screen, keeps a capture taken with no signal and sends it later, and can send a push when
> a deadline is close. The scale-out work is the last phase.
---
@@ -130,6 +131,30 @@ S3. The k8s manifests assume both.
Rate limiting is in memory and therefore per replica. At this scale that is deliberate;
the limiter is behind an interface for when it is not.
## Installing it, offline and push
`app/manifest.ts` and the icons in `apps/web/public` make the web app installable; the
icons are generated from one SVG by `apps/web/scripts/generate-icons.mjs` and committed, so
a build never needs a browser.
The service worker (`apps/web/src/lib/service-worker.js`) caches the app shell and the
`/offline` page and nothing else. No API response is cached: a tax figure that is quietly
out of date is worse than one that is honestly missing.
A scan taken with no network is stored whole in IndexedDB and sent when there is one. The
chip in the shell says so and clears itself.
Push needs VAPID keys on the API:
```bash
npx web-push generate-vapid-keys
```
Put them in `apps/api/.env` as `PUSH_VAPID_PUBLIC_KEY` and `PUSH_VAPID_PRIVATE_KEY`, plus
`PUSH_VAPID_SUBJECT` (an `https:` or `mailto:` URL) unless `APP_PUBLIC_URL` is already
https. Without keys the offer is hidden everywhere; with keys and no usable subject, push
switches itself off and logs why rather than taking the API down.
## Adding a locale
1. Add the code to `SUPPORTED_LOCALES` in `packages/i18n/src/locales.ts`.