instar 1.3.1169 → 1.3.1170
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 +50 -1
- 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.1170.md +55 -0
- package/upgrades/side-effects/subscriptions-group-by-provider.md +70 -0
package/dashboard/index.html
CHANGED
|
@@ -3634,6 +3634,11 @@
|
|
|
3634
3634
|
|
|
3635
3635
|
<!-- subscriptions tab (Subscription & Auth Standard P2.2 — live quota + pending logins) -->
|
|
3636
3636
|
<style>
|
|
3637
|
+
/* Provider group heading — only rendered when more than one provider is
|
|
3638
|
+
enrolled, so a single-provider install looks exactly as it did before. */
|
|
3639
|
+
.sub-provider-heading { font-size:12px; font-weight:700; letter-spacing:.06em; text-transform:uppercase;
|
|
3640
|
+
opacity:.6; margin:14px 0 8px; padding-bottom:6px; border-bottom:1px solid var(--border,#2a2a2a); }
|
|
3641
|
+
.sub-provider-heading:first-child { margin-top:0; }
|
|
3637
3642
|
.sub-account { border:1px solid var(--border,#2a2a2a); border-radius:10px; padding:14px 16px; margin-bottom:12px; }
|
|
3638
3643
|
.sub-account-inuse { border-color:#16a34a; box-shadow:0 0 0 1px #16a34a44; }
|
|
3639
3644
|
.sub-account-inuse-badge { font-size:11px; font-weight:600; color:#22c55e; margin-right:auto; }
|
|
@@ -195,6 +195,47 @@ export function quotaBar(doc, label, pct, resetIso, now = Date.now()) {
|
|
|
195
195
|
return wrap;
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Order accounts so each provider's accounts sit together, interleaving a heading
|
|
200
|
+
* marker before each group. Returns a flat list of accounts with `{__providerHeading}`
|
|
201
|
+
* markers spliced in, so the caller keeps ONE loop and one card-rendering path.
|
|
202
|
+
*
|
|
203
|
+
* Two deliberate choices:
|
|
204
|
+
*
|
|
205
|
+
* - **Groups appear in first-appearance order, and accounts keep their order within a
|
|
206
|
+
* group.** Nothing is sorted. The pool's order carries meaning the dashboard cannot
|
|
207
|
+
* see (enrolment order, which account is the default), so grouping re-associates
|
|
208
|
+
* without re-ranking.
|
|
209
|
+
*
|
|
210
|
+
* - **A single-provider list gets NO heading.** Every instar install with one provider
|
|
211
|
+
* renders exactly as it did before — the heading only appears once there is genuinely
|
|
212
|
+
* something to tell apart, which is the condition that prompted this. That property is
|
|
213
|
+
* pinned by a test, because "we grouped everything" would quietly add a redundant
|
|
214
|
+
* heading to the common case.
|
|
215
|
+
*/
|
|
216
|
+
export function groupAccountsByProvider(accounts) {
|
|
217
|
+
if (!Array.isArray(accounts)) return [];
|
|
218
|
+
const groups = new Map(); // provider key (raw) → accounts, in first-appearance order
|
|
219
|
+
for (const a of accounts) {
|
|
220
|
+
const key = a && typeof a.provider === 'string' ? a.provider : '';
|
|
221
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
222
|
+
groups.get(key).push(a);
|
|
223
|
+
}
|
|
224
|
+
const out = [];
|
|
225
|
+
const multi = groups.size > 1;
|
|
226
|
+
for (const [key, members] of groups) {
|
|
227
|
+
if (multi) {
|
|
228
|
+
// friendlyProvider maps anthropic→Claude / openai→Codex and SANITIZES anything
|
|
229
|
+
// else, so an unrecognised or hostile provider string cannot reach the DOM raw.
|
|
230
|
+
// An absent provider has no honest name, so say so rather than invent one.
|
|
231
|
+
const label = friendlyProvider(key) || 'Other';
|
|
232
|
+
out.push({ __providerHeading: label });
|
|
233
|
+
}
|
|
234
|
+
for (const m of members) out.push(m);
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
|
|
198
239
|
/** Per-account rows: nickname, status, provider·framework, 5h + weekly quota bars.
|
|
199
240
|
* `inUseAccountId` (optional) is the account the agent is CURRENTLY running on —
|
|
200
241
|
* that card gets an "In use" marker so "active" (healthy) reads distinct from
|
|
@@ -206,7 +247,15 @@ export function renderAccounts(doc, target, accounts, now = Date.now(), inUseAcc
|
|
|
206
247
|
target.appendChild(el(doc, 'div', 'sub-empty', 'No subscription accounts enrolled yet.'));
|
|
207
248
|
return;
|
|
208
249
|
}
|
|
209
|
-
for (const a of accounts) {
|
|
250
|
+
for (const a of groupAccountsByProvider(accounts)) {
|
|
251
|
+
// A group marker, not an account: render the provider heading and continue.
|
|
252
|
+
// (The operator's complaint was a flat interleaved list — Claude and Codex
|
|
253
|
+
// accounts mixed together with only a small per-card meta line to tell them
|
|
254
|
+
// apart. The heading is what makes the split scannable.)
|
|
255
|
+
if (a && a.__providerHeading) {
|
|
256
|
+
target.appendChild(el(doc, 'div', 'sub-provider-heading', a.__providerHeading));
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
210
259
|
const inUse = !!(inUseAccountId && a && a.id === inUseAccountId);
|
|
211
260
|
const card = el(doc, 'div', inUse ? 'sub-account sub-account-inuse' : 'sub-account');
|
|
212
261
|
const head = el(doc, 'div', 'sub-account-head');
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generatedFrom": "source-tree",
|
|
4
4
|
"registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
|
|
5
|
-
"packageVersion": "1.3.
|
|
5
|
+
"packageVersion": "1.3.1170",
|
|
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": "a280711e801cac1066b109eb5400956a867d0028f6b5825ad21b7b1ccde1f49f",
|
|
3
3
|
"registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1170"
|
|
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-08-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-08-16T03:47:55.829Z",
|
|
5
|
+
"instarVersion": "1.3.1170",
|
|
6
6
|
"entryCount": 202,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generatedFrom": "source-tree",
|
|
4
4
|
"registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
|
|
5
|
-
"packageVersion": "1.3.
|
|
5
|
+
"packageVersion": "1.3.1170",
|
|
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": "a280711e801cac1066b109eb5400956a867d0028f6b5825ad21b7b1ccde1f49f",
|
|
3
3
|
"registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1170"
|
|
5
5
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
The Subscriptions tab now groups your accounts by provider — Claude accounts together, Codex
|
|
9
|
+
accounts together, each under a heading.
|
|
10
|
+
|
|
11
|
+
Before, the list was one flat run of cards in enrolment order, so a Codex account could sit
|
|
12
|
+
between two Claude ones. Each card names its provider, but in small text partway down, so
|
|
13
|
+
telling them apart meant reading every card. With a single provider that was fine. With two it
|
|
14
|
+
stopped being fine.
|
|
15
|
+
|
|
16
|
+
The quota bars live inside the account cards, so grouping the accounts groups the quota view
|
|
17
|
+
along with them.
|
|
18
|
+
|
|
19
|
+
## What to Tell Your User
|
|
20
|
+
|
|
21
|
+
- "Your accounts list is grouped by provider now, so your Codex account no longer sits in the
|
|
22
|
+
middle of your Claude ones."
|
|
23
|
+
- "If you only have one provider, nothing changes at all."
|
|
24
|
+
|
|
25
|
+
## Summary of New Capabilities
|
|
26
|
+
|
|
27
|
+
None. This is a presentation change to an existing tab — no endpoint, no configuration.
|
|
28
|
+
|
|
29
|
+
## Evidence
|
|
30
|
+
|
|
31
|
+
8 tests in the existing jsdom suite, which exercises the shipped dashboard module against a real
|
|
32
|
+
DOM. Shown capable of failing: restoring the flat list fails exactly the four grouping
|
|
33
|
+
assertions while the four invariant tests keep passing.
|
|
34
|
+
|
|
35
|
+
The controls carry the weight — a single-provider list is asserted unchanged, and order within a
|
|
36
|
+
group is asserted preserved. Without them, "grouping works" would pass equally well against a
|
|
37
|
+
change that sorted everything alphabetically and added a redundant heading to every
|
|
38
|
+
single-provider install.
|
|
39
|
+
|
|
40
|
+
The heading is the one new dynamic string reaching the page, so it rides the tab's existing
|
|
41
|
+
sanitize-and-write-as-text contract; a test plants a hostile provider string and confirms no
|
|
42
|
+
live element and no event-handler attribute survives.
|
|
43
|
+
|
|
44
|
+
## Compatibility Notes
|
|
45
|
+
|
|
46
|
+
**A single-provider install is untouched**: with one provider no heading is drawn, and the list
|
|
47
|
+
renders exactly as before. That is deliberate — a heading reading "Claude" above a list of only
|
|
48
|
+
Claude accounts is noise — and it is pinned by a test.
|
|
49
|
+
|
|
50
|
+
Accounts keep their existing order within each group. Nothing is sorted alphabetically, because
|
|
51
|
+
the order accounts arrive in carries meaning the dashboard cannot see (which was enrolled first,
|
|
52
|
+
which is the default). The change re-associates without re-ranking.
|
|
53
|
+
|
|
54
|
+
An account with no provider is still shown, under a heading reading "Other", rather than being
|
|
55
|
+
dropped or filed under a provider it does not belong to.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Side-Effects Review — group the accounts list by provider
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
`renderAccounts` drew accounts in a flat pool-ordered run, interleaving providers. It now draws
|
|
6
|
+
each provider's accounts together under a heading, via a new pure `groupAccountsByProvider`.
|
|
7
|
+
|
|
8
|
+
## Decision-point inventory
|
|
9
|
+
|
|
10
|
+
None. This is presentation ordering. It gates nothing, grants nothing, and reads no state
|
|
11
|
+
beyond the accounts array it is already handed.
|
|
12
|
+
|
|
13
|
+
## 1. Over-block / 2. Under-block
|
|
14
|
+
|
|
15
|
+
Not applicable — no admission decision. The nearest analogue is dropping an account from the
|
|
16
|
+
list, which a test forbids explicitly (every account renders exactly once; count asserted).
|
|
17
|
+
|
|
18
|
+
## 3. Level-of-abstraction fit
|
|
19
|
+
|
|
20
|
+
Grouping belongs in the renderer, not the API. The pool's order is meaningful and the server
|
|
21
|
+
should keep returning it as-is; how it is PRESENTED is a dashboard concern. Sorting server-side
|
|
22
|
+
would have imposed one view on every consumer.
|
|
23
|
+
|
|
24
|
+
## 4. Signal vs authority
|
|
25
|
+
|
|
26
|
+
Not applicable — no decision logic.
|
|
27
|
+
|
|
28
|
+
## 5. Interactions
|
|
29
|
+
|
|
30
|
+
The in-use marker, quota bars, status, email and the no-quota fallback all render through the
|
|
31
|
+
unchanged card path; grouping only reorders and interleaves headings. The heading marker is a
|
|
32
|
+
sentinel object (`__providerHeading`) consumed at the top of the same loop, so there is one
|
|
33
|
+
card-rendering path, not two. A test asserts the in-use marker survives grouping.
|
|
34
|
+
|
|
35
|
+
## 6. Multi-machine posture
|
|
36
|
+
|
|
37
|
+
Machine-local BY DESIGN — this is dashboard rendering of data the server already merged.
|
|
38
|
+
Nothing replicates. The pool view's own machine tagging is untouched.
|
|
39
|
+
|
|
40
|
+
## 7. Failure modes
|
|
41
|
+
|
|
42
|
+
`groupAccountsByProvider` handles a non-array (returns []), a missing provider (grouped under
|
|
43
|
+
`Other`), and a non-string provider (coerced to the empty key). It has no I/O and cannot throw
|
|
44
|
+
for any accounts array the existing renderer would have accepted.
|
|
45
|
+
|
|
46
|
+
**Security**: the heading is the only new dynamic string reaching the DOM. It goes through
|
|
47
|
+
`friendlyProvider`, which maps known providers and sanitizes everything else, and is written
|
|
48
|
+
via the module's `el()` textContent helper. A test plants `<img src=x onerror=...>` as a
|
|
49
|
+
provider and asserts no live element and no `onerror` attribute survives.
|
|
50
|
+
|
|
51
|
+
## 8. Rollback cost
|
|
52
|
+
|
|
53
|
+
Change one call site back to `for (const a of accounts)`. No state, no migration, no config.
|
|
54
|
+
|
|
55
|
+
## Evidence
|
|
56
|
+
|
|
57
|
+
8 new tests in the existing jsdom suite, which drives the SHIPPED `dashboard/subscriptions.js`
|
|
58
|
+
against a real DOM. Shown capable of failing: reverting the call site fails exactly the 4
|
|
59
|
+
grouping assertions while the 4 invariant tests keep passing — the correct signature, since
|
|
60
|
+
those describe behaviour that must hold either way.
|
|
61
|
+
|
|
62
|
+
Controls carry the weight here: a single-provider list is asserted UNCHANGED (no heading), and
|
|
63
|
+
account order within a group is asserted preserved. Without those, "grouping works" would pass
|
|
64
|
+
equally well against a change that sorted everything alphabetically and added a redundant
|
|
65
|
+
heading to every single-provider install.
|
|
66
|
+
|
|
67
|
+
One test was WRONG on first run and the code was right: it asserted `innerHTML` lacks the
|
|
68
|
+
substring "onerror", but correctly-escaped text still reads `<img src=x onerror=...>`.
|
|
69
|
+
The assertion now tests the real property — no live element, no such attribute, heading has no
|
|
70
|
+
element children — rather than the absence of characters.
|