instar 1.3.1244 → 1.3.1245
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/index.html +5 -0
- package/dashboard/subscriptions.js +29 -2
- package/dist/data/standards-guard-index.json +1 -1
- package/dist/data/standards-guard-index.meta.json +2 -2
- package/dist/data/standards-registry.meta.json +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/src/data/standards-guard-index.json +1 -1
- package/src/data/standards-guard-index.meta.json +2 -2
- package/src/data/standards-registry.meta.json +1 -1
- package/upgrades/1.3.1245.md +29 -0
- package/upgrades/side-effects/quota-bar-severity-colors.md +139 -0
package/dashboard/index.html
CHANGED
|
@@ -3744,6 +3744,11 @@
|
|
|
3744
3744
|
.sub-quota-pct { opacity:.65; }
|
|
3745
3745
|
.sub-quota-track { height:8px; border-radius:4px; background:var(--border,#2a2a2a); overflow:hidden; }
|
|
3746
3746
|
.sub-quota-fill { height:100%; background:linear-gradient(90deg,#4caf80,#3a9); }
|
|
3747
|
+
/* Severity bands (dashboard/subscriptions.js quotaSeverity): <75% ok, 75-89% warn, >=90% critical.
|
|
3748
|
+
Colour is redundant with the always-present "N% used" text, never the only signal. */
|
|
3749
|
+
.sub-quota-fill.sub-quota-ok { background:linear-gradient(90deg,#4caf80,#3a9); }
|
|
3750
|
+
.sub-quota-fill.sub-quota-warn { background:linear-gradient(90deg,#f59e0b,#e0900a); }
|
|
3751
|
+
.sub-quota-fill.sub-quota-critical { background:linear-gradient(90deg,#f87171,#ef4444); }
|
|
3747
3752
|
.sub-pending { border:1px solid var(--border,#2a2a2a); border-radius:10px; padding:12px 16px; margin-bottom:10px; }
|
|
3748
3753
|
.sub-pending-head { display:flex; justify-content:space-between; align-items:baseline; gap:12px; }
|
|
3749
3754
|
.sub-pending-label { font-weight:600; }
|
|
@@ -202,18 +202,45 @@ function el(doc, tag, cls, text) {
|
|
|
202
202
|
return node;
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
// Quota-bar severity thresholds. A single green bar made a 92%-consumed account
|
|
206
|
+
// read the same as a 4%-consumed one at a glance, so the bar communicated width
|
|
207
|
+
// only. These bands give it colour as well: comfortable / getting tight / about
|
|
208
|
+
// to wall. PRESENTATION ONLY — nothing routes, sheds, or swaps on this class;
|
|
209
|
+
// placement and load-shedding keep reading the numeric quota they always read.
|
|
210
|
+
export const QUOTA_WARN_PCT = 75;
|
|
211
|
+
export const QUOTA_CRITICAL_PCT = 90;
|
|
212
|
+
|
|
213
|
+
/** Severity band for an already-clamped 0–100 integer. */
|
|
214
|
+
export function quotaSeverity(usedPct) {
|
|
215
|
+
const used = clampPct(usedPct);
|
|
216
|
+
if (used >= QUOTA_CRITICAL_PCT) return 'critical';
|
|
217
|
+
if (used >= QUOTA_WARN_PCT) return 'warn';
|
|
218
|
+
return 'ok';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Severity → STATIC class literal. The map is the reason the class attribute stays
|
|
222
|
+
// a fixed literal (the file's safety contract): the severity is chosen from a
|
|
223
|
+
// closed set of three, never interpolated from data.
|
|
224
|
+
const QUOTA_FILL_CLASS = Object.freeze({
|
|
225
|
+
ok: 'sub-quota-fill sub-quota-ok',
|
|
226
|
+
warn: 'sub-quota-fill sub-quota-warn',
|
|
227
|
+
critical: 'sub-quota-fill sub-quota-critical',
|
|
228
|
+
});
|
|
229
|
+
|
|
205
230
|
/** A labelled quota bar. `pct` is clamped to a 0–100 NUMBER before it reaches the
|
|
206
|
-
* only dynamic attribute (style width); the percent text is also from that number.
|
|
231
|
+
* only dynamic attribute (style width); the percent text is also from that number.
|
|
232
|
+
* The fill also carries a severity class derived from that same clamped number. */
|
|
207
233
|
export function quotaBar(doc, label, pct, resetIso, now = Date.now()) {
|
|
208
234
|
const wrap = el(doc, 'div', 'sub-quota');
|
|
209
235
|
const used = clampPct(pct);
|
|
236
|
+
const severity = quotaSeverity(used);
|
|
210
237
|
const head = el(doc, 'div', 'sub-quota-head');
|
|
211
238
|
head.appendChild(el(doc, 'span', 'sub-quota-label', sanitizeForDisplay(label, 'label')));
|
|
212
239
|
const resetTxt = resetIso ? countdown(resetIso, now, { expiredWord: 'resetting' }) : '';
|
|
213
240
|
head.appendChild(el(doc, 'span', 'sub-quota-pct', `${used}% used${resetTxt ? ` · resets in ${resetTxt}` : ''}`));
|
|
214
241
|
wrap.appendChild(head);
|
|
215
242
|
const track = el(doc, 'div', 'sub-quota-track');
|
|
216
|
-
const fill = el(doc, 'div',
|
|
243
|
+
const fill = el(doc, 'div', QUOTA_FILL_CLASS[severity]); // static literal from a closed set
|
|
217
244
|
fill.style.width = `${used}%`; // safe: `used` is a clamped integer
|
|
218
245
|
track.appendChild(fill);
|
|
219
246
|
wrap.appendChild(track);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generatedFrom": "source-tree",
|
|
4
4
|
"registrySha256": "890eec3dcf80e4388c396f43001a0bd3fabc80c88f79aa9525885d6402a6dfdf",
|
|
5
|
-
"packageVersion": "1.3.
|
|
5
|
+
"packageVersion": "1.3.1245",
|
|
6
6
|
"guards": [
|
|
7
7
|
{
|
|
8
8
|
"ref": "docs/audits/phase-b/f10-triage.md",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"sha256": "
|
|
2
|
+
"sha256": "7957af2c8bb2014be0ed964adc5be0a94c9be477962f55fd3869f8915640c1bd",
|
|
3
3
|
"registrySha256": "890eec3dcf80e4388c396f43001a0bd3fabc80c88f79aa9525885d6402a6dfdf",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1245"
|
|
5
5
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-09-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-09-19T01:17:14.478Z",
|
|
5
|
+
"instarVersion": "1.3.1245",
|
|
6
6
|
"entryCount": 203,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generatedFrom": "source-tree",
|
|
4
4
|
"registrySha256": "890eec3dcf80e4388c396f43001a0bd3fabc80c88f79aa9525885d6402a6dfdf",
|
|
5
|
-
"packageVersion": "1.3.
|
|
5
|
+
"packageVersion": "1.3.1245",
|
|
6
6
|
"guards": [
|
|
7
7
|
{
|
|
8
8
|
"ref": "docs/audits/phase-b/f10-triage.md",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"sha256": "
|
|
2
|
+
"sha256": "7957af2c8bb2014be0ed964adc5be0a94c9be477962f55fd3869f8915640c1bd",
|
|
3
3
|
"registrySha256": "890eec3dcf80e4388c396f43001a0bd3fabc80c88f79aa9525885d6402a6dfdf",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1245"
|
|
5
5
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
The Subscriptions dashboard's quota bars are now colour-coded by how much of each account's limit is used. Every bar previously rendered in the same green regardless of utilization, so the only difference between an account at 4% and one at 92% was the bar's length — a distinction that is hard to read at a glance across a stacked list of accounts, which is exactly where the list is read.
|
|
9
|
+
|
|
10
|
+
Each bar's fill now carries one of three severity classes derived from the same clamped 0–100 integer that already sets its width: green below 75% used, amber from 75% through 89%, red at 90% and above. The percent text that has always sat beside the bar is unchanged, so the colour is redundant with information already on screen rather than the only way to read the value.
|
|
11
|
+
|
|
12
|
+
This is presentation only. No routing, placement, load-shedding, proactive-swap or alerting decision reads the new class — those keep consuming the numeric quota they already consumed, on their own thresholds, unchanged.
|
|
13
|
+
|
|
14
|
+
## What to Tell Your User
|
|
15
|
+
|
|
16
|
+
Your Subscriptions dashboard now shows each account's quota bar in green, amber or red depending on how much of the limit is used, instead of always green. An account that is comfortable stays green, one that is getting tight turns amber at 75%, and one that is at or near its wall turns red at 90%. Nothing about how work is routed between your accounts changed — this only makes the page easier to scan. The percentage is still written next to every bar.
|
|
17
|
+
|
|
18
|
+
## Summary of New Capabilities
|
|
19
|
+
|
|
20
|
+
- Subscriptions quota bars are colour-coded: green (<75% used), amber (75–89%), red (≥90%).
|
|
21
|
+
- The colour is derived from the already-clamped percent, so a bad reading cannot produce a misleading colour — it clamps to 0% / green or 100% / red.
|
|
22
|
+
- Colour is never the only signal: the "N% used" text is unchanged and always present.
|
|
23
|
+
- No routing, shedding or alerting behaviour reads the new class.
|
|
24
|
+
|
|
25
|
+
## Evidence
|
|
26
|
+
|
|
27
|
+
- Visual: the shipped module and the shipped CSS rendered together against the account mix from the operator's reference screenshot — 0/22/68% green, 83/89% amber, 100% red.
|
|
28
|
+
- Unit (`tests/unit/subscriptions-render.test.ts`, 92 passing): both sides of both band boundaries (74/75 and 89/90), severity derived from the clamped number (1000 → critical, −50 → ok, non-numeric → ok, 74.6 → warn by rounding), the fill's class attribute, and a hostile percent proving the class stays a closed-set literal.
|
|
29
|
+
- Mutation-checked: flipping `>=` to `>` on the warn boundary fails the 75% case, so the boundary assertions are load-bearing rather than decorative.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Side-Effects Review — quota-bar severity colours
|
|
2
|
+
|
|
3
|
+
**Change:** `dashboard/subscriptions.js` `quotaBar()` now derives a severity band
|
|
4
|
+
(`ok` / `warn` / `critical`) from the already-clamped 0–100 percent and puts it on the
|
|
5
|
+
fill's class; `dashboard/index.html` gives each band a colour. Thresholds: ok <75,
|
|
6
|
+
warn 75–89, critical ≥90.
|
|
7
|
+
|
|
8
|
+
**Tier:** 1 — presentation-only, 3 files, no `src/` runtime surface, no new capability,
|
|
9
|
+
no migration, no irreversibility. (Note: `dashboard/` is outside
|
|
10
|
+
`scripts/instar-dev-precommit.js`'s `inScope()` filter, so the gate does not demand a
|
|
11
|
+
trace here. The artifact is produced anyway — see §9.)
|
|
12
|
+
|
|
13
|
+
## Phase 1 — Principle check (signal vs authority)
|
|
14
|
+
|
|
15
|
+
**Does this change involve a decision point — something that gates information flow,
|
|
16
|
+
blocks actions, filters messages, or constrains agent behaviour?**
|
|
17
|
+
|
|
18
|
+
No. `quotaSeverity()` is a pure function from a number the renderer already holds to one
|
|
19
|
+
of three CSS class literals. It has no consumer other than the DOM. It does not gate,
|
|
20
|
+
block, filter, route, shed, alert or gate-keep anything, and nothing reads it back.
|
|
21
|
+
|
|
22
|
+
The valid-reason category is "presentation of data already computed and already
|
|
23
|
+
displayed." The numeric quota that placement, proactive-swap, load-shedding and the
|
|
24
|
+
quota brake consume is untouched, and those subsystems keep their own thresholds — this
|
|
25
|
+
change deliberately does not unify them (see §5).
|
|
26
|
+
|
|
27
|
+
## 1. Over-block — what legitimate inputs does this reject that it shouldn't?
|
|
28
|
+
|
|
29
|
+
Nothing is rejected; there is no accept/reject decision. The nearest analogue is a
|
|
30
|
+
misleading colour. The band is computed from `clampPct()`'s output, so the reachable
|
|
31
|
+
inputs are exactly the integers 0–100 and the three bands partition that range with no
|
|
32
|
+
gap and no overlap. A non-finite or missing reading clamps to 0 and renders green — the
|
|
33
|
+
same "empty bar" a missing reading already rendered, so this does not invent a
|
|
34
|
+
reassuring colour for absent data. An account with no quota reading at all still takes
|
|
35
|
+
the separate `sub-account-noquota` path and draws no bar.
|
|
36
|
+
|
|
37
|
+
## 2. Under-block — what failure modes does this still miss?
|
|
38
|
+
|
|
39
|
+
- **A stale reading still looks authoritative.** A bar polled ten minutes ago renders
|
|
40
|
+
the same as one polled ten seconds ago; colour does not encode freshness. This is
|
|
41
|
+
pre-existing (the bar itself had the same property) and out of this change's reach.
|
|
42
|
+
- **0% green is ambiguous** between "genuinely unused" and "clamped from an unusable
|
|
43
|
+
reading." Also pre-existing — the width was already 0 in both cases — and the row's
|
|
44
|
+
status text is what distinguishes them.
|
|
45
|
+
- **Colour does not distinguish 90% from 100%.** Deliberate: both mean "do not plan
|
|
46
|
+
work here", and a fourth band buys resolution the operator does not act on differently.
|
|
47
|
+
- **The 5-hour and weekly bars are banded independently.** An account green on 5-hour and
|
|
48
|
+
red on weekly shows both, which is correct — they are different limits — but there is
|
|
49
|
+
no single per-account "worst band" summary. Not attempted here.
|
|
50
|
+
|
|
51
|
+
## 3. Level-of-abstraction fit
|
|
52
|
+
|
|
53
|
+
Right layer. The severity is a property of *this view* of the number, not of the number,
|
|
54
|
+
so it belongs in the renderer that owns the bar. Pushing it server-side would mean the
|
|
55
|
+
API asserting an operational verdict ("this account is critical") that routing does not
|
|
56
|
+
share, which would be a stronger claim than the data supports and would invite consumers
|
|
57
|
+
to treat a presentation band as authority.
|
|
58
|
+
|
|
59
|
+
A smarter gate does exist for the operational question — `QuotaTracker`, the proactive
|
|
60
|
+
swap's measured threshold, and the placement quota gate — and this change deliberately
|
|
61
|
+
does **not** feed them or read from them. It is a sibling view, not a parallel authority.
|
|
62
|
+
|
|
63
|
+
## 4. Signal vs authority compliance
|
|
64
|
+
|
|
65
|
+
Compliant, in the strongest available form: this produces neither. It holds no blocking
|
|
66
|
+
authority, and it is not even a signal (nothing consumes it programmatically). The
|
|
67
|
+
brittle-check-with-authority failure mode this question exists to catch is structurally
|
|
68
|
+
unreachable — the only consumer is CSS.
|
|
69
|
+
|
|
70
|
+
The constants are exported (`QUOTA_WARN_PCT`, `QUOTA_CRITICAL_PCT`) so the tests assert
|
|
71
|
+
against the same numbers the code uses. That export is a hazard worth naming: a future
|
|
72
|
+
change that imports them into a routing decision would silently convert a presentation
|
|
73
|
+
band into an authority. The file comment states the boundary; if that ever happens it
|
|
74
|
+
should be a reviewed change, not an import.
|
|
75
|
+
|
|
76
|
+
## 5. Interactions
|
|
77
|
+
|
|
78
|
+
- **Does it shadow another check?** No. It does not participate in any check.
|
|
79
|
+
- **Is it shadowed?** The `.sub-quota-fill` base rule still carries the green gradient
|
|
80
|
+
and the three band rules follow it with equal-or-higher specificity
|
|
81
|
+
(`.sub-quota-fill.sub-quota-warn` is one class more specific), so the band always wins
|
|
82
|
+
and the base rule remains a correct fallback if a future caller builds a fill without
|
|
83
|
+
a band class. No `!important` anywhere in the chain.
|
|
84
|
+
- **Does it double-fire or race?** No timers, no state, no async. `quotaBar()` is called
|
|
85
|
+
synchronously during render and is idempotent.
|
|
86
|
+
- **Threshold divergence is real and intentional.** The proactive swap's measured
|
|
87
|
+
threshold (default 80%) sits *between* this change's warn (75) and critical (90)
|
|
88
|
+
boundaries, so an operator can see an amber bar on an account the swap has not yet
|
|
89
|
+
moved off. That is honest — amber means "getting tight", not "the swap fired" — but it
|
|
90
|
+
is the most likely question this change generates, and it is why the two numbers are
|
|
91
|
+
not wired to each other.
|
|
92
|
+
|
|
93
|
+
## 6. External surfaces
|
|
94
|
+
|
|
95
|
+
- **Visible to the operator:** yes — that is the point. The change is additive to the
|
|
96
|
+
existing view; no element is removed, renamed or reordered, and the percent text,
|
|
97
|
+
reset countdown, labels and layout are byte-identical.
|
|
98
|
+
- **Visible to other agents / systems:** no. No API response, no state file, no message,
|
|
99
|
+
no log line changes. The class is never serialized off the page.
|
|
100
|
+
- **Accessibility:** colour is redundant with the always-present "N% used" text, so no
|
|
101
|
+
information is conveyed by colour alone. The three fills stay well above the track
|
|
102
|
+
background in luminance on the dark theme (the dashboard has no light theme and no
|
|
103
|
+
`prefers-color-scheme` block, so there is no second palette to maintain). Amber
|
|
104
|
+
`#f59e0b` and red `#f87171` are both already in the dashboard's existing palette, so
|
|
105
|
+
this introduces no new colours to the page.
|
|
106
|
+
- **Timing / runtime conditions:** none depended on.
|
|
107
|
+
|
|
108
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
109
|
+
|
|
110
|
+
**Machine-local by design, with no coherence surface.** The dashboard is served as static
|
|
111
|
+
assets by whichever machine the operator has open, and the banding is computed in that
|
|
112
|
+
browser from whatever account rows that page already fetched. There is no state to
|
|
113
|
+
replicate, nothing to proxy on read, and no durable record that could strand on a topic
|
|
114
|
+
transfer.
|
|
115
|
+
|
|
116
|
+
The pool-scope account view (`GET /subscription-pool?scope=pool`) is unchanged, and rows
|
|
117
|
+
merged from a peer machine render through the same `quotaBar()` as local rows — so a
|
|
118
|
+
mixed-machine list is banded consistently rather than one machine's rows being coloured
|
|
119
|
+
and another's not. Because the asset ships inside the instar package, a machine still on
|
|
120
|
+
an older version simply renders the old all-green bars until it updates; the two versions
|
|
121
|
+
do not disagree about any value, only about colour. No user-facing notice is emitted, so
|
|
122
|
+
there is no one-voice gating question. No URL is generated.
|
|
123
|
+
|
|
124
|
+
## 8. Rollback cost
|
|
125
|
+
|
|
126
|
+
Near-zero and immediate. Reverting the commit restores the previous rendering exactly —
|
|
127
|
+
there is no persisted state, no migration, no agent state to repair, and no consumer to
|
|
128
|
+
un-wire. A hot-fix release is not required for correctness: even left in place, a wrong
|
|
129
|
+
threshold only mis-colours a bar whose number is still printed next to it. An operator
|
|
130
|
+
who dislikes the bands before a release can neutralise them by removing three CSS rules.
|
|
131
|
+
|
|
132
|
+
## 9. Follow-up raised, not deferred
|
|
133
|
+
|
|
134
|
+
`inScope()` in `scripts/instar-dev-precommit.js` does not cover `dashboard/`, although
|
|
135
|
+
the dashboard ships to every install and is the operator's primary read surface. That is
|
|
136
|
+
a genuine gap in the gate — a dashboard change can land with no artifact and no trace.
|
|
137
|
+
This artifact was produced regardless, and the gap is filed as its own item rather than
|
|
138
|
+
widened into this PR (changing the gate's scope is a gate change, with its own blast
|
|
139
|
+
radius, and belongs in a change that can be reviewed on those terms).
|