Skip to content

Releases: humoge7502/VoltHub-CSMS

v1.8.1

Choose a tag to compare

@github-actions github-actions released this 23 Oct 21:33
036cb36

Ships the 2026-10-24 security/money-path review fixes and the console polish pass, plus the
1.8.0 wallet-rail content below, which was built but never tagged. Everything in this
section landed with regression tests; the full chain (16 suites) is green.

Added

  • Console motion + honesty layer (UI/UX polish pass). Live-session hero number
    glides between polls (useCountUp, snaps under prefers-reduced-motion); charts
    draw in once on mount and the live power trace carries a pulsing last point; polled
    dashboards keep the last good charts when a poll fails and show a reconnecting — showing last good data chip instead of wiping the view; route-level
    error.js/global-error.js boundaries replace Next's default white-screen crash.

Fixed

  • Contrast: --tx3 raised #6b7681 → #8a94a0 (3.9:1 → 5.9:1 on the panel surface) —
    it was commented "decorative only" but used for real 11px content (kv labels,
    hints, placeholders) and chart axis labels; the heatmap "peak" label (1.9:1) now uses
    the same safe tone. Chart/heatmap time labels render in a fixed Asia/Kolkata
    zone, so the "IST" suffix is truthful for non-IST visitors (was browser-local time
    labelled IST).
  • Heatmap legend renders the exported HEAT_RAMP (it had drifted from the actual
    render scale), and peak labels sit inside the AA threshold.
  • Session page: polls sequence-guard every response (no stale overwrites) and stop
    once the session ends; the aria-live region is scoped to the hero number (the old
    whole-page region re-announced the entire view every 5 s).
  • Deep link /invoices?id= actually opens the itemized panel (the param was dead);
    the itemized panel gained a close button; Pay has a busy state (double-POST
    guard); the invoices tab title no longer says "Wallet".
  • Reservations: "Plug in" sends the active tariff plan from the public
    /tariffs/active preview instead of a hardcoded planId: 2; reservation windows
    render both endpoints with date + zone (midnight-crossing windows read correctly).
  • History "Load more" guards against double clicks (duplicated rows).
  • Analytics / Notifications / Station detail gained real loading and error states
    (previously: blank-forever page, empty-grid blink, and an eternal skeleton on fetch
    failure respectively).
  • Wallet signer picker implements the ARIA radio pattern (arrow-key movement,
    roving tab index).

Changed

  • OCPP gateway: a MeterValues frame carrying only Power.Active.Import is now
    recorded (register carried forward from the start register / last reading) instead of
    being ACKed and dropped — the power trace keeps its cadence and billing is unchanged
    (BUG-061). A NoError StatusNotification now clears open OCPP faults on that
    connector; previously only the manual maintenance route ever set cleared_at
    (BUG-062). Regression: gateway-close t11.
  • Store mutex cleanup actually releases keys now (the comparison ran against a
    never-stored promise, so every locked key leaked into the map for the process
    lifetime) (BUG-046). Regression: run.js drain assertion.
  • Legacy $scrypt$ password verify runs on the thread pool (crypto.scrypt
    async) — a pre-Argon2 user logging in no longer blocks the event loop, closing the
    gap the PERF-002 async path was built for.
  • Release verification is no longer lighter than a push: release.yml now runs the
    npm audit gates (both lockfiles), the OpenAPI drift + snapshot checks, and the web
    build alongside the hermetic suites (B-59).
  • Docs drift reconciled: ARCHITECTURE.md stats (72 paths · 35 tables · 8 packages),
    the worker README's COPY … ON CONFLICT claim (Postgres COPY has no ON CONFLICT;
    the sink is a multi-row INSERT), and the k6 smoke threshold comment (5 s, matching
    the assertion).

Security

  • OCPP gateway refuses secretless charge points (SEC-041). A CP row without an
    auth_secret was previously accepted with no credentials at all in per-cp mode —
    on any seeded/hydrated deployment (identities are guessable), anyone could connect as
    any charger and fabricate StartTransaction/MeterValues billing input. Such
    connections now close with 4401. Regression: gateway-close t9.
  • Control plane is operator station-scoped (BUG-059). /control/plan/:siteId,
    certify, enforcement, decisions(+/:id), certificates(+/:id), grid-asset
    reads and composite-schedule enforced the role wall but not the station wall — an
    operator assigned to station A could actuate station B's chargers. Foreign sites are
    now 403 OUT_OF_SCOPE; unfiltered reads return only the operator's own stations.
    ADMIN bypasses, mirroring every sibling surface. Regression: run.js scope block.
  • Unsigned wallet top-up is opt-in (SEC-043). POST /me/wallet/topup contradicted
    ADR-0016's "the signature is the authorisation" story. It is now disabled in
    production (and in the public-demo preset; WALLET_UNSIGNED_TOPUP=1 re-enables,
    =0 force-disables everywhere). scripts/share-demo.sh sets the hardening flag.
    Regression: wallet W12.

Fixed

  • Absolute-register overbilling (BUG-054). StartTransaction.meterStart was
    ignored and start_meter_kwh hardcoded 0 while meterStop is an absolute register —
    every charger whose meter did not start at zero was overbilled by the register
    offset. The register base is now captured end-to-end (gateway → local store →
    Oracle charge_session_pkg.start_session via V010__meter_start.sql, default 0 for
    existing callers). Regression: gateway-close t10 (500 kWh base, 2.5 kWh delivered,
    invoice says 2.5).
  • Wallet double-credit race (W5 under concurrency). settleTopup checked intent
    status synchronously, then awaited real mirror I/O before flipping to SETTLED —
    two concurrent settles of one intent both credited. Settle now runs under the
    intent's mutex and the receipt row is identified by its unique note, not array
    position. Regression: wallet W11 (Promise.all settles → exactly one 201 + one 409,
    balance rises by exactly the intent amount).
  • ADMIN locked out of remote-start (B-16). The station-scope check ran for every
    role, so an ADMIN without an operator assignment always got 403 OUT_OF_SCOPE.
    Scope now applies to OPERATOR only, matching all sibling surfaces.
  • NaN pagination emptied /stations (B-17) — ?page=abc now coerces to 1.
  • Unvalidated planId poisoned sessions (B-18) — malformed or unknown plans are a
    422 INVALID_PLAN at the door instead of a NO_TARIFF_BAND failure at bill time.
  • /auth/refresh 500 for removed users (B-19) — now a clean 401 BAD_REFRESH.
  • Gateway send-window crashes (SEC-042) — the pre-try rate-limit/FormationViolation
    sends and the catch block's own InternalError send could throw on a dying socket
    and reject the async listener (process-fatal). All guarded.
  • Wallet page toasts never rendered (B-32) — the page called toast() but never
    mounted the <Toasts/> outlet; linked/credited/copied feedback silently vanished.

Changed

  • .env.example documents WALLET_UNSIGNED_TOPUP and the strict OCPP secretless-CP
    policy; scripts/share-demo.sh public preset prints the charger-secret caveat.

What's Changed

New Contributors

Full Changelog: v1.7.0...v1.8.1

v1.7.0

Choose a tag to compare

@github-actions github-actions released this 15 Sep 10:01
c9c5abd

Added

  • scripts/share-demo.sh — one command gives the running stack a public, shareable
    HTTPS link.
    Boots the compose stack, runs two cloudflared quick tunnels as Docker
    containers
    (web :3000, API :4000 — durable, --restart unless-stopped; plain
    background processes die with the session), repoints the web build at the public API
    origin (NEXT_PUBLIC_API_BASE is baked at build time), health-checks both URLs and
    prints the links. No VM, no DNS, no ports opened; --stop tears the tunnels down.
    DEPLOY.md §8 documents the flow and the named-tunnel/VM upgrade path for a permanent
    URL. Supporting change: compose interpolates TRUST_PROXY and WEB_ORIGIN (opt-in,
    defaults unchanged) so per-IP throttles keep working behind a proxy/tunnel (BUG-023)
    and CORS can be extended for prod origins from .env.

Dependencies

  • devDependency c8 10.1.3 → 12.0.0 (Dependabot #19; coverage suites unaffected).

Fixed

  • BUG-053 (P1, durable): V006 silently reverted BUG-050, so a freshly migrated
    database had the unbookable-charger defect back.
    V006__fk_native.sql replaces the
    reservation_pkg and charge_session_pkg bodies wholesale (packages have no partial
    replacement), so the expiry-hold release added to V003 never reached the object a
    fresh migrate.sh ends up with — V006 is applied last and wins. Reproduced on live
    Oracle: after applying V006, expire_stale flipped the window to EXPIRED and left
    the connector RESERVED (only V003's body released it). The release now exists in
    both bodies, V006's header states plainly that its sections 1–2 are the final
    definition, and the gate asserts the end state on a fully migrated database — so this
    class of "two copies drifted" bug fails the build instead of shipping.
  • BUG-052 (P1, live): the trg_connector_guard allow-list was open for the whole
    life of a pooled connection, so BR-07 (connector status only via the gateway/packages)
    was not actually enforced.
    The guard accepts a write only while CLIENT_IDENTIFIER
    is ocpp-gw or pkg:<owner>, and the packages set that identity — but never cleared
    it, and it is session-scoped. Measured on live Oracle 23ai: a fresh session was
    correctly refused with ORA-20801, and the same session allowed a direct
    UPDATE connector immediately after reservation_pkg.expire_stale ran, because the
    identity was still pkg:reservation_pkg.expire_stale. Every package connector write
    now goes through one guard_pkg.set_status(), which opens the identity for exactly
    one statement and closes it on every exit path (including a raise out of the UPDATE),
    and the nine direct UPDATE connector statements plus nine SET_IDENTIFIER calls are
    down to one each. Two consequences: reservation_pkg.cancel_reservation had been
    passing the guard only because of that leak (a fresh session was refused — also
    reproduced), and the identity can no longer be inherited by unrelated later DML.
  • The rate-limit tier and the guard now have executable evidence, not comments.
    test/sql/run-invariants.js gained behavioural probes that drive the real procedures
    and assert outcomes (a 0-rows SELECT cannot express either bug), on top of static
    source checks: GUARD-META-1/2 (the guard identity is set only by guard_pkg, and
    connector.status is written only by guard_pkg.set_status) and GUARD-META-3
    (both V003 and V006 release the hold on expiry). Red-validated: against the
    pre-fix SQL the gate reports the 9 leaked identifier sites, the 9 direct writes, the
    missing release in both files, GUARD-ORACLE-4 (hold leaked) and INV-12; green
    after the fix, idempotent across three consecutive runs with zero residue.
  • BUG-051 (P1, live): the worker's internal channel was throttled by the public
    per-IP ANON tier.
    throttle is mounted at the app root, so its /internal/*
    exclusion — added precisely because "the relay starves on 429s" — tested
    req.path, which there is /api/v1/internal/outbox and never matched. The worker
    polls three internal endpoints every 2 s (~90 req/min, one IP, no JWT) and was
    capped at 60/min: 74 of 120 relays were rejected in a measured probe, the
    outbox stopped draining and Timescale telemetry went stale. Observed live on the
    compose stack as an endless outbox poll 429 — backing off log with 85 unacked
    events
    . isInternalPath() now matches the full URL and exempts the channel from
    every tier (the global throttle and the router barriers). After the fix the same
    stack drained the entire backlog on the next 2 s cycle (relayed=85, outbox lag
    85 → 0, meter_tick 59 → 71, zero 429s). Regression: RL-7
    (apps/api/test/ratelimit.js), red against the pre-fix check.
  • BUG-050 (P1, live): reservation_pkg.expire_stale never released the connector
    it held.
    Expiry flipped only the reservation, while the local store also released
    the connector — so the two engines disagreed on bookability (ADR-0005) and, because
    hydrate() re-reads Oracle on boot, the charge point stayed unbookable forever:
    start_session on a RESERVED connector requires a BOOKED reservation for it,
    and the reservation is EXPIRED. Found live: connectors 1:1, 2:1 and 9:1
    stuck RESERVED with only EXPIRED/CANCELLED windows on them. The procedure now
    releases the hold exactly as cancel_reservation does (and sets the V004 guard
    identifier explicitly instead of relying on a session that happened to run another
    package call). Verified red→green on live Oracle, and gated by the new INV-12
    invariant (a RESERVED connector must be held by a BOOKED reservation) in
    db/oracle/invariants.sql + the local-store mirror in test/sql/run-invariants.js.
  • The web image could not be pointed at a non-localhost API. next.config.js
    inlines NEXT_PUBLIC_API_BASE at build time, but apps/web/Dockerfile never
    set it and compose only set it at runtime — so every built image shipped pointing at
    http://localhost:4000/api/v1 and the documented Caddy/TLS deployment
    (app.example + api.example) could not be built at all. The Dockerfile now takes
    NEXT_PUBLIC_API_BASE / NEXT_PUBLIC_SITE_URL as build args, compose passes them
    from the environment, and the value is verified to land in the client bundle
    (built with a sentinel origin and grepped out of .next/static).
  • A root .env was silently ignored by every compose command. Compose's project
    directory is the compose file's directory (infra/), so the cp .env.example .env
    step DEPLOY.md §1 prescribes had no effect: the stack booted with the hardcoded dev
    JWT_SECRET and no overrides. Measured: docker compose -f infra/docker-compose.yml config renders JWT_SECRET: dev-only-32-byte-secret-…
    with a root .env present, and the real value with --env-file .env. DEPLOY.md
    (all commands), README, CONTRIBUTING and a comment on the compose api service now
    say so. Note --env-file feeds interpolation only, which is what keeps the
    services' explicit ORACLE_HOST: oracle / TS_HOST: timescale wiring intact.
  • The simulator's two most likely failures were unreadable. (1) The gateway
    upgrades the socket and then closes it with OCPP 4401/4404, which the simulator
    never handled — an auth refusal surfaced 8 s later as a bare ocpp timeout BootNotification, hiding the cause (the demo DB's admin-provisioned charge points
    carry random secrets, not the dev-<identity> the simulator assumes). It now names
    the close code, the likely cause and the two remedies. (2) Re-running --provision
    against a database that already had the fleet pushed nothing into cps and died
    with Cannot read properties of undefined (reading 'connector_ref'); an existing
    charge point is now reused (verified: a real session tx=118 ran on the reused
    fleet).
  • The FastAPI sidecar booted through a deprecated hook. @app.on_event("startup")
    is removed in current FastAPI and emitted a DeprecationWarning on every boot and
    every test that imported the app; it is now a lifespan handler (verified by running
    the lifespan directly: model_loaded = True). pytest warnings: 3 → 1 (the remaining
    one is the environment's own pynvml notice).
  • The Oracle-backed contract suite failed on any database that was not freshly
    seeded.
    test/run.js looked for a free connector on stations[0] only, so on a
    used compose stack it aborted with need an AVAILABLE connector while 18 connectors
    sat free elsewhere. It now searches every station. Also: the bill/pay assertions
    carry the response body in their message, so a rare failure names its cause instead
    of printing a bare 500 == 201.
  • The router barriers did not key on the identity they documented. The
    barriers introduced by the CodeQL pass are mounted ahead of authRequired, so
    req.user was always undefined when the key generator ran: every caller on one
    host shared a single …:ip:<addr> bucket while the comment claimed "30/min per
    user". Bucket identity now comes from verifiedClaims() — the signature-verified
    sub (the same SEC-006 rule the global throttle uses), falling back to the
    IPv6-safe ipKeyGenerator — so minting extra tokens no longer buys extra budget.
    Pinned red→green by RL-2 (apps/api/test/ratelimit.js), which fails against the
    pre-fix req.user?.id ?? ip key.
  • The same barrier config was copy-pasted into three routers. It is now one
    routerBarrier({ key, limit, message }) in middleware/security.js, and the
    token verification inside throttle() collapsed into the same verifiedClaims()
    helper — one definition of "who is this caller", which is what the two must agree
    on.
  • Removed the dead getStore() boot path (db/index.js: never called, 0%
    covered, and a duplicate of the hydrate/seed/fallback logic server.js now runs
    through `upgradeS...
Read more

v1.6.0

Choose a tag to compare

@github-actions github-actions released this 15 Sep 00:15
dfbd72a

Added

  • ADR-0015 acceptance-envelope scheduling (E6). The allocator consumes the
    same acceptance curve as the certifier and the twin (acceptanceFactor — one
    definition, three consumers), so plan-time and realised feasibility cannot
    disagree. Measured (100 paired seeds/level): charging shortfall 42.6% →
    2.36%
    , deadline miss 56.6% → 15.7%, plan-honesty gap 1,280 kWh →
    0.011 kWh
    , with bit-identical schedules below the knee (H0). Receipts in
    bench/results/e6-taper.json; protocol in docs/perf.md.
  • Acceptance-envelope contract test (apps/api/test/control-aes.js, wired
    into npm test): AES is unit-identical to the power-based allocator below
    the knee, honest above it, and every certificate promise is deliverable by
    an AES schedule.
  • Grid Current web redesign (all 17 routes): server-rendered editorial home
    with a live /health island; driver/operator navigation split with a
    keyboard-friendly mobile drawer; native <dialog> replaces every
    window.confirm/prompt; new PageHead/Section/Statband/EmptyState/
    StationPicker primitives; keyboard-accessible map pins and connector
    tiles; responsive table wrappers; per-route metadata titles; expanded design
    tokens (spacing, motion, z-index, tinted elevation) with full
    prefers-reduced-motion coverage. Zero new frontend dependencies; API
    contracts, CSP, and polling cadences unchanged. Home page is now statically
    prerendered (fast LCP).
  • Submission documentation sets under docs/: startup product thesis,
    research paper, patent invention disclosure, recruitment portfolio.

Fixed

  • Worker drain failsafe armed at boot (restart loop). The relay's 10 s
    drain failsafe was armed unconditionally at process start, force-exiting the
    worker ~10 s after every boot; compose restarted it and each boot relayed a
    batch, masking the bug while inflating restart counts. The failsafe is now
    armed only when a shutdown signal arrives.
  • Control-plane rate limiting (CodeQL ×15). The whole control surface now
    sits behind a canonical express-rate-limit router barrier (30/min per
    user, per-IP backstop, RATE_LIMIT_OFF honored) — every authorizing route,
    reads included, carries an explicit recognized limiter.
  • User-controlled allocation bounds (CodeQL ×2). Untrusted plan/certify
    horizon/dtMin values are clamped to the documented allow-list
    (model.clampHorizon/clampDtMin) before they can size solver allocations;
    benchmark direct-call contracts unchanged.
  • Log injection in the profile probe (CodeQL ×1). The closed-loop probe
    logs a boolean shape verdict only — no wire-derived values reach the log.
  • Screenshot capture hardening. Boot-probes candidates and skips charge
    points whose secrets were admin-generated (dev-<identity> no longer
    assumed); compose WEB_ORIGIN now also allows the documented capture
    console port (:3120).

Security

  • CodeQL alert count on main: 0 (down from 18). All 12 required checks
    green; durable control-plane E2E passes on the compose stack.

Docs

  • README + docs-site screenshot strip re-recorded against the Grid Current
    console (real stack, real OCPP session, home.png + discover.png added).

Added (control loop closed end-to-end — the actuate/verify stages are now real)

  • The plan cycle now CERTIFIES before it schedules. control/controller.js resolves
    admission facts from the store instead of test hooks: energy requirement from the
    connected vehicle's battery + target SoC (vehicleStateFor), deadline from the
    reservation that owns the connector, floors from ACTIVE feasibility_certificate
    rows, and it issues/reuses one certificate per active session via
    model.certifyVehicle. Contention is computed over overlapping charging windows,
    not over every vehicle ever admitted. New route
    POST /control/sessions/{id}/certify shares the same code path (a promise must not
    depend on who asked for it); the plan response carries certifications.
  • Certificates are taper-aware. model.acceptanceFactor (CC-CV knee at 80% SoC,
    decaying to 15%) is now part of the worst-case simulation, so a promise made at 20%
    SoC is not silently reused at 95%. The flat floor is the rate still guaranteed at the
    END of the simulated window — conservative by construction.
  • The loop's missing edge: CSMS→CP ack correlation. The gateway previously handled
    only inbound CALLs and dropped every CALLRESULT (if (msg.kind !== 'CALL') return),
    which made actuation fire-and-forget. It now correlates results against a bounded
    pending-call registry: SetChargingProfile acks land on the charging_profile_push
    row, CALLERRORs are dead-lettered, and an unanswered call is swept to an explicit
    ACK_TIMEOUT instead of being assumed successful. RemoteStop/RemoteStart share
    the same plumbing.
  • The verify stage now consumes real telemetry. MeterValues records an enforcement
    tick (scheduled kW from the profile in force vs the metered kW) whenever a profile
    is in force — a push is "in force" only after dispatch (markPushSent) and before any
    rejection (Rejected/CALLERROR disqualify the row). No profile in force means no
    comparison, never a fabricated zero. New route GET /control/enforcement.
  • Solver: feasibility-first, then price-aware, and honest about windows. The shipped
    solveSchedule now runs certified floors → EDF feasibility fill → a cost-shift pass
    that moves energy only into cheaper slots inside each vehicle's own
    [arrival, deadline) window
    ; it is deterministic (ties break on sessionId). The
    previous dead code (rank/price computed and then voided) is gone. arrivalAt is
    a hard lower bound, which is what makes future-arrival experiments valid.
  • Digital twin + offline harness (apps/simulator/src/twin.js,
    baselines.js, offline.js): deterministic Poisson-ish arrivals with commute peaks,
    battery/SoC/deadlines, ToU tariffs, CC-CV acceptance (imported from the certificate
    implementation so promise and experiment cannot disagree), non-compliant chargers, a
    shared evaluator, and a percentile bootstrap for CIs.
  • Experiments E1b and E3 (bench/e1b-cost.js, bench/e3-deadline.js) with
    pre-registered hypotheses, paired seeds, identical populations and 95% CIs, run by
    npm run bench:e1b / npm run bench:e3.
  • OpenAPI snapshot is generated and drift-gated (scripts/gen-openapi.js,
    npm run openapi:gen, --check in CI): docs/openapi.json had silently fallen 14
    paths behind the live spec (52 vs 66) while the README called it a verified snapshot.

Added (durable-path verification — the loop is now proven through the DB engine)

  • Control-plane E2E suite (test/e2e/control-plane.js, npm run test:e2e:control,
    wired into the CI e2e job): drives certify → plan → actuate → typed outcome → durable
    audit over HTTP against the compose stack, refusing to run unless the API reports the
    durable engine (mode: oracle). It asserts the electrical invariant holds through
    Oracle's triggers (child cap above parent → GRID_CAP_MONOTONIC), that ADVISORY never
    touches the wire, that every ENFORCED actuation is either dispatched or a typed
    error (never a 500), that the promise is explicit (an admission commits real power; a
    refusal states why), and that no write-through mirror failure was recorded. Re-runnable:
    it reuses an existing site root, walks a ladder of booking offsets to find a genuinely
    free reservation window, and stops its own session so no active vehicle is left behind.
  • /health.process_started_at: monitoring needs it to tell "restarted just now" from
    "degraded for an hour", and it lets the durability test prove an endpoint serves
    evidence that predates the running process.

Fixed

  • Experiment receipts were not reproducible, despite the protocol saying they were. The
    twin anchored each scenario to Date.now() and read diurnal quantities with
    getHours(), so the horizon slid across ToU bands and commute peaks between runs (the
    same command produced different numbers minutes apart) and the results depended on the
    runner's timezone. Scenarios are now generated at a fixed UTC epoch
    (twin.SCENARIO_EPOCH, recorded in every receipt as args.scenario_epoch), touPrice
    and arrivalIntensity use UTC, and two new twin tests pin the anchor and the timezone
    independence. bench/results/e1b-cost.json and e3-deadline.json were regenerated and
    every figure in docs/perf.md, README.md and ADR-0014 updated to match — including a
    softer E3 taper-family claim (at ρ = 0.9 the deadline delta is now indistinguishable
    from static-cap rather than significantly worse; the honest statement is the softer one).
    Receipts are also compacted (offline.compactRow): the per-slot schedule matrices and
    the per-session arrival/deadline slot maps are scenario inputs, not results — dropping
    them took the two receipts from 14 MB of debug detail to ~5 MB of per-seed evidence,
    with every retained number byte-identical.
  • Station and charge-point provisioning returned 500 on the durable engine. Both
    write-through wrappers return await withMirrorRetry(...) — the mirror's return value,
    which is undefined — instead of the local method's result, so the routes that
    destructure { station, provisioned } blew up with PROVISION_FAILED: Cannot destructure property 'station' of undefined. POST /admin/stations and POST /admin/charge-points
    therefore worked on the local store and failed on Oracle, which is why the db-tests
    STORE=oracle suite was red. (The neighbouring updateStation wrapper already returned
    the right shape — the comment there even says so.) Both now return the local result.
  • A reservation could be advertised with an id Oracle had never issued. Oracle's
    identity sequence caches values, so a container...
Read more

v1.5.1

Choose a tag to compare

@github-actions github-actions released this 08 Sep 09:07
ff80543

Fixed

  • BUG-048 (durable engine, latent crash): interleaved Oracle mirror failures
    could GROW an undo array back and crash the ack route.
    The write-through
    adapter's undo paths rolled back appended rows with arr.length = savedLen;
    if two mirrors failed around a shared snapshot (A appends 2 → B captures → A
    fails and truncates to its snapshot → B fails and sets the length back to its
    own later snapshot), the array was left with sparse holes. Every later
    outbox.find(...) visited undefined and /internal/outbox/ack 500'd
    forever — observed live on the compose stack (worker acks failing, outbox lag
    stuck). All rollback sites now use truncateTo, which only ever shrinks
    (clamps to the saved length, never resurrects a hole; rows appended by other
    writers after the snapshot are kept — the correct at-least-once behaviour).
    Regression: apps/api/test/run.js BUG-048 drives two interleaved failing
    mirrors through the REAL exported truncateTo and asserts zero sparse holes
    (red against the pre-fix assignment), plus BUG-048b pins /internal/outbox/ack
    as 200 + idempotent for unknown/replayed/empty id batches.

Added

  • compose forwards RATE_LIMIT_OFF so DB-backed k6 load runs
    (bench/README.md experiments 1/2/5) can disable the 60 req/min throttle
    without editing compose (the middleware toggle already existed; the env was
    not plumbed through the api service).

What's Changed

  • fix(api): BUG-048 — shrink-only undo truncation (sparse-hole crash) + ack idempotency by @humoge7502 in #16
  • docs: v1.5.1 changelog (BUG-048 round) by @humoge7502 in #17

Full Changelog: v1.5.0...v1.5.1

v1.5.0

Choose a tag to compare

@github-actions github-actions released this 07 Sep 23:05
10e8814

Added

  • AI advisory platform (apps/ai, ADR-0008) on the 8× A100 host. A FastAPI
    sidecar with a GPU-batched fleet demand simulator (15× vs CPU), a trained
    per-station demand forecaster evaluated against seasonal-naive + ridge baselines
    on a held-out tail (MLP 56.4 < ridge 63.1 < naive 73.8 MAE; the first honest run
    shipped beats_naive: false before a target-normalization fix), a deterministic
    smart-charging LP (measured: stays on CPU at CSMS scale), and a measured GPU
    allocator (least-loaded pick + file-lock quota + utilization receipts; no K8s by
    design). API surface: /ai/forecast, /ai/anomalies, /ai/optimize — RBAC +
    operator station scope enforced BEFORE the sidecar, advisory: true on every
    response, audit-logged, 503 AI_UNAVAILABLE degradation (6 JS contract tests +
    8 pytest; GPU smoke opt-in). Docs: docs/ai-platform.md.

Fixed

  • BUG-039 (B2G-013b, authz): starting WITHOUT reservationId hijacked RESERVED
    connectors.
    startSession allowed RESERVED with no reservation id — omitting
    the field converted another driver's window (OCPP StartTransaction and REST
    /sessions/start both). Rule now: a RESERVED connector converts only for the
    owner of a BOOKED window on it (explicit id or owner-adoption for the OCPP idTag
    flow); everyone else gets 409 RESERVATION_MISMATCH and the window is untouched.
  • BUG-040 (robustness): an oversized OCPP frame crashed the whole API. The
    gateway had no socket error handler; a >maxPayload frame emitted an unhandled
    error event and took the process down. Frames are now capped at 256 KB (ws
    default was 100 MiB ⇒ ~1 GB/s parse load per malicious CP) and socket errors
    clean up via the close path. Regression: gateway-close.js t4.
  • BUG-041 (durable engine, latent P1): register published pub(Promise) under
    STORE=oracle.
    routes.js never awaited store.createUser, whose Oracle
    mirror wrapper (BUG-038) is promise-shaped — the durable register response was a
    garbage user + a sub: undefined JWT. All createUser call sites now await;
    regression test 27 pins promise-shape parity.
  • GAP-002 (provisioning): POST /admin/charge-points created NO connector. A
    standalone-provisioned CP could never charge (reservations 404'd "unknown
    connector"), diverging from provisionStation's default. Same default now: one
    TYPE2/22 kW connector unless connectors[] is given.
  • BUG-043 (contract parity): the Oracle packages raised ORA_20503-style codes
    while the local store throws OVERLAP/TICK_REJECTED/… — callers, routes and
    tests key on the canonical names, so the durable engine and the local profile
    disagreed on the error contract.
    fromDriver now restores the canonical code
    from the package number via an explicit ORA_CODE_BY_NUM table (mirror of the
    store's own ORA table; both keyed to the RAISE_APPLICATION_ERROR numbers in
    V003, with a comment demanding they stay in sync). Unknown numbers keep the
    ORA_XXXXX fallback; already-normalized errors pass through untouched.
    Regression test in apps/api/test/run.js.
  • BUG-044 (boot race, P1): the Oracle upgrade ran while the socket was ALREADY
    accepting.
    Requests in the hydration window wrote local-only ghost rows
    (register → user missing in Oracle, invoice → billing_pkg later 500'd) and the
    STORE=oracle CI step attached the adapter MID-SUITE at a nondeterministic test
    boundary. server.listen now awaits the store upgrade (attached or
    local-fallback) before binding the port — the durable engine is armed before the
    first request can arrive, in tests and in compose. Regression test in
    apps/api/test/run.js; the ordering itself is exercised by the STORE=oracle
    suite against a live Oracle.
  • BUG-045 (durable engine, exposed by BUG-044): session STARTS were local-only.
    REST /sessions/start and OCPP StartTransaction never reached Oracle — the
    first wrapped call on a fresh session 500'd ORA-01403 (no data found).
    db/oracle.js now wraps startSession with charge_session_pkg.start_session
    (local-first validation incl. B2G-013b owner-adoption; the package only sees
    validated starts) and remaps the read-cache to the durable id when the counters
    diverge (a failed mirror consumes an Oracle IDENTITY value — the remap makes it
    harmless). Red→green against a live Oracle.
  • BUG-046 (durable engine, exposed by BUG-044): the demo pre-seed poisoned the
    Oracle-hydrated read-cache.
    seedStore ran before the upgrade and hydrate()
    is additive, so 60 ghost demo sessions + their meter readings survived — every
    real tick on a fresh session read {deduped:true} and never flipped
    PREPARING→CHARGING. With ORACLE_HOST set the store now starts empty, hydrates
    to exactly what Oracle owns, and seeds only when the DB is genuinely empty (the
    rule getStore already documented).
  • BUG-047 (durable engine, exposed by BUG-044): admin hardware writes were
    local-only.
    POST /admin/stations, POST /admin/charge-points and
    PATCH /admin/stations/:id never reached Oracle — the first reservation on a
    provisioned connector 500'd ORA-01403 (no connector row). All three are now
    store methods mirrored with explicit local ids (BUG-038 pattern); the routes
    await them (BUG-041-class promise-shape) and the mirror preserves the local
    return shape. station.address_line now defaults to a non-empty sentinel
    ('' is NULL in Oracle, violating NOT NULL).
  • Seed + invariants-runner honesty fixes. The Oracle seed funded two wallets
    with no wallet_ledger rows (INV-2 failed on a fresh DB) — ledger rows added.
    test/sql/run-invariants.js split its SQL on ALL ; — the header comment
    carries one, so the oracle gate silently fell back to local checks on every run;
    it now strips full-line comments first and resolves oracledb from the hoisted
    root or apps/api/node_modules. The real SQL runs and gates CI.

Changed

  • PERF-002: Argon2id moved off the event loop for HTTP auth. Login verifies and
    register hashes via the sidecar's async (thread-pool) API; the sync forms remain
    for seeds/store-internal use. Motivation: the 100-charger connect-storm soak
    showed synchronous ~29 ms KDF burns freezing OCPP frame handling.

  • PERF-004: METER_REGRESSION check is O(1) per tick. recordTick scanned ALL
    readings per tick (quadratic across a concurrent fleet); a per-session max-meter
    index seeded lazily from hydrated rows replaces it. Measured: ingest 21,739 →
    42,553 ticks/s on the same bench.

  • PROTO-002/003 (OCPP correctness): the 10 msg/s rate-limit CALLERROR now
    carries the offending message's uid (was '0'), and StopTransaction on a
    never-charged (PREPARING) session ends CANCELLED with a normal CALLRESULT
    (was: InternalError from an ILLEGAL_TRANSITION). Regression: gateway-close.js
    t5/t6.

  • Simulator fleet mode: --provision builds N real charge points via the admin
    API (a burst of N must mean N connectors — the old flow mapped N flows onto 16
    seeded connectors and the one-socket-per-CP guard rightly killed duplicates), and
    the sim's OCPP tag is the sim driver's own TAG-<uid> (hardcoded TAG-1 was
    user 1). --ramp-ms staggers cold connects. Receipt: 100/100 provisioned
    fleet, 0 timeouts, 0 errors
    (was 7+ timeouts at 100 simultaneous).- RESIL chaos tests (relay): API-down accumulate/backoff, ack-failure replay
    dedupe (crash-after-append never duplicates), and recovery drain are pinned by
    three new worker tests using injected failure seams (relayOnce({fetchImpl, outFile})); the real mirror is never touched by tests.

  • Deps: pg promoted to a direct dependency of @volthub/api (was optional /
    transitive). The worker's Timescale relay (relay-timescale.js) loads pg and
    ships in the api image (apps/api/Dockerfile), so npm ci must install it — the
    compose worker failed on a missing pg after a clean npm ci.

  • SEC-013: Argon2id password hashing implemented (the documented production target).
    hashPassword now emits standard Argon2id PHC strings (m=19456 KiB, t=2, p=1 — the
    OWASP-recommended baseline) via @node-rs/argon2 (prebuilt binaries, no node-gyp,
    Node ≥10 so the 20/22 CI matrix stays honest). verifyPassword accepts both the new
    format and the legacy $scrypt$… shape, so hydrated durable rows and old seeds keep
    logging in — no data reset. Malformed/garbage stored hashes fail closed without throwing.
    The Oracle seed's demo hashes were regenerated as Argon2id; the SEC-011 dummy pad uses
    the same parameters, so unknown-email timing parity holds (re-measured ≈20 ms both paths).
    Regression-gated by TEST-SEC-ARGON2-1 (16 security tests, all green).

Fixed

  • BUG-031 (authz): operator cancel + remote-start ignored station scope.
    POST /reservations/:id/cancel accepted any operator for any booking, and
    POST /sessions/remote-start gated on role only — an operator assigned to station A
    could cancel bookings and drive chargers on station B. Both now enforce the same
    stationScope contract as PATCH /sessions/:id/state and requireOwned (scope
    checked in the route, re-asserted in cancelReservation for direct store callers).
    Regression tests 20/21 assert 403 OUT_OF_SCOPE out of scope and success/CP_OFFLINE
    in scope.
  • BUG-032 (input): /me/wallet/topup crashed on non-numeric bodies.
    Number(req.body.amount) on "lots"/undefined is NaN, which slipped past the
    store's amount > 0 guard as a 500. The route now returns 422 INVALID_AMOUNT for
    non-finite/non-positive amounts (per-transaction cap unchanged, still enforced in the
    store). Regression test 23.
  • BUG-033 (input): admin user + charge-point creation accepted garbage.
    POST /admin/users took any role string (Oracle CHECK allows DRIVER/OPERATOR/ADMIN)
    and any email; POST /admin/charge-points took any ocpp_identity (which is embedde...
Read more

v1.4.0

Choose a tag to compare

@github-actions github-actions released this 05 Sep 17:55

Added

  • httpOnly refresh cookie + logout (SEC-012, closes the tracked P2): login/register/
    refresh set vh_rt as an httpOnly, SameSite=Lax cookie scoped to /api/v1/auth
    (Secure in production); POST /auth/logout revokes the refresh family server-side
    and clears the cookie (audited as LOGOUT). The JSON body field remains for non-browser
    clients. CORS upgraded to explicit-origin allow-list + credentials: true. Web logout now
    calls the API (best-effort) before dropping the local token. Regression-gated by
    TEST-SEC-COOKIE-1..4 (13 security tests total, all green).
  • Flagship cross-link: README footer now points to Q-Trust
    — the two projects share one receipts-first discipline.
  • Benchmarks re-run post-SEC-012 (same hardware): race invariant intact (exactly 1×201 +
    19×409), auth-path overhead unchanged. Raw: bench/results-local.json; tables in docs/perf.md.

Full Changelog: v1.3.1...v1.4.0

v1.3.1

Choose a tag to compare

@github-actions github-actions released this 05 Sep 17:29

Added

  • Publishable REST contract: docs/openapi.json — the full OpenAPI 3.0 spec
    (48 paths / 52 operations) captured from the live /api/v1/docs endpoint and
    committed as a static artifact, so the API surface is reviewable without
    booting the stack; the CI drift gate keeps it honest against routes.js.
  • Community-health complete: CODE_OF_CONDUCT.md (precise, honest, useful —
    receipts bar applies to conduct too), SUPPORT.md (routing table with honest
    response expectations), CITATION.cff (repo is now citable; GitHub renders a
    Cite this repository button), and .vscode/extensions.json recommending
    the actual toolchain (eslint/prettier/mermaid/Oracle dev tools/rest-client).
  • README docs map + docs/README.md index the new contract artifact.

What's Changed

  • build(deps-dev): bump globals from 16.5.0 to 17.12.0 by @dependabot[bot] in #7

Full Changelog: v1.3.0...v1.3.1

v1.3.0

Choose a tag to compare

@github-actions github-actions released this 05 Sep 17:00

Added

  • Coverage receipts: npm run coverage (c8, V8 instrumentation, --all
    over apps/*/src + packages/*/src) and a 6th CI job publishing line
    coverage to Codecov. Informational by policy (codecov.yml) — the badge is
    a receipt, not a target; DB-backed suites stay in db-tests on purpose.
    Local: npm install then npm run coverage (text + lcov + json-summary).
  • Pages via Actions: pages.yml deploys docs/ as a Pages artifact
    (Settings → Pages → Source: GitHub Actions), with a lean-site budget check.
  • Community kit: full CONTRIBUTING.md (project map, two dev profiles,
    the 10-rung testing ladder, ground rules, ADR-first changes, release path);
    greetings.yml first-contact replies routed to the docs that matter;
    Discussions templates for ideas (trade-off required) and q&a
    (what-you-already-read required).
  • Visual identity refresh (electric indigo): README header banner,
    re-rendered architecture-hero.png (now shows the 6-job CI pipeline) and
    social-preview.png — all real renders, no third-party banner services.

Changed

  • oracledb 6.10.0 → 7.0.1 (the last deliberately-deferred upgrade): merged
    after a fully-green db-tests run against real Oracle 23ai — the exact gate the
    hardening report set (no blind driver bumps without DB-backed CI).
  • ci.yml push trigger scoped to main so v* tag pushes don't re-run the
    whole 6-job matrix (the tag-driven release.yml already re-verifies).
  • README: image-first header, Codecov badge, stats row and CI row updated to
    6 jobs; Quickstart gains the coverage rung.

What's Changed

  • build(deps): bump oracledb from 6.10.0 to 7.0.1 by @dependabot[bot] in #5

New Contributors

Full Changelog: v1.2.0...v1.3.0

v1.2.0

Choose a tag to compare

@github-actions github-actions released this 05 Sep 12:03

Repo / presentation (GitHub pack)

  • README v2: badge row (ci · release · OpenSSF Scorecard · Ask DeepWiki ·
    node 20|22 · Oracle · TimescaleDB · license · docs site), "New here?" reading
    paths, route/engine/package/ADR/CI/CVE stats row, "Stack, and why" trade-off
    table, mermaid system diagram, and a docs map.
  • GitHub Pages one-pager at docs/index.html (deploys from main + /docs)
    and a 1280×640 social preview docs/social-preview.png.
  • YAML issue forms (bug / feature / security-routing config.yml) replace the
    old markdown templates; .gitattributes keeps the language bar honest.
  • Tag-driven release.yml (verify → GitHub Release with CHANGELOG notes) and
    OpenSSF scorecard.yml (weekly + on push, SARIF → Code Scanning).
  • Profile README published at humoge7502/humoge7502.

Security (HARDEN-2026-09)

  • Zero known CVEs across both lockfiles: express 4.19 → 5.2.1 (DoS CVE
    chain GHSA-4mjr-xmp4-gh2g closed), Next 14.2.5 → 16.3.4 + React 19 (framework
    CVE cluster + postcss chain closed). npm audit is now a CI gate on both
    lockfiles (security job, 5-job pipeline).
  • SEC-011: login no longer leaks account existence by timing — unknown emails
    are verified against a fixed dummy scrypt hash (measured ≈42 ms both paths,
    gap 0 ms across 3 sample pairs). Regression-gated in apps/api/test/security.js.
  • SEC-010: x-powered-by framework fingerprint removed; proxy trust is now
    opt-in via TRUST_PROXY (BUG-023) so the documented Caddy deploy profile
    cannot collapse the per-IP login throttle into a platform-wide outage.

Changed

  • Platform toolchain: eslint 8 .eslintrc → eslint 10 flat config
    (eslint.config.js; stricter gate immediately caught an unused catch binding
    and a never-read assignment); pino 9 → 10.3.1; Node 22 LTS images with a
    Node 20/22 CI matrix keeping engines >=20 honest; checkout/setup-node v7;
    concurrency cancel-in-progress.
  • Web: Next 16 + React 19 — dynamic-route pages migrated from sync params
    props to useParams() (the /stations/:id + /session/:id routes were
    verified in a real browser, zero page errors).
  • scripts/check-openapi.js uses the app.router getter (Express 5 removed
    app._router) — the OpenAPI drift gate would otherwise have silently zeroed.
    Re-verified: spec=48 routes~52, no missing paths.

Fixed

  • BUG-021: a reconnecting charge point's stale socket deregistered its live
    successor and marked the charger OFFLINE — the gateway close handler now only
    clears the registration it owns. Regression suite apps/api/test/gateway-close.js
    was validated to catch the bug (unguarded handler → red).
  • BUG-022: worker/relay HTTP calls were unbounded — a hung API could freeze the
    telemetry pipeline. Every call now carries AbortSignal.timeout(5s).
  • No graceful shutdown anywhere: the API now drains on SIGTERM/SIGINT (stop
    accepting, close OCPP sockets, 10 s failsafe) and the worker stops between 2 s
    cycles; compose services gained stop_grace_period above the failsafes and
    restart: unless-stopped.

Added

  • Worker now syncs Timescale station_map from GET /internal/station-map on
    every loop (masterplan §26.5, previously unimplemented): Oracle-owned station
    metadata denormalized for query-time cagg enrichment, so v_tick_1m_enriched /
    v_tick_1h_enriched and the Grafana per-station dashboard have data in prod.
    Relay unit tests (apps/worker/test/relay.test.js) wired into npm test and
    CI; e2e verifies station_map is populated by the live worker.
  • The API Dockerfile never copied apps/worker (compose runs the worker from the
    same image), so the worker silently exited in the compose stack — B2G-001's
    relay was never live in e2e. The image now ships apps/worker.
  • (Superseded in HARDEN-2026-09 below: actions are now at v7 with a Node 20/22
    matrix — see the Security/Changed sections above.)

Fixed (migrations were never run end-to-end — “CI partly theater” closed)

  • db/oracle: V001 now owns the cp_id/connector_no FK pair so V002/V003
    compile on a fresh DB (V005's guarded ALTERs no-op for fresh runs, still
    repair legacy DBs).
  • db/oracle: audit_log.old_value/new_value dropped their CHECK (IS JSON)
    — audit_pkg.log writes plain text/NULL diffs, so the seed's tariff and
    package audits raised ORA-02290; V006 drops the constraint on legacy DBs.
  • db/oracle: maintenance_pkg now sets the pkg: CLIENT_IDENTIFIER the
    V004 connector guard allow-list requires (seed fault story raised ORA-20801).
  • db/oracle/seed: COMMIT per story (autonomous audits self-deadlocked a
    single giant transaction with ORA-00060); fixed an illegal scalar subquery
    in the fault story's procedure argument.
  • db/oracle/V004: least-privilege role bootstrap now guards for the missing
    CREATE ROLE privilege on the schema-owner migrate account — one note
    instead of 90 ORA lines when skipped; full grants when run privileged.
  • db/timescale/T002: continuous aggregates can only query one hypertable —
    rewritten join-free (P2V-01) with MODE() replaced by per-state FILTER
    counts + v_state_1m (P2V-02); enrichment moved to query-time views
    v_tick_1m_enriched/v_tick_1h_enriched.
  • scripts/migrate.sh: Oracle migrations exec sqlplus inside the running
    container (never a silent skip via docker run), and both Oracle and
    Timescale paths now fail loudly when a migration errors.
  • CI: the local-store suites (contract/race/security/xlayer) blank
    ORACLE_HOST so they can't nondeterministically attach the Oracle adapter
    mid-run; db-tests adds a Timescale cagg-refresh smoke (closes P2V-01/02 on
    every push); the e2e job provisions DBs via scripts/migrate.sh (initdb
    mounts hid the Timescale image's setup scripts and ran queries.sql) and
    waits for stable Oracle connects before migrating.

Full Changelog: v1.1.0...v1.2.0

VoltHub CSMS v1.1.0 — audit remediation complete + evidence kit

Choose a tag to compare

@humoge7502 humoge7502 released this 05 Sep 07:38

VoltHub CSMS v1.1.0

Two-engine EV charging management: Oracle 23ai (ACID reservations/billing/ledger in PL/SQL packages) + TimescaleDB (telemetry hypertables/caggs), driven by an OCPP 1.6J WebSocket gateway + simulator fleet, race-tested on both engines, with a Next.js "Grid Current" console.

Audit Round 2 (B2G) — P0/P1 security + integrity

  • Object-level authorization on invoices/sessions (BOLA triad closed, attacker-probe tests)
  • Real Oracle write-through adapter (ADR-0005), hydrate of the transactional world, durable outbox acks + idempotency under STORE=oracle
  • Pay-parity across engines (failed payment → invoice stays DUE) locked by a cross-layer test
  • Reservation-ownership at session start (hijack → 409), JWT alg pin, login throttle, role gates on analytics/faults
  • Timescale relay armed in compose + (session_id, seq_no, ts) sink PK, monotonic gateway sequencing
  • Docs truth pass (SECURITY/README/ADRs), lint + format CI gates, community kit (dependabot/templates/CODEOWNERS)

Audit Round 3 (B3G) — verification kit

  • V006 / ADR-0006: connector linkage is FK-native (cp_id, connector_no pair, sync triggers retired, D-02 band minutes + unique slot)
  • OCPP remote commands: operator RemoteStart/RemoteStop contract test (real WS, Basic auth) — 2 tests in npm test + CI
  • Orphaned-invoice guard, insertBatch rename, V004 wording, ADR-0005 multi-instance note
  • Bench harness + measured tables (discovery p95 2 ms, 20-race 1×201/19×409, 22,989 ticks/s) — methodology in docs/perf.md
  • Real-pixel README evidence: @2x screenshots (dashboard · live session · telemetry · invoice) + live OCPP-driven GIF; re-recordable via scripts/screenshots/
  • Web CSP hydration fix (console now actually hydrates in dev + prod)
  • Invariant suite extended to 11 checks (0 rows), OpenAPI drift gate zero-notes

Verification

npm test (API 16 · sim 2 · security 7 · XLAYER 4 · OCPP-remote 2 · invariants 11 · drift OK) · race suite (R1/R4) · e2e 7 steps · lint + prettier clean · Next prod build clean. P2V-01..03 (Timescale/Oracle runtime refresh) remain CI-covered per push — this host's shared disk is full; steps documented in docs/verification.md.