sideby 0.13.0 → 0.14.0
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/README.md +1 -1
- package/dist/build.json +1 -1
- package/dist/src/cli/commands.d.ts +3 -2
- package/dist/src/cli/commands.js +69 -8
- package/dist/src/cli/format.js +7 -3
- package/dist/src/cli/json-schemas.js +62 -2
- package/dist/src/cli/main.js +42 -4
- package/dist/src/core/config.d.ts +27 -0
- package/dist/src/core/config.js +48 -1
- package/dist/src/core/handoff-enable.js +10 -5
- package/dist/src/core/launch.d.ts +5 -0
- package/dist/src/core/launch.js +14 -3
- package/dist/src/core/quota-fetch.d.ts +22 -0
- package/dist/src/core/quota-fetch.js +98 -0
- package/dist/src/core/quota-levels.d.ts +3 -2
- package/dist/src/core/quota-levels.js +8 -4
- package/dist/src/core/quota-store.d.ts +19 -0
- package/dist/src/core/quota-store.js +62 -0
- package/dist/src/core/skill-check.d.ts +15 -0
- package/dist/src/core/skill-check.js +49 -3
- package/dist/src/core/update.d.ts +45 -0
- package/dist/src/core/update.js +72 -1
- package/dist/src/families/grok/handoff-hook.d.ts +6 -0
- package/dist/src/families/grok/handoff-hook.js +45 -2
- package/dist/src/families/grok/handoff.d.ts +6 -1
- package/dist/src/families/grok/handoff.js +30 -13
- package/dist/src/families/grok/index.js +5 -0
- package/dist/src/families/grok/quota-log.d.ts +30 -0
- package/dist/src/families/grok/quota-log.js +140 -0
- package/dist/src/families/grok/quota.d.ts +5 -0
- package/dist/src/families/grok/quota.js +112 -0
- package/dist/src/panel/handler.d.ts +9 -3
- package/dist/src/panel/handler.js +65 -3
- package/dist/src/panel/i18n.d.ts +25 -5
- package/dist/src/panel/i18n.js +52 -12
- package/dist/src/panel/page-logic.d.ts +18 -2
- package/dist/src/panel/page-logic.js +53 -6
- package/dist/src/panel/page-script.js +286 -92
- package/dist/src/panel/page-styles.js +30 -10
- package/dist/src/runtime.d.ts +30 -2
- package/dist/src/runtime.js +64 -21
- package/dist/src/types.d.ts +49 -1
- package/package.json +2 -2
- package/schemas/alias.json +62 -0
- package/schemas/app-install.json +62 -0
- package/schemas/app-uninstall.json +62 -0
- package/schemas/browser-open.json +62 -0
- package/schemas/browser-switch.json +62 -0
- package/schemas/browser.json +62 -0
- package/schemas/config.json +46 -0
- package/schemas/desktop-remove.json +62 -0
- package/schemas/desktop.json +62 -0
- package/schemas/doctor.json +62 -0
- package/schemas/error.json +62 -0
- package/schemas/families.json +62 -0
- package/schemas/handoff-disable.json +62 -0
- package/schemas/handoff-enable.json +62 -0
- package/schemas/handoff-ready.json +62 -0
- package/schemas/handoff-setup.json +62 -0
- package/schemas/handoff-status.json +62 -0
- package/schemas/handoff-teardown.json +62 -0
- package/schemas/list.json +62 -0
- package/schemas/model.json +62 -0
- package/schemas/new.json +62 -0
- package/schemas/next.json +62 -0
- package/schemas/plugins.json +62 -0
- package/schemas/quota-setup.json +62 -0
- package/schemas/quota-teardown.json +62 -0
- package/schemas/quota.json +76 -1
- package/schemas/remote-control.json +62 -0
- package/schemas/tune-change.json +62 -0
- package/schemas/tune.json +62 -0
- package/schemas/update.json +107 -1
- package/skills/sideby/SKILL.md +4 -2
- package/skills/sideby/references/diagnose.md +3 -2
- package/skills/sideby/references/handoff.md +1 -1
- package/skills/sideby/references/quota.md +5 -1
- package/skills/sideby/references/update.md +16 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The Panel page's inline script. Plain browser JavaScript in a string (no template literals inside, so String.raw
|
|
2
2
|
// keeps it verbatim); the pure helpers from page-logic.ts are prepended so the tested code is the code that runs.
|
|
3
|
-
import { aliasArgsOf, aliasIssue, attentionReasons, attentionWhy, cacheHitRate, formatDuration, handoffChangeOf, handoffHintAccount, loginClusters, loginKey, maskEmail, matchesQuery, mergeDaily, newAccountDir, nextReset, panelHint, panelLang, passedWindow, quotaPressure, remoteControlSummary, remoteControlTodo, sortAccounts, suggestAlias, suggestName, updatedAge, windowState, } from './page-logic.js';
|
|
3
|
+
import { aliasArgsOf, aliasIssue, attentionReasons, attentionWhy, cacheHitRate, formatDuration, handoffChangeOf, handoffHintAccount, inDisplayOrder, loginClusters, loginKey, maskEmail, matchesQuery, mergeDaily, newAccountDir, nextReset, panelHint, panelLang, passedWindow, quotaPressure, remoteControlSummary, remoteControlTodo, reorder, sortAccounts, suggestAlias, suggestName, updatedAge, windowState, } from './page-logic.js';
|
|
4
4
|
const LOGIC = [
|
|
5
5
|
aliasArgsOf,
|
|
6
6
|
aliasIssue,
|
|
@@ -10,6 +10,7 @@ const LOGIC = [
|
|
|
10
10
|
formatDuration,
|
|
11
11
|
handoffChangeOf,
|
|
12
12
|
handoffHintAccount,
|
|
13
|
+
inDisplayOrder,
|
|
13
14
|
loginClusters,
|
|
14
15
|
loginKey,
|
|
15
16
|
maskEmail,
|
|
@@ -23,6 +24,7 @@ const LOGIC = [
|
|
|
23
24
|
quotaPressure,
|
|
24
25
|
remoteControlSummary,
|
|
25
26
|
remoteControlTodo,
|
|
27
|
+
reorder,
|
|
26
28
|
sortAccounts,
|
|
27
29
|
suggestAlias,
|
|
28
30
|
suggestName,
|
|
@@ -57,14 +59,14 @@ const MAIN = String.raw `
|
|
|
57
59
|
silent: false, lastTry: 0, pending: false, fastUntil: 0, fastFamily: null,
|
|
58
60
|
}
|
|
59
61
|
// View state that is not saved: the search text, the attention filter, the Health check filter.
|
|
60
|
-
const ui = { query: '', attention: false, allFindings: false, healthRef: null, checkingRef: null, desktopRef: null, browserRef: null, appBusy: null, appRemove: null, appNew: null, highlight: null, fade: true, loginRefs: new
|
|
62
|
+
const ui = { query: '', attention: false, allFindings: false, healthRef: null, checkingRef: null, desktopRef: null, browserRef: null, appBusy: null, appRemove: null, appNew: null, highlight: null, fade: true, loginRefs: new Map(), order: { families: [], units: {} }, drag: null }
|
|
61
63
|
const $ = (id) => document.getElementById(id)
|
|
62
64
|
|
|
63
65
|
// ---------- preferences ----------
|
|
64
66
|
// Kept in localStorage, which can be missing or throw (private windows, blocked storage): every access is guarded
|
|
65
67
|
// and the defaults apply.
|
|
66
68
|
const PREFS_KEY = 'sideby.panel.v2'
|
|
67
|
-
const CHOICES = { view: ['list', 'cards'], sort: ['pressure', 'name', 'recent'], quota: ['used', 'left'], theme: ['system', 'light', 'dark'], lang: ['en', 'zh'] }
|
|
69
|
+
const CHOICES = { view: ['list', 'cards'], sort: ['pressure', 'name', 'recent', 'manual'], quota: ['used', 'left'], theme: ['system', 'light', 'dark'], lang: ['en', 'zh'] }
|
|
68
70
|
const prefs = loadPrefs()
|
|
69
71
|
function loadPrefs() {
|
|
70
72
|
let raw = {}
|
|
@@ -235,6 +237,8 @@ const MAIN = String.raw `
|
|
|
235
237
|
function uiBusy() {
|
|
236
238
|
// A row or group in the middle of folding; a render now would cut the motion short.
|
|
237
239
|
for (const el of folding) if (el.isConnected) return true
|
|
240
|
+
// A row or group being dragged into a new place (spec §3.25).
|
|
241
|
+
if (ui.drag) return true
|
|
238
242
|
if (menu.el || (dlg.el && dlg.el.open) || document.querySelector('.tip.is-open')) return true
|
|
239
243
|
// A plan on the Auto Handoff page, and the change a person opened to read, stay as they are.
|
|
240
244
|
if (!$('handoff-page').hidden && (hp.preview || hp.askSame || document.querySelector('#handoff-page details[open]'))) return true
|
|
@@ -249,6 +253,7 @@ const MAIN = String.raw `
|
|
|
249
253
|
search: 'M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14zM20 20l-4.2-4.2',
|
|
250
254
|
chevron: 'M9 6l6 6-6 6',
|
|
251
255
|
more: 'M5 12h.01M12 12h.01M19 12h.01',
|
|
256
|
+
grip: 'M9 6h.01M9 12h.01M9 18h.01M15 6h.01M15 12h.01M15 18h.01',
|
|
252
257
|
plus: 'M12 5v14M5 12h14',
|
|
253
258
|
sun: 'M12 16a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4',
|
|
254
259
|
moon: 'M20 14.5A8 8 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5z',
|
|
@@ -779,15 +784,17 @@ const MAIN = String.raw `
|
|
|
779
784
|
CHOICES.sort.map((v) => h('option', { value: v }, t('sort.' + v))))
|
|
780
785
|
tb.sort.value = prefs.sort
|
|
781
786
|
tb.sort.addEventListener('change', () => { prefs.sort = tb.sort.value; savePrefs(); renderAccounts() })
|
|
787
|
+
tb.reset = h('button', { class: 'btn btn-sm tb-reset', type: 'button', title: t('order.resetHint'), onclick: resetOrder }, t('order.resetButton'))
|
|
782
788
|
const display = h('button', { class: 'btn tb-display', type: 'button', 'aria-haspopup': 'dialog', 'aria-expanded': 'false', title: t('tb.display') }, icon('sliders'), h('span', null, t('tb.display')))
|
|
783
789
|
display.addEventListener('click', () => openPopover(display, 'display', t('tb.display'), displayPanel, 'end'))
|
|
784
790
|
const create = BOOT.readOnly ? null : h('button', { class: 'btn btn-primary', type: 'button', 'aria-label': t('tb.new'), onclick: () => openCreate(null) }, icon('plus'), h('span', null, t('tb.new')))
|
|
785
791
|
setKids($('toolbar'), [
|
|
786
792
|
h('label', { class: 'search' }, icon('search', 'search-ic'), tb.search, h('kbd', { class: 'kbd', 'aria-hidden': 'true' }, '/')),
|
|
787
|
-
h('div', { class: 'tools' }, tb.family, tb.sort, display),
|
|
793
|
+
h('div', { class: 'tools' }, tb.family, tb.sort, tb.reset, display),
|
|
788
794
|
create,
|
|
789
795
|
])
|
|
790
796
|
renderFamilyFilter()
|
|
797
|
+
renderOrderReset()
|
|
791
798
|
if (hadFocus) tb.search.focus()
|
|
792
799
|
}
|
|
793
800
|
// "Display": the view settings people change rarely, one popover instead of three buttons in the toolbar.
|
|
@@ -829,12 +836,16 @@ const MAIN = String.raw `
|
|
|
829
836
|
// One column per Quota window label. 5h and 7d always have one, so rows of every group line up under the same
|
|
830
837
|
// header; a label only some account reports (such as 60m) adds a column, at most three in all.
|
|
831
838
|
const KNOWN_WINDOWS = { '5h': 300, '7d': 10080 }
|
|
839
|
+
// A window's column: the models it covers when it covers only some (Cursor's "Other models"), else its length.
|
|
840
|
+
function winKey(w) { return w.name || w.label }
|
|
841
|
+
// One group's columns: the windows its accounts report, shortest first; 5h and 7d when none reports any yet.
|
|
832
842
|
function quotaColumns(list) {
|
|
833
|
-
const mins =
|
|
843
|
+
const mins = {}
|
|
834
844
|
for (const a of list)
|
|
835
845
|
if (a.quota && a.quota.status === 'ok')
|
|
836
|
-
for (const w of a.quota.windows || []) if (!Object.hasOwn(mins, w
|
|
837
|
-
|
|
846
|
+
for (const w of a.quota.windows || []) if (!Object.hasOwn(mins, winKey(w))) mins[winKey(w)] = (Number(w.windowMinutes) || 0) + (w.name ? -0.5 : 0)
|
|
847
|
+
if (!Object.keys(mins).length) Object.assign(mins, KNOWN_WINDOWS)
|
|
848
|
+
return Object.keys(mins).sort((x, y) => mins[x] - mins[y]).slice(0, 4)
|
|
838
849
|
}
|
|
839
850
|
// A window's cell: percent and countdown on one line, the bar under them. The label shows where no column
|
|
840
851
|
// header does (cards, narrow screens).
|
|
@@ -843,13 +854,15 @@ const MAIN = String.raw `
|
|
|
843
854
|
const passed = passedWindow(w, observedAt, now)
|
|
844
855
|
const left = prefs.quota === 'left'
|
|
845
856
|
const shown = left ? 100 - st.pct : st.pct
|
|
846
|
-
const
|
|
857
|
+
const asOf = stale ? t('quota.asOf', { ago: ago(observedAt) }) : ''
|
|
858
|
+
const title = [passed ? t('quota.resetNoData', { date: day(passed.since) }) : t('quota.resetsAt', { at: clock(w.resetsAt) }), asOf].filter(Boolean).join(' · ')
|
|
847
859
|
return h('div', { class: 'meter lvl-' + st.level + (stale || passed ? ' lvl-stale' : ''), title },
|
|
848
860
|
h('div', { class: 'm-top' },
|
|
849
|
-
h('span', { class: 'm-label' }, w
|
|
850
|
-
h('span', { class: 'm-pct' }, shown + '%', h('span', { class: 'm-unit' }, ' ' + t(left ? 'quota.unitLeft' : 'quota.unitUsed'))
|
|
861
|
+
h('span', { class: 'm-label' }, winKey(w)),
|
|
862
|
+
h('span', { class: 'm-pct' }, shown + '%', h('span', { class: 'm-unit' }, ' ' + t(left ? 'quota.unitLeft' : 'quota.unitUsed')),
|
|
863
|
+
stale ? h('span', { class: 'm-asof', role: 'img', 'aria-label': asOf }, icon('clock')) : null),
|
|
851
864
|
h('span', { class: 'm-reset' + (passed ? ' m-passed' : '') }, passed ? t('quota.resetShort') : countdown(w.resetsAt, now))),
|
|
852
|
-
h('div', { class: 'bar', role: 'meter', 'aria-label': t(left ? 'quota.meterLeft' : 'quota.meterUsed', { label: w
|
|
865
|
+
h('div', { class: 'bar', role: 'meter', 'aria-label': t(left ? 'quota.meterLeft' : 'quota.meterUsed', { label: winKey(w) }), 'aria-valuemin': '0', 'aria-valuemax': '100', 'aria-valuenow': String(shown), 'aria-valuetext': t(left ? 'quota.pctLeft' : 'quota.pctUsed', { n: shown }) }, h('span', { css: { width: shown + '%' } })))
|
|
853
866
|
}
|
|
854
867
|
// A column whose window this account does not report right now, such as a 5h window that has not started.
|
|
855
868
|
function noMeter(label) {
|
|
@@ -859,6 +872,9 @@ const MAIN = String.raw `
|
|
|
859
872
|
function quotaNote(a, q) {
|
|
860
873
|
const setup = setupFor(a.family)
|
|
861
874
|
// One setup turns quota on for every account of the Family, so the switch lives in the group header only.
|
|
875
|
+
// A check that asks the Host (Grok) and is turned off, or failed (spec §3.26): say why and what to do.
|
|
876
|
+
if (q.reason === 'fetch-failed') return h('div', { class: 'q-note q-off' }, h('span', { class: 'muted' }, t('quota.fetchFailed')), tipRaw(t('quota.fetchFailed'), q.detail || ''))
|
|
877
|
+
if (q.reason === 'not-enabled' && checkOf(a.family)) return h('div', { class: 'q-note q-off' }, h('span', { class: 'muted' }, t('quota.checkOff')), tipRaw(t('quota.checkOff'), q.detail || t('quota.checkOffHint')))
|
|
862
878
|
if (q.reason === 'not-enabled') {
|
|
863
879
|
const host = famTitle(a)
|
|
864
880
|
const body = setup && setup.plan.status === 'ready' && !BOOT.readOnly ? t('tip.quotaOff.body', { host })
|
|
@@ -870,22 +886,23 @@ const MAIN = String.raw `
|
|
|
870
886
|
const text = { 'no-session': 'quota.noSession', 'no-source': 'quota.noSource', unrecognized: 'quota.unrecognized' }[q.reason]
|
|
871
887
|
return h('div', { class: 'q-note', title: q.reason === 'no-session' ? t('quota.noSessionHint') : q.detail || null }, text ? t(text) : t('quota.unavailable', { reason: q.reason }))
|
|
872
888
|
}
|
|
873
|
-
|
|
874
|
-
function quotaBlock(a, now, cols, readFrom) {
|
|
889
|
+
function quotaBlock(a, now, cols) {
|
|
875
890
|
const q = a.quota
|
|
876
891
|
if (!q) return h('div', { class: 'quota' }, h('div', { class: 'q-note' }, '—'))
|
|
877
892
|
if (q.status !== 'ok') return h('div', { class: 'quota' }, quotaNote(a, q))
|
|
878
893
|
const stale = isStale(q.observedAt, now)
|
|
879
894
|
const wins = q.windows || []
|
|
880
895
|
// A reading borrowed from another Account of the same login says where it came from (spec §3.21).
|
|
881
|
-
const from =
|
|
882
|
-
|
|
883
|
-
|
|
896
|
+
const from = q.from ? t('quota.sharedFrom', { name: refName(q.from), ago: ago(q.observedAt) }) : null
|
|
897
|
+
// Where the numbers came from when the Host was asked (spec §3.26), and why the latest ask failed.
|
|
898
|
+
const via = q.fetched ? t('quota.fetchedFrom', { host: famTitle(a), ago: ago(q.observedAt) }) : null
|
|
899
|
+
const failed = q.fetchError ? t('quota.lastCheckFailed', { why: q.fetchError }) : null
|
|
900
|
+
const title = [from || via || (stale ? null : t('quota.dataFrom', { ago: ago(q.observedAt) })), failed].filter(Boolean).join('\n') || null
|
|
901
|
+
return h('div', { class: 'quota' + (stale ? ' is-stale' : ''), title },
|
|
884
902
|
wins.length ? cols.map((c) => {
|
|
885
|
-
const w = wins.find((x) => x
|
|
903
|
+
const w = wins.find((x) => winKey(x) === c)
|
|
886
904
|
return w ? meter(w, now, stale, q.observedAt) : noMeter(c)
|
|
887
|
-
}) : h('div', { class: 'q-note' }, t('quota.noWindows'))
|
|
888
|
-
stale ? h('div', { class: 'q-asof' }, icon('clock'), t('quota.asOf', { ago: ago(q.observedAt) })) : null)
|
|
905
|
+
}) : h('div', { class: 'q-note' }, t('quota.noWindows')))
|
|
889
906
|
}
|
|
890
907
|
function usageBlock(a) {
|
|
891
908
|
const u = a.usage
|
|
@@ -921,9 +938,12 @@ const MAIN = String.raw `
|
|
|
921
938
|
}
|
|
922
939
|
// A dot after the name when the account needs a look that no tag shows: a fuller quota window or a Health
|
|
923
940
|
// finding. Its reason is on hover; the details say more.
|
|
924
|
-
// Inside a login group the quota warning is the
|
|
941
|
+
// Inside a login group the quota warning is the first row's, so another row's dot is only about the row itself.
|
|
925
942
|
function attentionDot(a) {
|
|
926
|
-
const
|
|
943
|
+
const lead = loginLead(a)
|
|
944
|
+
const subject = !lead ? a
|
|
945
|
+
: lead !== a.ref ? Object.assign({}, a, { quota: undefined })
|
|
946
|
+
: Object.assign({}, a, { quota: a.quota && Object.assign({}, a.quota, { from: undefined }) })
|
|
927
947
|
const r = reasonsOf(subject, Date.now())
|
|
928
948
|
const lvl = r.includes('health-fail') ? 'fail' : r.includes('quota-high') || r.includes('health-warn') ? 'warn' : ''
|
|
929
949
|
if (!lvl) return null
|
|
@@ -947,6 +967,8 @@ const MAIN = String.raw `
|
|
|
947
967
|
: null,
|
|
948
968
|
a.hostInstalled === false ? h('span', { class: 'tag tag-warn' }, t(family(a.family).brokenBin ? 'acct.broken' : 'acct.notInstalled')) : null)
|
|
949
969
|
}
|
|
970
|
+
// An organization name as the vendor wrote it, without a trailing comma or full stop it sometimes carries.
|
|
971
|
+
function orgText(org) { return org ? String(org).replace(/[\s,,;;、。]+$/u, '') : '' }
|
|
950
972
|
// The name part of a ref (claude:001 gives 001), as rows show other Accounts of the same Family.
|
|
951
973
|
function refName(ref) { return String(ref).slice(String(ref).indexOf(':') + 1) }
|
|
952
974
|
// Who the account is signed in as, under its name; masked while "Hide emails" is on.
|
|
@@ -954,7 +976,7 @@ const MAIN = String.raw `
|
|
|
954
976
|
const id = a.identity
|
|
955
977
|
if (!id || (!id.email && !browserButton(a))) return null
|
|
956
978
|
const email = id.email ? emailText(id.email) : ''
|
|
957
|
-
return h('div', { class: 'ident-email', title: [email, id.org].filter(Boolean).join(' · ') },
|
|
979
|
+
return h('div', { class: 'ident-email', title: [email, orgText(id.org)].filter(Boolean).join(' · ') },
|
|
958
980
|
dots([email ? h('button', { class: 'email email-btn', type: 'button', title: t('menu.copyEmail'), 'aria-label': t('menu.copyEmail') + ' ' + email, onclick: (e) => { e.stopPropagation(); copyText(id.email) } }, email) : null, browserButton(a)]))
|
|
959
981
|
}
|
|
960
982
|
function subLine(a) {
|
|
@@ -1101,10 +1123,11 @@ const MAIN = String.raw `
|
|
|
1101
1123
|
}
|
|
1102
1124
|
// What a row or card shows depends on more than the account (health, view preferences, language); the
|
|
1103
1125
|
// fingerprint covers what its click handlers use.
|
|
1104
|
-
function rowSig(a) { return sig([a, healthOf(a.ref), isNext(a), ui.desktopRef === a.ref, ui.browserRef === a.ref, browserFor(a), prefs.hideEmail, isOpen(a),
|
|
1105
|
-
// Whether the row sits in a login group (spec §3.21):
|
|
1126
|
+
function rowSig(a) { return sig([a, healthOf(a.ref), isNext(a), ui.desktopRef === a.ref, ui.browserRef === a.ref, browserFor(a), prefs.hideEmail, isOpen(a), loginLead(a)]) }
|
|
1127
|
+
// Whether the row sits in a login group (spec §3.21): its first row then shows the quota for all of them.
|
|
1106
1128
|
function inLogin(a) { return ui.loginRefs.has(a.ref) }
|
|
1107
|
-
|
|
1129
|
+
// The group's first row, which shows the quota for all of them.
|
|
1130
|
+
function loginLead(a) { return ui.loginRefs.get(a.ref) || null }
|
|
1108
1131
|
// A row shows who the account is, its quota and its last use; a click opens the details under it (model, tokens,
|
|
1109
1132
|
// Health) and closes them again. The choice is kept in this browser's preferences.
|
|
1110
1133
|
function isOpen(a) { return prefs.open[a.ref] === true }
|
|
@@ -1176,7 +1199,7 @@ const MAIN = String.raw `
|
|
|
1176
1199
|
el.dataset.sig = rowSig(a)
|
|
1177
1200
|
let detail = rowDetails(el)[0]
|
|
1178
1201
|
if (open && !detail) {
|
|
1179
|
-
detail = rowDetail(a, Boolean(
|
|
1202
|
+
detail = rowDetail(a, Boolean(emailLine(a)))
|
|
1180
1203
|
const more = el.querySelector(':scope > .more')
|
|
1181
1204
|
el.insertBefore(detail, more ? more.nextSibling : null)
|
|
1182
1205
|
detail.style.opacity = '0'
|
|
@@ -1215,72 +1238,34 @@ const MAIN = String.raw `
|
|
|
1215
1238
|
ease(now, from, [], parts, 1, () => {})
|
|
1216
1239
|
for (const p of parts) p.style.opacity = ''
|
|
1217
1240
|
}
|
|
1218
|
-
// A Same Login group folds its directory rows like a family group, and remembers it the same way.
|
|
1219
|
-
function toggleLogin(key) {
|
|
1220
|
-
const find = () => document.querySelector('#accounts .login[data-key="' + CSS.escape(key) + '"]')
|
|
1221
|
-
const el = find()
|
|
1222
|
-
const open = prefs.collapsed[key] === true
|
|
1223
|
-
if (open) delete prefs.collapsed[key]; else prefs.collapsed[key] = true
|
|
1224
|
-
savePrefs()
|
|
1225
|
-
if (!el || !moving()) { renderAccounts(); return }
|
|
1226
|
-
const from = el.getBoundingClientRect().height
|
|
1227
|
-
const rowsOf = (x) => Array.from(x.querySelectorAll(':scope > .login-rows'))
|
|
1228
|
-
if (!open) {
|
|
1229
|
-
const btn = el.querySelector('.login-head .row-toggle')
|
|
1230
|
-
if (btn) btn.setAttribute('aria-expanded', 'false')
|
|
1231
|
-
const parts = rowsOf(el)
|
|
1232
|
-
ease(el, from, parts, parts, 0, renderAccounts)
|
|
1233
|
-
return
|
|
1234
|
-
}
|
|
1235
|
-
renderAccounts()
|
|
1236
|
-
const now = find()
|
|
1237
|
-
if (!now) return
|
|
1238
|
-
const parts = rowsOf(now)
|
|
1239
|
-
for (const p of parts) p.style.opacity = '0'
|
|
1240
|
-
ease(now, from, [], parts, 1, () => {})
|
|
1241
|
-
for (const p of parts) p.style.opacity = ''
|
|
1242
|
-
}
|
|
1243
1241
|
// The Account 'sideby next <family>' would pick; the server ranks it with the CLI's rule (spec §3.13).
|
|
1244
1242
|
function isNext(a) { return Boolean(S.state && S.state.handoff && S.state.handoff[a.family] === a.ref) }
|
|
1245
1243
|
function row(a, now, cols) {
|
|
1246
1244
|
const open = isOpen(a)
|
|
1247
|
-
const email =
|
|
1245
|
+
const email = emailLine(a)
|
|
1246
|
+
const lead = loginLead(a)
|
|
1248
1247
|
return h('div', { class: 'row fold-hit' + (open ? ' row-open' : '') + (a.error ? ' row-error' : ''), role: 'listitem', 'data-ref': a.ref, 'data-key': a.ref, 'data-sig': rowSig(a), onclick: foldClick(a) },
|
|
1249
|
-
h('div', { class: 'ident' }, rowToggle(a), badge(a.family, 'sm'), h('div', { class: 'ident-text' }, nameLine(a), email || subLine(a),
|
|
1250
|
-
|
|
1251
|
-
inLogin(a) ? null : quotaBlock(a, now, cols),
|
|
1248
|
+
h('div', { class: 'ident' }, dragHandle('account', a.family, a.name, a.name), rowToggle(a), badge(a.family, 'sm'), h('div', { class: 'ident-text' }, nameLine(a), email || subLine(a))),
|
|
1249
|
+
lead && lead !== a.ref ? sharedQuota(lead) : quotaBlock(a, now, cols),
|
|
1252
1250
|
lastUsed(a),
|
|
1253
1251
|
moreButton(a),
|
|
1254
1252
|
open ? rowDetail(a, Boolean(email)) : null,
|
|
1255
1253
|
notes(a))
|
|
1256
1254
|
}
|
|
1257
|
-
// Directories signed in as one login (spec §3.21): the vendor counts one quota, so
|
|
1258
|
-
//
|
|
1255
|
+
// Directories signed in as one login (spec §3.21): the vendor counts one quota, so it shows once, on the group's
|
|
1256
|
+
// first row; every directory stays an ordinary row (name, email, browser, model, tokens, Health), and a rail on
|
|
1257
|
+
// the left ties the rows together.
|
|
1259
1258
|
function loginGroup(members, now, cols) {
|
|
1260
|
-
const first = members[0]
|
|
1261
|
-
const source = members.find((a) => a.quota && a.quota.status === 'ok' && !a.quota.from) || first
|
|
1262
|
-
const id = first.identity || {}
|
|
1263
1259
|
const names = members.map((a) => a.name).join(', ')
|
|
1264
|
-
|
|
1265
|
-
const
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
const
|
|
1272
|
-
return h('div', { class: '
|
|
1273
|
-
h('div', { class: 'row login-head fold-hit', onclick: foldHit(() => toggleLogin(key)) },
|
|
1274
|
-
h('div', { class: 'ident' }, toggle, badge(first.family, 'sm'),
|
|
1275
|
-
h('div', { class: 'ident-text' },
|
|
1276
|
-
h('div', { class: 'ident-name' },
|
|
1277
|
-
email ? h('button', { class: 'name email-btn', type: 'button', title: t('menu.copyEmail'), 'aria-label': t('menu.copyEmail') + ' ' + email, onclick: () => copyText(id.email) }, email) : h('span', { class: 'name' }, names),
|
|
1278
|
-
why ? h('span', { class: 'dot dot-warn att-dot', title: why, role: 'img', 'aria-label': why }) : null),
|
|
1279
|
-
h('div', { class: 'ident-sub', title: hint }, dots([id.org ? h('span', null, id.org) : null, h('span', null, t('login.count', { n: members.length })), collapsed ? h('span', null, names) : null])))),
|
|
1280
|
-
quotaBlock(source, now, cols, source.quota && source.quota.status === 'ok' ? source.ref : null),
|
|
1281
|
-
h('div', { class: 'last' }),
|
|
1282
|
-
h('div', { class: 'more' })),
|
|
1283
|
-
collapsed ? null : h('div', { class: 'login-rows', role: 'list', 'data-key': 'rows', 'data-patch': '' }, members.map((a) => row(a, now, cols))))
|
|
1260
|
+
// The first row as sorted carries the quota.
|
|
1261
|
+
for (const a of members) ui.loginRefs.set(a.ref, members[0].ref)
|
|
1262
|
+
return h('div', { class: 'login', role: 'listitem', 'aria-label': t('login.label', { names }), title: t('login.hint', { names }), 'data-key': loginKey(members.map((a) => a.ref)) },
|
|
1263
|
+
h('div', { class: 'login-rows', role: 'list', 'data-key': 'rows', 'data-patch': '' }, members.map((a) => row(a, now, cols))))
|
|
1264
|
+
}
|
|
1265
|
+
// The quota cell of a group's other rows: the numbers are the first row's.
|
|
1266
|
+
function sharedQuota(lead) {
|
|
1267
|
+
const name = refName(lead)
|
|
1268
|
+
return h('div', { class: 'quota' }, h('div', { class: 'q-note q-shared', title: t('login.sharedHint', { name }) }, icon('arrowUp'), t('login.shared', { name })))
|
|
1284
1269
|
}
|
|
1285
1270
|
// What a click on a row opens: how it runs (ref, model and effort, a shared login), its tokens and its Health.
|
|
1286
1271
|
function sameLoginNote(a) {
|
|
@@ -1484,7 +1469,9 @@ const MAIN = String.raw `
|
|
|
1484
1469
|
const off = list.some((a) => a.quota && a.quota.status === 'unavailable' && a.quota.reason === 'not-enabled')
|
|
1485
1470
|
const bodyId = 'grp-' + fam.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
1486
1471
|
const cards = prefs.view === 'cards'
|
|
1472
|
+
if (!cards) ui.order.units[fam] = loginClusters(list).map((x) => Array.isArray(x) ? x.map((a) => a.name) : [x.name])
|
|
1487
1473
|
const head = h('div', { class: 'group-head', 'data-key': 'head' },
|
|
1474
|
+
dragHandle('family', fam, fam, info.title),
|
|
1488
1475
|
h('button', { class: 'group-toggle', type: 'button', 'aria-expanded': String(!collapsed), 'aria-controls': bodyId, title: t(collapsed ? 'group.expand' : 'group.collapse'), 'aria-label': t('group.toggle', { title: info.title, count: list.length === total ? String(total) : list.length + ' / ' + total, action: t(collapsed ? 'group.expand' : 'group.collapse') }), onclick: () => toggleGroup(fam, 'g:' + fam) },
|
|
1489
1476
|
icon('chevron', 'chev'), badge(fam, 'sm'), h('span', { class: 'group-title' }, info.title),
|
|
1490
1477
|
h('span', { class: 'group-count' }, list.length === total ? String(total) : list.length + ' / ' + total)),
|
|
@@ -1500,7 +1487,7 @@ const MAIN = String.raw `
|
|
|
1500
1487
|
const body = collapsed ? null : cards
|
|
1501
1488
|
? h('div', { class: 'grid', id: bodyId, 'data-key': 'cards', 'data-patch': '' }, list.map((a) => card(a, now, cols)))
|
|
1502
1489
|
: h('div', { class: 'rows', id: bodyId, role: 'list', 'data-key': 'rows', 'data-patch': '' }, loginClusters(list).map((x) => Array.isArray(x) ? loginGroup(x, now, cols) : row(x, now, cols)))
|
|
1503
|
-
return h('section', { class: 'group' + (cards ? ' group-cards' : ' group-list') + (collapsed ? ' is-collapsed' : ''), 'aria-label': info.title, 'data-key': 'g:' + fam, 'data-patch': '' },
|
|
1490
|
+
return h('section', { class: 'group' + (cards ? ' group-cards' : ' group-list') + (collapsed ? ' is-collapsed' : ''), 'aria-label': info.title, 'data-key': 'g:' + fam, 'data-fam': fam, 'data-patch': '' },
|
|
1504
1491
|
head, collapsed || cards ? null : colHead(cols, list), body)
|
|
1505
1492
|
}
|
|
1506
1493
|
function emptyState() {
|
|
@@ -1536,6 +1523,159 @@ const MAIN = String.raw `
|
|
|
1536
1523
|
h('p', null, S.loadError.message),
|
|
1537
1524
|
h('button', { class: 'btn', type: 'button', onclick: () => load({ check: !S.report }) }, icon('refresh'), t('btn.retry')))
|
|
1538
1525
|
}
|
|
1526
|
+
// ---------- Display Order (spec §3.25) ----------
|
|
1527
|
+
function displayOf() { return (S.state && S.state.display) || {} }
|
|
1528
|
+
// Every Family id in the Display Order, the ones only accounts name (a plugin that failed) last.
|
|
1529
|
+
function familyOrder() {
|
|
1530
|
+
const ids = S.state.families.map((f) => f.id)
|
|
1531
|
+
for (const a of S.state.accounts) if (!ids.includes(a.family)) ids.push(a.family)
|
|
1532
|
+
return inDisplayOrder(ids, (x) => x, displayOf().families)
|
|
1533
|
+
}
|
|
1534
|
+
function accountOrder(fam) { return (displayOf().accounts || {})[fam] }
|
|
1535
|
+
// Every account name of a Family, hidden ones too, in the Display Order.
|
|
1536
|
+
function allNames(fam) {
|
|
1537
|
+
return sortAccounts(S.state.accounts.filter((a) => a.family === fam), 'manual', Date.now(), accountOrder(fam)).map((a) => a.name)
|
|
1538
|
+
}
|
|
1539
|
+
function ordered() { const d = displayOf(); return Boolean((d.families && d.families.length) || (d.accounts && Object.keys(d.accounts).length)) }
|
|
1540
|
+
// A grip to drag a Family group or an account (a Same Login group moves as one) into a new place, or to move it
|
|
1541
|
+
// with the arrow keys. Not in the card view, nor on a read-only Panel.
|
|
1542
|
+
function dragHandle(kind, fam, key, name) {
|
|
1543
|
+
if (BOOT.readOnly || prefs.view === 'cards') return null
|
|
1544
|
+
const label = t(kind === 'family' ? 'order.moveFamily' : 'order.moveAccount', { name })
|
|
1545
|
+
return h('button', { class: 'icon-btn drag-handle', type: 'button', draggable: 'true', 'data-move': kind, 'data-fam': fam, 'data-name': key, 'aria-label': label, title: t('order.handle'),
|
|
1546
|
+
onclick: (e) => e.stopPropagation(),
|
|
1547
|
+
onkeydown: (e) => {
|
|
1548
|
+
const keys = { ArrowUp: -1, ArrowDown: 1, Home: -Infinity, End: Infinity }
|
|
1549
|
+
if (!Object.hasOwn(keys, e.key)) return
|
|
1550
|
+
e.preventDefault()
|
|
1551
|
+
e.stopPropagation()
|
|
1552
|
+
const units = unitsOf(kind, fam)
|
|
1553
|
+
const from = units.findIndex((u) => u.includes(key))
|
|
1554
|
+
const step = keys[e.key]
|
|
1555
|
+
const to = Math.max(0, Math.min(units.length - 1, Number.isFinite(step) ? from + step : step < 0 ? 0 : units.length - 1))
|
|
1556
|
+
moveUnit(kind, fam, from, to, { focus: key, label: name })
|
|
1557
|
+
},
|
|
1558
|
+
ondragstart: (e) => {
|
|
1559
|
+
const units = unitsOf(kind, fam)
|
|
1560
|
+
ui.drag = { kind, fam, from: units.findIndex((u) => u.includes(key)), to: null }
|
|
1561
|
+
e.dataTransfer.effectAllowed = 'move'
|
|
1562
|
+
try { e.dataTransfer.setData('text/plain', name) } catch {}
|
|
1563
|
+
const el = unitElement(kind, fam, key)
|
|
1564
|
+
if (el) { e.dataTransfer.setDragImage(el, 16, 16); requestAnimationFrame(() => el.classList.add('is-dragging')) }
|
|
1565
|
+
},
|
|
1566
|
+
ondragend: () => endDrag(),
|
|
1567
|
+
}, icon('grip'))
|
|
1568
|
+
}
|
|
1569
|
+
function unitsOf(kind, fam) { return kind === 'family' ? ui.order.families.map((f) => [f]) : (ui.order.units[fam] || []) }
|
|
1570
|
+
// The element that moves: a Family's group, an account's row, or the Same Login group the row is in.
|
|
1571
|
+
function unitElement(kind, fam, key) {
|
|
1572
|
+
if (kind === 'family') return document.querySelector('#accounts > section[data-fam="' + CSS.escape(fam) + '"]')
|
|
1573
|
+
const rowEl = document.querySelector('#accounts .row[data-ref="' + CSS.escape(fam + ':' + key) + '"]')
|
|
1574
|
+
return rowEl && (rowEl.closest('.login') || rowEl)
|
|
1575
|
+
}
|
|
1576
|
+
// Where a drop at the pointer lands: the list of places it can go and the index among them, or null outside.
|
|
1577
|
+
function dropTarget(e) {
|
|
1578
|
+
const d = ui.drag
|
|
1579
|
+
if (!d) return null
|
|
1580
|
+
if (d.kind === 'family') {
|
|
1581
|
+
const els = Array.from(document.querySelectorAll('#accounts > section[data-fam]'))
|
|
1582
|
+
return els.length ? { els, at: slotAt(els, e.clientY) } : null
|
|
1583
|
+
}
|
|
1584
|
+
const sec = e.target.closest && e.target.closest('#accounts > section[data-fam]')
|
|
1585
|
+
if (!sec || sec.getAttribute('data-fam') !== d.fam) return null
|
|
1586
|
+
const box = sec.querySelector('.rows')
|
|
1587
|
+
if (!box) return null
|
|
1588
|
+
const els = Array.from(box.children)
|
|
1589
|
+
return { els, at: slotAt(els, e.clientY) }
|
|
1590
|
+
}
|
|
1591
|
+
function slotAt(els, y) {
|
|
1592
|
+
for (let i = 0; i < els.length; i++) {
|
|
1593
|
+
const r = els[i].getBoundingClientRect()
|
|
1594
|
+
if (y < r.top + r.height / 2) return i
|
|
1595
|
+
}
|
|
1596
|
+
return els.length
|
|
1597
|
+
}
|
|
1598
|
+
function clearDropMarks() { for (const el of document.querySelectorAll('.drop-before, .drop-after')) el.classList.remove('drop-before', 'drop-after') }
|
|
1599
|
+
function endDrag() {
|
|
1600
|
+
clearDropMarks()
|
|
1601
|
+
for (const el of document.querySelectorAll('.is-dragging')) el.classList.remove('is-dragging')
|
|
1602
|
+
ui.drag = null
|
|
1603
|
+
if (S.pending) { S.pending = false; renderAll() }
|
|
1604
|
+
}
|
|
1605
|
+
function onDragOver(e) {
|
|
1606
|
+
const tgt = dropTarget(e)
|
|
1607
|
+
if (!tgt) return
|
|
1608
|
+
e.preventDefault()
|
|
1609
|
+
e.dataTransfer.dropEffect = 'move'
|
|
1610
|
+
clearDropMarks()
|
|
1611
|
+
if (tgt.at < tgt.els.length) tgt.els[tgt.at].classList.add('drop-before')
|
|
1612
|
+
else tgt.els[tgt.els.length - 1].classList.add('drop-after')
|
|
1613
|
+
// Dropped after itself means one place earlier once it is taken out.
|
|
1614
|
+
ui.drag.to = tgt.at > ui.drag.from ? tgt.at - 1 : tgt.at
|
|
1615
|
+
}
|
|
1616
|
+
function onDrop(e) {
|
|
1617
|
+
const d = ui.drag
|
|
1618
|
+
if (!d || !dropTarget(e)) return
|
|
1619
|
+
e.preventDefault()
|
|
1620
|
+
const to = d.to
|
|
1621
|
+
endDrag()
|
|
1622
|
+
if (to !== null && to !== d.from) moveUnit(d.kind, d.fam, d.from, to)
|
|
1623
|
+
}
|
|
1624
|
+
// Records the new Display Order: shown at once, written to the config, and put back if the write fails.
|
|
1625
|
+
async function moveUnit(kind, fam, from, to, opts) {
|
|
1626
|
+
opts = opts || {}
|
|
1627
|
+
const units = unitsOf(kind, fam)
|
|
1628
|
+
if (from < 0 || to < 0 || from >= units.length || to >= units.length) return
|
|
1629
|
+
if (from === to) { if (opts.label) say(t('order.position', { name: opts.label, n: from + 1, total: units.length })); return }
|
|
1630
|
+
const before = displayOf()
|
|
1631
|
+
const next = { families: before.families ? before.families.slice() : [], accounts: Object.assign({}, before.accounts || {}) }
|
|
1632
|
+
if (kind === 'family') next.families = reorder(familyOrder(), units, from, to)
|
|
1633
|
+
else next.accounts[fam] = reorder(allNames(fam), units, from, to)
|
|
1634
|
+
S.state.display = next
|
|
1635
|
+
// Moving an account says "my order" from now on; Families follow it in every sort.
|
|
1636
|
+
if (kind === 'account' && prefs.sort !== 'manual') { prefs.sort = 'manual'; savePrefs(); if (tb.sort) tb.sort.value = 'manual' }
|
|
1637
|
+
renderAccounts()
|
|
1638
|
+
if (opts.focus) {
|
|
1639
|
+
const btn = document.querySelector('#accounts .drag-handle[data-move="' + kind + '"][data-fam="' + CSS.escape(fam) + '"][data-name="' + CSS.escape(opts.focus) + '"]')
|
|
1640
|
+
if (btn) btn.focus({ preventScroll: false })
|
|
1641
|
+
say(t('order.position', { name: opts.label, n: to + 1, total: units.length }))
|
|
1642
|
+
}
|
|
1643
|
+
try {
|
|
1644
|
+
const r = await api('/api/display-order', { display: next })
|
|
1645
|
+
S.state.display = r.display
|
|
1646
|
+
} catch (e) {
|
|
1647
|
+
S.state.display = before
|
|
1648
|
+
renderAccounts()
|
|
1649
|
+
toast(t('order.failed', { msg: e.message }), { kind: 'fail' })
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
async function resetOrder() {
|
|
1653
|
+
const before = displayOf()
|
|
1654
|
+
S.state.display = {}
|
|
1655
|
+
if (prefs.sort === 'manual') { prefs.sort = 'pressure'; savePrefs(); if (tb.sort) tb.sort.value = 'pressure' }
|
|
1656
|
+
renderAccounts()
|
|
1657
|
+
try {
|
|
1658
|
+
const r = await api('/api/display-order', { display: null })
|
|
1659
|
+
S.state.display = r.display
|
|
1660
|
+
toast(t('order.reset'))
|
|
1661
|
+
} catch (e) {
|
|
1662
|
+
S.state.display = before
|
|
1663
|
+
renderAccounts()
|
|
1664
|
+
toast(t('order.failed', { msg: e.message }), { kind: 'fail' })
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
// The toolbar's "Default order" button, shown once the user has put things in an order of their own.
|
|
1668
|
+
function renderOrderReset() {
|
|
1669
|
+
if (!tb.reset) return
|
|
1670
|
+
tb.reset.hidden = BOOT.readOnly || !ordered()
|
|
1671
|
+
}
|
|
1672
|
+
// Words for screen readers only, such as the new place of something moved with the keyboard.
|
|
1673
|
+
let live = null
|
|
1674
|
+
function say(text) {
|
|
1675
|
+
if (!live) { live = h('div', { class: 'sr-only', 'aria-live': 'polite' }); document.body.append(live) }
|
|
1676
|
+
live.textContent = ''
|
|
1677
|
+
setTimeout(() => { live.textContent = text }, 30)
|
|
1678
|
+
}
|
|
1539
1679
|
function renderAccounts() {
|
|
1540
1680
|
closeMenu()
|
|
1541
1681
|
const box = $('accounts')
|
|
@@ -1556,18 +1696,25 @@ const MAIN = String.raw `
|
|
|
1556
1696
|
: tn('count.accounts', all.length))
|
|
1557
1697
|
const apps = appGroups()
|
|
1558
1698
|
if (!shown.length && !apps.length) { patch(box, noMatch()); return }
|
|
1559
|
-
|
|
1560
|
-
box.style.setProperty('--qn', String(cols.length))
|
|
1561
|
-
ui.loginRefs = new Set()
|
|
1699
|
+
ui.loginRefs = new Map()
|
|
1562
1700
|
if (prefs.view !== 'cards')
|
|
1563
|
-
for (const x of loginClusters(shown)) if (Array.isArray(x)) for (const a of x) ui.loginRefs.
|
|
1564
|
-
|
|
1565
|
-
|
|
1701
|
+
for (const x of loginClusters(shown)) if (Array.isArray(x)) for (const a of x) ui.loginRefs.set(a.ref, x[0].ref)
|
|
1702
|
+
// Families in the Display Order (spec §3.25) whatever the sort; each group gets the quota columns it uses.
|
|
1703
|
+
// Every group is as wide as the one with the most windows, so the columns after them line up.
|
|
1704
|
+
ui.order = { families: [], units: {} }
|
|
1566
1705
|
const groups = []
|
|
1567
|
-
|
|
1706
|
+
let width = 1
|
|
1707
|
+
for (const fam of familyOrder()) {
|
|
1568
1708
|
const list = shown.filter((a) => a.family === fam)
|
|
1569
|
-
if (list.length)
|
|
1709
|
+
if (!list.length) continue
|
|
1710
|
+
ui.order.families.push(fam)
|
|
1711
|
+
const sorted = sortAccounts(list, prefs.sort, now, accountOrder(fam))
|
|
1712
|
+
const cols = quotaColumns(sorted)
|
|
1713
|
+
width = Math.max(width, cols.length)
|
|
1714
|
+
groups.push(group(fam, sorted, all.filter((a) => a.family === fam).length, now, cols))
|
|
1570
1715
|
}
|
|
1716
|
+
box.style.setProperty('--qn', String(width))
|
|
1717
|
+
renderOrderReset()
|
|
1571
1718
|
patch(box, groups.concat(apps))
|
|
1572
1719
|
if (ui.fade && !REDUCED) {
|
|
1573
1720
|
box.classList.add('view-in')
|
|
@@ -2842,6 +2989,29 @@ const MAIN = String.raw `
|
|
|
2842
2989
|
const api = S.state.accounts.filter((a) => a.kind === 'api').map((a) => a.ref)
|
|
2843
2990
|
return remoteControlSummary(S.state.remoteControl || [], api)
|
|
2844
2991
|
}
|
|
2992
|
+
// The refresh button reads everything again; Families that ask their Host for Quota (Grok, spec §3.26) are asked
|
|
2993
|
+
// again first, so the numbers are new and not the kept ones.
|
|
2994
|
+
function asking() { return !BOOT.readOnly && S.state && (S.state.quotaChecks || []).some((c) => c.on && !c.blocked) }
|
|
2995
|
+
async function refreshAll() {
|
|
2996
|
+
if (asking()) {
|
|
2997
|
+
const btn = $('refresh')
|
|
2998
|
+
btn.disabled = true
|
|
2999
|
+
btn.classList.add('is-spinning')
|
|
3000
|
+
try { await api('/api/quota/refresh', {}) } catch (e) { toast(t('quota.refreshFailed', { msg: e.message }), { kind: 'fail' }) }
|
|
3001
|
+
btn.classList.remove('is-spinning')
|
|
3002
|
+
btn.disabled = false
|
|
3003
|
+
}
|
|
3004
|
+
load({ check: true, announce: true })
|
|
3005
|
+
}
|
|
3006
|
+
function checkOf(fam) { return S.state && (S.state.quotaChecks || []).find((c) => c.family === fam) }
|
|
3007
|
+
async function setCheck(fam, on) {
|
|
3008
|
+
try {
|
|
3009
|
+
await api('/api/quota/check', { family: fam, on })
|
|
3010
|
+
toast(t(on ? 'quota.checkOnToast' : 'quota.checkOffToast', { host: famTitleOf(fam) }))
|
|
3011
|
+
} catch (e) { toast(t('quota.checkFailed', { msg: e.message }), { kind: 'fail' }) }
|
|
3012
|
+
load({ silent: true })
|
|
3013
|
+
}
|
|
3014
|
+
function famTitleOf(fam) { const f = S.state && S.state.families.find((x) => x.id === fam); return f ? f.title : fam }
|
|
2845
3015
|
// ---------- settings ----------
|
|
2846
3016
|
// One header button holds what is set once and rarely changed: language, theme, Remote Control and Auto Handoff.
|
|
2847
3017
|
// A dot on it says Remote Control starts by itself, so that state stays visible after its banner is gone.
|
|
@@ -2851,6 +3021,7 @@ const MAIN = String.raw `
|
|
|
2851
3021
|
if (BOOT.colorScheme === 'auto') out.push('theme')
|
|
2852
3022
|
if (S.state && rcSummary()) out.push('rc')
|
|
2853
3023
|
if (S.state && ahState()) out.push('ah')
|
|
3024
|
+
if (S.state && (S.state.quotaChecks || []).length) out.push('checks')
|
|
2854
3025
|
return out
|
|
2855
3026
|
}
|
|
2856
3027
|
function renderSettingsButton() {
|
|
@@ -2882,6 +3053,18 @@ const MAIN = String.raw `
|
|
|
2882
3053
|
const on = ahState().settings.auto
|
|
2883
3054
|
out.push(link(t('ah.button'), on ? t('ah.on') : t('ah.off'), on ? 'chip-ok' : '', () => setPage('handoff')))
|
|
2884
3055
|
}
|
|
3056
|
+
// Quota checks that ask the Host (spec §3.26): one switch per Family, with what it does in plain words.
|
|
3057
|
+
if (secs.includes('checks')) {
|
|
3058
|
+
if (out.length) out.push(h('div', { class: 'menu-sep', role: 'separator' }))
|
|
3059
|
+
for (const c of S.state.quotaChecks) {
|
|
3060
|
+
const label = t('set.quotaCheck', { host: c.title })
|
|
3061
|
+
out.push(h('div', { class: 'set-row set-check' },
|
|
3062
|
+
h('div', { class: 'set-text' }, h('span', { class: 'set-label' }, label), h('span', { class: 'set-hint' }, c.blocked ? c.blocked : t('set.quotaCheckHint', { host: c.title, n: c.everyMinutes }))),
|
|
3063
|
+
BOOT.readOnly ? h('span', { class: 'chip chip-sm' + (c.on ? ' chip-ok' : '') }, c.on ? t('ah.on') : t('ah.off'))
|
|
3064
|
+
: h('div', { class: 'seg', role: 'group', 'aria-label': label }, [[true, t('ah.on')], [false, t('ah.off')]].map(([v, text]) =>
|
|
3065
|
+
h('button', { class: 'seg-btn', type: 'button', 'aria-pressed': String(v === c.on), onclick: () => { if (v !== c.on) { closeMenu(); setCheck(c.family, v) } } }, text)))))
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
2885
3068
|
return out
|
|
2886
3069
|
}
|
|
2887
3070
|
function setLang(v) {
|
|
@@ -3198,6 +3381,14 @@ const MAIN = String.raw `
|
|
|
3198
3381
|
up.autoBusy = false
|
|
3199
3382
|
renderUpdate()
|
|
3200
3383
|
}
|
|
3384
|
+
// The installed sideby skill follows the CLI (spec §3.23): said before the Update, or what to run when sideby may not.
|
|
3385
|
+
function updSkill(u) {
|
|
3386
|
+
const s = u.skillUpdate
|
|
3387
|
+
if (!s) return [h('p', { class: 'small' }, t('upd.skill')), codeCopy(u.skill)]
|
|
3388
|
+
if (!s.outdated) return []
|
|
3389
|
+
if (s.canRun) return [h('p', { class: 'muted small' }, t('upd.skillToo'))]
|
|
3390
|
+
return [h('p', { class: 'small' }, t('upd.skillManaged', { why: s.reason || '' })), codeCopy(s.command)]
|
|
3391
|
+
}
|
|
3201
3392
|
function renderUpdate() {
|
|
3202
3393
|
if (!up || dlg.kind !== 'update') return
|
|
3203
3394
|
const u = updState()
|
|
@@ -3211,7 +3402,8 @@ const MAIN = String.raw `
|
|
|
3211
3402
|
if (up.done) {
|
|
3212
3403
|
body.push(h('p', { class: 'note' }, t('upd.done', { v: up.done.latest })))
|
|
3213
3404
|
body.push(h('p', { class: 'muted small' }, Date.now() > up.waitUntil ? t('upd.restartHint') : [spinner(), ' ', t('upd.reloading')]))
|
|
3214
|
-
body.push(h('p', { class: 'small' }, t('upd.
|
|
3405
|
+
if (up.done.skillUpdated) body.push(h('p', { class: 'small' }, t('upd.skillDone')))
|
|
3406
|
+
else body.push(...updSkill(u))
|
|
3215
3407
|
} else if (u.available) {
|
|
3216
3408
|
if (up.changed) body.push(h('p', { class: 'note note-warn' }, t('upd.changed', { v: u.latest })))
|
|
3217
3409
|
body.push(h('p', null, h('b', null, t('upd.available', { v: u.latest })), ' ',
|
|
@@ -3223,7 +3415,7 @@ const MAIN = String.raw `
|
|
|
3223
3415
|
body.push(h('p', { class: 'note note-warn' }, t('upd.cannot', { why: u.method.reason || u.method.kind })))
|
|
3224
3416
|
body.push(codeCopy(u.command))
|
|
3225
3417
|
}
|
|
3226
|
-
body.push(
|
|
3418
|
+
body.push(...updSkill(u))
|
|
3227
3419
|
} else if (up.checked && u.latest) {
|
|
3228
3420
|
body.push(h('p', { class: 'note' }, t('upd.latestNow')))
|
|
3229
3421
|
} else if (u.checksOff) {
|
|
@@ -3258,7 +3450,9 @@ const MAIN = String.raw `
|
|
|
3258
3450
|
$('ver').addEventListener('click', () => openVerPop($('ver')))
|
|
3259
3451
|
$('update').addEventListener('click', openUpdate)
|
|
3260
3452
|
$('ver-top').addEventListener('click', () => openVerPop($('ver-top')))
|
|
3261
|
-
$('refresh').addEventListener('click',
|
|
3453
|
+
$('refresh').addEventListener('click', refreshAll)
|
|
3454
|
+
$('accounts').addEventListener('dragover', onDragOver)
|
|
3455
|
+
$('accounts').addEventListener('drop', onDrop)
|
|
3262
3456
|
$('settings').addEventListener('click', () => openPopover($('settings'), 'settings', t('set.button'), settingsPop, 'end'))
|
|
3263
3457
|
applyTheme()
|
|
3264
3458
|
renderStatic()
|