Commit Graph

205 Commits

Author SHA1 Message Date
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
c92e5a8e07 ice_stocks: add backfill extractor for historical daily stocks
The ICE API at /marketdata/api/reports/293/results stores all historical
daily XLS reports date-descending. Previously the extractor only fetched
the latest. New extract_ice_backfill entry point pages through the API
and downloads all matching 'Daily Warehouse Stocks' reports.

- ice_api.py: add find_all_reports() alongside find_latest_report()
- execute.py: add extract_ice_stocks_backfill(max_pages=3) — default
  covers ~6 months; max_pages=20 fetches ~3 years of history
- pyproject.toml: register extract_ice_backfill entry point

Ran backfill: 131 files, 2025-08-15 → 2026-02-20

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 01:35:57 +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
0d78a22023 changelog: bring up to date through Feb 2026
- [Unreleased]: ICE overhaul (aging + by-port + API discovery + XLS parsing),
  extract_all meta-pipeline, Origin Intelligence redesign + HTMX, axis labels
- [0.2.0]: CFTC COT, KC=F prices, ICE daily stocks, methodology page, supervisor
- [0.1.0]: initial dashboard, country comparison, REST API, plan tiers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 23:22:04 +01:00
Deeman
6d18a4a7c2 vision: update current state to reflect ICE overhaul + dashboard work shipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 23:00:03 +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
493ce64fde fix ice_stocks XLS date parsing: handle 'Feb 20, 2026' format
ICE changed the daily stocks XLS header from 'As of: 1/30/2026' to
'As of: Feb 20, 2026  1:35:39PM'. Expand _build_canonical_csv_from_xls
to try multiple strptime formats (%m/%d/%Y, %b %d, %Y, etc.) on both
single-token and three-token date candidates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 22:18:17 +01:00
Deeman
562e2d1847 Add extract_all meta-pipeline: runs all four data source extractors in sequence
Sequences: extract (PSD) → extract_cot (CFTC) → extract_prices (KC=F) → extract_ice_all (ICE)
Stops and reports on first failure. META_PIPELINES dict makes it easy to
add more meta-pipelines as sources expand.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 22:00:10 +01:00
Deeman
ff896685d2 Add extract_ice_all command to run all three ICE extractors in sequence
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 21:59:08 +01:00
Deeman
6ba1afd8c3 Merge worktree-ice-extraction-overhaul: ICE aging + by-port app integration
Serving models, API endpoints, and dashboard charts for both new ICE datasets.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 21:52:39 +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
04f8df88fe Merge worktree-ice-extraction-overhaul: ICE extraction overhaul
API discovery + aging report + historical by-port backfill.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 21:13:21 +01:00
Deeman
ff7301d6a8 ICE extraction overhaul: API discovery + aging report + historical backfill
- Replace brittle ICE_STOCKS_URL env var with API-based URL discovery via
  the private ICE Report Center JSON API (no auth required)
- Add rolling CSV → XLS fallback in extract_ice_stocks() using
  find_latest_report() from ice_api.py
- Add ice_api.py: fetch_report_listings(), find_latest_report() with
  pagination up to MAX_API_PAGES
- Add xls_parse.py: detect_file_format() (magic bytes), xls_to_rows()
  using xlrd for OLE2/BIFF XLS files
- Add extract_ice_aging(): monthly certified stock aging report by
  age bucket × port → ice_aging/ landing dir
- Add extract_ice_historical(): 30-year EOM by-port stocks from static
  ICE URL → ice_stocks_by_port/ landing dir
- Add xlrd>=2.0.1 (parse XLS), xlwt>=1.3.0 (dev, test fixtures)
- Add SQLMesh raw + foundation models for both new datasets
- Add ice_aging_glob(), ice_stocks_by_port_glob() macros
- Add extract_ice_aging + extract_ice_historical pipeline entries
- Add 12 unit tests (format detection, XLS roundtrip, API mock, CSV output)

Seed files (data/landing/ice_aging/seed/ and ice_stocks_by_port/seed/)
must be created locally — data/ is gitignored.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 21:13:18 +01:00
Deeman
ff39d65dc6 scout: extract to standalone repo at Projects/scout
Move scout MCP server out of tools/scout/ into its own repo at
/var/home/Deeman/Projects/scout. Update .mcp.json to use absolute path
so any project can reference it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 17:58:03 +01:00
Deeman
079c189e0a scout: add scout_click_coords tool, document Sourcepoint limitation
- Add scout_click_coords for manual coordinate-based clicks (useful when
  CSS selectors can't reach cross-origin iframes)
- Document in _dismiss_cookie_banner why Sourcepoint is not auto-dismissed:
  HAR captures traffic regardless of banner visibility; coordinate clicks
  are too brittle across screen sizes
- Add missing asyncio import to server.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 17:34:08 +01:00
Deeman
d96f977c0f fix scout_js: reference browser._state not undefined _state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 17:26:50 +01:00
Deeman
ab9dc62dd6 scout: add German DSGVO text patterns + Usercentrics shadow DOM support
- German accept texts: Alle akzeptieren, Akzeptieren, Zustimmen, Einverstanden, etc.
- Usercentrics (shadow DOM) support — very common with German publishers
  (Bild, Spiegel, Focus, etc.) — requires shadowRoot traversal, not addressable
  by normal CSS selectors
- Consentmanager selectors — another common German CMP
- Note: German sites tested (Spiegel, Zeit, finanzen.net, Bild) showed no banners
  because Pydoll reuses the existing Chrome user profile with stored consents.
  New-site behaviour will be handled by the added patterns.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 17:23:33 +01:00
Deeman
ec7cfda605 scout: JS-based cookie dismiss + scout_js tool
- _dismiss_cookie_banner: switch to execute_script for CSS selector clicks
  (OneTrust on ICE uses pointer-events:none overlay — mouse clicks don't reach it,
  but JS .click() bypasses this). Falls back to text-based JS search.
- Selectors cover: OneTrust, Cookiebot, CookieYes, generic [id/class*=accept/consent]
- Text fallback covers: IAB TCF "Allow All" pattern (Reuters, etc.)
- Add scout_js tool: run arbitrary JS on current page — useful for shadow DOM,
  z-index overlays, and any element that resists normal CSS/text selectors
- Add _click_via_js helper for targeted JS injection clicks

Tested patterns:
  ICE (theice.com) — OneTrust #onetrust-accept-btn-handler — requires JS click
  CFTC (cftc.gov) — no banner
  Reuters — IAB TCF "Allow All" — text click works

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 17:19:34 +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
b167a0a9f4 Add scout MCP server for browser recon + msgspec workspace dep
- tools/scout/: browser automation MCP server using Pydoll (CDP, no WebDriver)
  - scout_visit, scout_elements (text-first), scout_click, scout_fill, scout_select
  - scout_scroll, scout_text, scout_screenshot (opt-in)
  - scout_har_start / scout_har_stop (asyncio task holds recording context open)
  - scout_analyze: HAR parsing with HarEntry/HarSummary msgspec structs
  - Standalone project (not workspace member — websockets conflict with prefect)
  - Runs via: uv run --directory tools/scout scout-server

- .mcp.json: registers scout as Claude Code MCP server (project scope)

- msgspec>=0.19 added to root project deps (workspace-wide struct/validation)

- coding_philosophy.md: document msgspec as approved dep, usage rules

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 15:44:02 +01:00
Deeman
c9e9562030 Update vision.md: reflect Phase 1 completion as of Feb 2026
All Phase 1 data sources shipped. Mark ready for outreach.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 11:42:26 +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
423fb8c619 Fix extract and SQLMesh pipeline to build DuckDB lakehouse
extract: wrap response.content in BytesIO before passing to
normalize_zipped_csv, and call .read() on the returned BytesIO before
write_bytes (two bugs: wrong type in, wrong type out)

sqlmesh: {{ var() }} inside SQL string literals is not substituted by
SQLMesh's Jinja (SQL parser treats them as opaque strings). Replace with
a @psd_glob() macro that evaluates LANDING_DIR at render time and returns
a quoted glob path string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 17:02:59 +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
fa14f94a4f Merge admin-upgrade: sidebar layout, feedback, waitlist, sitemap for BeanFlows 2026-02-20 15:26:13 +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
94e8a5e6c3 Fix dev-login URLs in development script
- Update dev_run.sh with better dev-login URLs including appropriate email parameters
- Add user-login URL: auth/dev-login?email=trader@beanflows.coffee
- Add admin-login URL: auth/dev-login?email=admin@beanflows.coffee
- Keep admin-panel URL: admin/dev-login for direct admin session
- Add ADMIN_EMAILS config to .env.example for auto-granting admin role

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
2026-02-19 22:55:57 +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
866746093b Update uv.lock after copier template merge
Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
2026-02-19 22:47:39 +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
3f1cd8bd0c Update copier answers and docker-compose prod config
- Record v0.4.0 commit in .copier-answers.yml
- Apply flattened paths in docker-compose.prod.yml

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
2026-02-19 22:35:55 +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