PowerForecast Docs
Back to Wizard
This page belongs to the full documentation. You don't need it to plan: back to the essentials.

Developer Guide

How PowerForecast is built, how to run it locally, how to test it and where to change things.

Architecture

forecast-wizard (static)                         PowerAdmin            PowerAI
  index.html  shell: sidebar, the run ─────────► template (steps,  ──► per-step AI assist,
  wizard.html 4 steps (Alpine)                    fields, options)      pre-flight review, critic
  results.html one Results page
        │  POST /jobs → poll /jobs/{id} → /result + /diagnostics
        ▼
forecast-api (FastAPI, port 8020)
  Profiler → Validator → Selector → Engine (cross-validation champion) → Diagnostics
        │
        ▼
Redis job store (spec / result / diagnostics, with a time-to-live; PowerOrchestrator fallback)
StatsForecast: AutoARIMA, ETS, Theta, MSTL, SeasonalNaive, Naive

Project structure

PowerForecast/
├── forecast-api/                  # engine (Python 3.11, FastAPI)
│   ├── app/main.py, config.py, models.py, run_store.py, response_envelope.py
│   ├── app/routers/forecast.py    # /jobs, /forecast, /models; execute_forecast_pipeline(); every job stored as a run
│   ├── app/routers/runs.py        # /runs: list, read, rename, copy-spec, delete / restore / purge, audit
│   ├── app/store/                 # SQLite: db (open + migrations, ids), runs, audit, import_redis
│   ├── db/migrations/*.sql        # numbered schema migrations, applied at start
│   ├── app/pipeline/              # robust_profiler, profile_validator, enterprise_model_selector,
│   │                              # engine (StatsForecast, evaluate_models), enterprise_diagnostics,
│   │                              # transformer, outliers, decision_logger
│   └── tests/                     # pytest: engine, pipeline, async jobs (fakeredis), real-data regression
├── forecast-wizard/               # the UI (static files)
│   ├── index.html                 # shell: navigation, run (PFApp.runForecast, called by step 4), file operations, theme, language
│   ├── wizard.html                # the 4 steps; window.importSpec / exportSpec for the shell
│   ├── results.html               # Results page: verdict + Overview · Forecast table · Your data · Quality checks
│   ├── css/theme.css, js/theme.js # design tokens and theme switch (same as PowerSolver)
│   ├── css/wizard.css, results.css, result-pages.css, ai-advisor.css
│   ├── js/app.js, wizard-state.js # navigation, Alpine store, buildSpec(), draft keeping (_resume)
│   ├── js/steps/step1-4.js        # Goal · Your data · How far ahead · Review
│   ├── js/forecast-results.js     # PFResult: the Results page's view model (verdict, checks, rows), pure
│   ├── js/forecast-workbook.js    # ForecastWorkbook: Excel/CSV <-> settings + series, pure
│   ├── js/workbook-io.js          # WorkbookIO: read/write .xlsx and CSV (ExcelJS in js/vendor/)
│   ├── js/components/             # workbook-panel, assist-panel, powerai-ui, wizard-modal
│   ├── forecasts.html             # Forecasts page: the stored runs (RunsAPI)
│   ├── js/services/               # api, runs-api, template-service, powerai-client, ai-advisor, forecast-critic, ai-model-prefs
│   ├── locales/{en,fr,de,es,nl}.json
│   ├── tools/make-demo-kit.mjs    # a demo's workbook from its specification
│   └── tests/*.test.mjs           # node tests (no dependency)
├── forecast-demos/                # demos; each with input/*.xlsx, SCENARIO, EXPECTED_RESULTS, DEMO_SCRIPT
├── forecast-datasets/             # real public data + provenance (MANIFEST.md)
├── forecast-templates/            # real-data templates used by the engine tests
└── Documentation/                 # Markdown guides; tools/build_html.py builds html/

Running locally

# engine (needs Redis: REDIS_URL, default redis://localhost:6379/0)
docker run -p 6379:6379 redis:7-alpine
cd forecast-api && pip install -r requirements.txt && uvicorn app.main:app --reload --port 8020

# wizard (any static server)
cd forecast-wizard && python -m http.server 8094

forecast-wizard/config.json holds engineApiUrl (the engine) and sqliteApiUrl (PowerAdmin, for the template, the languages and AI). Sign-in goes through PowerAdmin (js/ppt-auth.js).

The engine pipeline

execute_forecast_pipeline() (app/routers/forecast.py) wires the stages:

  1. Profiling: RobustProfiler.profile_all(). Trend, seasonality (and period), variance type and stationarity, each decided by several methods that must agree (consensus), with a reliability score. Zero profiles is an error (PIPELINE_PRODUCED_EMPTY_RESULTS), never an empty success.
  2. Validation (validate_patterns): ProfileValidator.validate_all() keeps a detected pattern only if modelling it improves a holdout forecast (Diebold-Mariano test).
  3. Shortlist: EnterpriseModelSelector.select_models(), candidate methods per series from its profile.
  4. One cross-validation pass: engine.evaluate_models() scores every candidate and the Naive baseline (MAPE, RMSE, MAE). The lowest CV-RMSE becomes the champion. The result's accuracy comes from here.
  5. Forecast with the champion (transforms and outlier handling applied), batched by seasonal period.
  6. Diagnostics reuse the champion's cross-validation residuals (no refit): bias, normality, autocorrelation, heteroscedasticity.

Every choice is written to the decision log (decisions[]), shown under How the method was chosen.

Speed. Profiling with validation plus cross-validation of several candidates costs about 15 – 20 s per monthly series of 11 years on one core (demo 04: 20 series in 5½ – 6 minutes); long daily or weekly series cost more. FORECAST_N_JOBS sets StatsForecast's parallelism (default 1, safe in Docker; -1 = all cores on bare metal). Use /jobs, not /forecast, for many series.

Adding a method: add it to the engine's model factory (create_models in app/pipeline/engine.py) and to the selector's candidates (enterprise_model_selector.py). The cross-validation pass will score it. Then add it to the ForecastModel enum (app/models.py), to forecast-templates/validate_templates.py, and to the template's manual_models in PowerAdmin if users may pick it.

The wizard

  • Template-driven. Step questions, help and option lists (targets, granularity, time units, confidence levels, scenarios, methods) come from the PowerForecast template in PowerAdmin (TemplateService); there is no hard-coded fallback. Template step ids: goal, time, models, meta. Step 3 renders time and models (the latter under Advanced); step 4 renders meta.
  • State. Alpine.store('wizard') (js/wizard-state.js); buildSpec() is the exact /jobs body. The shell reads it through window.exportSpec().
  • Drafts. On pagehide the wizard keeps pfWizardDraft (spec + step) in sessionStorage; WizardApp._resume() restores it, or, with wizard.html#fix=N, loads the last run's forecastSpec at step N (the Results page's Change the forecast). Reset Wizard clears the draft.
  • Excel. ForecastWorkbook.fromSheets(sheets) → { settings, data, problems }; toSheets(spec) writes How to fill / Setup / Data. Both the step 1 panel and the data step use it; CSV goes through the same reader.
  • A run (PFApp.runForecast): POST /jobs, poll, fetch result and diagnostics, store forecastResult, forecastDiagnostics, forecastSpec, forecastRunId in sessionStorage, save the 4 files, open Results.

The Results page

PFResult.build(result, diagnostics) (js/forecast-results.js) is pure: it returns the verdict, the checks (codes + facts, never sentences), per-series facts and the KPI numbers. results.html turns them into sentences (locales results.*). The rule of the page: a number is shown only when the engine measured it; totals per week/month/… carry no range (PFResult.rows). The AI Forecast Critic calls window.__renderForecastDashboard(result) after a re-forecast; the page then drops the old diagnostics (they belong to the previous run).

Tests

cd forecast-api && python -m pytest tests/              # engine, pipeline, /jobs, real-data regression
cd forecast-wizard && node tests/forecast-results.test.mjs  # verdict, checks, rows (any timezone)
cd forecast-wizard && node tests/demo-kits.test.mjs         # every demo workbook gives back its specification

Test and demo data are real public series (forecast-datasets/MANIFEST.md).

Demo kits

A demo's workbook is generated, never edited by hand:

node forecast-wizard/tools/make-demo-kit.mjs forecast-demos/<demo>/spec.<name>.json   # → <demo>/input/spec.<name>.xlsx
node forecast-wizard/tests/demo-kits.test.mjs

Translations

Every visible text has a key in locales/{en,fr,de,es,nl}.json and an English fallback in the code (_t(key, fallback)). New text: add the key to all five files. data-i18n attributes are applied by js/i18n.js; the shell forwards language changes to the page in the frame.

AI

The engine has no LLM code. Per-step drafting, the pre-flight review, Explain my data and the critic call PowerAI through PowerAdmin (js/services/powerai-client.js), with prompt templates keyed by (producer = PowerForecast, category) in PowerAdmin. The AI model is chosen once, platform-wide (AIModelPrefs).

Configuration (forecast-api)

PORT=8020
LOG_LEVEL=INFO
REDIS_URL=redis://redis:6379/0
RUN_TTL_SECONDS=3600       # finished jobs stay in Redis this long; the stored run stays for good
FORECAST_DB_PATH=/data/powerforecast.db   # stored runs (SQLite)
FORECAST_MAX_JSON_MB=50    # larger requests are refused (TOO_LARGE)
ORCHESTRATOR_URL=          # optional: fallback retrieval for expired jobs
FORECAST_N_JOBS=1          # StatsForecast parallelism (bare metal: -1)
CORS_ORIGINS=*

Stored runs (SQLite)

app/store/db.py opens FORECAST_DB_PATH with WAL, foreign keys and a 5 s busy timeout, and applies db/migrations/NNNN_name.sql in order at start (table schema_migrations). A released migration is never edited: a change is a new numbered file. Writes take WRITE_LOCK. At start the app also moves the finished jobs still in Redis into the database (import_redis.py, once; they carry no input history) and marks runs a restart interrupted as FAILED. Ids are UUIDv7 text, dates ISO-8601 UTC, every row has created_at/by, updated_at/by, owner, deleted_at/by, and audit_log records each change (actor_verified 0 until the product has its own users) — the same conventions as PowerSolver and PowerDecision. Tests: every test gets its own database (conftest.py).

Documentation

The Markdown files in Documentation/ are the only source. python -I Documentation/tools/build_html.py writes Documentation/html/ (shared template, table of contents, essentials / full switch). --check fails if a page is out of date.