Commit Graph

23 Commits

Author SHA1 Message Date
Carter Perez 21741e0906
Merge pull request #286 from CarterPerez-dev/project/ja3-ja4-tls-fingerprinting
Project/ja3 ja4 tls fingerprinting
2026-06-18 19:38:51 -04:00
CarterPerez-dev e87efe7e29 i count the tiles every morning. today there's one more. there's always one more. 2026-06-18 19:28:21 -04:00
dependabot[bot] f18e8e5e5e
chore(deps): bump axios
Bumps [axios](https://github.com/axios/axios) from 1.15.2 to 1.16.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.15.2...v1.16.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.16.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-05 08:33:15 +00:00
dependabot[bot] adf26769a9
chore(deps): bump flatted
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.3 to 3.4.2.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-18 14:02:41 +00:00
Carter Perez c1f5322ea2
Merge branch 'main' into dependabot/npm_and_yarn/PROJECTS/beginner/canary-token-generator/frontend/axios-1.15.2 2026-05-18 10:00:49 -04:00
dependabot[bot] a71926ca09
chore(deps): bump fast-uri
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.0 to 3.1.2.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.0...v3.1.2)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-18 04:48:05 +00:00
dependabot[bot] edf66fb576
chore(deps): bump axios
Bumps [axios](https://github.com/axios/axios) from 1.13.2 to 1.15.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.13.2...v1.15.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.15.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-18 04:47:56 +00:00
CarterPerez-dev 9655120ebe chore(canary): phase B audit fixes + frontend asset rename + gitignore
Audit Phase B (backend): F5 trusted-proxy gate wiring, F7 ErrValidation
sentinel, F8 dedup unique-IP semantic via SCard, F9 notify worker pool +
graceful Shutdown, F12 token ID collision retry.

Frontend: public/asset/ → public/assets/ rename + index.html paths.

Housekeeping: .npmrc strict-dep-builds=false (pnpm fix), .gitignore now
excludes all .env.* except .env.example.
2026-05-17 23:34:42 -04:00
CarterPerez-dev 8d2599c01a feat(canary): mark MySQL type as disabled when mysql.enabled=false
Operator request: when MYSQL_FAKE_ENABLED=false (the dev default + the only
viable setting behind Cloudflare Tunnel since CF Tunnel doesn't carry raw
TCP), surface that in the UI so users can see the species but understand
they can't deploy it on this instance. Avoids the silent-broken case where
someone creates a mysql token, gets a connection_string artifact, points
their MySQL client at it, and just times out with no clue why.

Backend (3-line change):
- TypeDescriptor gains `Enabled bool` + optional `DisabledReason string`
- TypeDescriptors() now takes `mysqlEnabled bool`; mysql is the one whose
  Enabled tracks the flag, all others are always true (they don't need
  any deployment-level infra beyond HTTP)
- Handler stores mysqlEnabled, passes to descriptors call; main.go wires
  cfg.MySQL.Enabled in. Handler tests updated to pass false explicitly.

Frontend (matching schema + visual treatment):
- typeDescriptorSchema picks up `enabled` (default true for backward compat)
  and `disabled_reason` (optional string)
- TypeCard reads descriptor.enabled, sets `data-disabled` on the label,
  disables the radio input, replaces the blurb with the disabled_reason
  when present (so the user sees WHY rather than guessing), and renders
  a small "disabled" pill in the top-right corner of the card
- SCSS: dashed border, muted palette, opacity 0.55, glyph faded, cursor
  not-allowed, hover effect suppressed — reads as "shelf is here but the
  specimen is checked out" rather than a broken button

In the operator UI: mysql card renders ghosted with the explanatory text
"Requires direct TCP exposure (port 3306). Not reachable via Cloudflare
Tunnel — only enable on a VPS with raw TCP access." Selecting it is
mechanically impossible (input is disabled), no form-level guard needed.

Flip MYSQL_FAKE_ENABLED=true in env when you stand up a VPS later and the
card re-enables with no other code changes.

Backend builds clean + token-package tests pass.
Frontend gates green: typecheck + biome + lint:scss + build (431ms).
2026-05-17 22:45:53 -04:00
CarterPerez-dev 44927b245c fix(canary): accept null events from manage response
Backend serializes `Events []Event` as JSON `null` when the slice is nil
(Go's zero value for slices). Spec §8.4 shows it as an array — stale spec,
verified actual behavior with `curl http://localhost:22784/api/m/<uuid>`
returning `"events": null` on a freshly-created token with no triggers.

Zod schema was `events: z.array(eventResponseSchema)` — failed to parse the
null, threw PARSE_ERROR, manage page rendered "CANNOT REACH ARCHIVE" instead
of the empty dossier.

Fixed: `.nullish().transform(v => v ?? [])` — accepts null/undefined and
normalizes to []. Manage page's `events.length === 0` empty-state check
("No events recorded yet") now reachable; consumers always see an array.

Caught by memory rule feedback_verify_data_shapes.md: trust reality > spec.
I trusted §8.4's example object during Phase 14 schema design instead of
hitting the live endpoint. Same kind of drift as the four Phase-14 spec-vs-code
divergences cleared at the time — this one slipped through because
TokenManagePage was never end-to-end exercised pre-UI.
2026-05-17 17:58:13 -04:00
CarterPerez-dev 915d471b8d fix(canary): vite dev proxy — correct backend port + stop stripping /api
Two latent template-leftover bugs in vite.config.ts proxy that 404'd every
useTokenTypes/useCreateToken/useManageToken/useDeleteToken call:

- Default target was http://localhost:8000. Backend listens on :8080
  (backend/config.yaml `server.port: 8080`, default registered in
  internal/config/config.go). Off-by-one-port silent for the whole
  Phase 14 cycle because Phase 14 didn't end-to-end-test.

- `rewrite: (p) => p.replace(/^\/api/, '')` stripped /api before forwarding.
  But backend mounts routes UNDER /api via `r.Route("/api", ...)`
  (cmd/canary/main.go:322). So a request for /api/tokens/types got rewritten
  to /tokens/types, which backend doesn't serve — 404 page not found.

Frontend axios uses baseURL `/api` and path `/tokens/types` → full URL
`/api/tokens/types`. With these fixes the proxy now passes that through
verbatim to http://localhost:8080/api/tokens/types, which is where chi
actually mounts the route.

dev.compose.yml: added VITE_API_TARGET=http://canary:8080 to the frontend
container's env so the dev-compose path also works (inside the container
"localhost" is the vite container, not the canary container — has to use
docker DNS).

Surfaced today when the operator manually eyeballed the landing page and
SpeciesSection rendered "Could not load species catalog. Try again in a
moment." (the descriptorsError branch in landing/index.tsx SpeciesSection).
2026-05-17 17:37:36 -04:00
CarterPerez-dev 6ef5f68b59 feat(canary): web fonts — Archivo Black / Space Grotesk / Departure Mono
System stack was the floor; this is the intended pairing. Three roles, three
voices:

- Archivo Black (display) — the big confident archive voice. CANARIES,
  TRAP TRIPPED, DOSSIER NOT FOUND, SPECIMEN ISSUED, NO SPECIMEN HERE. Single
  weight 900 black grotesque, wide letterforms, built for huge display. Pairs
  visually with the PARADISE-style pixelated typography in the
  vector-minimalism reference folder.
- Space Grotesk (sans) — speaking voice. Body, section titles, blurbs,
  button labels, error text. Geometric joints + open apertures read as
  "designed" where system extra-bold read as "just heavy." Weights 400-700
  cover the range; section titles dropped 800→700 since Space Grotesk caps
  at 700 (no perceptible difference in the design).
- Departure Mono (mono) — technical scribblings. SerialBar, timestamps,
  field labels, strip pills, ID codes, all the spec-sheet micro-text. Its
  pixelated character is the deliberate echo of image 1 (PARADISE) that
  ui-monospace can't make.

Loaded via Google Fonts <link> in index.html (preconnect + display=swap),
no npm dep. ~50KB cold cache; cached-warm thereafter. System sans / system
mono remain in the fallback chain at the end of each stack so a CDN miss
degrades to the previous design without breaking.

$font-display token added to _fonts.scss for the giant headlines; $font-sans
and $font-mono updated to lead with the new families. Headlines previously at
font-weight: 900 dropped the weight (Archivo Black is single-weight), and
letter-spacing/line-height tightened slightly to match the new metrics.

Gates green: typecheck + biome + lint:scss + build (419ms; no chunk size
delta — fonts load from CDN, not the bundle).
2026-05-17 17:27:35 -04:00
CarterPerez-dev da126ad889 fix(canary-phase-ui-design): single-source form fields + URL revoke + eventExtra schema
Parallel audit (superpowers:code-reviewer + general-purpose spec adherence)
returned PASS-WITH-NOTES + PASS. All actionable SHOULD-FIX and NIT items
addressed in this commit per the fix-in-phase rule.

SHOULD-FIX (code review):
- Dead ternary at manage/index.tsx:253 (`cursor === undefined ? idx+1 : idx+1`)
  collapsed to `idx + 1`. EventLogSection prop renamed `cursor` → `cursorIsSet`
  (boolean) since the value itself was never consumed past the dead branch.
- URL.createObjectURL leak in artifact.tsx — both file (base64) and text
  variants now run inside useEffect with cleanup that calls
  URL.revokeObjectURL on dependency-change AND on unmount. Extracted into
  two co-located hooks: useBase64ObjectUrl, useTextObjectUrl.
- Text inputs lacked <label htmlFor=…> association. Refactored the landing
  form to use the existing Field primitives (TextField, TextareaField) which
  wire htmlFor↔id via useId. This single change also closed the next finding.
- Dead infrastructure: Field primitives existed but no consumer used them.
  Landing form's local FieldLabel removed; TextField/TextareaField now have
  consumers. Net diff: −181 LOC.
- LeaderLabel: truly unused primitive, no foreseeable consumer in this scope.
  Deleted (`frontend/src/components/LeaderLabel/`) and removed from barrel.

SHOULD-FIX (spec adherence):
- §8.6 row 410 → TOKEN_DISABLED: spec is stale. Backend Go ground truth
  (verified via `grep StatusGone\|TOKEN_DISABLED backend/internal/`) emits
  neither — disabled tokens come back as 200 with `token.enabled=false`,
  which the DossierCard already renders with alarm-pill + HEADLINE_DISABLED.
  Per feedback_verify_data_shapes.md: backend Go beats spec when they diverge.
  No code change; documented here so the next AI doesn't re-open it.
- eventResponseSchema.extra: tightened from `z.unknown()` to
  `z.record(z.string(), z.unknown())` to match backend's `json.RawMessage`
  default of `{}` (event/repository.go:47). Added EventExtra type alias;
  event-row.tsx now uses it instead of casting through unknown.

NIT:
- SerialBar: extracted `BAR_COUNT = 22` const; `FALLBACK_BARS` constant
  string replaces the BAR_CHARS array that had length-9 / length-22
  inconsistency between the empty-input and computed branches.
- Glyph: removed `default` branch — switch is now exhaustive over the
  closed TokenType enum, so a future 8th type would be a compile error
  rather than silently rendering WebbugGlyph.
- Dead `export { TOKEN_BLURB }` + `export { ArtifactDisplay }` from
  landing/index.tsx — no observed consumer, dropped.
- App.tsx and main.tsx header banners normalized to the project-canonical
  19 = signs (matches all other files in repo).

NOT addressed in this commit (intentional):
- CreateTokenRequest `metadata` as `z.unknown()`. Refactor to a discriminated
  request schema keyed on `type` would tighten compile-time safety for any
  future caller bypassing buildPayload — defer because (a) the form is the
  only caller and validates via Zod on submit, (b) the change touches the
  Phase 14 contract that was just stabilized two commits before this UI phase.
- NotFound (frontend route fallback) vs ManageNotFound (manage 404) live
  separately. Both work; the two paths are intentional — off-path vs
  off-dossier are semantically different events.

Gates green: typecheck + biome check + lint:scss + build (414ms; landing
chunk dropped 21KB → 19.93KB after dead style removal).
2026-05-17 15:07:50 -04:00
CarterPerez-dev 4a4f465d24 feat(canary): manage dossier page + /m/:manageId route + 404 fallback
Manage route renders the per-token dossier — the "specimen catalog entry"
twin of the intake page. Same vector-minimalism mind: archive strip,
big-blocky headline, hairline-rule cards, mono labels, alarm-red only
when the trap is alive (eventsTotal > 0) or disabled.

Page composition:
- Top Strip (FIELD STATION / SECTION / SPECIES pill)
- Hero block — headline state-shifts: QUIET (no events, ink), TRAP TRIPPED
  (alarm red), TRAP MUTED (ink-mute). Memo rendered as printer-block quote.
- Halftone separator
- SpecimenCard with armed status / trigger count / silenced (15m window) /
  last seen / created / alert channel / filename — plus Trigger URL CopyField
- EventLogSection — cursor-paginated list of EventRow cards (timestamp +
  source_ip + geo + ASN + UA + referer + notify_status pill + expandable
  fingerprint extra). "Load older →" + "← back to most recent" controls.
- DeleteSection — two-step arm/confirm pattern (no native confirm() dialog),
  alarm-tone Button on confirm, success → toast + navigate('/')
- Bottom Strip with "new specimen ↗" link

States: useManageToken loading | ApiError(NOT_FOUND) → ManageNotFound |
other error → ManageError with retry | success → ManageView.

Pagination policy per plan anti-relit: singular useQuery, NO useInfiniteQuery.
Each cursor change replaces the visible page (operator-friendly default).
Auto-refresh every 5s via QUERY_STRATEGIES.frequent (Phase 14 contract).

Routes:
- /  → landing
- /m/:manageId → manage (new)
- *  → notfound (new — replaces previous fallback-to-landing)

config.ts: ROUTES.MANAGE added; manageRoute(id) helper centralizes the
URI-encoded path so result.tsx and any future caller cannot drift.

notfound page: same ethos in miniature. 404 strip pill, big OFF-ROUTE
headline, link back to intake.

Gates green: typecheck + biome + stylelint + build (414ms, manage chunk
10.9KB / 3.7KB gzip, notfound chunk 1.0KB / 0.5KB gzip).
2026-05-17 14:54:27 -04:00
CarterPerez-dev 5562f82f34 feat(canary): landing — specimen requisition page + Turnstile widget
Landing route (/) is the token-creation page, framed as a "specimen
requisition" intake form per the vector-minimalism ethos. Mind: cataloging
a thing that resists cataloging — a trap baited for an unseen visitor.

Page composition:
- Top archive Strip (FIELD STATION / VOL / ISSUE pill)
- Hero block: large blocky "CANARIES" headline, Latin subtitle, purpose line
- Halftone separator
- Five sections (01 species / 02 annotate / 03 route / 04 species config / 05 verify)
- Bottom Strip with routing/license metadata

Form sub-sections (each a small component to keep complexity under biome's
maxAllowedComplexity=25 cap on FormView):
- SpeciesSection — 7-card grid via TypePicker (radio fieldset, sr-only inputs)
- AnnotateSection — memo textarea + conditional filename (when file-kind)
- RouteSection — Telegram | Webhook radio fieldset + conditional credentials
- ConfigSection — slowredirect destination_url or envfile include_keys
- VerifySection — Turnstile widget if VITE_TURNSTILE_SITE_KEY present
- SubmitFooter — release button + privacy note

Validation: client uses createTokenRequestSchema.safeParse, surfaces field
errors inline (mono caps in alarm color); server-side ApiError.fields are
mapped back into the same FieldErrors shape via buildSubmissionState (Phase 14
single-source envelope contract preserved).

Result view: SpecimenCard with full token dossier (DataRows), kind-discriminated
artifact display (url → CopyField; file/text → download Blob via object-URL;
connection_string → CopyField with note), Manage + Trigger CopyFields, link to
the dossier route /m/:manage_id (route wired in next chunk).

Turnstile: explicit-render widget, lazy script-load, registers token via
setTurnstileTokenProvider on success; cleans up on unmount; no key = no widget
(matches backend dev-bypass at internal/turnstile/verifier.go).

Gates green: typecheck + biome + stylelint + build (406ms, landing chunk
123KB / 38KB gzip).
2026-05-17 14:49:37 -04:00
CarterPerez-dev f5d893ab8c feat(canary): UI design layer — vector-minimalism tokens + primitives
Operator (Carter) redirected from infra Phase 15 to UI design layer; design ethos
"vector-minimalism" (specimen-cataloging mind, off-white paper, deep ink, alarm-red
accent, hairline rules, halftone/grain overlays). Foundation only — pages follow.

Tokens: 7-step paper/ink/alarm/signal palette + paper-grain SVG noise + halftone
dot data-uri + hairline width. Stripped color palette in white-canvas commit is
deliberately NOT re-introduced; this is a project-owned palette, not the template's.

Primitives (each in own folder with module SCSS):
- Strip / StripItem    — archive header (vol/issue/serial pills)
- LeaderLabel          — index + rule + caption annotation
- SpecimenCard         — tag + body + serial container (paper|ink|alarm tone)
- DataRow              — field=value with dotted leader (mono|emphasize|alarm)
- Pill                 — paper|ink|alarm|signal label
- CopyField            — labelled mono code with clipboard button
- Halftone             — repeating dot strip (sparse|normal|dense)
- SerialBar            — fingerprint bar + code two-row mono display
- Glyph                — 7 token-type SVG icons (currentColor, 24×24)
- Field (Text/Textarea/Select/Wrap) — labelled form inputs with leader rule
- Button               — primary|ghost|alarm × sm|md|lg

Gates green: pnpm typecheck + pnpm biome check + pnpm lint:scss + pnpm build (294ms).

No backend changes. No page-level routing changes yet.
2026-05-17 14:39:30 -04:00
CarterPerez-dev 1d205c1791 chore(canary): white-canvas frontend design layer pre-operator UI
Strip the inherited template aesthetic so the next AI/designer starts
from a true blank canvas. AI tunnel-vision pattern: when there's an
existing layout/sidebar/color palette, the model tends to redesign
in place rather than truly start fresh. Removing the visual scaffold
forces fresh-canvas thinking when operator-driven UI work begins.

Removed:
- pages/dashboard, pages/settings — template "Welcome / Available
  Stores / Suggested Features" stubs that have no canary purpose.
- pages/landing/landing.module.scss, core/app/{shell,toast}.module.scss
  — empty header stubs imported only for side effects.
- core/lib/shell.ui.store.ts — zustand store for sidebar open/
  collapsed/theme state; entirely template-coupled. core/lib/index.ts
  reduced to an empty barrel ready for canary stores.
- core/app/shell.tsx sidebar+nav+header layout; replaced with bare
  ErrorBoundary > Suspense > Outlet. Keeps robustness pattern,
  drops the navbar template a designer would otherwise inherit.
- App.tsx hardcoded dark Toaster theme + wrapping .app div.
- styles/_tokens.scss color palette ($bg-page, $text-default, etc.)
  — biased toward a specific dark aesthetic. Spacing, typography,
  weights, line heights, letter spacing, radius, z-index, transitions,
  breakpoints, container widths all KEPT — neutral design primitives,
  not aesthetic choices.
- styles/_reset.scss body bg-color: #fff / color: #000 defaults.
  Designer chooses the palette; reset stays purely structural.
- styles.scss .app class wrapper.
- config.ts USERS endpoints, USERS query keys, STORAGE_KEYS,
  HTTP_STATUS, PAGINATION — template constants with no canary
  consumer. Reduced to ROUTES.HOME and QUERY_CONFIG (consumed by
  query.config.ts). Designer adds back what they actually need.

Kept (logic / infrastructure / Phase 14 work):
- Full api/ and core/api/ — Zod schemas, axios client, hooks,
  ApiError, QueryClient, retry strategies.
- main.tsx, index.html (already canary-titled with favicons in
  public/asset).
- QueryClientProvider + RouterProvider + bare Toaster + ReactQuery
  Devtools provider stack.
- Lazy-route + Suspense + ErrorBoundary pattern in shell.
- styles/_index.scss, _fonts.scss (system font stacks), _mixins.scss
  (breakpoint utilities, flex helpers, sr-only, truncate, etc.) —
  all neutral.

Gate: pnpm typecheck, pnpm biome check, pnpm lint:scss, pnpm build
all clean. Production bundle: 140-byte landing chunk (the <div />),
~37 KB index chunk (api/core/router/zod machinery), no leftover
template CSS.
2026-05-17 13:12:24 -04:00
CarterPerez-dev 7c0a504d0b fix(canary-phase14): single-source envelope parsing + query-key splay
Address audit findings S-1, S-3, S-4, S-7 pre-rollup (fix-in-phase).

S-1: Eliminate hand-rolled envelope error parser duplication.
  Moved transformAxiosError out of core/api/errors.ts (where it
  required a private parseEnvelopeError mirror of the Zod schema)
  into api/client.ts where it now uses apiErrorEnvelopeSchema directly
  via safeParse. core/api/errors.ts is now ApiError class + code
  constants + module augmentation only, with no canary-envelope
  knowledge — keeps layering core <- universal, api <- canary clean.

S-3: useManageToken query key now splays cursor/limit individually
  instead of carrying the params object reference. Without this,
  callers passing a freshly-allocated { cursor } each render would
  miss the cache even with identical values.

S-4: unwrapEnvelope's PARSE_ERROR now includes the first Zod issue
  (path + message) in the ApiError.message so operators see WHICH
  field shape failed, not just that something did.

S-7: Add request interceptor rejection handler so request-phase
  errors propagate via Promise.reject instead of throwing
  synchronously.
2026-05-17 13:03:27 -04:00
CarterPerez-dev f6194d87b9 feat(canary): TanStack Query hooks for token lifecycle
Four hooks wired through the canary api/client envelope helpers.
Module-augmented defaultError surfaces ApiError everywhere.

- useTokenTypes: GET /tokens/types, parsed via typeDescriptorListSchema.
  Uses QUERY_STRATEGIES.static (Infinity stale, no refetch on focus)
  since the discoverable types list is build-time stable.
- useCreateToken: POST /tokens (CreateTokenInput -> CreateTokenResponse).
  On success invalidates ['canary','manage', manage_id] so a freshly
  created token's manage page hydrates with the new row on first hit.
- useManageToken: GET /m/{id} with cursor pagination. URLSearchParams
  builds ?cursor=&limit= only for defined values; manageId is
  URI-encoded. QUERY_STRATEGIES.frequent enables 30s polling so the
  events list feels live. enabled=false when manageId is empty.
- useDeleteToken: DELETE /m/{id}. On success removes the cached
  manage data entirely (no point keeping it after a delete).
2026-05-17 12:56:03 -04:00
CarterPerez-dev 2d13e649a6 feat(canary): axios client with Turnstile header interceptor
- api/client.ts: axios instance with baseURL from VITE_API_URL env
  (falls back to /api) and 15s timeout.
- Request interceptor adds CF-Turnstile-Response header from a
  pluggable provider (setTurnstileTokenProvider). Backend's
  middleware/turnstile.go reads header first then body field; using
  the header keeps the request body shape clean and matches the plan.
- Response interceptor routes axios errors through transformAxiosError
  (canary envelope-aware) so callers see typed ApiError everywhere.
- Envelope-aware helpers apiGet / apiPost / apiDelete:
  - apiGet/apiPost unwrap {success:true,data:T} via successEnvelope
    factory; on shape mismatch throw ApiError(PARSE_ERROR).
  - apiDelete returns void for 204 responses; envelope error path
    handled by the interceptor.
- Plumbed into api/index.ts barrel.
2026-05-17 12:55:42 -04:00
CarterPerez-dev db98a0d497 feat(canary): Zod schemas + canary envelope error normalization
- Add Zod 4 schemas mirroring backend handler shapes:
  - api/types/token.ts: tokenResponseSchema, manageTokenViewSchema,
    artifactSchema (discriminatedUnion on kind), typeDescriptorSchema,
    createTokenRequestSchema (with superRefine for telegram/webhook
    conditional required), per-type metadata helpers.
  - api/types/event.ts: eventResponseSchema with GeoView, NotifyStatus,
    manageResponseSchema with cursor pagination.
  - api/types/error.ts: apiErrorEnvelopeSchema (canary {success,error}
    envelope) + successEnvelope<T> factory for typed unwrap.
- Refactor core/api/errors.ts: ApiError now carries canary error codes
  (VALIDATION_ERROR, TURNSTILE_FAILED, BAD_CURSOR, etc.) and a
  fields: Record<string,string> map. transformAxiosError parses the
  canary envelope first, falls back to HTTP status code if envelope
  shape is missing.
- Update core/api/query.config.ts no-retry list for canary codes
  (user-error codes plus RATE_LIMITED to avoid burning rate budget).
- Remove obsolete template apiClient at core/api/api.config.ts; the
  new canary client lives at api/client.ts (Phase 14 next commit).
2026-05-17 12:55:10 -04:00
CarterPerez-dev 697f4909d7 fix(canary-phase1): clear all post-phase-1 audit observations + header normalization
No 'logged for later' — every non-blocking finding from the Phase 1 audits
is fixed now, in this commit, before declaring the phase truly closed.

Code cleanups (5 items):
1. Database.SQLDB() *sql.DB accessor on core.Database; main.go uses
   db.SQLDB() instead of the awkward db.DB.DB triple-chain.
2. Hoisted list-default literals (50, 20) to package-level
   defaultListLimit consts in token + event repositories. MEMORY.md
   'no magic numbers' rule honored.
3. Moved ptr[T any] helper to internal/testutil/ptr.go as testutil.Ptr;
   removed local copies from token + event repository_test.go.
4. Renamed CHECK constraints in 0001_create_tokens.sql to match spec
   text exactly: chk_token_type → chk_type, chk_alert_channel → chk_channel.
   Spec was the original contract; impl now aligns.
5. Resolved APP_ENVIRONMENT vs ENVIRONMENT divergence per spec §12.1:
   - compose.yml: ENVIRONMENT=production → APP_ENVIRONMENT=production
   - dev.compose.yml: ENVIRONMENT=development → APP_ENVIRONMENT=development
   - config.go envKeyMap: ENVIRONMENT → APP_ENVIRONMENT mapping

Concurrency bug fix (real, not just polish):
- core/migrations.go: added sync.Mutex around goose calls. goose's
  package-level state (SetBaseFS, SetDialect) raced under t.Parallel()
  testcontainers tests. Race detector caught it under -race after
  parallel test counts climbed. Mutex serializes goose invocation
  globally; testcontainer-parallelism otherwise unaffected.

Header normalization (50 files):
- Bulk-normalized every project file's header to canonical
  '©AngelaMos | 2026' (no space, with © glyph, year 2026) per MEMORY.md
  style rule. Eliminated 4 distinct non-canonical variants: '// AngelaMos',
  '// ©AngelaMos | 2025', '// © AngelaMos | 202X', '# AngelaMos'.
- Verified clean: grep returns zero non-canonical headers across the
  whole project tree (excluding gitignored docs/ and node_modules/).

Backlog discipline:
- Created docs/plans/BACKLOG.md with strict format: open items only,
  HIGH/MEDIUM/LOW severity, must-clear-before-ship contract.
- BACKLOG currently has zero open items — every observation was fixed
  here, not deferred. Closed-items section logs what was cleared.

Verification:
- go build ./...                                     clean
- go vet ./...                                       clean
- go test -tags=integration -race ./internal/token/...  11/11 PASS
- go test -tags=integration -race ./internal/event/...   9/9 PASS
- docker compose -f compose.yml config              parses
- docker compose -f dev.compose.yml config          parses
- grep for non-canonical headers                     zero matches
2026-05-10 06:15:26 -04:00
CarterPerez-dev a4811d1c04 feat(canary): import operator's React + Vite frontend template
Frontend stack (pre-rebranded by operator before Phase 0):
- React 19 + react-router-dom v7
- TypeScript 5.9 with project references
- Vite 7 (rolldown-vite — Rust port)
- TanStack Query v5 + axios for data
- Zustand v5 for client state
- Zod v4 for runtime validation
- SCSS (Sass), not Tailwind — operator stylistic choice
- Biome 2 (lint + format), Stylelint for SCSS
- react-error-boundary, sonner (toasts)
- react-icons

Source layout (operator-authored skeleton):
- src/App.tsx, main.tsx, config.ts, styles.scss
- src/api/{hooks,types,index.ts}        TanStack Query hooks
- src/components/                        UI primitives (operator-designed later)
- src/core/{api,app,lib}                 axios client, routers, utilities
- src/pages/                             page-level containers
- src/styles/                            SCSS partials

Per implementation plan §16.3, Phase 14 stops at API client only —
operator owns visual/component design from there.

Pre-commit hooks fixed:
- site.webmanifest: missing trailing newline
- index.html: trailing whitespace stripped
- .stylelintignore + stylelint.config.js: chmod -x (no shebang, not executable)

node_modules/ is gitignored.
2026-05-10 05:28:09 -04:00