Commit Graph

825 Commits

Author SHA1 Message Date
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 7a98ef8d72 chore(canary-phase1): database & repositories complete
Phase 1 deliverables (audited PASS by superpowers:code-reviewer + general-purpose):

Implementation (1 commit):
- b2558e2e  Postgres schema + token & event repositories + testcontainers helper

Schema:
- 3 goose migrations under internal/core/migrations/ (tokens, events, indexes)
- All spec §7.1 columns + types + CHECK constraints present
- All 8 spec indexes present, including 2 partial indexes
- FK ON DELETE CASCADE on events.token_id (verified by TestRepository_FKCascade)
- chk_token_type enforces the 7-token locked enum exactly

Repositories:
- token: Insert / GetByID / GetByManageID / DeleteByManageID / IncrementTriggerCount /
         SetEnabled / ListAll / CountAll
- event: Insert / GetByID / ListByToken (cursor pagination, LIMIT N+1 trick) /
         CountByToken / AttachFingerprint (jsonb || merge into most-recent within
         configurable window) / UpdateNotifyStatus / PruneToLimit (window function)
- Both: ErrNotFound on sql.ErrNoRows; all errors wrapped with %w; Repository struct
        + NewRepository(*sqlx.DB) constructor pattern matches template

Tests:
- 11 token integration tests + 9 event integration tests, all PASS
- testcontainers/postgres:18-alpine spins per test (~5-7s each)
- Tagged //go:build integration so plain `go test ./...` stays cheap
- Coverage: insert, get, not-found, delete with cascade, atomic increment,
  enable toggle, list pagination across 3 pages, fingerprint merge preserving
  existing extras, prune-keeps-newest, prune-rejects-zero, type CHECK rejection

Wired into main.go: core.RunMigrations(db.DB.DB) on startup; tokenRepo +
eventRepo blank-assigned (services consume them in Phase 9/10).

Audit findings (informational, non-blocking, deferred):
- Spec text says CHECK names 'chk_type'/'chk_channel'; implementation uses
  'chk_token_type'/'chk_alert_channel' (audit prompt's expected names — both
  agents endorse). Functionally identical; spec literal could be amended.
- main.go uses 'db.DB.DB' triple-chain to pass *sql.DB to goose. Visually
  awkward; a Database.SQLDB() accessor would clean it up. Cleanup pass later.
- Default list limits (50, 20) are inline literals; hoisting to package consts
  would satisfy MEMORY.md 'no magic numbers' rule. Minor.
- ptr[T any] helper duplicated across token + event test files. Could move to
  testutil. Trivial.
- spec §12.4 ENVIRONMENT vs APP_ENVIRONMENT mismatch (logged in Phase 0
  rollup) carries forward; not Phase 1's concern.

Verification at phase boundary:
- go build ./...                                clean
- go vet ./...                                  clean
- go test -tags=integration ./internal/token/...   11/11 PASS
- go test -tags=integration ./internal/event/...    9/9 PASS

Phase 1 complete. Next: Phase 2 (generator interface + webbug + 1×1 GIF).
2026-05-10 05:54:03 -04:00
CarterPerez-dev b2558e2e41 feat(canary): Phase 1 — Postgres schema + token & event repositories
Schema (3 goose migrations under internal/core/migrations/):
- 0001_create_tokens.sql: tokens table per spec §7.1
  Columns: id (varchar 12, PK), manage_id (UUID unique), type, memo,
           filename, alert_channel, telegram_bot/chat, webhook_url,
           created_at/ip/fp, enabled, trigger_count, last_triggered, metadata (jsonb)
  CHECK constraints: chk_token_type (7-token enum), chk_alert_channel (telegram|webhook),
                     chk_telegram_complete (when channel=telegram, bot+chat required),
                     chk_webhook_complete (when channel=webhook, url required)

- 0002_create_events.sql: events table
  Columns: id (bigserial PK), token_id (FK with ON DELETE CASCADE),
           triggered_at, source_ip (inet), user_agent, referer,
           geo_country/region/city/asn/asn_org, extra (jsonb),
           notify_status, notified_at
  CHECK chk_notify_status (pending|sent|failed|deduped)

- 0003_indexes.sql: full index set per spec §7.1
  tokens(created_ip), tokens(created_fp), tokens(created_at DESC),
  tokens(type), partial idx_tokens_trigger_count WHERE trigger_count > 0,
  events(token_id, triggered_at DESC), events(source_ip),
  partial idx_events_notify_pending WHERE notify_status = 'pending'

Migration runner (internal/core/migrations.go):
- go:embed migrations/*.sql baked into binary
- core.RunMigrations(*sql.DB) called from main.go after DB connect
- pressly/goose v3.27.1 (research-recommended; no dirty-state bug)

Token domain (internal/token/):
- entity.go: Token struct + typed Type/AlertChannel enums with Valid() guards
- dto.go: CreateRequest with validator/v10 tags (oneof, required_if, url, max),
          Response shape with ToResponse(triggerURL, manageURL) helper
- repository.go: Insert (RETURNING created_at + counters), GetByID,
                 GetByManageID, DeleteByManageID (FK cascade), IncrementTriggerCount,
                 SetEnabled, ListAll + CountAll. ErrNotFound on sql.ErrNoRows.
                 Named-parameter binding via sqlx.NamedExecContext.
- repository_test.go (//go:build integration): 11 tests covering insert,
  get-by-id, get-by-manage-id, not-found paths, delete with cascade,
  trigger count increment, enable toggle, list pagination, type CHECK

Event domain (internal/event/):
- entity.go: Event struct + NotifyStatus enum
- dto.go: GeoView + Response with ToResponse() flattens geo into nested object
- repository.go: Insert, GetByID, ListByToken (cursor pagination via
                 LIMIT N+1, returns NextCursor + HasMore), CountByToken,
                 AttachFingerprint (UPDATE most-recent within window using
                 jsonb || merge), UpdateNotifyStatus, PruneToLimit
                 (window function row_number() PARTITION BY token_id)
- repository_test.go (//go:build integration): 9 tests covering insert,
  cursor-paginated listing, FK cascade from token deletion, fingerprint
  merge into existing extra jsonb, notify status update, prune-to-N
  preserves newest-first, prune rejects zero limit

Test infrastructure:
- internal/testutil/postgres.go: testcontainers-go helper that spins up
  postgres:18-alpine, applies migrations via core.RunMigrations, returns
  ready *sql.DB with t.Cleanup-registered teardown. ~5-7s per test;
  20 integration tests run in ~13s end-to-end.

Wired into main.go:
- core.RunMigrations(db.DB.DB) called after NewDatabase
- token + event repos instantiated; blank-assigned for now (services
  consume them in Phase 9/10)

Verification:
- go build ./...                                      clean
- go vet ./...                                        clean
- go test -tags=integration ./internal/token/...      11 of 11 PASS
- go test -tags=integration ./internal/event/...      9 of 9 PASS
2026-05-10 05:47:24 -04:00
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