Commit Graph

922 Commits

Author SHA1 Message Date
CarterPerez-dev a86939f412 chore(canary-phase0): bootstrap & cleanup complete
Phase 0 deliverables (audited via superpowers:code-reviewer + general-purpose):

Implementation (8 commits + 1 audit-fix commit):
- 5ccabe00  Comprehensive .gitignore (Go, Node, env, OS, builds, caches)
- 382890cb  Rebrand no-auth-template → canary-token-generator across compose
- 7fa2861e  Strip JWT/users, rename module to project-local path, rewrite main
- 8b70bfbb  Merge backend compose into project-root (single deployment unit)
- a5c8d171  canary.dev (Air) + canary.prod (distroless static) Dockerfiles
- 369c9548  Unified Justfile with frontend/backend/lint/compose/tunnel groups
- a2d6f09a  Project-root .env.example + idempotent init.sh
- a4811d1c  Import operator's React + Vite frontend template
- 98c00689  Address audit findings (vite.prod paths, broken backend/Dockerfile,
            backend/Justfile, admin AuthService residue, header glyphs)

Verification at phase boundary:
- go build ./... + go vet ./...    clean
- docker compose -f compose.yml config + dev.compose.yml config + cloudflared overlay  all parse
- just --list parses, randomized host ports preserved (22784, 58495, 15723, 5447, 6022, 16686, 4317, 4318)
- grep returns zero hits for: react-scss, carterperez-dev/templates, JWTConfig,
  JWT_PRIVATE, lestrrat, InvalidateAllSessions
- backend has no host port in production compose
- module path: github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend

Deliberately deferred (logged for later phases):
- Template-imported Go files (server.go, logging.go, request_id.go, headers.go,
  ratelimit.go, health/handler.go, core/*.go, config/config.go) carry
  '// AngelaMos | 2026' headers without ©. Out of Phase 0 scope. Bulk hygiene
  pass possible later.
- frontend/vite.config.ts and frontend/stylelint.config.js carry 2025 in their
  headers (operator-imported template files). Same deferred bucket.
- backend/internal/admin/handler.go now has handler-only structure; OperatorBearer
  middleware wiring is Phase 12.
- Recovery middleware not yet present in main.go middleware chain — Phase 9
  introduces it alongside the token domain.
- config.go envKeyMap does not yet know about canary-specific env vars
  (PUBLIC_BASE_URL, TURNSTILE_SECRET, OPERATOR_TOKEN, GEOLITE_PATH,
  WEBHOOK_HMAC_SECRET, MYSQL_FAKE_ENABLED). compose.yml passes them in via
  ENV but config.go cannot read them yet — Phase 9 extends this.

Phase 0 complete. Next: Phase 1 (database schema + repositories).
2026-05-10 05:38:27 -04:00
CarterPerez-dev 98c006897d fix(canary-phase0): address audit findings before phase rollup
Phase 0 audit (superpowers:code-reviewer + general-purpose) returned FAIL
on shared findings. This commit resolves the blockers + important items.

Blockers:
- infra/docker/vite.prod: replace 'react-scss/' COPY paths with 'frontend/'
  (would have broken 'docker compose build nginx')
- Delete backend/Dockerfile (broken template debris referencing deleted
  cmd/api, missing migrations/, deleted keys/)
- Delete backend/Justfile (docker compose recipes pointed at deleted
  backend/compose.yml + backend/dev.compose.yml; project-root justfile
  [backend] group covers everything operationally)
- backend/internal/admin/handler.go: strip the residual JWT/auth scaffolding
  (AuthService interface, authSvc field, authenticator+adminOnly mw params).
  Phase 12 will gate /admin under OperatorBearer; for now RegisterRoutes
  takes only chi.Router.
- Delete backend/.env + backend/.env.example: stale template duplicates
  containing only template keys; project-root .env.example is the source
  of truth per spec §12.4.

Important:
- Add © glyph to file headers in cloudflared.compose.yml, infra/docker/
  vite.{dev,prod}, backend/.gitignore (operator's standing rule:
  '©AngelaMos | 2026', not 'AngelaMos | 2026')
- backend/.gitignore: drop stale 'keys/*.pem' / 'keys/*.key' entries
  (backend/keys/ no longer exists)
- scripts/init.sh: rename 'local_tmp' variable to 'tmp_dir' to remove
  visual ambiguity with the 'local' bash keyword (both audit agents
  briefly misread it as 'local local_tmp=...')
- .env.example: add inline comment explaining the dual NGINX_HOST_PORT
  defaults (22784 prod compose, 58495 dev compose; do not override in
  shared .env)

Deferred (logged but not blocking Phase 0):
- Pre-existing template Go files (internal/{config,core,server,health,
  middleware/{request_id,logging,headers,ratelimit},admin}/*.go) carry
  '// AngelaMos | 2026' without ©. Bulk-fixing these belongs to a
  hygiene pass; not Phase 0 scope.
- frontend/vite.config.ts and frontend/stylelint.config.js reference
  '2025'; operator-imported template files, separate concern.
- APP_ENVIRONMENT vs ENVIRONMENT spec/impl mismatch: spec §12.1 sample
  uses APP_ENVIRONMENT; envKeyMap in config.go uses ENVIRONMENT (template
  default). Implementation is internally consistent. Logged in plan
  Appendix C as a non-blocking spec amendment candidate.

Verification: go build ./... + go vet ./... clean. grep returns no
hits for react-scss / carterperez-dev/templates / JWTConfig /
InvalidateAllSessions across backend/, infra/, scripts/, compose files.
2026-05-10 05:37:32 -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
CarterPerez-dev a2d6f09ab3 feat(canary): add project-root .env.example + idempotent init.sh
.env.example covers every env var the canary stack reads:
- Public: APP_NAME, NGINX_HOST_PORT, PUBLIC_BASE_URL, VITE_APP_TITLE, VITE_API_URL
- Anti-bot: TURNSTILE_SITE_KEY/SECRET + VITE_TURNSTILE_SITE_KEY (frontend mirror)
- Operator: OPERATOR_TOKEN (admin endpoints)
- DB: POSTGRES_PASSWORD + POSTGRES_DEV_PORT
- Cache: REDIS_DEV_PORT
- GeoIP: MAXMIND_ACCOUNT_ID/LICENSE_KEY (optional)
- Webhooks: WEBHOOK_HMAC_SECRET (optional)
- Fake MySQL: MYSQL_FAKE_ENABLED + MYSQL_HOST_PORT (optional, off by default)
- Logging: LOG_LEVEL, LOG_FORMAT
- Telemetry: OTEL_ENABLED, OTEL_EXPORTER_OTLP_ENDPOINT + Jaeger ports
- Tunnel: CLOUDFLARE_TUNNEL_TOKEN (for cloudflared.compose.yml)

scripts/init.sh rewritten as idempotent setup helper:
- Copies .env.example -> .env on first run
- Generates POSTGRES_PASSWORD + OPERATOR_TOKEN via openssl rand -hex 32 if blank
- Skips already-set values (idempotent)
- Conditionally fetches GeoLite2-City.mmdb when MAXMIND creds present
- All sed usage is operator-side (script run-time), not Claude tool-time

scripts/randomize-ports.sh kept as-is (operator helper for spinning up
sibling projects on non-conflicting ports).
2026-05-10 05:27:30 -04:00
CarterPerez-dev 369c954892 feat(canary): unified Justfile with frontend + backend + compose groups
Replaces template Justfile (which referenced ./react-scss/). Groups:

[frontend]   pnpm install/dev/build/preview, biome, stylelint, tsc
             Targets at fe-install/fe-dev/fe-build/fe-preview prefixed for clarity
[backend]    go mod tidy, vet, golangci-lint, test (unit + integration tags),
             coverage HTML, run, static build, air hot reload
[lint]       lint = be-lint + biome + stylelint + tsc;  ci = lint + test
[compose]    docker compose lifecycle for prod (up/start/down/stop/build/logs/ps)
[tunnel]     overlay with cloudflared.compose.yml for prod+tunnel
[dev]        docker compose lifecycle for dev.compose.yml
[util]       init (gen secrets + fetch GeoLite2), ports, info, clean

Justfile parses cleanly via 'just --list'. Frontend dir reference fixed
(now ./frontend, not ./react-scss).
2026-05-10 05:25:27 -04:00
CarterPerez-dev a5c8d17134 feat(canary): add canary.dev + canary.prod Dockerfiles + import infra/
Production Dockerfile (infra/docker/canary.prod):
- Multi-stage: golang:1.25-alpine builder → distroless/static:nonroot final
- CGO_ENABLED=0, -trimpath, -ldflags='-s -w' → minimal static binary
- Embeds config.yaml; runs as nonroot user
- ENTRYPOINT /canary, CMD -config /config.yaml
- EXPOSE 8080

Development Dockerfile (infra/docker/canary.dev):
- golang:1.25-alpine + air-verse/air for hot reload
- Source bind-mounted by dev.compose.yml; .air.toml drives rebuilds
- go mod download cached at image-build time, dep refresh at runtime if changed
- EXPOSE 8080

Also imports the rest of the template's infra/ that was untracked:
- infra/docker/vite.dev + vite.prod (existing, unchanged)
- infra/nginx/{nginx.conf, dev.nginx, prod.nginx, nginx.prod.conf}
  These will be extended in Phase 15 with /api, /c, /k upstream blocks.
2026-05-10 05:24:42 -04:00
CarterPerez-dev 8b70bfbba9 chore(canary): merge backend compose into project root
Single project-root compose.yml + dev.compose.yml manage all services.

Production stack (compose.yml):
- nginx          public ingress on \${NGINX_HOST_PORT:-22784}:80
- canary         Go backend, NO host port (reachable only via nginx)
                 distroless/static:nonroot Dockerfile
                 healthcheck → /healthz, depends on postgres + redis
- postgres       postgres:18-alpine, no host port, named volume pgdata
- redis          redis:7-alpine, no host port, named volume redisdata
- (geolite vol)  read-only mount /data for GeoLite2-City.mmdb

Dev stack (dev.compose.yml):
- nginx          dev ingress on \${NGINX_HOST_PORT:-58495}:80
- frontend       Vite HMR on \${FRONTEND_HOST_PORT:-15723}:5173
- canary         Air hot-reload (canary.dev Dockerfile, bind-mounts ./backend)
                 OTel exports to jaeger:4317
- postgres       host port \${POSTGRES_DEV_PORT:-5447}:5432 for psql access
- redis          host port \${REDIS_DEV_PORT:-6022}:6379 for redis-cli access
- jaeger         UI 16686, OTLP gRPC 4317, OTLP HTTP 4318
- volumes        gocache + gomodcache speed up rebuilds

All randomized host ports preserved exactly:
  22784 (prod nginx), 58495 (dev nginx), 15723 (vite), 5447 (pg dev),
  6022 (redis dev), 16686/4317/4318 (jaeger).

Backend has no host port in production: nginx is the single entrypoint.
Cloudflare Tunnel sidecar terminates at nginx:80 via cloudflared.compose.yml.

backend/compose.yml + backend/dev.compose.yml deleted (merged here).

Validation:
- docker compose -f compose.yml config           — OK
- docker compose -f dev.compose.yml config       — OK
- docker compose -f compose.yml -f cloudflared.compose.yml config — OK
2026-05-10 05:23:55 -04:00
CarterPerez-dev 7fa2861e7a feat(canary): backend bootstrap — strip JWT/users, rename module, rewrite main
Removes the template's JWT auth + user domain (Phase 0 §0.4):
- Deleted backend/internal/auth/ (entire JWT auth domain)
- Deleted backend/internal/user/ (user CRUD domain)
- Deleted backend/keys/ (JWT signing keys directory)
- Deleted backend/internal/middleware/auth.go (Authenticator + RequireAdmin)
- Deleted Justfile generate-keys recipe (no JWT keys to generate)
- Removed lestrrat-go/jwx/v3 + transitive deps via go mod tidy
- Stripped JWTConfig type, defaults, env mappings, validators from config.go
- Removed jwt: section from config.yaml
- Removed JWT_* lines from backend/.env and .env.example

Renames Go module to project-local path (Phase 0 §0.5):
  github.com/carterperez-dev/templates/go-backend
  → github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend
- Rewrote imports in 6 remaining .go files using Edit tool (NEVER sed per repo rule)
- Updated .golangci.yml local-prefixes + gci section ordering

Renames cmd/api → cmd/canary and rewrites main.go (Phase 0 §0.6):
- mv cmd/api cmd/canary
- Rewrote cmd/canary/main.go as canary bootstrap (config, telemetry, db,
  redis, middleware chain, health, /api stub) — no auth wiring
- Updated .air.toml cmd path
- Updated backend/Justfile run/build targets to cmd/canary + bin/canary
- Renamed docker-build image tag to canary-token-generator:latest

Rebrands defaults:
- config.go: app.name "Go Backend" → "Canary Token Generator"
- config.go: otel.service_name "go-backend" → "canary-token-generator"
- config.yaml: app.name "Go Backend Template" → "Canary Token Generator"
- backend/.env(.example): OTEL_SERVICE_NAME → canary-token-generator

Drops user-aware rate-limit helpers from middleware/ratelimit.go:
- Removed KeyByUser, KeyByUserAndEndpoint, normalizeEndpoint, isUUID, isNumeric
- Removed TierConfig, DefaultTiers, TieredRateLimiter (referenced GetUserID)
- KeyByIP, NewRateLimiter, PerMinute/PerSecond/PerHour preserved

Verification:
- go build ./...    — clean
- go vet ./...      — clean
- go mod tidy       — silent (deps consolidated)
- grep -r "carterperez-dev/templates|JWTConfig|JWT_PRIVATE|lestrrat" → empty

backend/compose.yml + backend/dev.compose.yml are still present here; they
get merged into project-root compose files in Task 0.7 (next commit).
2026-05-10 05:22:08 -04:00
CarterPerez-dev 382890cb56 chore(canary): rebrand from no-auth-template to canary-token-generator
- compose.yml: APP_NAME default → canary-token-generator
- dev.compose.yml: APP_NAME default → canary-token-generator
                   ./react-scss bind paths → ./frontend (template flatten)
- cloudflared.compose.yml: APP_NAME default → canary-token-generator
- VITE_APP_TITLE default in both compose files → "Canary Token Generator"

Frontend package.json and index.html were already rebranded by operator
between sessions. All three compose stacks now validate via docker compose
config (prod, dev, prod+tunnel overlay).
2026-05-10 05:15:09 -04:00
CarterPerez-dev 5ccabe00b5 chore(canary): expand project .gitignore for Go + Node + secrets
- Ignore docs/ (dev-only planning material)
- Ignore env files except .env.example
- Ignore backend Go build/test artifacts (bin, tmp, coverage)
- Ignore frontend Node + Vite caches and dist
- Ignore local data volumes (geoip mmdb, sqlite databases)
- Ignore OS/editor cruft and lint caches
2026-05-10 05:13:19 -04:00
CarterPerez-dev 7d69339413 fix: extract osv package string literals to constants
goconst counts occurrences across the whole package (including test files)
when deciding whether to flag a non-test file — the exclusion only suppresses
reports FROM test files. Add unexported constants for severity levels, CVSS
types, ecosystem, and reference types in client.go, and update client_test.go
to use them so the total raw-string count per literal drops below threshold.
2026-05-08 06:30:08 -04:00
CarterPerez-dev 25671b4f9d fix: resolve simple-vulnerability-scanner goconst failures
Migrate .golangci.yml from v1 issues.exclude-rules to v2
linters.exclusions.rules — the v1 key was silently ignored by golangci-lint
v2, so test-file goconst exclusions weren't applied. With the config fixed,
all test-file violations disappear. Extract severity constants in output.go
to fix the 4 remaining non-test goconst violations (CRITICAL/HIGH/MODERATE/
LOW each appear 3x in severityRank, severityBreakdown, and severityColorFn).
2026-05-08 06:21:26 -04:00
CarterPerez-dev 5863be7dac fix: pin pnpm to v10 in CI and add .npmrc to all frontend projects
pnpm latest on Node 22 resolves to pnpm 11 which rejects lockfileVersion
9.0 lockfiles with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH. Pinning to pnpm 10
keeps compatibility with existing lockfiles. .npmrc sets
strict-dep-builds=false so build-script warnings don't hard-fail the
frozen install.
2026-05-08 05:46:38 -04:00
CarterPerez-dev adeec9b36d fix: resolve CI lint failures across frontend and Go projects
Node.js bumped to 22 in lint workflow (pnpm 11.x requires >=22.13).
Extract goconst-flagged string literals in secrets-scanner to named
constants. Carry along monitor-dashboard docker/package fixes.
2026-05-08 05:40:04 -04:00
Carter Perez ca8382482b
Merge pull request #208 from CarterPerez-dev/project/monitor-the-situation-dashboard
add learn folder to IM MONITORING THE SITUATION DASHBOARD project
2026-05-08 04:55:09 -04:00
CarterPerez-dev 1b9fcbac13 add learn folder to IM MONITORING THE SITUATION DASHBOARD project 2026-05-08 04:43:29 -04:00
Carter Perez 95f5622320
Merge pull request #205 from CarterPerez-dev/dependabot/npm_and_yarn/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/axios-1.15.0
chore(deps): bump axios from 1.13.2 to 1.15.0 in /PROJECTS/advanced/monitor-the-situation-dashboard/frontend
2026-05-04 19:52:33 -04:00
dependabot[bot] 509c79a3d1
chore(deps): bump axios
Bumps [axios](https://github.com/axios/axios) from 1.13.2 to 1.15.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.13.2...v1.15.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-04 22:59:57 +00:00
Carter Perez 1a04003ce1
Merge pull request #204 from CarterPerez-dev/project/monitor-the-situation-dashboard
Project/monitor the situation dashboard
2026-05-04 18:56:41 -04:00
CarterPerez-dev 5e7f2c36de I'm being gangstalked by the CIA, they put a chip in my brain 2026-05-04 18:53:54 -04:00
Carter Perez 480503dbfa
Update README.md 2026-05-04 03:48:28 -04:00
CarterPerez-dev 6e35f49d2a feat(monitor/backend): swpc emits rich space_weather payload (kp + bz_gsm + speed/density + xray flux/class) 2026-05-03 19:39:41 -04:00
CarterPerez-dev 6f062bf49b feat(monitor/frontend): earthquake + outage panels (left + right column, USGS / CF Radar) 2026-05-03 19:25:03 -04:00
CarterPerez-dev 887dc217d2 fix(monitor/frontend): drop --type-body 13px → 12px (more density for table data; KPI/hero numbers unchanged) 2026-05-03 19:18:03 -04:00
CarterPerez-dev 0695f5d9a4 fix(monitor/frontend): bgp panel CC column wider (HK was truncating to H...) 2026-05-03 19:15:26 -04:00
CarterPerez-dev cf38f88efd fix(monitor/frontend): supplementary centroids (HK et al) + bgp schema lenient (victimAsns nullable, dedup geo/threat types) 2026-05-03 19:11:12 -04:00
CarterPerez-dev 0f53e67236 feat(monitor/frontend): bgp hijack panel + globe plot via abuseipdb-enriched country 2026-05-03 19:06:11 -04:00
CarterPerez-dev c25fd57eaf feat(monitor/frontend): dashboard layout — CVE Velocity to left under ISS, BGP HIJACKS to right top 2026-05-03 19:05:45 -04:00
CarterPerez-dev 0705f37330 fix(monitor/frontend): dshield panel (topports record has date/limit siblings of port entries) 2026-05-03 18:54:34 -04:00
CarterPerez-dev 038de64605 fix(monitor/backend): gdelt switch to timelinevol mode (article-snippet source of malformed escapes is gone) 2026-05-03 18:42:27 -04:00
CarterPerez-dev e216331aeb feat(monitor/backend): abuseipdb enrichment for bgp hijacks (country + abuse_score in payload) 2026-05-03 18:20:15 -04:00
CarterPerez-dev e690b30e92 feat(monitor/frontend): country-centroid plotting for ransomware + internet_outage on globe 2026-05-03 18:11:15 -04:00
CarterPerez-dev 0a41102e75 fix(monitor/frontend): globe snapshot seed + pushPoint dedup-by-id (instant eq+iss dots, single moving iss) 2026-05-03 17:56:50 -04:00
CarterPerez-dev a6dcaed7dc feat(monitor/frontend): globe country outlines (thin --fg-4 stroke, local 110m geojson) 2026-05-03 17:49:55 -04:00
CarterPerez-dev aa9a24bdd0 feat(monitor/frontend): dashboard lifecycle hook (snapshot bootstrap → WS → typed routing)
useDashboardLifecycle() orchestrates the three pieces that make the
dashboard alive:

1. Audio gesture unlock — unlockOnFirstGesture() on mount so playChime()
   is ready when Phase 6 wires alert triggers.
2. Snapshot-then-WS — useSnapshot()'s isSuccess gates createDashboardWS
   so we never open the WS before initial state is in TanStack cache
   (per spec §10.2 race resolution). After connect+setReady the server
   stops buffering and starts streaming deltas.
3. Globe ring eviction — every 5min calls useGlobeEvents.evict(now)
   so expired pulse rings drop off.

Topic routing (typed switch, no `as any` — every payload narrows to a
local interface that matches the verified backend shape):
  cve_new          → useCveStore.push
  kev_added        → useKevStore.push
  ransomware_victim → useRansomwareStore.push
  coinbase_price   → usePrices.pushTick (ISO ts → number, snake_case
                     volume_24h → camelCase)
  earthquake       → globe point + ring pulse (4s TTL)
  iss_position     → globe point (id='iss-current' so each tick replaces
                     the previous) + setQueryData(iss_position) so the
                     ISS panel sees the latest
  wiki_itn         → ticker push (source 'Wikipedia')
  gdelt_spike      → ticker push (source 'GDELT', headline includes
                     z-score and count)
  space_weather/internet_outage/bgp_hijack/scan_firehose → setQueryData
                     to merge into snapshot (panels read from there)
  heartbeat        → no-op (connection liveness only)

Small bug fix in browserDriver: WebSocket.send() throws when readyState
is CONNECTING. createDashboardWS.setReady() can be called before the
socket opens (we call it immediately after snapshot resolves), so the
inner sock.send is now guarded by a readyState===OPEN check. The
onOpen handler in createDashboardWS resends init when the socket
actually opens — belt-and-suspenders.

This is the load-bearing wiring per the plan. Panels stop rendering
single snapshot rows and start growing as WS events arrive.
2026-05-03 09:47:40 -04:00
CarterPerez-dev 71e0529960 feat(monitor/frontend): about modal (meme + project explainer, ethos-minimal)
Native <dialog> opened from the TopStrip ? icon via useUIStore.aboutOpen.
Ref-driven showModal()/close() in useEffect, onClose synced to the store
so Escape closes both the dialog and the store flag in one round trip
(no listener fight). Three short paragraphs: the meme origin, the data
sources, and the keyboard shortcuts. The kbd-styled spans (mono +
1px --fg-4 outline) call out F and Esc.

No animation on open/close beyond the browser's default <dialog>
showModal transition (which is OS UI, not decoration). No backdrop-click
to close — Escape and the close button are enough; backdrop-click adds
a fragile target===dialog comparison for marginal value. ::backdrop
gets a 60% black tint so it reads as a modal, not a layer cake.

prose line-height 1.4 (looser than the 1.25 table tightness because
prose needs readability — operator-density still applies but Tufte
data-ink isn't violated by paragraph leading).

Plan 5 Task 23 Design QA: no illustration, no confetti, no tooltip
arrows, monochrome border, native ::backdrop only.
2026-05-03 09:43:27 -04:00
CarterPerez-dev 21f54390df feat(monitor/frontend): audio chime infrastructure (load/unlock/play, phase 6 wires the trigger)
Per spec §10.5 — three pure functions sitting on top of the existing
useAudioStore (so React surfaces can subscribe to .unlocked if a
"click anywhere to enable audio" toast is needed later):

  loadChime(file)              fetch / decode arraybuffer into AudioBuffer
  unlockOnFirstGesture()       attach one-shot pointerdown listener that
                               resumes a suspended AudioContext, sets
                               unlocked=true. Idempotent across re-mounts
                               via module-level listenerAttached guard
                               (StrictMode double-mount safe).
  playChime() → boolean        returns false if the audio context isn't
                               ready / buffer not loaded / not unlocked,
                               so callers can fall back to a toast.

Phase 6 calls playChime() on alert match; Phase 5 only wires the
unlockOnFirstGesture() call into the dashboard lifecycle (Task 24)
so the audio is ready when Phase 6 lands.
2026-05-03 09:41:22 -04:00
CarterPerez-dev 43df53f845 feat(monitor/frontend): ISS panel (live position + next-pass placeholder for phase 6)
Four-row layout: Lat/Lon (degree symbol, mono), Alt (km, mono tabular),
Vel (km/h with thousands separator, mono tabular), Next Pass (— stub).

Reads snapshot.iss_position directly (no per-panel store — current
position is the only thing the panel cares about, and the wheretheiss.at
collector pushes a complete state every 10s, so latest-event semantics
match what the panel needs). Real shape verified: latitude/longitude/
altitude/velocity/timestamp/fetched_at — all numeric except fetched_at
ISO string. Stale threshold 30s (3x the 10s poll cadence).

Next Pass row stays "—" muted --fg-3 — Phase 6 wires the SGP4-driven
user-observer-location pass prediction per spec §10.4. No "sign in to
add observer" CTA: operator UI doesn't onboard.

Plan 5 Task 21 Design QA: degree symbol + mono coords, alt/vel mono
tabular, no decorative ISS-tracing widget (the centerpiece globe owns
that), next-pass row visible-but-muted (never hidden).
2026-05-03 09:40:02 -04:00
CarterPerez-dev f0bb0dcb8d feat(monitor/frontend): space weather panel (Kp 9-bar, solar wind, X-ray flux)
Three-row layout (label | value | secondary or 9-bar). Kp index renders as
a 9-segment grid bar — segments below kp filled --fg-2, at-or-above outlined
--fg-4. When kp >= 7 (G3+ geomagnetic storm) the entire filled portion goes
amber AND the panel's stale dot fires amber — one of the few legitimate
amber-data uses per ethos. X-ray class M or X also flips amber.

Backend snapshot caveat: the SWPC collector currently emits only
{ts, pushed} as the WS event payload — the actual kp / bz_gsm / speed_kms
/ density / xray_class / xray_flux readings live in Redis ring buffers
(per spec §6.2) but aren't yet projected into snapshot.space_weather.
The TS interface defines all fields optimistically; the panel renders
"—" / empty 9-bar for missing fields. When the backend extends the SWPC
event payload (or a separate snapshot enrichment lands), the panel
auto-fills with no frontend changes.

KP_SEGMENT_KEYS is an explicit string array instead of Array.from(...,
i) to avoid biome's noArrayIndexKey warning while preserving the
positional segment semantics.

Plan 5 Task 20 Design QA: Kp bar uses ONLY --fg-2 / --fg-4 / --amber
(no other colors), values mono tabular, X-ray class letter sans, no
sun glyph / aurora photo / weather-forecast iconography.
2026-05-03 09:38:52 -04:00
CarterPerez-dev b67729ae13 feat(monitor/frontend): ETH panel (mirror of BTC, separate file pre-abstraction)
Mirrors BTCPanel structure 1:1 with SYMBOL='ETH-USD' and the Coinbase
ethereum price page as the raw link. Per Plan 5 Task 19: don't refactor
into a shared <PricePanel symbol="..." /> until Phase 6 — 50 lines of
duplication beats locking ourselves into a shared contract before BTC
and ETH might diverge in display (gas-fee tile for ETH only, etc.).

Same monochrome discipline: --fg-1 hero price, △/▽ glyph + sign for
direction, no green/red, no animated count-up. 24H change deferred for
the same reason as BTC (60-min history cap can't carry 24h yet).
2026-05-03 09:35:52 -04:00
CarterPerez-dev 71ea453b82 feat(monitor/frontend): BTC panel (hero price + glyph-based change indicator, no color direction)
Reads from the existing usePrices store (latest tick + 60-min history).
Seeds latest from snapshot.coinbase_price on mount, converting the ISO ts
string and snake_case volume_24h to the store's number-ts and camelCase
volume24h shapes (verified against live snapshot).

Hero price uses --type-num-xl mono with tabular-nums — the dashboard's
biggest single number by design. Color stays --fg-1 regardless of
direction. The 1H change row uses △ / ▽ glyph + sign for direction
(not green/red) per the ethos no-branded-color-per-metric rule.
Compute is (last - first) / first * 100 over the 60 minute-bar closes.

Stale indicator goes amber when last tick > 60s ago (Coinbase WS expects
sub-second updates; 60s of silence = real staleness).

24H change is deferred — would need backend snapshot to send 24h of
minute bars; the store caps at 60. Plan said both 1H and 24H but 24H
isn't computable from the available data yet. Showing only 1H is
honest — the missing row would be permanent "—" otherwise.

Sparkline is the 60-minute close ladder. Renders null while history is
shorter than 2 points (Sparkline primitive's own contract). Empty
panel surface is fine — operator UI doesn't onboard.

Plan 5 Task 18 Design QA: hero is the largest single number, price
color stays --fg-1, change row uses glyph + sign (no color), sparkline
is single-stroke currentColor with no fill or axes, no animated
count-up on price change.
2026-05-03 09:34:58 -04:00
CarterPerez-dev 9d391434e0 feat(monitor/frontend): ransomware panel (victim feed + 600ms data-changed flash)
RansomwareStore (zustand) accumulates RansomwareVictim items dedup-by
composite key (post_title|group_name|discovered) — the JSON payload has
no id field (the backend computes one via Row.ID() but doesn't include
it in the WS payload), so the composite key is what we have. Capped 200.

6-row tight table — victim mono primary, group sans --fg-2, country
sans --fg-3, ago mono muted right-aligned. table-layout fixed +
ellipsis. Hover --bg-row-hover.

Flash on new arrival: a useRef<Set> tracks seen composite keys; new
items not in the set get added to a flashKeys state for 600ms, which
applies a row-flash keyframe animation that fades from --fg-4
background to transparent. Per ethos motion=meaning rule — the only
panel motion allowed because new ransomware victims literally are
"data changed." Initial seed (when seenKeys is empty) does NOT trigger
a flash — the first batch is "what's there now," not "new arrivals."

No skull / lock / "RANSOMWARE!!" iconography (per Plan 5 Task 17 Design
QA). No colored severity. After flash, row visually identical to others.

Real shape verified against live snapshot.ransomware_victim: snake_case
post_title/group_name/discovered/country/activity/website/description.
TS interface matches.
2026-05-03 09:32:20 -04:00
CarterPerez-dev 4724f7fe92 feat(monitor/frontend): KEV panel (recent CISA additions, 6-row tight table)
KevStore (zustand) accumulates KevEntry items dedup-by-cveID, capped at
200. Panel seeds the store from snapshot.kev_added on mount.

6-row table — CVE-ID mono / vendor·product sans (--fg-2) / dateAdded mono
muted (--fg-3). table-layout: fixed + per-column widths (38% / 40% / 22%)
+ ellipsis on overflow. Hover --bg-row-hover (subtle, no transition).

No severity column, no fluff per the plan: every row in this catalog is
a known-exploited vulnerability, the existence of the row carries the
weight. No "ransomware-use" badge — that's the separate Ransomware panel's
job. No CTA — operator clicks the catalog via the raw link.

Real KevEntry shape verified against live snapshot.kev_added: camelCase
keys cveID/vendorProject/product/vulnerabilityName/dateAdded/dueDate/
knownRansomwareCampaignUse/shortDescription/requiredAction. Mirrored
exactly in the TS interface (no transform layer where bugs hide).

Plan 5 Task 16 Design QA: six rows max, CVE-ID mono primary text, vendor
· product body sans secondary, date mono muted, no severity badge.
2026-05-03 09:22:02 -04:00
CarterPerez-dev 79cbdaaa51 feat(monitor/frontend): CVE velocity panel (1h/6h/24h KPIs + 24h sparkline + recent table)
CveStore (zustand) accumulates CveEvent items dedup-by-CveID, capped at
500. The panel seeds the store from snapshot.cve_new on mount via
useEffect — Task 24's WS routing will extend this with live deltas.

Three KPIs derived in-component from the store (1h/6h/24h counts, mono
tabular --type-num-l). 24h sparkline computed from per-hour bucketing
of items[].Published (one of the few places a chart is justified —
shape-of-CVE-velocity IS the insight per ethos rule on charts). Bottom
table shows the 5 most recent: CVE-ID mono, severity sans plain (NOT a
colored badge — severity stays monochrome per ethos), EPSS percentile
mono right-aligned, relative-time mono muted right-aligned. table-layout:
fixed + per-column widths + ellipsis to keep rows on a single line in
the 320px column.

Real CveEvent shape matches the backend Row struct (verified against
live snapshot): PascalCase keys CveID/Published/LastModified/Severity/
CVSS/EPSSScore/EPSSPercentile/InKEV. Earlier plan-author assumption of
{recent_24h:[]} was wrong — snapshot delivers a single latest event,
the running list builds in the store over time.

Plan 5 Task 15 Design QA: KPIs mono tabular, sparkline pure stroke
(currentColor, no fill/axes), severity NOT a colored badge, EPSS mono %,
ago muted, no "view all" CTA — operator clicks NVD via the raw link.
2026-05-03 09:20:58 -04:00
CarterPerez-dev 9ea40198d0 fix(monitor/frontend): stack DShield tables vertically (no horizontal overflow in 320px column)
Plan 5 said "two compact tables side-by-side" but didn't account for the
320px right-column width — tables overflowed and the source-IP table's
Reports/Tgt columns scrolled off-screen. Replaces grid 1fr 1.2fr with a
flex column so each table gets the full panel width: top ports on top,
top sources below.
2026-05-03 09:16:58 -04:00
CarterPerez-dev d01fc57989 fix(monitor/frontend): DShield panel matches actual snapshot.scan_firehose shape
The panel crashed at runtime with "(ds.topports ?? []).slice is not a
function" because Plan 5 assumed shapes that didn't match what the Phase 2
DShield collector actually emits. Reality:

  topports: Record<string, Port>      // dict keyed "0".."N", not Array
  port field: targetport              // not "port"
  topips[].source                     // the IP — not "ip"
  topips[].reports                    // the count — not "records"
  topips                              // has no country/CC field
  dailysummary[].date                 // ISO date string, ascending order

toArray() helper accepts both Record<string,T> and T[] defensively (so
either shape works if the backend changes its mind). Both lists sort by
rank before slicing. The CC column is dropped (data doesn't carry it);
its slot becomes the per-source target count which the data does carry.
Daily summary picks the latest entry by date instead of index 0 (the
order is ascending, so [0] would be the oldest day).

Bigger discovery: most snapshot keys (cve_new / kev_added /
ransomware_victim / coinbase_price / etc.) are LATEST-event payloads,
not the recent-list arrays Plan 5 anticipated. Tasks 15-21 will need
per-topic Zustand stores that accumulate over time, populated by Task 24
from snapshot bootstrap + WS messages. Will discuss with Carter before
committing to that architecture.
2026-05-03 09:12:36 -04:00
CarterPerez-dev a91f290595 feat(monitor/frontend): DShield panel (top ports + top source IPs, dense tables)
Two side-by-side tables fed from snapshot.scan_firehose: Top Ports (port +
24h hits) and Top Sources (IP + CC + hits). Below them a one-line muted
mono summary "Xk records · Yk sources · Z targets — last 24h." Tables, not
bar charts — operator wants to read the port number 22 / 3389, not stare
at a horizontal bar.

Port and IP and hit counts are mono with tabular-nums (digit alignment
matters). Country code stays sans (text label, not a number). Hover row
background var(--bg-row-hover) — subtle, no transform / no transition /
no theatre. Defensive empty-state — when snapshot has no scan_firehose
yet, tables render as empty (per ethos: no "no data" message).

PORT_ROW_LIMIT / SOURCE_ROW_LIMIT / THOUSAND / MILLION constants at module
scope (no inline magic). Renders in the right column as the bottom panel
per spec §11.1 column ordering.
2026-05-03 07:15:16 -04:00
CarterPerez-dev 44f0ff3c3d feat(monitor/frontend): shared Panel shell + KPI + Sparkline + StaleIndicator primitives
The shared shell every operator panel uses. 28px header strip with
UPPERCASE letter-spaced title (--type-label, --fg-2), optional muted
subtitle (--fg-3), StaleIndicator dot, and an external-link "raw"
icon → upstream source. Body fills remaining space and scrolls
internally. Border-bottom 1px --fg-4 separates panels in the column.

Primitives in shared/:
  KPI — value (mono, --type-num-l, tabular-nums, --fg-1) + label
        (UPPERCASE, --type-label, --fg-3). No card wrapper, no
        rounded corner, no background. The number IS the unit.
  Sparkline — d3-scale + d3-array driven inline SVG. Single stroke
        in currentColor, no axes / no fill / no animation. Used
        only when the shape IS the insight (per ethos). Returns
        null with <2 points so empty panels don't ship a flat line
        as a placeholder.
  StaleIndicator — 6px square dot. --ok green (fresh), --amber
        (stale, breached threshold), --fg-4 muted (unknown / no
        data yet). Title attribute carries last update UTC time
        for hover context.

RawLink not extracted yet — Panel inlines the icon since one raw
link per panel is the rule. Extract when a panel needs more.

Adds d3-scale 4.0.2 + d3-array 3.2.4 + their @types — small d3
modules (not the full umbrella) per spec §10.3 to keep bundle lean.

Plan 5 Task 13 Design QA: panel header 28px, title UPPERCASE
letter-spaced, sparkline pure stroke (no axes / fill), stale dot
6px, no border-radius anywhere, no hover scale.
2026-05-03 07:12:20 -04:00
CarterPerez-dev e3af0197b7 feat(monitor/frontend): globe centerpiece (merged-points, imperative camera, no decorative motion)
react-globe.gl 2.37 + three 0.184 added. Globe component is React.memo'd,
absolute-fills the .center grid cell, sizes itself via ResizeObserver on
the wrapper div (the library defaults to window.innerWidth/Height which
would blow past the 1247px cell). pointsMerge: true so hundreds of
points cost one ThreeJS Mesh draw call.

Layer dataset built by useGlobePoints / useGlobeRings hooks in
globeLayers.ts, projecting each store point onto color/altitude/radius
lookup tables keyed on the 6 GlobePointTypes:
  earthquake/ransomware/scan = monochrome (--fg-2/--fg-1/--fg-4)
  iss = --ok (live position is functional, not decorative)
  outage/hijack = --amber (the only globe surfaces with alarm color —
  earthquakes and ransomware stay monochrome because an M2.5 quake or
  daily victim is not user-actionable)

Rings are simple white pulses (no per-ring color decoration), single
pulse (ringRepeatPeriod=0). Camera panTo() in globeCamera.ts is the
only motion API; the dashboard lifecycle (Task 24) will trigger it on
high-mag quakes etc. Auto-rotate disabled on globe ready — motion is
meaning, a globe that spins for no reason violates the rule.

atmosphereAltitude=0.12, atmosphereColor=#1f2937 per spec §10.4 — a thin
glow that gives the sphere silhouette without becoming "3D space
ambiance." backgroundColor=rgba(0,0,0,0) so the panel cell --bg shows
through. No starfield, no nebulas. No globeImageUrl — keeps the build
self-contained (no CDN dep); we add a local texture later if geographic
context needs to be richer.

Plan 5 Task 12 Design QA: no starfield/nebula/extra glow, monochrome
quake dots, amber confined to outage/hijack, no hover tooltip on globe.
2026-05-03 07:09:44 -04:00
CarterPerez-dev 02ecc1c214 feat(monitor/frontend): F-key presentation mode + localStorage persistence
Press F → hide TopStrip + BottomTicker, drop the panel column borders, and
shrink panel columns from 320 to 280 so the globe gets more room. Esc →
exit. Mode persists to localStorage at STORAGE_KEYS.PRESENTATION_MODE so
refresh maintains it (use case: second-monitor wall).

Implementation: each chrome component (TopStrip, BottomTicker) reads
presentationMode from the ui store and early-returns null. Dashboard
applies a .presentation modifier to .root that re-templates the inner
.grid columns and zeroes the aside borders. CSS Modules-scoped — no
:global escape, no clsx dep.

isTypingInForm guards F so it doesn't fire when typing in inputs / textareas
/ contentEditable. Escape always exits — universal "get me out" semantics.
useUIStore.getState() inside the keydown handler avoids stale closure.

Plan 5 Task 11 Design QA: F hides chrome cleanly without animation flash,
globe area expands, panels stay readable, Esc restores, state persists.
2026-05-03 07:02:56 -04:00