Commit Graph

54 Commits

Author SHA1 Message Date
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
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
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
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
a9fb0d38c1 merge: weather data integration — serving layer + web app + browser auto-open 2026-02-26 02:55:19 +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
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
Deeman
4dcf1e7e84 Fix dashboard error handling, settings billing route, update vision.md
- routes.py: return_exceptions=True on gather, log individual query failures
  with per-result defaults so one bad query doesn't blank the whole page
- settings.html: fix billing.portal → billing.manage (correct blueprint route)
- vision.md: update current state to February 2026, document shipped features

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 00:02:41 +01:00
Deeman
88e408b279 Make dashboard gather resilient to missing analytics tables
Use return_exceptions=True so a CatalogException from a single query
(e.g. table not yet populated in a fresh env) degrades gracefully
instead of crashing the whole dashboard render.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 23:33:39 +01:00
Deeman
2962bf5e3b Fix COT pipeline: TRY_CAST nulls, dim_commodity leading zeros, correct CFTC codes
- config.yaml: remove ambiguousorinvalidcolumn linter rule (false positives on read_csv TVFs)
- fct_cot_positioning: use TRY_CAST throughout — CFTC uses '.' as null in many columns
- raw/cot_disaggregated: add columns() declaration for 33 varchar cols
- dim_commodity: switch from SEED to FULL model with SQL VALUES to preserve leading zeros
  Pandas auto-converts '083' → 83 even with varchar column declarations in SEED models
- seeds/dim_commodity.csv: correct cftc_commodity_code from '083731' (contract market code)
  to '083' (3-digit CFTC commodity code); add CSV quoting
- test_cot_foundation.yaml: fix output key name, vars for time range, partial: true,
  and correct cftc_commodity_code to '083'
- analytics.py: COFFEE_CFTC_CODE '083731' → '083' to match actual data

Result: serving.cot_positioning has 685 rows (2013-01-08 to 2026-02-17), 23/23 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 23:28:10 +01:00
Deeman
0a83b2cb74 Add CFTC COT data integration with foundation data model layer
- New extraction package (cftc_cot): downloads yearly Disaggregated Futures ZIPs
  from CFTC, etag-based dedup, dynamic inner filename discovery, gzip normalization
- SQLMesh 3-layer architecture: raw (technical) → foundation (business model) → serving (mart)
- dim_commodity seed: conformed dimension mapping USDA ↔ CFTC codes — the commodity ontology
- fct_cot_positioning: typed, deduplicated weekly positioning facts for all commodities
- obt_cot_positioning: Coffee C mart with COT Index (26w/52w), WoW delta, OI ratios
- Analytics functions + REST API endpoints: /commodities/<code>/positioning[/latest]
- Dashboard widget: Managed Money net, COT Index card, dual-axis Chart.js chart
- 23 passing tests (10 unit + 2 SQLMesh model + existing regression suite)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 23:28:10 +01:00
Deeman
d09ba91023 Remove password admin login, seed dev accounts, add regression tests
Admin flow:
- Remove /admin/login (password-based) and /admin/dev-login routes entirely
- admin_required now checks only the 'admin' role; redirects to auth.login
- auth/dev-login with an ADMIN_EMAILS address redirects directly to /admin/
- .env.example: replace ADMIN_PASSWORD with ADMIN_EMAILS=admin@beanflows.coffee

Dev seeding:
- Add dev_seed.py: idempotent upsert of 4 fixed accounts (admin, free,
  starter, pro) so every access tier is testable after dev_run.sh
- dev_run.sh: seed after migrations, show all 4 login shortcuts

Regression tests (37 passing):
- test_analytics.py: concurrent fetch_analytics calls return correct row
  counts (cursor thread-safety regression), column names are lowercase
- test_roles.py TestAdminAuthFlow: password login routes return 404,
  admin_required redirects to auth.login, dev-login grants admin role
  and redirects to admin panel when email is in ADMIN_EMAILS
- conftest.py: add mock_analytics fixture (fixes 7 pre-existing dashboard
  test errors); fix assertion text and lowercase metric param in tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 20:10:45 +01:00
Deeman
fef9f3d705 Fix concurrent DuckDB queries: use cursor() per thread
_conn.execute() is not thread-safe for concurrent calls from multiple
threads. asyncio.gather submits each analytics query to the thread pool
via asyncio.to_thread, causing race conditions that silently returned
empty result sets. _conn.cursor() creates an independent cursor that is
safe to use from separate threads simultaneously.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 19:04:09 +01:00
Deeman
d569ba0162 Fix metric column name casing: DuckDB returns lowercase, align everywhere
SQLMesh normalizes unquoted identifiers to lowercase in physical tables,
so commodity_metrics columns are e.g. 'production' not 'Production'.
Update ALLOWED_METRICS, all analytics.py SQL queries, dashboard routes,
and both dashboard templates (Jinja + JS chart references) to use
lowercase column names consistently.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 17:14:52 +01:00
Deeman
d05e522c88 Add migration 0001: create feedback and waitlist tables
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 15:36:40 +01:00
Deeman
8e7af53ff6 Fix admin auth, impersonation session handling, and stale stripe column
- admin_required now accepts users with 'admin' role (via g.user) in
  addition to the password-based is_admin session flag, so both auth
  methods grant access
- impersonate stores the admin's user_id (not True) in admin_impersonating
  so stop-impersonating can restore the correct session
- stop_impersonating restores user_id from admin_impersonating instead of
  just popping it
- remove s.stripe_customer_id from get_user_by_id (Paddle project, no
  stripe_customer_id column in subscriptions)

Fixes 3 test_roles.py failures: test_admin_index_accessible_with_admin_role,
test_impersonate_stores_admin_id, test_stop_impersonating_restores_admin

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 02:33:10 +01:00
Deeman
48bea5c198 Add admin sidebar layout, feedback, waitlist, sitemap to BeanFlows web
- Admin sidebar layout (base_admin.html) with espresso/copper coffee theme,
  220px sidebar, responsive collapse, nav for Dashboard/Users/Tasks/Feedback/Waitlist
- Convert all admin templates to extend base_admin.html using Tailwind classes
- Feedback system: schema, public POST route (rate-limited), base.html widget
  with HTMX popover (coffee-themed), admin viewer with mark-read
- Waitlist mode: WAITLIST_MODE config, waitlist_gate decorator,
  capture_waitlist_email helper, auth route integration, confirmation pages,
  send_waitlist_confirmation worker task, admin table
- Sitemap.xml and robots.txt public routes
- Dashboard stats updated with waitlist_count, feedback_unread alongside
  existing commodity DuckDB analytics stats

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 02:27:26 +01:00
Deeman
e80e262e25 Fix NoneType error when user has no subscription
g.subscription is explicitly set to None in load_user, so
g.get("subscription", {}) returns None (key exists), not {}.
Use (g.get(...) or {}) to coalesce None to an empty dict.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 23:10:56 +01:00
Deeman
642b529c4d Fix column name mismatch after copier template update
The subscriptions table still had paddle_subscription_id but the new
code references provider_subscription_id.  Renamed the DB column and
updated all queries in billing/routes.py to match.

Also removed unused get_subscription import from dashboard/routes.py.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 23:09:12 +01:00
Deeman
18c6e0da4f Fix dashboard routes after copier update
- Remove import of get_user_with_subscription (function was removed)
- Use g.user and g.subscription from eager loading instead
- Fixes ImportError in dashboard routes

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
2026-02-19 22:48:53 +01:00
Deeman
edd439245b Merge branch 'copier-update' 2026-02-19 22:46:41 +01:00
Deeman
32132974b2 Clean up web changes and add favicon
- Update uv.lock dependencies
- Remove web/CLAUDE.md (moved to root)
- Update base.html template
- Add favicon.svg

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
2026-02-19 22:46:33 +01:00
Deeman
4b7d4d5a74 Update from Copier template v0.4.0
- Accept RBAC system: user_roles table, role_required decorator, grant_role/revoke_role/ensure_admin_role functions
- Accept improved billing architecture: billing_customers table separation, provider-agnostic naming
- Accept enhanced user loading with subscription/roles eager loading in app.py
- Accept improved email templates with branded styling
- Accept new infrastructure: migration tracking, transaction logging, A/B testing
- Accept template improvements: Resend SDK, Tailwind build stage, UMAMI analytics config
- Keep beanflows-specific configs: BASE_URL 5001, coffee PLAN_FEATURES/PLAN_LIMITS
- Keep beanflows analytics integration and DuckDB health check
- Add new test files and utility scripts from template

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
2026-02-19 22:22:13 +01:00
Deeman
1e8a173ae8 Merge branch 'frontend-upgrade'
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 20:49:07 +01:00
Deeman
f722854c07 Rewrite frontend templates: Pico CSS → Tailwind + trader-focused copy
Replace all Pico CSS patterns (classless articles, role="button", inline
styles, var(--pico-*)) with Tailwind component classes. Add Fraunces
display font, mobile hamburger nav, brand chart colors, and new component
layer (hero, feature-card, metric-card, auth-card, pricing-card, etc.).

Rewrite marketing copy from generic SaaS boilerplate to coffee-trader
focused messaging. Rebrand pricing tiers to Explorer/Trader/Analyst.
Delete stale custom.css. No Python code changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 20:47:56 +01:00