Commit Graph

74 Commits

Author SHA1 Message Date
Deeman
c8b86569ff chore: consolidate to single ruff config in root pyproject.toml
All checks were successful
CI / test-cli (push) Successful in 11s
CI / test-sqlmesh (push) Successful in 14s
CI / test-web (push) Successful in 14s
CI / tag (push) Successful in 2s
- Merge web ruff settings (select E/F/I/UP, line-length 100) into root config
- Remove [tool.ruff] section from web/pyproject.toml
- Remove "web" from root ruff exclude list
- Simplify pre-commit hook to one command: ruff check .
- Update CI to use: uv run ruff check . (from repo root)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 12:21:01 +01:00
Deeman
42c1309b20 chore: add pre-commit ruff hook with auto-fix
Some checks failed
CI / test-cli (push) Successful in 11s
CI / test-sqlmesh (push) Successful in 12s
CI / test-web (push) Failing after 14s
CI / tag (push) Has been skipped
- scripts/hooks/pre-commit: runs ruff --fix for root and web/ (matching CI)
  and re-stages any auto-fixed files so they land in the commit
- Makefile: add install-hooks target (run once after clone)
- pyproject.toml: exclude web/ from root ruff (web has its own config)
- Fix remaining import sort warnings caught by the new hook

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 10:19:29 +01:00
Deeman
c5a218490e chore(web): fix ruff warnings in src/ (unused imports, unsorted imports)
All checks were successful
CI / test-cli (push) Successful in 11s
CI / test-sqlmesh (push) Successful in 12s
CI / test-web (push) Successful in 14s
CI / tag (push) Successful in 2s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 10:11:41 +01:00
Deeman
52bd731fc3 chore: fix all ruff lint warnings (unused imports, unsorted imports, unused vars)
Some checks failed
CI / test-cli (push) Successful in 11s
CI / test-sqlmesh (push) Successful in 13s
CI / test-web (push) Failing after 14s
CI / tag (push) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 10:05:05 +01:00
Deeman
e85d0eab63 fix(api): lowercase default metric names to match ALLOWED_METRICS
Some checks failed
CI / test-cli (push) Successful in 12s
CI / test-sqlmesh (push) Successful in 13s
CI / test-web (push) Failing after 14s
CI / tag (push) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 09:55:13 +01:00
Deeman
e872ba0204 fix(tests): resolve all CI test failures (verified locally, 218 pass)
Some checks failed
CI / test-cli (push) Successful in 10s
CI / test-sqlmesh (push) Successful in 12s
CI / test-web (push) Failing after 12s
CI / tag (push) Has been skipped
- billing/routes: replace httpx calls with paddle_billing SDK; add
  _paddle_client() factory; switch webhook verification to
  Notifications.Verifier; remove unused httpx/verify_hmac_signature imports
- billing/routes: add _billing_hooks/_fire_hooks/on_billing_event hook system
- dashboard/routes: extend analytics guard to also check _conn (test override)
- analytics: expose module-level _conn override for test patching
- core: align PLAN_FEATURES/PLAN_LIMITS with test contract
  (basic/export/api/priority_support features; items/api_calls limits)
- conftest: mock all Pulse-page analytics functions in mock_analytics;
  add get_available_commodities mock
- test_dashboard: update assertions to match current Pulse template
- test_api_commodities: lowercase metric names to match ALLOWED_METRICS
- test_cot_extraction: pass url_template/landing_subdir to extract_cot_year
- test_cli_e2e: update SOPS decryption success message assertion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 02:10:06 +01:00
Deeman
8d1dbace0f fix(analytics): add _conn module-level override for test patching
Some checks failed
CI / test-cli (push) Successful in 11s
CI / test-sqlmesh (push) Successful in 12s
CI / test-web (push) Failing after 12s
CI / tag (push) Has been skipped
Tests monkeypatch analytics._conn to inject a temp DuckDB connection.
The attribute didn't exist; fetch_analytics now uses it when set,
bypassing the _db_path / threading.local path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 02:00:11 +01:00
Deeman
efb5a165e7 fix(billing): add missing hook infrastructure (_billing_hooks, on_billing_event, _fire_hooks)
Some checks failed
CI / test-cli (push) Successful in 11s
CI / test-sqlmesh (push) Successful in 13s
CI / tag (push) Has been cancelled
CI / test-web (push) Has been cancelled
Tests expected a billing event hook system that was never implemented.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 01:57:48 +01:00
Deeman
a5d2a61cfb fix(billing): add missing helper functions and fix upsert_subscription signature
- Add upsert_billing_customer / get_billing_customer (billing_customers table)
- Add record_transaction (idempotent on provider_transaction_id)
- Fix upsert_subscription: remove provider_customer_id param, key by
  provider_subscription_id instead of user_id (allows multi-sub)
- Update webhook handler to call upsert_billing_customer separately

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 14:43:14 +01:00
Deeman
dee0600ee8 chore: delete stale web/ deployment files (now at repo root)
Removes: web/Dockerfile, web/docker-compose.yml, web/docker-compose.prod.yml,
web/deploy.sh, web/litestream.yml, web/router/, web/.copier-answers.yml,
web/.env.example — all superseded by root-level counterparts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 10:26:26 +01:00
Deeman
3a8dd6ba00 refactor: add .copier-answers.yml at root + feature flags + .env.example
.copier-answers.yml (new, at repo root):
- Points to local template path (was GitLab remote)
- _commit: v0.19.0 (enables copier update)
- Reflects actual feature set: enable_cms, enable_daas, not directory/i18n/leads

web/src/beanflows/core.py:
- Added ENABLE_CMS/ENABLE_DAAS/ENABLE_DIRECTORY/ENABLE_LEADS/BUSINESS_MODEL
  to Config class (mirrors copier.yml questions for runtime feature gating)

.env.example (new, at repo root):
- Moved from web/.env.example; updated DUCKDB_PATH/SERVING_DUCKDB_PATH
  to root-relative paths (local.duckdb, analytics.duckdb)

.gitignore:
- Added web/src/beanflows/static/css/output.css (previously only in web/.gitignore)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 10:25:28 +01:00
Deeman
7c5235ff39 feat(admin): flat sidebar + horizontal subnav navigation
Replace grouped section labels + 9 individual links with 5 flat
section-level items (Dashboard, Manage, Content, Engagement, System)
and a horizontal tab strip for multi-page sections. Active state
derived via _section_map dict — no JS required.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 07:53:07 +01:00
Deeman
c778903264 merge(infra): consolidate tool installs in setup, strip bootstrap to essentials
Merges worktree-sops-supervisor-docs → master.

Summary of changes:
- setup_server.sh: now installs all tools (git, curl, age, sops, rclone, uv) —
  single source of truth for server provisioning
- bootstrap_supervisor.sh: stripped to ~45 lines — zero tool installs, only
  clone/fetch + decrypt + uv sync + systemd enable
- readme.md: updated descriptions to reflect new responsibilities

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 06:57:09 +01:00
Deeman
0317cb885f feat(infra): use beanflows_service for supervisor
- materia-supervisor.service: User=root → User=beanflows_service,
  add PATH so uv (~/.local/bin) is found without a login shell
- setup_server.sh: full rewrite — creates beanflows_service (nologin),
  generates SSH deploy key + age keypair as service user at XDG path
  (~/.config/sops/age/keys.txt), installs age/sops/rclone as root,
  prints both public keys + numbered next-step instructions
- bootstrap_supervisor.sh: full rewrite — removes GITLAB_READ_TOKEN
  requirement, clones via SSH as service user, installs uv as service
  user, decrypts with SOPS auto-discovery, uv sync as service user,
  systemctl as root
- web/deploy.sh: remove self-contained sops/age install + keypair
  generation; replace with simple sops check (exit if missing) and
  SOPS auto-discovery decrypt (no explicit key file needed)
- infra/readme.md: update architecture diagram for beanflows_service
  paths, update setup steps to match new scripts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 21:33:31 +01:00
Deeman
8f97c6b0c9 fix(positioning): prevent canvas collapse on type/range toggle
- Lock #positioning-canvas min-height to current offsetHeight before each
  HTMX swap, release it in htmx:afterSwap — prevents flash-to-zero during
  Chart.js initialization in the new content
- Add CSS min-height:200px fallback on all canvas containers so they never
  fully collapse even before JS runs
- Extract _swapCanvas() helper to deduplicate setRange/setType logic

Root cause of visual collapse: cot_positioning_combined table missing
(needs sqlmesh plan prod + export_serving to materialize).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 20:04:19 +01:00
Deeman
d3c9d95386 fix(analytics): wrap max_date in ANY_VALUE() in get_weather_stress_latest
DuckDB requires all selected columns to be aggregate expressions when there
is no GROUP BY. latest.max_date is a scalar CTE value but still needs
ANY_VALUE() wrapping to satisfy the binder.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 19:50:27 +01:00
Deeman
6461c58957 fix(web): fix Chart.js sizing after HTMX swaps on all dashboard pages
Two-part fix for charts going tiny on range changes (especially 3m) and
staying broken after subsequent navigations:

1. dashboard_base.html: global htmx:beforeSwap handler destroys any Chart.js
   instances in the swap target before HTMX replaces the DOM. Without this,
   the old chart's ResizeObserver remains attached to the parent container and
   interferes with the newly created chart instance's dimension calculations.

2. All chart pages (positioning, supply, warehouse, weather): afterSwap handler
   now wraps chart resize in requestAnimationFrame, ensuring the browser has
   completed layout before Chart.js measures container dimensions. MA toggle
   state is also restored inside the rAF callback after resize.

Root cause: chart init scripts run synchronously during innerHTML swap, before
browser layout is complete. Fast server responses (e.g. 3m = small dataset)
gave even less time for layout, making the timing issue reproducible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 15:16:13 +01:00
Deeman
411aea3954 merge: SOPS migration + Python supervisor + docs (3 repos) 2026-02-26 12:15:35 +01:00
Deeman
64687d192c merge: CFTC COT combined (futures+options) report — extractor, transform, web toggle 2026-02-26 11:29:20 +01:00
Deeman
0326e5c83d feat(web): add F+O Combined toggle to positioning dashboard
- analytics.py: add _cot_table() helper; add combined=False param to
  get_cot_positioning_time_series(), get_cot_positioning_latest(),
  get_cot_index_trend(); add get_cot_options_delta() for MM net delta
  between combined and futures-only
- dashboard/routes.py: read ?type=fut|combined param; pass combined flag
  to analytics calls; conditionally fetch options_delta when combined
- api/routes.py: add ?type= param to /positioning and /positioning/latest
  endpoints; returned JSON includes type field
- positioning.html: add report type pill group (Futures / F+O Combined)
  with setType() JS; setRange() and popstate now preserve the type param
- positioning_canvas.html: sync type pills on HTMX swap; show Opt Δ badge
  on MM Net card when combined+options_delta available; conditional chart
  title and subtitle reflect which report variant is shown

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:25:05 +01:00
Deeman
4c7e520804 fix(deploy): add analytics.duckdb bind-mount to docker-compose.prod.yml
App containers need access to the serving DuckDB populated by the
pipeline supervisor. Bind-mounts /data/materia/analytics.duckdb as
read-only and sets SERVING_DUCKDB_PATH in container environment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 10:59:33 +01:00
Deeman
f253e39c2c feat(deploy): port padelnomics deploy.sh improvements to web/deploy.sh
- Auto-install sops + age binaries to web/bin/ if not present
- Generate age keypair at repo root age-key.txt if missing (prints public
  key with instructions to add to .sops.yaml, then exits)
- Decrypt .env.prod.sops → web/.env at deploy time (no CI secrets needed)
- Backup SQLite DB before migration (timestamped, keeps last 3)
- Rollback on health check failure: dump logs + restore DB backup
- Reset nginx router to current slot before --wait to avoid upstream errors
- Remove web/scripts/deploy.sh (duplicate)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 10:59:07 +01:00
Deeman
643c0b2db9 feat(secrets): update core.py dotenv to load from repo root .env
Load .env from repo root first (created by `make secrets-decrypt-dev`),
falling back to web/.env for legacy setups. Also fixes import sort order
and removes unused httpx import.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 10:47:34 +01:00
Deeman
4fae358f97 fix(extract,transform): fix COT/prices column name mismatches + OWM rate limit skip
- fct_cot_positioning: quote Swap__Positions_Short_All and Swap__Positions_Spread_All
  (CSV uses double underscore; DuckDB preserves header names exactly)
- fct_cot_positioning: quote Report_Date_as_YYYY-MM-DD (dashes preserved in header)
- fct_coffee_prices: quote "Adj Close" (space in CSV header)
- openmeteo/execute.py: skip API call in backfill when all daily files already exist
  (_count_existing_files pre-check prevents 429 rate limit on re-runs)
- dev_run.sh: open browser as admin@beanflows.coffee instead of pro@

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 09:46:34 +01:00
Deeman
611a4af966 fix(dev): restore execute permission on dev_run.sh 2026-02-26 02:56:49 +01:00
Deeman
a9fb0d38c1 merge: weather data integration — serving layer + web app + browser auto-open 2026-02-26 02:55:19 +01:00
Deeman
8628496881 feat(dev): open browser automatically on dev server ready
Polls /auth/dev-login until the app responds, then opens an incognito/private
window — same pattern as padelnomics. Tries flatpak Chrome → flatpak Firefox
→ system Chrome → Chromium → Firefox in that order.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 02:52:45 +01:00
Deeman
3629783bbf feat: add CMS/pSEO engine, feature flags, email log (template v0.17.0 backport) ... 2026-02-26 02:43:10 +01:00
Deeman
494f7ff1ee feat(web): integrate crop stress into Pulse page
- index() route: add get_weather_stress_latest() and get_weather_stress_trend(90d)
  to asyncio.gather; pass weather_stress_latest and weather_stress_trend to template
- pulse.html: add 5th metric card (Crop Stress Index, color-coded green/copper/danger)
- pulse.html: add 5th sparkline card (90d avg stress trend) linking to /dashboard/weather
- pulse.html: update spark-grid to auto-fit (minmax 280px) to accommodate 5 cards
- pulse.html: add Weather freshness badge to the freshness bar

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 02:39:29 +01:00
Deeman
89c9f89c8e feat(web): add weather API endpoints (locations, series, stress, alerts)
Adds 4 REST endpoints under /api/v1/weather/:
- GET /weather/locations — 12 locations with latest stress, sorted by severity
- GET /weather/locations/<id> — daily series for one location (?metrics, ?days)
- GET /weather/stress — global daily stress trend (?days)
- GET /weather/alerts — locations with active crop stress flags

All endpoints use @api_key_required(scopes=["read"]) and return {"data": ...}.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 02:39:24 +01:00
Deeman
a8cfd68eda feat(web): add Weather dashboard page with Leaflet map, location cards, and stress charts
- routes.py: add weather() route (range/location params, asyncio.gather, HTMX support)
- weather.html: page shell loading Leaflet + Chart.js, HTMX canvas scaffold
- weather_canvas.html: HTMX partial with overview (map, metric cards, global stress chart,
  alert table, location card grid) and detail view (stress+precip chart, temp+water chart)
- dashboard_base.html: add Weather to sidebar (after Warehouse) and mobile bottom nav
  (replaces Origins; Origins remains in desktop sidebar)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 02:39:19 +01:00
Deeman
127881f7d8 feat(web): add weather analytics query functions to analytics.py
Adds ALLOWED_WEATHER_METRICS frozenset and 5 new query functions:
- get_weather_locations(): 12 locations with latest stress index for map/cards
- get_weather_location_series(): time series for one location (dynamic metrics)
- get_weather_stress_latest(): global snapshot for Pulse metric card
- get_weather_stress_trend(): daily global avg/max for chart and sparkline
- get_weather_active_alerts(): locations with active stress flags

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 02:39:12 +01:00
Deeman
1814a76e74 legal: add imprint page, upgrade privacy policy to GDPR-proper
- Add /imprint route and template (§5 DDG compliant, Hendrik's details)
- Rewrite privacy.html: data controller, legal basis per GDPR Art. 6,
  sub-processors (Paddle/Resend/Umami/Hetzner), retention periods,
  GDPR rights with article references, BfDI supervisory authority link
- Add /imprint to sitemap.xml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:54:26 +01:00
Deeman
930ebec259 fix: ADMIN_EMAIL → ADMIN_EMAILS, add default admin emails
Rename env var to plural (CSV list) in CI yml to match the actual
config key. Add hendrik@beanflow.coffee and simon@beanflows.coffee
as hardcoded defaults so they get admin access without needing the
env var set explicitly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 14:59:52 +01:00
Deeman
fc4121183c fix: replace stale analytics._conn checks with _db_path
dashboard/routes.py (4 places) and admin/routes.py still checked
analytics._conn is not None after _conn was removed in the two-file
refactor — causing AttributeError → 500 on every dashboard page.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 13:04:32 +01:00
Deeman
9ee7a3d9d3 fix: export_serving — Arrow-based copy, rename to analytics.duckdb
Two bugs fixed:

1. Cross-connection COPY: DuckDB doesn't support referencing another
   connection's tables as src.serving.table. Replace with Arrow as
   intermediate: src reads to Arrow, dst.register() + CREATE TABLE.

2. Catalog/schema name collision: naming the export file serving.duckdb
   made DuckDB assign catalog name "serving" — same as the schema we
   create inside it. Every serving.table query became ambiguous. Rename
   to analytics.duckdb (catalog "analytics", schema "serving" = no clash).

   SERVING_DUCKDB_PATH values updated: serving.duckdb → analytics.duckdb
   in supervisor, service, bootstrap, dev_run.sh, .env.example, docker-compose.

3. Temp file: use _export.duckdb (not serving.duckdb.tmp) to avoid
   the same catalog collision during the write phase.

Verified: 6 tables exported, serving.* queries work read-only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 12:54:39 +01:00
Deeman
ac8ab47448 feat: dev_run.sh — auto-run pipeline on first startup
On the first `./scripts/dev_run.sh` invocation (serving.duckdb absent),
automatically run extract → transform → export_serving from the repo root
so the dashboard is populated without any manual steps.

Subsequent runs skip the pipeline for a fast startup. Delete serving.duckdb
from the repo root to force a full pipeline re-run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 11:15:34 +01:00
Deeman
cb799ff019 fix: analytics fetch_analytics returns [] when DB not configured
The assert _db_path in fetch_analytics() would crash dashboard routes
locally when SERVING_DUCKDB_PATH is unset or serving.duckdb doesn't
exist yet. Change to graceful return [] so the app degrades cleanly.

Also add SERVING_DUCKDB_PATH=../serving.duckdb to local .env so the
web app will auto-connect once `materia pipeline run export_serving`
has been run for the first time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 11:10:36 +01:00
Deeman
b899bcbad4 feat: DuckDB two-file architecture — resolve SQLMesh/web-app lock contention
Split the single lakehouse.duckdb into two files to eliminate the exclusive
write-lock conflict between SQLMesh (pipeline) and the Quart web app (reader):

  lakehouse.duckdb  — SQLMesh exclusive (all pipeline layers)
  serving.duckdb    — web app reads (serving tables only, atomically swapped)

Changes:

web/src/beanflows/analytics.py
- Replace persistent global _conn with per-thread connections (threading.local)
- Add _get_conn(): opens read_only=True on first call per thread, reopens
  automatically on inode change (~1μs os.stat) to pick up atomic file swaps
- Switch env var from DUCKDB_PATH → SERVING_DUCKDB_PATH
- Add module docstring documenting architecture + DuckLake migration path

web/src/beanflows/app.py
- Startup check: use SERVING_DUCKDB_PATH
- Health check: use _db_path instead of _conn

src/materia/export_serving.py (new)
- Reads all serving.* tables from lakehouse.duckdb (read_only)
- Writes to serving_new.duckdb, then os.rename → serving.duckdb (atomic)
- ~50 lines; runs after each SQLMesh transform

src/materia/pipelines.py
- Add export_serving pipeline entry (uv run python -c ...)

infra/supervisor/supervisor.sh
- Add SERVING_DUCKDB_PATH env var comment
- Add export step: uv run materia pipeline run export_serving

infra/supervisor/materia-supervisor.service
- Add Environment=SERVING_DUCKDB_PATH=/data/materia/serving.duckdb

infra/bootstrap_supervisor.sh
- Add SERVING_DUCKDB_PATH to .env template

web/.env.example + web/docker-compose.yml
- Document both env vars; switch web service to SERVING_DUCKDB_PATH

web/src/beanflows/dashboard/templates/settings.html
- Minor settings page fix from prior session

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 11:06:55 +01:00
Deeman
ca7b2ab18b settings: remove Write scope, add billing portal error handling
- Remove 'Write' scope checkbox from API key creation form — BeanFlows
  is a read-only data platform, write keys are meaningless to users.
  Scope is now always 'read' via hidden input.
- Add try/except in billing.manage route so Paddle API failures (e.g.
  no live credentials in dev) show a user-facing flash error instead
  of a 500.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 01:38:51 +01:00
Deeman
090fcb4fdb dashboard: JTBD-driven restructure — Pulse, Supply, Positioning, Warehouse
Replace monolithic Overview (8 charts, 24 metric cards, no filters) with
a JTBD-driven 5-page dashboard optimised for the data-drop moment.

Navigation (sidebar + mobile nav):
- Pulse      /dashboard/         — full-picture overview, 10-second read
- Supply     /dashboard/supply   — USDA WASDE deep dive, range + metric filters
- Positioning /dashboard/positioning — KC=F price + CFTC COT, range filter
- Warehouse  /dashboard/warehouse — ICE certified stocks, range + view filters
- Origins    /dashboard/countries — unchanged (HTMX already live)
- Settings                       — unchanged

New templates:
- pulse.html: 4 metric cards + freshness bar + 2×2 sparkline grid
- supply.html + supply_canvas.html: HTMX partial with 5Y/10Y/Max and
  Production/Exports/Imports/Stocks filter pills; free plan gated at 5Y
- positioning.html + positioning_canvas.html: price chart + COT dual-axis;
  client-side MA toggles (no server round-trip)
- warehouse.html + warehouse_canvas.html: Daily Stocks / Aging / By Port
  view switcher; only active view's queries fire

routes.py:
- RANGE_MAP dict maps URL param → {days, weeks, months, years}
- _safe() helper absorbs asyncio.gather exceptions with defaults
- index() rewritten: 8 lightweight queries, renders pulse.html
- supply(), positioning(), warehouse() routes added; HX-Request detection
  returns canvas partial; full request returns page shell

input.css:
- All cc-* component classes moved from countries.html inline style to
  global stylesheet (cc-chart-card, cc-trow 3-col grid, cc-empty, etc.)
- filter-bar, filter-pills, filter-pill, canvas-loading, freshness-badge
- cc-chart-body canvas max-height 340px (prevents gigantic charts on 4K)

_feedback_widget.html:
- Mobile: collapses to circular icon button at bottom:72px to clear 5-item
  nav bar; "Feedback" label hidden on mobile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 01:27:44 +01:00
Deeman
fb8c6cdb3d overview: add missing axis labels to supply/demand, STU, and top-producers charts
- Supply & Demand chart: Y-axis → '1,000 60-kg bags'
- Stock-to-Use chart: Y-axis → 'Stock-to-Use (%)'
- Top Producers bar: X-axis → '1,000 60-kg bags'
- YoY table: Production column header → 'Production (1k bags)'

COT, price, ICE stocks, aging, and by-port charts already had labels.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 22:55:16 +01:00
Deeman
3f31e33d12 countries: show metric label on chart Y-axis and table value column
- Chart Y-axis title: "production  (1k 60-kg bags)" via Chart.js title
- Rankings table: column header row with "Country" / "production (1k bags)"
- Table section header changes to "Latest snapshot · <metric>"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 22:53:34 +01:00
Deeman
8af7d5e189 rename Countries nav item to Origins
Matches the 'Origin Intelligence' page heading.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 22:48:34 +01:00
Deeman
32e54f0381 countries: HATEOAS + HTMX — click origin to update chart instantly
Replace Apply button flow with immediate HTMX partial fetches:
- toggleCountry() does an optimistic UI update (row + badge) then
  calls htmx.ajax() targeting #cc-canvas with swap=innerHTML
- URL is pushed to history on every selection change (bookmarkable)
- HX-Request now returns countries_canvas.html fragment (chips +
  chart/empty + inline IIFE that re-syncs globals + re-inits Chart.js)
- Panel (dark) is never swapped; canvas fades during in-flight request
- PALETTE, buildRankings(), initChart() defined once on page load,
  called by both initial render and partial IIFE after each swap
- Apply button removed; Clear triggers fetchCanvas() with empty codes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 22:40:59 +01:00
Deeman
91a9fb83be redesign Countries page: commodity intelligence terminal aesthetic
Replace generic multi-select + plain card with a two-panel layout:
- Dark espresso selector panel (sticky, searchable, click-to-toggle)
  with country rows showing rank, name, production figure, checkbox
- Right canvas: metric segment tabs, selected-country chips (colored),
  Chart.js line chart with dark espresso tooltip, and a JS-built
  rankings table with proportional colored bars (latest year)
- Smooth fade-in animations, monospaced figures, copper accent palette

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 22:30:37 +01:00
Deeman
ff956b0138 ICE aging + by-port: serving models, API endpoints, dashboard integration
- serving/ice_aging_stocks.sql: pass-through from foundation, parses age
  bucket string to start/end days ints for correct sort order
- serving/ice_warehouse_stocks_by_port.sql: monthly by-port since 1996,
  adds MoM change, MoM %, 12-month rolling average
- analytics.py: get_ice_aging_latest(), get_ice_aging_trend(),
  get_ice_stocks_by_port_trend(), get_ice_stocks_by_port_latest()
- api/routes.py: GET /commodities/<code>/stocks/aging and
  GET /commodities/<code>/stocks/by-port with auth + rate limiting
- dashboard/routes.py: add 3 new queries to asyncio.gather(), pass to template
- index.html: aging stacked bar chart (age buckets × port) with 4 metric
  cards; by-port stacked area chart (30-year history) with 4 metric cards

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 21:52:35 +01:00
Deeman
3d3f375e01 Merge worktree-cot-integration: Phase 1 + scout MCP server
- Phase 1A-C: KC=F price extraction, SQLMesh models, dashboard charts, API endpoints
- ICE warehouse stocks: extraction package, SQLMesh models, dashboard + API
- Methodology page (/methodology) with all data sources documented
- Supervisor pipeline automation with webhook alerting
- Scout MCP server (tools/scout/) for browser recon via Pydoll
- msgspec added as workspace dependency for typed boundary structs
- vision.md updated to reflect Phase 1 completion (Feb 2026)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 15:57:49 +01:00
Deeman
67c048485b Add Phase 1A-C + ICE warehouse stocks: prices, methodology, pipeline automation
Phase 1A — KC=F Coffee Futures Prices:
- New extract/coffee_prices/ package (yfinance): downloads KC=F daily OHLCV,
  stores as gzip CSV with SHA256-based idempotency
- SQLMesh models: raw/coffee_prices → foundation/fct_coffee_prices →
  serving/coffee_prices (with 20d/50d SMA, 52-week high/low, daily return %)
- Dashboard: 4 metric cards + dual-line chart (close, 20d MA, 50d MA)
- API: GET /commodities/<ticker>/prices

Phase 1B — Data Methodology Page:
- New /methodology route with full-page template (base.html)
- 6 anchored sections: USDA PSD, CFTC COT, KC=F price, ICE warehouse stocks,
  data quality model, update schedule table
- "Methodology" link added to marketing footer

Phase 1C — Automated Pipeline:
- supervisor.sh updated: runs extract_cot, extract_prices, extract_ice in
  sequence before transform
- Webhook failure alerting via ALERT_WEBHOOK_URL env var (ntfy/Slack/Telegram)

ICE Warehouse Stocks:
- New extract/ice_stocks/ package (niquests): normalizes ICE Report Center CSV
  to canonical schema, hash-based idempotency, soft-fail on 404 with guidance
- SQLMesh models: raw/ice_warehouse_stocks → foundation/fct_ice_warehouse_stocks
  → serving/ice_warehouse_stocks (30d avg, WoW change, 52w drawdown)
- Dashboard: 4 metric cards + line chart (certified bags + 30d avg)
- API: GET /commodities/<code>/stocks

Foundation:
- dim_commodity: added ticker (KC=F) and ice_stock_report_code (COFFEE-C) columns
- macros/__init__.py: added prices_glob() and ice_stocks_glob()
- pipelines.py: added extract_prices and extract_ice entries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 11:41:43 +01:00
Deeman
1a39082514 Add sidenav layout for authenticated dashboard
- Create dashboard_base.html: standalone app shell with 56px sticky
  header (logo + user email + sign out), 220px left sidebar with
  Overview/Countries/Settings nav items (SVG icons, active state via
  request.path), and fixed mobile bottom tab bar (md:hidden)
- Add CSS component classes: .app-shell, .app-header, .app-sidebar,
  .sidebar-item, .app-content, .mobile-bottom-nav, .mobile-nav-item
- Extract feedback widget into _feedback_widget.html partial; include
  from both base.html and dashboard_base.html
- Switch index.html, countries.html, settings.html to extend
  dashboard_base.html; remove <main class="container-page"> wrappers
- Remove "Back to Dashboard" button from countries.html (sidebar
  provides persistent navigation)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 01:15:25 +01:00