Release notes
All notable changes to Numeralens are recorded here. The format follows Keep a Changelog and the project uses semantic versioning.
[Unreleased]
Changed
- Renamed from FinanceIQ to Numeralens. Product name, package name,
default
NEXT_PUBLIC_APP_NAME, docs, licence and screenshots. ThelocalStoragekeys (numeralens.*) and session cookie (numeralens_session) were renamed too, so saved settings reset and users sign in again once after upgrading.
Added
- Link-share previews. Open Graph and Twitter card metadata with a
1200×630 dashboard image (
src/app/opengraph-image.png), so links shared on X, LinkedIn, Slack and marketplaces show a title, description and image. Self-hosted deployments setSITE_URLso the image URL resolves.
Fixed
.env.examplewas never shipped..gitignoreexcluded every.env*file, so the template the docs tell you to copy was missing. It is now committed and lists every variable the app reads.- Docs still described pre-1.2.0 auth. The buyer guide listed
authentication and server-side authorisation as not included; the licence,
support page, help centre FAQ, white-label checklist and fallback sign-in
screen said permissions were UI-only or pointed at
auth-provider.tsx. All now match what ships. Test count updated to 237 and the QA report carries a v1.2.0 re-run.
Changed
- Licence v1.1. Section 8 security wording now reflects the bundled server-side checks. The Licensee's responsibilities are unchanged.
- Default AI model is now Claude Opus 5.5 (
claude-opus-5-5), up from Claude Opus 5: lower per-token price, same request shape. Every call already setseffortexplicitly and uses adaptive thinking, so nothing else changes.AI_MODEL=claude-opus-5restores the previous default. - AI output ceiling raised to 16,000 tokens per call (was 1,200–4,096).
Thinking counts toward the limit, so tight caps could truncate the JSON and
send the request to the deterministic fallback. Billing is on tokens
actually generated; per-call
effortstill keeps answers short.
[1.2.0] — 2026-09-18
Authentication and authorisation stop being a diagram.
Added
- Real authentication. A
/loginroute backed by a server action, scrypt password hashing with a per-password salt and a constant-time compare, and a signed session in anhttpOnlycookie (src/lib/auth/session.ts). A wrong password and an unknown address return the same message, because a form that distinguishes them is a way to enumerate your users. Sign-in attempts are limited to ten per five minutes per address. AUTH_SECRET. Required in production with demo mode off — the app refuses to start without it rather than silently signing sessions with the public development key.- Demo accounts, one per role, all with the published password
demo1234and listed on the sign-in screen. Demo mode now authenticates like any other deployment; the point is to let a visitor sign in as a Viewer and be refused. tests/auth.test.ts— 13 tests over token forgery, tampered payloads, expiry, credential handling and the permission matrix.
Fixed
Authorisation was decorative. 29 API route files contained zero access checks — not one reference to a session, token or Authorization header. The role lived in
localStorageandsetRolewas exposed on the context, so any visitor could grant themselvesadminfrom the console, or skip the UI and request/api/general-ledgerdirectly. The permission matrix hid navigation and protected nothing.All 26 data routes now declare the permission they require and it is checked on the server against the verified session, before the handler runs; the three utility routes require a session.
src/lib/auth/dal.tsis the single boundary. Denials are401/403JSON, never a redirect to an HTML page.The role switcher lied. It changed client state only, so the sidebar hid pages while the API kept serving them. It now re-issues the session, so switching to Viewer genuinely produces
403s — and it refuses outright when demo mode is off, where it would otherwise be a privilege-escalation endpoint.
Changed
src/proxy.tsredirects signed-out visitors to/login, preserving the requested path. Next.js 16 deprecatedmiddleware.tsand renamed it toproxy.ts; a file namedmiddleware.tsdoes not run. It is an optimisation, not a boundary — it only reads the cookie and may be served from a CDN.AuthProviderno longer derives the user. The session is resolved once on the server in the root layout and passed down, so the client cannot promote itself:can()decides what renders,dal.tsindependently decides what is returned.docs/authentication.mdrewritten, and the README's "Auth-ready" claim replaced. The old text told buyers the matrix was UI-level and their API must enforce the rules — true then, misleading now that it does.- Vitest resolves
server-onlyto its no-op build so server modules can be unit tested;tests/auth.test.tsruns in the node environment becausejoserejects aUint8Arraycreated in the jsdom realm.
Verification
TypeScript strict clean · ESLint clean (0 errors) · 237 tests passing (13 new), 0 failing · production build with the proxy registered.
Checked against a running production build, not only unit tests: unauthenticated
/api/dashboard returns 401; /dashboard returns 307 to
/login?next=/dashboard; a Viewer session gets 200 on dashboard and customers
and 403 on ledger, transactions, budget and inventory; a token with a tampered
signature returns 401.
Not verified: the sign-in form driven through a real browser. The server action and the enforcement path were exercised over HTTP directly.
[1.1.2] — 2026-09-18
Demo scaffolding stops shipping to deployments that never run it.
Fixed
- The demo dataset was in the client bundle of every route, in every
deployment.
src/services/index.tsis imported by every data hook in the product, and it statically imported the mock adapter — which pulls in the calculation engine, the fictional company, its customers and vendors, and the synthetic volume builder.LiveProvider, which wraps the whole app, statically imported the live ledger and dataset on top of that. A buyer pointing Numeralens at their own REST backend downloaded all of it on every page load and executed none of it. Both are now loaded throughimport()at the point of use: demo mode pays one dynamic import on the first query, REST mode never pays at all. - The Performance Lab shipped enabled, in the primary navigation. It measures this deployment's rendering and rebuilds the demo ledger at 10K–250K lines — a sales and diagnostics tool, sitting in the sidebar between Intelligence and Settings where a reader looking for a balance sheet met it on the way.
Changed
enablePerformanceLabnow defaults toappConfig.demoModerather thantrue. A demo deployment keeps the Lab; a deployment withNEXT_PUBLIC_ENABLE_DEMO_MODE=falsestarts with it hidden. It remains a runtime toggle in Settings → Modules, so nothing is taken away.- The Lab moved out of the main navigation into a Diagnostics group at the foot of the settings sidebar, and carries its own link back to Settings. Its route is unchanged, so existing links still work.
getAdapter()is now async, and the 24 service facades with it. EveryDataAdaptermethod already returned a promise and every caller already awaited one, so no feature code changed.- New
src/mock/dataset-size.tsholds the size catalogue, the active size and the record of which sizes have been built. These have to stay synchronous — React Query folds the dataset size into query keys, and the Lab renders the size buttons during render — and separating them from the data is what let the dataset itself become async. It contains no demo data. - The Lab reads its ledger row count only in demo mode. Asking for the count is what loads the dataset module, so in REST mode the request would have pulled in demo data the deployment has no use for and reported a figure that is not the buyer's. The metric renders as "—" instead.
- Switching dataset size resolves the dataset module before starting the build timer, so module fetch and evaluation are not counted as build cost. The Lab publishes that number as a measurement.
Added
- Vercel Web Analytics and Speed Insights, behind
NEXT_PUBLIC_ENABLE_VERCEL_INSIGHTS(defaulttrue). This is field measurement, as against the Performance Lab's session measurement: Core Web Vitals from real visitors on real devices, broken down by route, which is the one thing the Lab structurally cannot tell you. Both components are inert unless the app is served from Vercel, and their data goes to the Vercel project serving the deployment — so a buyer self-hosting elsewhere carries nothing, and a buyer on Vercel gets their own field data, or sets the flag tofalse.
Verification
TypeScript strict clean · ESLint clean (0 errors) · 217 tests passing (6 new), 0 failing · production build of 67 routes.
tests/demo-code-split.test.ts walks the static import graph from the services
barrel and the provider tree and fails if any demo module becomes reachable by
static import again. This is a regression nothing else would catch: re-adding
the import breaks no test, changes no behaviour and produces no error — it only
makes the bundle permanently larger for every customer. The test was confirmed
to fail against the previous code, and carries a case asserting the walker
traverses, so it cannot pass by doing nothing.
Not verified: browser rendering of the changed data path. The facade tests cover the adapter seam under jsdom; the full React Query path through a real browser was not exercised for this release.
[1.1.1] — 2026-09-15
Fixed
- Two of the six chart series read as grey.
--chart-2(teal) and--chart-6(slate) sat below the chroma floor, so on a breakdown with many categories they blended into each other and into the grid. All six slots are re-stepped; the palette now passes lightness-band, chroma, colour-blind separation, normal-vision separation and 3:1 surface contrast in both light and dark, verified with a validator rather than by eye. Worst adjacent pair: ΔE 9.2 (light) / 10.2 (dark) simulated CVD, against a ≥ 8 target. - The dark palette was too light for its surface. Five of six slots sat
above the dark lightness band, washing out against
--surface. The dark column is now the same six hues stepped for the dark surface, selected and validated as a set rather than flipped from the light values. - The donut "Other" slice was the same colour as the first category. A
limitof 6 plus the synthetic "Other" bucket produced 7 slices against a 6-colour cycle, soi % 6wrapped "Other" back onto slot 1. "Other" is not an entity and now takes the neutral.
Changed
- New
--chart-neutraltoken for marks that are deliberately recessive — budget reference lines and forecast prediction bands. These previously borrowed--chart-6, which meant slot 6 was doing two contradictory jobs: a recessive baseline in budget, and a categorical identity in every breakdown chart. Separating them is what allowed slot 6 to become a real hue.
1.1.0 — 2026-09-14
Scale, and an intelligence layer on top of the calculation engine.
Added
Performance
- Dataset scaling to 10K / 25K / 50K / 100K / 250K ledger lines, generated from the same deterministic seed. Every synthetic document is balanced, so the trial balance balances and the balance sheet reconciles at every size.
- Performance Lab (
/performance): real Web Vitals, a query benchmark against the active dataset, a live-update stress test, and a DOM-row counter that reads the live document. Measured, never estimated — an unreported metric renders as "—". Detects a backgrounded tab and says so, rather than reporting browser timer throttling as an application limit. - Row virtualization in the shared data table. 26 DOM rows for a 100,006-line ledger, measured in the browser.
- Windowed loading (
useInfiniteTransactions): server-paginated batches of 500 as you scroll, so a 100K ledger is never transferred in one response. - Live-update simulator with a deterministic, double-entry-correct ledger and rates of 100 / 250 / 500 / 1,000 events per minute. Emission and rendering are separate loops: the ledger absorbs 246,615 events/second, and the UI publishes once a second at 11.8 ms per batch (p95 57.2 ms).
npm run bench— a reproducible engine benchmark across every dataset size.
Mobile
- Card layout below 768px for every table in the product, driven by per-column
mobilemetadata. Primary fields on the card face, the rest behind a labelled disclosure — nothing is dropped to make the UI compact. - A sort menu for the card layout, which has no column headers to click.
Financial engine
- Decimal-safe money arithmetic (
src/lib/calculations/money.ts): integer-cent addition, subtraction, multiplication, division, currency rounding, and largest-remainder allocation so a split sums back exactly. - Liquidity and efficiency ratios: current, quick and cash ratio, working
capital, DSO, DPO, DIO and the cash conversion cycle. A ratio that cannot be
formed returns
null, never0orInfinity. - Statistical forecasting: seasonal decomposition, OLS regression with genuine prediction intervals, and a seeded Monte Carlo scenario engine.
Intelligence layer (five capabilities)
- Executive Narrative — three structured insights with evidence chips, three tones, and copy-to-email / copy-to-board-pack.
- CFO Copilot — global (⌘J), natural language compiled to a closed-enum semantic query, executed by the engine. No generated SQL anywhere.
- Predictive cash flow —
/intelligence/forecast, fully deterministic, with 80% and 95% prediction intervals and what-if scenarios. - Anomaly Sentry —
/intelligence/anomalies, eight deterministic rules plus a Benford first-digit test with a documented minimum sample size, each finding carrying its supporting statistics. - AR Collection Risk —
/intelligence/collections, a documented 1–100 scoring model where every point is attributable to a named driver, plus reminder drafting in four tones. AIAdapterabstraction with a deterministic adapter and an Anthropic adapter. Every AI feature works with no API key, and an outage degrades the prose rather than removing the feature.- Zod validation on every model boundary; a response that fails is never rendered.
- Prompt-injection defence: structural separation, delimiter integrity, length caps, and — the layer that matters — no tool the model could act through.
- Rate limiting on AI routes, separate from the deterministic data routes.
Engineering
- 129 new tests (211 total).
docs/AI_ARCHITECTURE.md,docs/PERFORMANCE.md,docs/QA_REPORT.md.
Changed
- The engine is indexed: binary search over the date-sorted ledger, a pre-built search haystack, and per-account and per-reference maps. Page, search and filter on a 100K ledger are all under 8 ms.
QueryStateaccepts any query-shaped object, so paged and infinite queries share one loading/error contract.- Transactions gains a Paged / Continuous toggle. Paged is unchanged from 1.0.
- Two new feature flags:
enableAI,enablePerformanceLab.
Fixed
- Inventory drove to −$7.2M over the demo period. Every cost of sale relieved stock and nothing replenished it. The books balanced, so the trial balance never complained — but the balance sheet showed negative stock, the quick ratio exceeded the current ratio, and the cash conversion cycle was −319 days. Only physical goods now relieve stock, and stock is replenished monthly.
- Every customer showed a "120+ days" oldest balance. Partial payments on two-year-old invoices never settled, so the ageing columns carried no information.
getExpensestook 1,352 ms on a 100K ledger — the anomaly rule scanned the whole ledger once per bill. Now 167 ms.- The dashboard rescanned every cash account once per chart bucket: 481 ms → ~262 ms at 100K, before further indexing work.
- Generating 250K rows overflowed the call stack (
push(...array)). - The cash-flow forecast threw
RangeError: Invalid time value. - Forecast seasonality never engaged — 24 months of history minus the partial current month left one short of the two cycles required.
- The Copilot reported informational caveats as refusals, answered "why" questions with a circular single total, silently dropped ageing constraints, and could report a share above 100%.
- One noisy anomaly rule could crowd every other rule out of the findings list.
- The production AI adapter failed to load (a missing
server-onlydependency) and the resolver fell back silently — a deployment with a real API key would have received templated prose with no indication why. The dependency is now declared and the fallback logs the cause and the fix.
Verification
TypeScript strict clean · ESLint clean (0 errors) · 211 tests passing, 0 failing · production build of 62 routes · browser verification of the dashboard, all three intelligence modules, the Copilot and the Performance Lab against a production build. Full detail, including what was not verified, in docs/QA_REPORT.md.
1.0.0 — 2026-09-04
First public release.
Added
Pages (32)
- Executive dashboard with eight KPI cards, sparklines, comparison indicators and ten interactive widgets
- Analytics: revenue, expenses (with anomaly detection), profitability, cash flow
- Reports: profit & loss, balance sheet, cash flow statement, general ledger, trial balance, accounts receivable, accounts payable
- Customer and vendor directories with detail pages
- Transactions explorer with journal-entry drawer and deep-linkable filters
- Budget & forecast with monthly, quarterly and yearly rollups
- Inventory and cost analysis
- Custom dashboard with a 17-widget library, reorder, resize and reset
- Ten settings screens: general, branding, appearance, currency, date & time, modules, feature flags, data source, notifications, users & roles
- In-app help centre
Data layer
DataAdapterinterface with 23 methods as the single integration point- Mock adapter over a deterministic 25-month double-entry demo dataset
- HTTP adapter with query serialisation, bearer/cookie auth, timeouts and typed errors
- Identity mappers as the documented place to reshape a foreign API
- 23 demo REST routes doubling as a reference implementation
- Runtime data-source switching with a live connection test
Platform
- White-label branding applied through CSS variables at runtime
- 15 feature flags with route-level module gating
- Five roles with a permission matrix driving navigation and access
- Global filter bar: date presets, custom ranges, four comparison modes, eight dimension filters, and a chart interval control (auto, daily, weekly, monthly, quarterly, yearly)
- Organisation switcher in the top bar for consolidated or per-entity reporting
- Transaction explorer filters for type, status, amount range and free text, with journal-entry drill-in from both the transactions table and the general ledger
- Drill-down system with a stacking drawer and breadcrumb trail
- Advanced data table: sorting, search, client and server pagination, column visibility, row selection, export, sticky headers
- Export to CSV, Excel-compatible CSV and print/PDF
- Global search (⌘K), notification centre, dark mode, responsive layouts
- Per-widget error boundaries with typed, actionable error states
Engineering
- 82 automated tests covering calculations, dates, engine invariants, adapters, export and table behaviour
- Multi-stage Dockerfile with non-root runtime and standalone output
- Twelve documentation guides plus
.env.example
Verification
Released after a full QA pass: TypeScript strict with no errors, ESLint clean, 82 unit and component tests passing, a successful production build of all 53 routes, and a 20-check browser test run covering filters, drill-downs, sorting, search, exports, feature-flag gating, live branding and mobile layout.