@modelstatus/cli 0.1.76 → 0.1.78
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/package.json +1 -1
- package/src/index.js +15 -2
- package/src/telemetry.js +13 -8
- package/src/tui/app.js +59 -3
- package/src/tui/feedback.js +121 -0
- package/src/tui/views/account.js +4 -2
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ The free CLI + TUI for [LLM Status](https://llmstatus.ai) — scans your repo fo
|
|
|
8
8
|
npx @modelstatus/cli status
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
That's it. No sign-in, no account, and the scan runs entirely on your machine — you get a snapshot of every model in your repo plus health badges and replacement suggestions. (
|
|
11
|
+
That's it. No sign-in, no account, and the scan runs entirely on your machine — you get a snapshot of every model in your repo plus health badges and replacement suggestions. (Anonymous usage analytics — event names + counts only, never code, model names, or paths — can be turned off anytime: `mm analytics off`, or `MM_NO_ANALYTICS=1`.)
|
|
12
12
|
|
|
13
13
|
## Install
|
|
14
14
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modelstatus/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.78",
|
|
4
4
|
"description": "Track which AI models you use, where, and never get surprised by a retirement. Free offline model-health for any repo (mm status), browser sign-in for cloud inventory + alerts.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm",
|
package/src/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import { redactValue } from "./redact.js";
|
|
|
14
14
|
import { assignProjects, buildUsages } from "./upload.js";
|
|
15
15
|
import { loginViaBrowser } from "./auth.js";
|
|
16
16
|
import { maybeCheckForUpdate, forceUpdate } from "./updater.js";
|
|
17
|
-
import { track,
|
|
17
|
+
import { track, analyticsState } from "./telemetry.js";
|
|
18
18
|
import { BUILD_VERSION } from "./version.js";
|
|
19
19
|
|
|
20
20
|
// TRUE BACKGROUND SCAN — hidden worker dispatch. MUST be the first executable
|
|
@@ -763,6 +763,7 @@ Usage:
|
|
|
763
763
|
mm login [api_key] Browser sign-in with polling (or paste a key)
|
|
764
764
|
mm signup Create an account in the browser, then poll
|
|
765
765
|
mm logout Forget the saved API key
|
|
766
|
+
mm analytics [on|off] Anonymous usage stats (event counts only — never code or paths)
|
|
766
767
|
mm scan [dir] Scan for model usage; interactive TUI, or --ci/--json for pipelines
|
|
767
768
|
mm fix [dir] Rewrite dying model ids to their replacement, in place (--dry-run previews; --model <slug> limits; --yes skips the confirm)
|
|
768
769
|
mm ci [dir] CI gate: fail the build on deprecated/retiring models (GitHub annotations)
|
|
@@ -853,7 +854,6 @@ async function main() {
|
|
|
853
854
|
|
|
854
855
|
// Anonymous, opt-out usage analytics (one-time disclosure, then a single
|
|
855
856
|
// event per invocation). No-op without a baked key / when opted out.
|
|
856
|
-
maybeAnalyticsNotice();
|
|
857
857
|
track("cli_command", { command: cmd || "tui" });
|
|
858
858
|
|
|
859
859
|
// Explicit self-update: `mm update` (command) or `--update` (flag on any
|
|
@@ -878,6 +878,19 @@ async function main() {
|
|
|
878
878
|
if (cmd === "login") await cmdLogin(positional, flags);
|
|
879
879
|
else if (cmd === "signup") await cmdSignup(positional, flags);
|
|
880
880
|
else if (cmd === "logout") cmdLogout();
|
|
881
|
+
else if (cmd === "analytics") {
|
|
882
|
+
const arg = (positional[1] || "").toLowerCase();
|
|
883
|
+
if (arg === "on" || arg === "off") {
|
|
884
|
+
const { setConfigValue } = await import("./config.js");
|
|
885
|
+
setConfigValue("analyticsOptOut", arg === "off");
|
|
886
|
+
console.log(`Anonymous usage analytics ${arg}.`);
|
|
887
|
+
} else {
|
|
888
|
+
const st = analyticsState();
|
|
889
|
+
console.log(`Anonymous usage analytics: ${st.on ? "on" : `off${st.reason ? ` — ${st.reason}` : ""}`}`);
|
|
890
|
+
console.log(" When on, mm sends event names + counts only — never code, model names, or paths.");
|
|
891
|
+
console.log(" Toggle: mm analytics on|off · also honored: MM_NO_ANALYTICS=1, DO_NOT_TRACK=1, CI=1");
|
|
892
|
+
}
|
|
893
|
+
}
|
|
881
894
|
else if (cmd === "scan") await cmdScan(positional, flags);
|
|
882
895
|
else if (cmd === "fix") await cmdFix(positional, flags);
|
|
883
896
|
else if (cmd === "ci") await cmdCi(positional, flags);
|
package/src/telemetry.js
CHANGED
|
@@ -14,7 +14,10 @@ import { BUILD_VERSION, UPDATE_CHANNEL } from "./version.js";
|
|
|
14
14
|
const POSTHOG_KEY = typeof __POSTHOG_KEY__ !== "undefined" ? __POSTHOG_KEY__ : (process.env.MM_POSTHOG_KEY || "");
|
|
15
15
|
const POSTHOG_HOST = (process.env.MM_POSTHOG_HOST || "https://us.i.posthog.com").replace(/\/$/, "");
|
|
16
16
|
|
|
17
|
-
const optedOut = () =>
|
|
17
|
+
const optedOut = () => {
|
|
18
|
+
if (process.env.MM_NO_ANALYTICS || process.env.DO_NOT_TRACK || process.env.CI) return true;
|
|
19
|
+
try { return !!loadConfig().analyticsOptOut; } catch { return false; }
|
|
20
|
+
};
|
|
18
21
|
const enabled = () => !!POSTHOG_KEY && !optedOut();
|
|
19
22
|
|
|
20
23
|
let cachedId = null;
|
|
@@ -27,13 +30,15 @@ function distinctId() {
|
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
/** One-time, pre-TUI stderr disclosure. Honors opt-out + only shows once. */
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
process.
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
/** Current analytics state + why — powers `mm analytics` and the Account tab.
|
|
34
|
+
* Disclosure lives there + README/docs/--help (not as a banner interrupting
|
|
35
|
+
* scan output). */
|
|
36
|
+
export function analyticsState() {
|
|
37
|
+
if (process.env.MM_NO_ANALYTICS || process.env.DO_NOT_TRACK || process.env.CI) {
|
|
38
|
+
return { on: false, reason: "env (MM_NO_ANALYTICS / DO_NOT_TRACK / CI)" };
|
|
39
|
+
}
|
|
40
|
+
try { if (loadConfig().analyticsOptOut) return { on: false, reason: "mm analytics off" }; } catch { /* ignore */ }
|
|
41
|
+
return { on: !!POSTHOG_KEY, reason: POSTHOG_KEY ? "" : "no key baked in" };
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
/** Fire-and-forget capture. Never throws, never blocks the CLI meaningfully. */
|
package/src/tui/app.js
CHANGED
|
@@ -4,6 +4,7 @@ import { render, Box, Text, useInput, useApp, useStdout } from "ink";
|
|
|
4
4
|
import { createClient } from "../api.js";
|
|
5
5
|
import { track } from "../telemetry.js";
|
|
6
6
|
import { BUILD_VERSION } from "../version.js";
|
|
7
|
+
import { feedbackEligible, recordFeedbackShown, recordFeedbackSubmitted, submitFeedback, FeedbackStrip, FeedbackCard, ACTION_THRESHOLD } from "./feedback.js";
|
|
7
8
|
import {
|
|
8
9
|
h, C, GLYPH, useAsync, Window, TrafficLights, TabStrip, StatusBar, legendSegments,
|
|
9
10
|
KeyBar, EmptyCard,
|
|
@@ -146,6 +147,12 @@ export function App({ apiBase, apiKey, dir, initialView, onSignedIn, fresh }) {
|
|
|
146
147
|
// changes (reset below). Keeps the keybar honest for tabbed views (What's New,
|
|
147
148
|
// Alerts) where the active keys depend on which sub-tab you're on.
|
|
148
149
|
const [dynKeys, setDynKeys] = React.useState(null);
|
|
150
|
+
// Feedback: null | {stage:"strip"} | {stage:"card", score, comment, sending}.
|
|
151
|
+
// The strip auto-shows once ACTION_THRESHOLD real actions happen (30-day
|
|
152
|
+
// cooldown, never after a submit); `!` opens the card from anywhere, always.
|
|
153
|
+
const [feedback, setFeedback] = React.useState(null);
|
|
154
|
+
const actionsRef = React.useRef(0);
|
|
155
|
+
const fbEligibleRef = React.useRef(feedbackEligible());
|
|
149
156
|
const me = useAsync(async () => (apiKey ? client.me() : null), [apiKey]);
|
|
150
157
|
|
|
151
158
|
const { outer, termRows } = useTermDims();
|
|
@@ -187,6 +194,21 @@ export function App({ apiBase, apiKey, dir, initialView, onSignedIn, fresh }) {
|
|
|
187
194
|
useInput(
|
|
188
195
|
(input, key) => {
|
|
189
196
|
if (key.ctrl && input === "c") return exit();
|
|
197
|
+
if (input === "!") {
|
|
198
|
+
setFeedback({ stage: "card", score: null, comment: "", sending: false });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
// Count "real" actions (letter/symbol keys that do work — not tab digits,
|
|
202
|
+
// not bare navigation, not q). Arrows/enter arrive with empty input, so
|
|
203
|
+
// pure browsing never trips the ask.
|
|
204
|
+
if (fbEligibleRef.current && !feedback && input && !key.ctrl && !key.meta && !/^[0-9]$/.test(input) && input !== "q") {
|
|
205
|
+
actionsRef.current += 1;
|
|
206
|
+
if (actionsRef.current >= ACTION_THRESHOLD) {
|
|
207
|
+
fbEligibleRef.current = false;
|
|
208
|
+
recordFeedbackShown();
|
|
209
|
+
setFeedback({ stage: "strip" });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
190
212
|
// Ctrl-L: hard redraw. Clears screen + scrollback, then bumps `redraw` so
|
|
191
213
|
// the frame string changes (ink skips writing identical output) and ink
|
|
192
214
|
// repaints from a clean top. Fixes a display knocked askew by a resize /
|
|
@@ -219,7 +241,37 @@ export function App({ apiBase, apiKey, dir, initialView, onSignedIn, fresh }) {
|
|
|
219
241
|
}
|
|
220
242
|
if (input === "q") return exit();
|
|
221
243
|
},
|
|
222
|
-
{ isActive: !prompt && !capturing },
|
|
244
|
+
{ isActive: !prompt && !capturing && feedback?.stage !== "card" },
|
|
245
|
+
);
|
|
246
|
+
// Feedback card is modal: digits score it (until a comment starts), printable
|
|
247
|
+
// chars build the comment, ←→ nudges the score, ↵ sends, esc closes.
|
|
248
|
+
useInput(
|
|
249
|
+
(input, key) => {
|
|
250
|
+
const fb = feedback;
|
|
251
|
+
if (!fb || fb.sending) return;
|
|
252
|
+
if (key.escape) return setFeedback(null);
|
|
253
|
+
if (key.return) {
|
|
254
|
+
if (!fb.score) return showToast("pick a score first (1-5)", "#d97706");
|
|
255
|
+
setFeedback({ ...fb, sending: true });
|
|
256
|
+
submitFeedback({ apiBase, score: fb.score, comment: fb.comment }).then((ok) => {
|
|
257
|
+
if (ok) {
|
|
258
|
+
recordFeedbackSubmitted();
|
|
259
|
+
setFeedback(null);
|
|
260
|
+
showToast("🙏 thanks — feedback sent to the humans");
|
|
261
|
+
} else {
|
|
262
|
+
setFeedback((f) => (f ? { ...f, sending: false } : f));
|
|
263
|
+
showToast("couldn't send feedback — check your connection, try again", "#dc2626");
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (key.leftArrow) return setFeedback({ ...fb, score: Math.max(1, (fb.score || 4) - 1) });
|
|
269
|
+
if (key.rightArrow) return setFeedback({ ...fb, score: Math.min(5, (fb.score || 2) + 1) });
|
|
270
|
+
if (key.backspace || key.delete) return setFeedback({ ...fb, comment: fb.comment.slice(0, -1) });
|
|
271
|
+
if (/^[1-5]$/.test(input) && !fb.comment) return setFeedback({ ...fb, score: Number(input) });
|
|
272
|
+
if (input && !key.ctrl && !key.meta) return setFeedback({ ...fb, comment: (fb.comment + input).slice(0, 500) });
|
|
273
|
+
},
|
|
274
|
+
{ isActive: feedback?.stage === "card" },
|
|
223
275
|
);
|
|
224
276
|
useInput(
|
|
225
277
|
(input, key) => {
|
|
@@ -257,9 +309,13 @@ export function App({ apiBase, apiKey, dir, initialView, onSignedIn, fresh }) {
|
|
|
257
309
|
});
|
|
258
310
|
keys = GATE_KEYS;
|
|
259
311
|
} else {
|
|
260
|
-
body = h(View, { client, me: account, refreshMe: me.reload, dir, apiBase, ui, width: W, height: bodyRows, active: !prompt, fresh });
|
|
312
|
+
body = h(View, { client, me: account, refreshMe: me.reload, dir, apiBase, ui, width: W, height: bodyRows, active: !prompt && feedback?.stage !== "card", fresh });
|
|
261
313
|
keys = dynKeys || (current2.meta && current2.meta.keys) || [];
|
|
262
314
|
}
|
|
315
|
+
if (feedback?.stage === "card") {
|
|
316
|
+
body = h(FeedbackCard, { score: feedback.score, comment: feedback.comment, sending: feedback.sending, width: W });
|
|
317
|
+
keys = [{ k: "1-5", label: "score" }, { k: "↵", label: "send" }, { k: "esc", label: "close" }];
|
|
318
|
+
}
|
|
263
319
|
|
|
264
320
|
// Status bar segments.
|
|
265
321
|
const live = status && status.forKey === current2.key ? status : null;
|
|
@@ -282,7 +338,7 @@ export function App({ apiBase, apiKey, dir, initialView, onSignedIn, fresh }) {
|
|
|
282
338
|
h(Text, {}, ""),
|
|
283
339
|
h(Box, { flexDirection: "column", width: W, minHeight: bodyRows }, body),
|
|
284
340
|
h(PromptRow, { prompt }),
|
|
285
|
-
h(Text, {}, ""),
|
|
341
|
+
feedback?.stage === "strip" ? h(FeedbackStrip, null) : h(Text, {}, ""),
|
|
286
342
|
toast ? h(Text, { color: toast.color }, ` ${toast.msg}`) : h(Text, {}, ""),
|
|
287
343
|
h(StatusBar, { segsLeft, segsRight, width: W }),
|
|
288
344
|
h(KeyBar, { keys: keys.some((k) => k.k === "P") ? keys : [...keys, { k: "P", label: "play 🦍" }], width: W }),
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/* In-TUI feedback: a quiet "♥ enjoying mm? press ! to rate it" strip appears
|
|
2
|
+
* once the user has taken a handful of real actions, and `!` (always available
|
|
3
|
+
* on the Account tab too) opens a modal card — score 1–5 (1 = hot garbage,
|
|
4
|
+
* 5 = best thing since sliced bread) + optional comment. Submission goes to
|
|
5
|
+
* POST /api/v1/feedback on our API (works signed-out); the SERVER forwards to
|
|
6
|
+
* PostHog + the ops Slack channel — the public binary never holds a secret.
|
|
7
|
+
*
|
|
8
|
+
* Nag policy (user-chosen): auto-show at most once per 30 days; submitting
|
|
9
|
+
* means never auto-show again. The `!` key keeps working regardless.
|
|
10
|
+
*/
|
|
11
|
+
import React from "react";
|
|
12
|
+
import { Box, Text } from "ink";
|
|
13
|
+
import { h, C } from "./ui.js";
|
|
14
|
+
import { loadConfig, setConfigValue } from "../config.js";
|
|
15
|
+
import { BUILD_VERSION } from "../version.js";
|
|
16
|
+
|
|
17
|
+
const COOLDOWN_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
18
|
+
export const ACTION_THRESHOLD = 5; // strip appears after this many real actions
|
|
19
|
+
|
|
20
|
+
/** Should the auto-strip show this session? (The `!` key is always allowed.) */
|
|
21
|
+
export function feedbackEligible(cfg = loadConfig(), now = Date.now()) {
|
|
22
|
+
const f = cfg.feedback || {};
|
|
23
|
+
if (f.submittedAt) return false; // they already told us — never auto-nag again
|
|
24
|
+
if (f.shownAt && now - f.shownAt < COOLDOWN_MS) return false;
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Record that the strip was shown (ignoring it counts as dismissal). */
|
|
29
|
+
export function recordFeedbackShown(now = Date.now()) {
|
|
30
|
+
try {
|
|
31
|
+
setConfigValue("feedback", { ...(loadConfig().feedback || {}), shownAt: now });
|
|
32
|
+
} catch { /* best effort */ }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function recordFeedbackSubmitted(now = Date.now()) {
|
|
36
|
+
try {
|
|
37
|
+
setConfigValue("feedback", { ...(loadConfig().feedback || {}), submittedAt: now });
|
|
38
|
+
} catch { /* best effort */ }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** POST to our API. Returns true on success; never throws. */
|
|
42
|
+
export async function submitFeedback({ apiBase, score, comment }) {
|
|
43
|
+
try {
|
|
44
|
+
const cfg = loadConfig();
|
|
45
|
+
const res = await fetch(`${(apiBase || "https://llmstatus.ai").replace(/\/$/, "")}/api/v1/feedback`, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: { "content-type": "application/json" },
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
score,
|
|
50
|
+
thumbs: score >= 4 ? "up" : score <= 2 ? "down" : undefined,
|
|
51
|
+
comment: comment || undefined,
|
|
52
|
+
version: typeof BUILD_VERSION === "string" ? BUILD_VERSION : "",
|
|
53
|
+
platform: `${process.platform}-${process.arch}`,
|
|
54
|
+
distinct_id: cfg.anonId || undefined,
|
|
55
|
+
}),
|
|
56
|
+
signal: AbortSignal.timeout(5_000),
|
|
57
|
+
});
|
|
58
|
+
return res.ok;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const SCORE_LABELS = {
|
|
65
|
+
1: "hot garbage",
|
|
66
|
+
2: "rough",
|
|
67
|
+
3: "it's fine",
|
|
68
|
+
4: "really good",
|
|
69
|
+
5: "best thing since sliced bread",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** The one-line invite, rendered by app.js above the keybar. */
|
|
73
|
+
export function FeedbackStrip() {
|
|
74
|
+
return h(
|
|
75
|
+
Text,
|
|
76
|
+
{},
|
|
77
|
+
h(Text, { color: "#fb7185" }, " ♥ "),
|
|
78
|
+
h(Text, { color: C.FG }, "enjoying mm? "),
|
|
79
|
+
h(Text, { color: C.ACCENT, bold: true }, "press ! to rate it"),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The modal card. Pure render — app.js owns the state + key handling. */
|
|
84
|
+
export function FeedbackCard({ score, comment, sending, width = 78 }) {
|
|
85
|
+
const cells = [];
|
|
86
|
+
for (let n = 1; n <= 5; n++) {
|
|
87
|
+
const active = n === score;
|
|
88
|
+
cells.push(
|
|
89
|
+
h(
|
|
90
|
+
Text,
|
|
91
|
+
{ key: `s${n}` },
|
|
92
|
+
h(Text, { color: active ? C.ACCENT : C.FG_DIM, bold: active }, active ? ` ▸${n}◂ ` : ` ${n} `),
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const label = score ? SCORE_LABELS[score] : "pick a number";
|
|
97
|
+
return h(
|
|
98
|
+
Box,
|
|
99
|
+
{ flexDirection: "column", paddingX: 1 },
|
|
100
|
+
h(Text, {}, h(Text, { color: C.ACCENT, bold: true }, " ✦ feedback "), h(Text, { color: C.FG_DIM }, "— 30 seconds, straight to the humans")),
|
|
101
|
+
h(Text, {}, ""),
|
|
102
|
+
h(Text, { color: C.FG }, " How do you like mm? (1 = hot garbage · 5 = best thing since sliced bread)"),
|
|
103
|
+
h(Text, {}, ""),
|
|
104
|
+
h(Box, {}, h(Text, {}, " "), ...cells, h(Text, { color: C.FG_DIM }, ` ${label}`)),
|
|
105
|
+
h(Text, {}, ""),
|
|
106
|
+
h(Text, { color: C.FG }, " anything we should know? (optional)"),
|
|
107
|
+
h(
|
|
108
|
+
Text,
|
|
109
|
+
{},
|
|
110
|
+
h(Text, { color: C.FG_DIM }, " │ "),
|
|
111
|
+
h(Text, { color: C.FG }, comment || ""),
|
|
112
|
+
h(Text, { color: C.ACCENT }, "▌"),
|
|
113
|
+
),
|
|
114
|
+
h(Text, {}, ""),
|
|
115
|
+
h(
|
|
116
|
+
Text,
|
|
117
|
+
{ color: C.FG_DIM },
|
|
118
|
+
sending ? " sending…" : " ←→ or 1-5 score · type to comment · ↵ send · esc close",
|
|
119
|
+
),
|
|
120
|
+
);
|
|
121
|
+
}
|
package/src/tui/views/account.js
CHANGED
|
@@ -2,20 +2,21 @@ import React from "react";
|
|
|
2
2
|
import { Box, Text, useInput } from "ink";
|
|
3
3
|
import { h, C, GLYPH } from "../ui.js";
|
|
4
4
|
import { loadConfig, configFilePath } from "../../config.js";
|
|
5
|
+
import { analyticsState } from "../../telemetry.js";
|
|
5
6
|
import { openUrl } from "../../openUrl.js";
|
|
6
7
|
|
|
7
8
|
const FREE_LIMITS = { projects: 1, usages: 15, channels: 1 };
|
|
8
9
|
// Background paint only when colors are on (mirrors ui.js BG_ON gate; ui handles the rest).
|
|
9
10
|
const BG_ON = process.env.MM_ASCII !== "1" && process.env.TERM !== "dumb" && process.env.NO_COLOR == null;
|
|
10
11
|
|
|
11
|
-
export const meta = { keys: [{ k: "u", label: "upgrade" }, { k: "g", label: "refresh" }] };
|
|
12
|
+
export const meta = { keys: [{ k: "u", label: "upgrade" }, { k: "g", label: "refresh" }, { k: "!", label: "feedback" }] };
|
|
12
13
|
|
|
13
14
|
/** label/value row: padded accent label + value. */
|
|
14
15
|
function Row({ label, value, color = C.FG, bold = false }) {
|
|
15
16
|
return h(
|
|
16
17
|
Text,
|
|
17
18
|
{},
|
|
18
|
-
h(Text, { color: C.ACCENT }, label.padEnd(
|
|
19
|
+
h(Text, { color: C.ACCENT }, label.padEnd(10)),
|
|
19
20
|
h(Text, { color, bold }, value),
|
|
20
21
|
);
|
|
21
22
|
}
|
|
@@ -98,6 +99,7 @@ export function AccountView({ client, me, refreshMe, apiBase, ui, active }) {
|
|
|
98
99
|
h(Row, { label: "retiring", value: `${me?.retiring_window_days ?? 90} day window` }),
|
|
99
100
|
h(Row, { label: "endpoint", value: endpoint }),
|
|
100
101
|
h(Row, { label: "key", value: keyPrefix }),
|
|
102
|
+
h(Row, { label: "analytics", value: analyticsState().on ? "on · anonymous event counts only · mm analytics off" : "off", color: C.FG_FAINT }),
|
|
101
103
|
h(Row, { label: "config", value: configFilePath, color: C.FG_FAINT }),
|
|
102
104
|
),
|
|
103
105
|
h(Text, {}, ""),
|