QuantumBNG 1.4.0 — Release Notes

This is the first generally available release. Everything before it was
alpha, and these notes describe the product as it ships rather than as a
delta between alpha builds. Where a change matters because the alpha behaved
differently, that is called out against "the alpha" as a whole — individual
pre-GA version numbers are not carried forward.

Covers the QuantumTouch BNG appliance and console together with the Quantum
BNG controller. Controller-layer items are marked [controller];
everything else is the QuantumTouch console, installer and evaluation
appliance.

Overview

The box becomes the system of record, and the console operates it.

The alpha staged two capabilities it could not use: capacity licensing, with a
page that displayed a licence but could not activate one, and usage records
that were held but never produced from live traffic. Both are complete in GA.

Three further capabilities arrive with them. Local AAA becomes the
authoritative subscriber store
rather than a mirror of controller
configuration, with enforcement overlays that converge on every reconcile pass.
Data-cap enforcement is delivered end to end, from the policy an operator
sets to the rate applied on the wire. And accounting no longer depends on the
browser
: a background service drains the controller's spool into a durable
local ledger every 30 seconds, where records were previously drained only while
a page was open.

The console also adds what an operator needs when something is wrong — a
single-action support bundle that opens with a written summary, and health
reporting that states how often the dataplane connection has dropped rather than
only whether it is currently up.

Component versions

What GA ships. Pre-GA component versions are not listed — the alpha is
treated as a single prior state rather than a series to diff against.

Component Version
Quantum BNG controller v1.4.0
quantum_core v1.6.0
quantumtouch-bng (console plugin) 0.10.37
quantumtouch-core v0.10.24
CPESIM plugin 0.4.18
VPP 25.10.0-23 (v25.10.0-23~g744d3c715)
Installer bundle 1.4.0-14

Requirements

  • Ubuntu 24.04 (Noble), x86_64, kernel 6.x. Ubuntu 24.04.4 ships
    6.8, which is the kernel this release is validated on; installer preflight
    warns below 6.14 because a few DPDK plugins fall back there. If that
    fallback matters for your NIC, linux-generic-hwe-24.04 provides 6.14 —
    it is not required.
  • TPM 2.0 (/dev/tpmrm0) on the target is required to activate a capacity
    licence. The free tier requires no TPM.
  • VPP stats-segment sizing. The BNG allocates a forwarding entry and
    per-session counters for every subscriber, so the statistics segment must be
    sized to the deployment's subscriber count — VPP's 32 MB default is exhausted
    well before scale. 1.4.0 sizes this on install; see No validated VPP sizing
    profiles
    under Known issues for what is and is not automatic.

Completed for GA

Capacity-tier session licensing — now operable from the console

The alpha shipped the Licensing page and said plainly that it "cannot yet
operate activation." It can now. Three defects stood between the page and a
working activation, all fixed:

  • The three licence routes were missing their /v1 prefix and returned 404
    on every call, so the page could not read status or fingerprint at all.
  • A trailing newline invalidated valid licences. A licence pasted from a
    text editor or downloaded to a file failed verification for a reason the
    operator could not see.
  • Activation itself did not work from the console — the shipped CLI carries
    the service key, while activation requires admin_only.

The page now reads status, shows the fingerprint, and activates a pasted or
uploaded licence. The capacity in force, and its provenance, are stated rather
than inferred from the number.

Full accounting — records now come from live traffic, and nothing waits on a browser

The alpha marked usage records a preview: held, downloadable and prunable,
but not produced from live subscriber traffic.

GA produces them. The console now runs a background drain that claims the controller's
accounting spool every 30 seconds, writes it to a durable SQLite ledger in one
transaction, and acks only after the commit lands. That ordering is the one
non-negotiable property in the design: a crash before the ack costs a harmless
re-drain, while a crash the other way destroys billing records permanently,
because the ack prunes them upstream.

Previously the browser consumed that queue. Records drained only when
someone opened a page, and the ack — which permanently removes what it consumes
— was driven from the UI.

The ledger holds immutable CDRs for completed sessions, a live row per open
session, and per-subscriber daily usage rollups, with a daily prune and a
scheduled export that exports what is about to be pruned rather than the whole
ledger every day.


New features

AAA as system of record [console]

Local AAA is now the authoritative subscriber store, not a view over controller
config. subscriber_id is treated as an exact opaque key everywhere — never
folded, never case-normalised, never inferred from a locator. Enforcement
overlays converge on every reconcile pass rather than drifting until something
rewrites them, and a subscriber the console does not manage has its overlays
left alone.

Data-cap enforcement [console]

An operator sets a monthly cap and a consequence; the box applies it. The
subscriber drawer carries a data-cap panel showing the policy, the usage against
it and the grant history. Disabling enforcement clears every overlay rather than
leaving the last one in force.

Static IP assignment [console]

Assign a fixed address from the subscriber drawer. A conflicting address is
rejected before it is written rather than after. "Not mentioned" and "unassign
me" are now distinguishable, which they were not — an update that simply did not
mention an address used to read as a request to remove it.

Per-subscriber raw rate [console]

A raw per-subscriber rate, expressed as a plan, settable from the subscriber
drawer — with the consequence for that subscriber's cap stated on the same
screen rather than discovered later.

Subscriber sessions [console]

An account's live sessions are listed on its own page, and an operator can end
them from there.

Support bundle [console]

One button produces a diagnostic bundle for a ticket. It leads with a written
summary rather than a directory dump, and does its own first-pass triage: GRUB
drift, a licensed box mislabelled free_baseline, a packet capture that caught
nothing, VPP workers against rx-queues, retained config revisions.

It collects intent as well as running state — what the operator meant to
configure, not only what is currently live — and runs detached, because an
in-worker collection thread used to take the dashboard down with it.

New in this release: the controller's cumulative health counters. GET /bng/health is point-in-time and structurally cannot say the dataplane
connection has dropped four times today; the bundle samples it three times at
ten-second intervals, so it catches a flap only if one happens inside that
window. connection_lost_count states it for the whole uptime.

Add-on lifecycle [console]

Update checking that knows when to stay quiet, install from an uploaded tarball
for an offline box, a catalogue whose URL an offline mirror can own, and
retention of one previous artifact so a failed update can roll back without a
download.

Health page

The VPP card now qualifies "Connected" with how often the connection has
dropped, and goes amber when connected-but-flapping. A green pill over a link
that dropped four times reads as "nothing to see here" to the operator who is on
the phone about it.


Alerting and the metrics reference

Six new Prometheus alert rules ship in the bundle, covering data-cap
enforcement — which previously had none, despite being the subsystem that
throttles paying subscribers:

alert fires when
BngCapEvaluationStopped the evaluation loop has died — no caps applied, lifted or rolled over
BngCapOverlayFailing a cap was decided but the push to the controller failed
BngCapEnforcementSpike mass enforcement against the recent baseline
BngAccountingRecordsQuarantined a record could not be parsed, so it will not be billed
BngAaaUnmanagedSubscribers subscribers exist on the box that the console does not manage
BngAaaPartialPushPersisting pending provisioning pushes have stopped draining

BngSessionCapApproaching is also fixed: in the alpha it could never
fire, which is worse than no alert because it reads as coverage.

docs/metrics/prometheus-metrics.md now documents all 36 exported metric
families, up from 18. Everything the 1.4.0 subsystems added — data caps, the
accounting drain, AAA reconcile — was previously undocumented, which left half
the /metrics surface unusable for building your own dashboards. The reference
now also states which families are emitted on the 60-second accounting-drain
tick rather than the 5-second sampler tick, since that changes what a sensible
rate() window looks like.


From the controller [controller]

Delivered upstream in controller 1.4.0 and available to every console in this
release:

  • Faster subscriber onboarding. The DHCP onboarding path runs on a dedicated
    worker pipeline with request batching and de-duplication, raising the sustained
    rate at which subscribers can join and smoothing bursts.
  • Reboot resilience. After a bridge or controller restart, DHCP punting
    re-subscribes correctly and health reports the real state rather than
    reporting OK while delivering nothing.
  • Session recovery. A session the data plane retained through a recovery
    cycle is re-adopted rather than dropped, removing a class of divergence
    between the controller and the data plane.
  • Accounting across an unclean restart. Open accounting records are
    reconciled and tagged correctly rather than being left open.
  • Captive-portal refinements. A short portal lease settles activation
    faster, and additional TCP ports can be opened through the walled garden.

Behaviour changes

Read these before upgrading. None is a bug fix; each changes what the box does.

Licence enforcement for add-ons is now LIVE

This is the most consequential change in the release.

The console read licence entitlements from a field named features. The
controller reports them as grants and has never sent features, so every
entitlement resolved as unknown — and unknown failed open. Add-ons installed
regardless of licence
, while the console reported that enforcement was not
active.

This is corrected. The consequence: a paid add-on on a box whose licence does
not grant it is now denied.
No existing deployment is affected, because the
only add-on shipped to date is the Grafana dashboard pack, which is published as
permanently free and is permitted by name rather than by grant.

Two related changes:

  • The licensing API response field features is now grants, carrying a
    map of name to value rather than a list of names, so an entitlement can
    express a quantity as well as a yes or no.
  • Add-on entitlements are named under *`addon.`**.

Idle timeout now defaults to 1 hour [controller]

Raised from 5 minutes, sized for always-on subscribers so that an ordinary
traffic lull no longer reclaims a live session. Any value can still be set per
subscriber.

The Subscriber column is removed from the sessions table

It could not work on controller 1.4.0. GET /bng/sessions returns no
subscriber_id and no circuit_id/remote_id, so the column's join had nothing
to join on and every session rendered "unprovisioned." A column that is
always wrong is worse than no column: it actively tells the operator their fleet
is unprovisioned.

It returns in 1.5.0, joined on subscriber_id, which the controller adds to the
session summary in that release.


Breaking changes **[controller]**

Controller 1.4.0 reworks the subscriber-identity and provisioning contract. A
management plane integrated against 1.3 must be updated, and the subscriber base
must be re-provisioned — these are not backward compatible.

The console in this release is already built against the new contract, so an
operator using the console needs to take no action beyond the re-provisioning
below. These matter to anyone driving the REST API directly.

  • Subscriber records are keyed by an operator-assigned account identity and
    carry their locators explicitly, replacing the previous line-id keying. The
    subscriber base must be re-provisioned under the account model.
  • The AAA source is selected at a new /bng/aaa endpoint. The previous
    open/gated mode field on the local-AAA configuration is removed.
  • A live plan or status change is made by writing the subscriber record,
    which cascades into the running session. The separate per-session plan and
    revoke calls are replaced.
  • Bulk provisioning request and response fields are renamed to the account
    identity.
  • PUT /bng/session is now a partial merge — fields omitted from a request
    are preserved rather than cleared. A caller that relied on omission to clear a
    field must now clear it explicitly.

The controller's own release notes carry a migration checklist mapping each old
call to its replacement, and a Feature Integration Guide with the request and
response contract per capability. Both are the authority for integration work;
this section only states what breaks.


Bug fixes

Selected; the ones whose absence changed behaviour an operator would notice.

Subscriber simulator (CPESIM)

  • The demo passes traffic again. CPESIM 0.4.17 shipped without the commands
    that clear stale veth devices before recreating them, so the simulator's
    uplink path came up half-built and the demo onboarded subscribers while moving
    no traffic — ~0 Mbps, with the Simulator page showing subscribers connected and
    reporting no error, because the engine's failure logging had been dropped in
    the same build. 1.4.0-14 pins CPESIM 0.4.18, which restores both, and makes
    a burst that moves no bytes report the reason it already had. Measured on an
    evaluation appliance: 0.001 Mbps before, 1,097 Mbps after.

Sessions

  • An unavailable session list no longer renders as an empty one. A failed
    fetch of the session list reported nothing, so the table came up blank and
    silent — indistinguishable from "this box has no sessions". That is the most
    misleading thing the page can say, because the request is most likely to fail
    precisely when there are a great many sessions to return. The page now says
    which it is, and a failed refresh keeps the last good list on screen rather
    than blanking a table you are reading. This makes a failure legible; it does
    not change how the page performs at scale — see Console polling load under
    Known issues.

Snapshots

  • A restore now actually restores. Per-block PUTs could only restore what had
    an endpoint a restore could safely drive; the subscriber base, the
    access-interface set and the switch registration had none, so a REPLACE restore
    could not put a wiped box back however many blocks it applied. It now applies
    the controller's whole captured state.
  • A restore would have disabled every access port in the snapshot. The
    access-port call sent the wrong key names with no address, which the schema
    rejected — and that rejection was the only thing preventing worse. The field it
    read to decide enabled/disabled does not exist on 1.4.0, so it evaluated false
    for every interface.

Accounting and the ledger

  • Three residual ways the daily maintenance could destroy billing data.
  • A quarantined billing record is no longer invisible.
  • An implausible retention_days is refused rather than silently deleting a year.
  • A retained live population no longer starves the drain forever.
  • CDRs are filtered in SQL, so an old date range is reachable.
  • A failed CDR fetch is distinguished from an empty ledger.

Data caps

  • Cross-subscriber leakage in the grant panel.
  • Disabling enforcement clears every overlay.
  • status() queries hoisted out of the per-subscriber loop.

Controller state

  • The retained config history is now bounded (upstream bng#41). Nothing read
    old config revisions, and unbounded growth cost provisioning latency roughly
    tenfold at the same subscriber base — a slowdown that survived a restart.
  • Importing as non-root no longer dies on the CA probe.

Console

  • The captive-portal portal_ip save no longer erases the walled garden.
  • The metrics sampler no longer fetches the whole controller config every tick —
    about 1.9 MB per call at 10,000 subscribers, every five seconds, for a key no
    controller has ever published.
  • The cap caption no longer says "Community" on a licensed box, and the sessions
    chart draws the cap line at the cap actually in force.
  • AAA faults surface in the alert centre.
  • The wipe notice names aaa_source among what was not restored.

Known issues

Issue references below are internal tracking identifiers, not public links.
#NN refers to the QuantumTouch console and appliance; bng#NN to the
Quantum BNG controller — the two are numbered separately, so #38 and
bng#38 are different items. Quote them when raising a support ticket.

Session reclamation at high scale [controller] — bng#50

Under sustained churn at 18,750 concurrent sessions, the controller's session
view stops reclaiming released sessions: measured at exactly its high-water mark
for 550 seconds with a drain rate of zero, while clients had released down to
8,191. Nothing is lost — orphans were zero in every run — and the cap, dataplane
and onboarding paths are unaffected.

Traced upstream to two defects, both fixed in controller 1.5: relayed DHCP
RELEASEs were dropped entirely (so reclamation only happened on lease timeout),
and teardown ran per-packet, capping reclaim at a few hundred per second.

Not reproducible at 7,500 and below across three runs.

Session count under sustained churn [controller] — bng#36

A separate effect from the reclamation issue above, and it presents differently:
under sustained high churn the controller's reported session view can
under-count a cohort of otherwise-stable sessions. The sessions themselves
remain bound and carrying traffic; it is the count that is short, and the
affected sessions reappear in the view when they next renew.

Reclamation above is a count that stays too high; this is a count that reads
too low. Both are view-level, and neither loses a session. Investigation is
ongoing upstream, with verification scheduled for a post-1.4 test cycle.

Practical consequence: during a churn event, treat the session count as
indicative and the pool's allocation figure as authoritative.

The Dataplane page reports the BNG's own NICs as unassigned — #49

The console's devbind view sorts NICs into kernel, DPDK and VPP. A NIC is
placed under VPP only when it appears in the stored port-assignment list;
the bucket records the assignment an operator made, not what VPP is driving at
that moment. On a box whose ports were never assigned through the console — a
hand-configured startup.conf, or a commissioning run that stopped before the
Dataplane step — the access and uplink NICs are correctly shown as DPDK-bound
but never attributed to VPP, even while VPP is forwarding on them.

The display is the smaller half. The same stored list is what makes
/api/dpdk/devbind/bind, /unbind and /reset-all-to-kernel refuse an
operation on a port VPP is using: with the list empty those endpoints do not
return 409 VPP_MUST_BE_STOPPED, so a rebind requested against a live
dataplane NIC is carried out instead of refused. The rebind also re-renders
/etc/vpp/startup.conf. Nothing does this on its own — it needs an operator or
a script to ask for it — but the guard that should stop it is not armed.

Workaround. Assign the dataplane ports on the Dataplane page as part of
commissioning, before the box carries subscribers. That records the assignment,
restores the VPP attribution and re-arms the guard. On a box already in service,
do not use the bind / unbind / reset-to-kernel controls to fix the display:
the operation will be allowed and will interrupt forwarding. Assign the ports
during the next planned maintenance window instead.

Affects the console's reporting and that guard only. Subscriber onboarding,
forwarding, QoS and the session cap do not consult this list, and the dataplane
itself is unaffected — this is visible in the 1.4.0 validation run, where the
check for VPP-attributed NICs is the single failing check on a box that passed
every forwarding, onboarding and QoS measurement in the same run.

RADIUS is not active — #17

The /bng/radius surface is inert in this build; local AAA is the AAA backend.
GET reports active: false and PUT is refused, deliberately, so an operator
cannot configure a backend that silently does nothing. An ISP that already runs
an external AAA or OSS cannot integrate with this release.

There is no RADIUS page in the console — it has been unrouted since v1.2, so
nothing in the UI invites you to configure a backend that would not work. The
Health page reports AAA (source, subscriber count, and the unknown-CPE
policy) rather than RADIUS server reachability. Earlier builds showed two red
"RADIUS Unreachable" cards on every box, which was a false alarm for a subsystem
that is inert by design; that is fixed in this release.

External AAA integration is tracked as #17, as a paid plugin covering
FreeRADIUS, Splynx, Sonar, UISP and Powercode — provisioning in, records out.

No live coverage that an add-on installs — #38

The add-on install path is thoroughly unit-tested, but every one of those tests
mocks the fetch, the hash and the unpack. Nothing verifies that a real pack
downloads from its distribution point, installs on a real box and then functions.
Tracked for 1.5.0 as #38.

The Subscriber column — #27, restored by #37

Removed — see Behaviour changes.

Only subscribers reconcile after a controller upgrade — #31

See Upgrading. The controller discards its configuration replica on upgrade,
and nothing re-pushes it automatically. The console is the authority, but it
has no drift-triggered full-state reconciler in 1.4.0: there is no config_hash
drift detection in the console at all, and the only reconciler that exists
operates on the AAA store.

Recovery is therefore operator-initiated: take a snapshot first and restore it.
That works — see Upgrading for the two restore paths and what each covers —
but it is a manual step, and a box upgraded without one has to be reconfigured
by hand. #31 is the full-state reconciler that would make it automatic.

No durable per-subscriber usage history — #33

Accounting interim records are collapsed into one mutable row per open session
plus a daily rollup, which discards the time dimension. So "how much did this
subscriber use, by hour, last week" cannot be answered from the box.

Current and cycle-to-date usage are correct and are what data-cap enforcement
acts on — this affects history only. Note the controller's own per-session rate
history is an in-memory ring (~10 minutes, deliberately lost on restart), so
until #33 lands there is no durable per-subscriber series on either side.

No validated VPP sizing profiles — #29

The stats segment is now sized by the installer. Earlier releases left it
at VPP's default, so it had to be set by hand. GA sets it to 1 GiB on install,
raises a smaller existing value, and never lowers a larger one — if you sized
it up yourself, your value is kept.

The figure is a deliberate margin rather than a calculation. Measured on a
25,000-session-licensed box, usage is a fraction of a percent of that
1 GiB, and it scales with the VPP worker-thread count and the number of
interfaces and graph nodes — not with the number of subscribers online.
A box with more workers needs more at any subscriber count. The margin is
there because the segment is cheap on any supported host and because the
behaviour under a recovery replay, which is where exhaustion would be worst,
is not something these figures cover.

1.4.0 sets processor_statseg_size_mb = 1024 on install, raises a smaller
existing value, and never lowers a larger one. If you sized it up yourself,
your value is kept.

What is still missing is the per-tier part: 1024M is a known-good ceiling
for 25,000 sessions, not a derived minimum, so it may be over-provisioned for
smaller deployments. There are no profiles for other subscriber or throughput
targets, and worker counts and buffer sizing are still derived by the operator
outside the tested configuration. #29 tracks shipping validated profiles.

You can watch it: GET /api/compute/memory_breakdown returns
vpp_stats_segment with total_kb, used_kb and free_kb, and the console's
memory panel renders it — so approaching the limit is visible rather than
something you discover under load.

Console polling load scales with session count and open tabs — #14, #15

Two separate effects, neither of which affects correctness:

  • The sessions view fetches the whole session list every 5 s and differences
    it client-side to derive rates. At 25,000 sessions that is a large repeated
    read. The controller endpoints that let us replace it with a server-side
    top-N and per-session rate land in controller 1.5 (bng#38);
    retiring the full-list pull is #14.
  • Each open browser tab runs its own polling loops, so controller load scales
    with (tabs x pages x loops). Coalescing them through the backend is #15.

Operationally: on a large deployment, prefer fewer simultaneously-open console
tabs during a scale test or a maintenance window.

A licence activated on the evaluation appliance can revert to the free tier — bng#52

You do not need a licence to evaluate. The evaluation appliance runs on the
free tier, which needs neither a licence nor a TPM, and licence activation is
not part of the supported evaluation path. If you never activate, this issue
cannot affect you.

If you do activate a capacity licence on the evaluation appliance, it can later
report EXPIRED, and the session cap returns to the free tier's 1,000. It has
been seen after a power cycle, but the validity check also re-runs every 60
seconds while the box is up, so the box does not have to be restarted for the
state to change.

What is not happening is as useful as what is. The machine fingerprint is
unchanged across the restart, and the sealed licence material is still read back
successfully — a licence that could not be unsealed would report NOT_ACTIVATED
rather than EXPIRED. Identity and sealing are intact; the validity window itself
is being computed wrongly. That window is derived from the TPM clock, and on the
evaluation appliance the TPM is emulated. Whether a discrete hardware TPM
behaves the same way has not yet been measured; bng#52 tracks that.

Workaround. Re-submit the same licence file on the Licensing page. It
returns to VALID with the full capacity immediately, and no other state is
disturbed. Otherwise evaluate on the free tier, which is unaffected.

Capacity is the only thing this touches. Licence enforcement in this release is
capacity-only: no feature is gated on holding a licence, only quantity is, and a
missing, expired or tampered licence reverts to the free tier rather than
withdrawing function. Configuration, subscriber onboarding, forwarding, QoS,
accounting and the console all continue to work normally at up to 1,000
sessions.

Usage Records is slow to appear on a box with a large ledger — #57

The Usage Records page reads the whole stored CDR ledger in one request. On a
box that has been carrying subscribers for a while that request is large, and
until it finishes the page shows two dashes in place of the Stored CDRs and
Live sessions figures, and an empty table
. On a lab box holding 609,000
records it took about 45 seconds; it scales with how many records you are
holding.

Nothing is wrong with the data while this is happening. The figures and the
records are intact and arrive in full once the read completes — the page is
slow to render them, not missing them.

Workaround. Narrow the read before you load it. The Since / Until
and Subscriber ID filters are applied by the server, so a filtered view
returns promptly on any ledger size. Filtering to the period or the subscriber
you are actually reconciling is both faster and usually what you wanted.
Export CSV and Export JSON are also server-side and unaffected.

If you have a very large ledger and the page is unresponsive rather than merely
slow, prefer the exports, and prune the ledger on its normal schedule.

Billing is unaffected. The accounting drain, the stored records, the daily
usage totals and the exports all behave normally; this is a display path only.


Installing and upgrading

A fresh install is the expected path for GA, and the one the release is
validated on. Follow the Baremetal Quick Start;
nothing in this section applies.

The rest of this section is for a box currently running an alpha build.
Note first that the subscriber model changed — see Breaking changes — so an
alpha box's subscriber base must be re-provisioned under the account model
regardless of how the software gets there. For many alpha deployments a
fresh install plus a re-provision is simpler than an upgrade.

Upgrading from an alpha build

The controller wipes its configuration replica on upgrade, by design: the
console is the authority and the controller's copy is a disposable replica.

Take a snapshot before upgrading. This is required, not advisory.

What the wipe discards is the controller's whole replica: pools,
access_interfaces, uplinks, edge_routers, captive_portal, plans,
session, DNS, and the local AAA store (aaa_local, aaa_source).

Nothing is re-pushed automatically on drift in this release — there is no
drift-triggered full-state reconciler (#31). An upgrade without a snapshot
leaves a box that onboards nobody, because its access interfaces and pools are
gone.

The snapshot is what puts it back, and it has two paths:

  • Replace (normal) — the snapshot carries the controller's own whole-state
    envelope, and the restore applies it via a single
    POST /v1/quantum/persistent/state/latest. This returns the entire
    configuration, including the subscriber base and the access-interface set.
  • Merge (fallback, when a snapshot has no envelope) — per-block writes
    covering DNS, pools, plans, RADIUS, the captive portal, access ports and
    uplinks. This path cannot fully restore a wiped box: the subscriber base,
    the access-interface set and the switch registration have no endpoint a
    per-block restore can safely drive. The restore reports which path it took.

Both paths are materially better than in the alpha, including a fix
for a restore that could not restore an access interface's address at all
(#30) — which is why the snapshot is now the documented recovery route rather
than a precaution.

Installer bundle 1.4.0-14. Standard procedure, unchanged from the alpha.


Appendix — every change in this release

Every change between the last alpha build and GA — 248 commits,
grouped by kind. The Bug fixes section above is a selection; this is the
complete record. Generated from v1.3.0-09..69e614a; the commit that writes
this appendix is necessarily not listed in it.

Features (60)

  • a692e82 feat(alerts): watch data caps, and fix an alert that could never fire (#35)
  • db09e49 feat(health): qualify "Connected" with how often it has dropped (#28 part 2)
  • 8766f0c feat(support): cumulative health counters — a flap was invisible (#28)
  • 6547841 feat(support): say whether the box is licensed, and why the cap disagrees
  • 159900e feat(events): AAA faults in the alert centre
  • a7d3d75 feat(controller): bound the retained config history (bng#41)
  • 6e66f4c feat(support): collect intent, not just running state; fix the capture
  • 1ddc2cb feat(support): consolidate the bundle and lead with a written summary
  • 6c998af feat(support): one-button diagnostic bundle for a support ticket
  • d27b574 feat(aaa): subscriber_id is an exact key everywhere (#25)
  • dd2ad55 feat(rates): A-06 in the subscriber drawer, with the cap consequence stated
  • dbf33ec feat(rates): A-06 — a raw per-subscriber rate, expressed as a plan
  • a93def0 feat(caps): wire the per-subscriber surfaces an operator could not reach
  • 6520bf5 feat(billing): customer_ref reaches exported CDRs — closes the G-11 gap
  • f350e5d feat(caps): data-cap panel on the subscriber drawer
  • e938fa5 feat(caps): Data Caps page and its client
  • 51768fa feat(caps): the operator API for data-cap policy
  • d490741 feat(caps): run the evaluator on the drain tick, publish bngcap*
  • 4c8c146 feat(aaa): converge enforcement overlays every reconcile pass
  • 52f51d9 feat(caps): declare the data-cap webhook events at registration
  • e12bee1 feat(caps): evaluate_once -- usage in, enforcement rows out
  • 5c33019 feat(caps): resolution and decision, as pure functions
  • 92c7914 feat(caps): sum usage over a cap cycle window
  • c807ca0 feat(caps): CapPolicyStore -- allowances, overrides, grants, cycle state
  • 442dd65 feat(caps): the cap cycle calculator
  • a2f89b5 feat(aaa): surface unmanaged subscribers with adopt and remove
  • fa36791 feat(aaa): run the reconciler and publish what it does
  • 2800a25 feat(aaa): write durably before pushing to the controller
  • 9dacdcd feat(aaa): detect a wiped controller and restore the base
  • 5a09424 feat(aaa): converge the controller onto our store
  • 8c1ba55 feat(aaa): one-time adoption of the controller's subscriber base
  • 5268a80 feat(aaa): durable subscriber store
  • a86eecf feat(accounting): the page becomes a view over the ledger
  • 303fc76 feat(accounting): publish drain lag, age and errors
  • b2673e8 feat(ledger): daily prune and scheduled export
  • cf5ca6a feat(accounting): read and export the ledger
  • 3c3544d feat(ledger): 13-month retention and historised identity
  • c6cf03e feat(accounting): drain the spool server-side, ack only after commit
  • 5e77571 feat(ledger): daily usage by differencing cumulative counters
  • dd94744 feat(ledger): durable accounting store
  • 7e5c82b feat(about): stamp the wheel with the commit it was built from
  • 1290760 feat(subscribers): show an account's sessions, and let an operator end them
  • 6be6c4d feat(static-ip): assign an address from the subscriber drawer
  • 39829b6 feat(static-ip): reject a conflicting address before it is written
  • 02aa0eb feat(static-ip): CSV columns, and rename the observed address
  • 1654ede feat(static-ip): reject an address the pool will later lease
  • 0caca48 feat(capacity): make a full box impossible to miss
  • da92855 feat(add-ons): close the update-checking half of the lifecycle
  • 90312bb feat(marketplace): brighter selector, and Check now shows its work
  • 92f2797 feat(marketplace): restore the page selector between Licensing and Add-ons
  • 605f411 feat(bundle): pin upstream v1.4.0 and core 0.10.17
  • 82cc22c feat(webui): two AAA controls, and delete the inert recovery-age knob
  • 079b383 feat(aaa): locators, subscriber_id keying, and the new bulk shape
  • e1e70d0 feat(aaa): source selector replaces the removed mode field
  • ac535aa feat(webui): Licensing leads with the fingerprint and the cap in force
  • 86d43d0 feat(webui): Marketplace section with the Add-ons page
  • c6a45af feat(addons): update checking that knows when to stay quiet
  • 5b2e3f2 feat(addons): both modules install from an uploaded tarball
  • 8086199 feat(addons): the catalogue, with a URL an offline mirror can own
  • e313512 feat(addons): retain one previous artifact, and roll back to it offline

Fixes (91)

  • 66c8283 fix(sessions): an unavailable session list must not render as an empty one
  • d05ed72 fix(webui): drop two false claims from the topology, and stop the session drawer opening blank
  • 5f31de2 fix(statseg): the sizing axis is threads, not subscribers
  • 6a47925 fix(install): the statseg fix wrote a key core never reads — bundle -10 is a no-op
  • ccc0c56 fix(docs): the stats segment IS measurable — I checked the wrong route
  • 21c5c10 fix(install): size VPP's stats segment — nothing in the product did (#42)
  • 72862b3 fix(health): two red RADIUS cards on every healthy 1.4.0 box were a false alarm (#39)
  • 00d05df fix(webui): the Topology page had no AAA element at all, and Pools read DNS from a key that does not exist (#39, #40)
  • c2e9128 fix(restore): verify an access-port write landed instead of trusting 200 (#30)
  • 1389fed fix(sessions): drop the Subscriber column for 1.4.0; restore it in 1.5 (#27)
  • c180ff6 fix(licensing): read grants, not a field the controller never sent (#32)
  • 4936c90 fix(snapshots): a restore would have disabled every access port (#30)
  • b2d734c fix(webui): the cap caption said "Community" on a licensed box
  • 991ab53 fix(sessions): draw the cap line at the cap actually in force
  • c1c9ed3 fix(licensing): activation works from the console
  • f116a40 fix(licensing): a trailing newline was invalidating valid licences
  • 8259ab9 fix(snapshots): make restore actually restore
  • 4f6d033 fix(captive-portal): stop the portal_ip save erasing the walled garden
  • af961cf fix(support): run the collection detached — an in-worker thread killed the dashboard
  • e0f8175 fix(support): parse BUNDLE_VERSION; bundle 1.4.0-05
  • 7de3579 fix(webui): an unprovisioned subscriber_id is a write key, not a join key (#25)
  • 3ad4709 fix(tests): stop the suite running production daemons (#21)
  • cb2468f fix(aaa): deleting a capped subscriber no longer strands its overlay
  • 012e639 fix(controller): importing as non-root must not die on the CA probe
  • 348244e fix(aaa): leave unmanaged subscribers' enforcement overlays alone
  • c2addbc fix(webui): data-caps client uses fetchWithAuth; input bounds match server
  • 6c8b8f6 fix(aaa): make post-reprovision overlay replay non-fatal
  • 6fa4654 fix(caps): hoist status() queries out of the per-subscriber loop
  • e7fa291 fix(caps): add grants_in_window; drop two dead helpers
  • b4f01ba fix(caps): disabling enforcement now clears every overlay
  • 5eb75a0 fix(caps): stop cross-subscriber leakage in the grant panel
  • 0434621 fix(caps): surface write failures, drop grant form, protect settings draft
  • fc1a2ad fix(caps): fix round 1 -- connection leak, coercion 500s, test gaps
  • 7947075 fix(aaa): guard enforcement convergence against non-controller failures
  • c87dac5 fix(caps): test coverage and boolean logic for NULL handling
  • 1524590 fix(tests): stop the suite running a live metrics sampler
  • 2bdae92 fix(api-keys): survive, and repair, a keys file still in the bare-list shape
  • 9ca5d80 fix(proxy): close the third door into the AAA subscriber base
  • de1c146 fix(aaa): drop a subscriber from the unmanaged list once it becomes ours
  • d52c944 fix(aaa): restore the merge base for a record we do not hold
  • 52f9e41 fix(webui): the wipe notice must name aaa_source among what was not restored
  • b415e3c fix(aaa): debounce the wipe guard, tell its conditions apart, and correct §5
  • eca818e fix(aaa): close the two proxy routes that write only the controller
  • b2b5c08 fix(aaa): merge over the record, not over the controller
  • 06475c5 fix(aaa): stop an orphan annotation poisoning the whole restore
  • 46e073d fix(aaa): say the switch may need re-registering before the plans
  • 678e419 fix(aaa): stop a refused restore wedging the reconciler, and say why
  • a4d6665 fix(aaa): let the circuit decide which line a mint may reuse
  • b72441b fix(aaa): roll back a rejected apply, and stop one bad row killing reconcile
  • c39b7e3 fix(aaa): record a bulk import before pushing it, closing the set
  • a5aff25 fix(aaa): close the delete and mint holes the inversion opened
  • 169b7a2 fix(aaa): surface reactivation_history through get_subscriber
  • 3ccbb2c fix(ledger): three residual ways the daily maintenance could destroy billing data
  • 31c7730 fix(accounting): a quarantined billing record must not be invisible
  • 2bc628c fix(accounting): a retained live population must not starve the drain forever
  • 8e33280 fix(ledger): export what is about to be pruned, not the whole ledger daily
  • 41f7a9b fix(accounting): filter CDRs in SQL so an old date range is reachable
  • 9afd934 fix(ledger): refuse an implausible retention_days instead of deleting a year
  • ebcc8c3 fix(accounting): distinguish a failed CDR fetch from an empty ledger
  • 4b8a749 fix(accounting): count a raised drain cycle as an error too
  • f0bf65f fix(ledger): drain thread's sqlite connection was unusable cross-thread
  • 863b36e fix(accounting): page forward within a cycle instead of re-fetching a static window
  • 53bede3 fix(accounting): review fix wave -- vacuous tests, reset heuristic, late-interim double-credit
  • 7cb61c2 fix(ledger): per-record SAVEPOINT so a DB error on delete can't split a record
  • 9257da3 fix(ledger): make write_records genuinely atomic
  • 8b4a2e0 fix(build): only tracked changes make a build dirty
  • ef6a848 fix(static-ip): narrow the unassign sentinel to framed_ip/subnet_mask
  • ecb0da7 fix(static-ip): check zero-padded addresses, and two tests that could not fail
  • 7084e34 fix(subscribers): a failed session fetch is not an empty account
  • 92c0810 fix(aaa): stop the raw subscriber PUT proxy erasing a static IP
  • b4838f2 fix(static-ip): tell "not mentioned" apart from "unassign me"
  • 1e42a42 fix(static-ip): wire framed_ip/subnet_mask into compute_diff
  • 2d27b9b fix(webui): restore the markup two descriptions lost to a narrow type
  • cc8aa10 fix(typecheck): clear the 16-error backlog exposed by the tsconfig rootDir fix
  • 3dadea1 fix(add-ons): the primary Install button never said Installing…
  • 75e652e fix(add-ons): restore the expander that described what the pack contains
  • 6a4062e fix: the eval appliance HAS a TPM — correcting a wrong premise
  • 9b694ef fix: two error states that lied, and the CPESIM pin
  • d52be84 fix(eval-appliance): find the bundle our own build script produces
  • 5b1d434 fix(aaa): merge subscriber PUT over stored record, not just match
  • 6061b2e fix(1.4.0): whole-branch defects — locator erasure, bulk crash, false alert
  • fa7e334 fix(webui): guard health.radius reads against the 1.4.0 shape
  • cd3e05e fix(health): stop fabricating a radius block in degraded fallbacks
  • 2958fee fix(portal): voucher redemption can finally activate a session
  • 35e8775 fix(aaa): carry locator columns through the upload pipeline, spec-validate the two rewritten bulk tests
  • fa03fd3 fix(aaa): rewrite the two route-level tests Task 3s rewrite broke
  • 9f75262 fix(aaa): delete the cascade planner that never fired
  • 131f376 fix(deps): declare jsonschema/referencing as dev extras
  • 0568042 fix(webui): API Keys page rendered blank on every box; replace two screenshots
  • 6a8b3f8 fix(appliance): clear intermittent first-boot hang with rcu_normal; ship 1.3.0-13
  • d89225f fix(licensing): the three controller paths were missing /v1 and 404d

Performance (2)

  • c29a377 perf(metrics): stop shelling out every 5s for counters we discard (#36)
  • b8c4d3c perf(metrics): stop fetching the whole controller config every tick (#14)

Documentation (57)

  • 84ae8d6 docs: point the dataplane known issue at its real issue number (#49)
  • 7aeebba docs: record the dataplane port-assignment issue and its workaround
  • 9a12835 docs(release): regenerate the appendix against 433b916 (243 commits)
  • 433b916 docs: roll the 1.4.0 doc set to bundle 1.4.0-13 / plugin 0.10.36
  • 75a7bcc docs(release): frame 1.4.0 as GA, with everything prior as one alpha bucket
  • 897c4bb docs(release): append the complete commit record
  • 9e7b755 docs(release): the commit count was stale — 229, not 204
  • 51ed4d4 docs(release): carry the controller and VPP side of 1.4.0 (#43)
  • 7d3dd4f docs(release): remove internal framing and the "shim" terminology
  • 84c7f4b docs(release): bundle 1.4.0-10, and say what the installer now does about statseg
  • efee8aa docs(aaa): correct a comment I generalised from one configuration
  • 236b0ee docs(release): say what the console actually shows for RADIUS and AAA
  • c7ace1e docs(release): 1.4.0 notes — known issues now cover the open set, and Upgrading no longer over-promises
  • d586aff docs(metrics): document all 36 families, and guard against the drift (#34)
  • 8f1e614 docs(lifecycle): mark §4 and §9.3 superseded by the state-envelope restore
  • 10eb38a docs(spec): record the #24 amendments
  • a998128 docs(spec): §5.2's enabled_ts floor is day-granular — state the limit
  • 2ec2311 docs(plan): sub-project E implementation plan
  • dcb9614 docs(spec): sub-project E — data-cap enforcement
  • 0e5682a docs(plan): AAA as system of record (sub-project F)
  • d7eb45c docs(spec): AAA as system of record (sub-project F)
  • 96aeea3 docs(lifecycle): QuantumTouch takes ownership of AAA
  • 8e1f111 docs: state ownership and lifecycle — what the controller must persist
  • 8a6be39 docs(spec): the binding spec described behaviour that was deliberately deleted
  • b186183 docs(plan): accounting ledger (sub-project D)
  • 1aab859 docs(spec): accounting ledger (sub-project D)
  • 3ff8da5 docs(plan): sub-project C — static IP and subscriber sessions
  • a939894 docs(spec): static IP and subscriber sessions (sub-project C)
  • bc4c816 docs: preserve the 1.4.0 port execution ledger; ignore vagrant state
  • 7c96972 docs(artifacts): the _test endpoints are NOT served in production — correction
  • d6b4baf docs(artifacts): correct the health.radius claim — twice wrong
  • 366f931 docs(plan): implementation plan for the controller 1.4.0 port
  • c818b2e docs(spec): the cascade has never fired, and sessions do carry subscriber_id
  • 2504d03 docs(spec): controller 1.4.0 port — design for sub-project A
  • e109e42 docs(artifacts): import the 1.4.0 controller API spec and evaluate it against our tree
  • 50c3bdb docs: direct upload for disconnected boxes; URL path is the orchestrator seam
  • 5163235 docs: rename About to System and reserve it for platform lifecycle
  • 12aeee9 docs: correct -- capacity licences are perpetual, not subscriptions
  • ca3fdbe docs: draw the licence expiry cliff, and free amber for it
  • 2bea687 docs: record the three capacity bands, and two consequences of replace
  • 978203b docs: scope the design to QuantumTouch; hand the store findings off
  • 99699d5 docs: session cap replaces rather than stacks
  • 845b417 docs: nav group is Marketplace, holding Licensing and Add-ons
  • 9e81489 docs: CPESIM need not stay pinned -- the blocker is an unversioned engine API
  • b7a1e27 docs: read-only operator gets an "ask an administrator" fingerprint card
  • 536dee1 docs: machine fingerprint leads the Licensing page
  • fc25bad docs: speed caps replace rather than stack; interface config is a prerequisite
  • 16bcc14 docs: design for the add-ons + entitlements console section
  • 3bbe2f4 docs: publishing instructions for the 1.3.0 docs web implementer
  • b627fa5 docs: state survey ahead of lifecycle management
  • d248b24 docs(grafana): customer guide for the dashboard pack; pack is free and unlicensed
  • bcbaf0c docs: capacity licensing is staged for 1.4.0, not a 1.3.0 capability
  • 19b9f6d docs: correct the licensing documentation — the console flow does not work
  • 2e437fc docs: response to the feature-query interface proposal (#33)
  • 7b9796a docs: re-shoot the console screenshots, and fix two things they caught
  • 68cbb67 docs: update the operator guides for 1.3.0
  • 3027512 docs: review the 19 published QuantumBNG docs against 1.3.0

Tests (14)

  • 56f8baa test(radius): tag lab-dependent tests so a release run can drop them
  • d4707f4 test(aaa): cover the branches that had none, and verify each catches its regression
  • a10af89 test(aaa): prove the reconciler is actually reachable from the drain thread
  • c0b397a test(aaa): pin remove_unmanaged's unreachable-controller ordering
  • dd5741a test(accounting): assert the test drain thread does not outlive its test
  • e4c55ad test(ledger): pin the INSERT OR IGNORE rule at the statement itself
  • d650317 test(accounting): pin the real lag computation, and ship spec 8's second alert
  • 3225513 test(subscribers): cover the session client directly, not through the drawer
  • 12140d1 test(subscribers): replace a vacuous role="alert" check with a real one
  • 17b91b7 test(static-ip): pin the conflict/generic-failure routing to the error code
  • 6fadfa6 test(health): update happy-path fixture to the 1.4.0 aaa shape
  • abbe19b test(aaa): fix stale mode fixture, cover radius rejection
  • 0e75d2a test: a controller double that enforces the real 1.4.0 contract
  • 5457c62 test(webui): repair two stale Dashboard tests and the pool card router context

Build and chores (7)

  • fe8dc75 chore(deps): lock @quantumtouch/core to the v0.10.23 commit
  • da25223 chore(deps): core 0.10.23 for plugin webhook event registration
  • a6ea37e chore(deps): lock @quantumtouch/core to the v0.10.22 commit
  • e63c9d9 chore(deps): core 0.10.22, and a guard so the two pins cannot drift
  • 4cbcab9 chore(deps): quantumtouch-core 0.10.21 — accessible Field labels
  • ab40ce3 chore(bundle): cut 1.4.0-02
  • 54acdd4 chore: ignore .worktrees/

Release (9)

  • 69e614a release: bundle 1.4.0-14, plugin 0.10.37, CPESIM 0.4.18
  • a47b639 release: bundle 1.4.0-13, plugin 0.10.36
  • a79fa2a release: cut installer bundle 1.4.0-12
  • 04ed60a release: bundle 1.4.0-11
  • 44a426a release: plugin 0.10.34 — the version never moved across 1.4.0
  • 2caaaf9 release: bundle 1.4.0-10
  • d4807b8 release: record the bundle 1.4.0-09 version bump that shipped to the rig
  • 1fcf348 release: bundle 1.4.0-04
  • 8673133 release: bundle 1.4.0-03

Other (8)

  • 915dc44 Merge release/1.4.0-13: sessions fetch-failure fix + 1.4.0 doc set
  • c5db7c5 Revert the Pools DNS change: #40 was not a bug, and my test disproved it
  • be425dd Merge support-bundle health counters, snapshot access-port fix, cap caption
  • cd0f367 Merge A-06: per-subscriber raw rate override
  • ae00fbf Merge #24 follow-ups: billing interop, wired cap surfaces, overlay ownership
  • d39660c Merge sub-project E: data-cap enforcement
  • 0f0fc5f Restore installedLinks, capability reload, and backstop poller on Add-ons
  • cc432d3 Merge feat/controller-1.4.0-port: port to controller 1.4.0