@omg-dev/admin 0.4.29 → 0.4.31
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 +2 -4
- package/dist/index.mjs +2 -2
- package/dist/{react-BNHn5Pe0.mjs → react-6ZXjCUwb.mjs} +10 -438
- package/dist/server.mjs +0 -1
- package/dist/standalone.mjs +1 -1
- package/package.json +1 -1
- package/src/client.ts +0 -81
- package/src/index.ts +1 -7
- package/src/react.tsx +8 -306
- package/src/safe-url.ts +2 -2
- package/src/server.ts +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @omg-dev/admin
|
|
2
2
|
|
|
3
3
|
Embeddable internal admin console — a panel-registry React component
|
|
4
|
-
(**Feature Flags**, **Users**,
|
|
4
|
+
(**Feature Flags**, **Users**, and read-only **Pricing/Billing**)
|
|
5
5
|
that drops into any host as a single `<AdminConsole/>`. Host-agnostic: you pass
|
|
6
6
|
it where the control-plane lives and how to mint a bearer token; it does the
|
|
7
7
|
rest. Self-styled (no Tailwind dependency), and every endpoint it calls is
|
|
@@ -56,7 +56,7 @@ if (res) return res; // null = not our path; fall through
|
|
|
56
56
|
<AdminConsole config={{ apiBase: "/_omg", getToken: () => null }} />
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
The proxy only forwards `/api/flags/*`, `/api/users/*`,
|
|
59
|
+
The proxy only forwards `/api/flags/*`, `/api/users/*`,
|
|
60
60
|
and `/api/adminBilling/*` (the admin surface), so a leaked mount can't wield the
|
|
61
61
|
token elsewhere. `OMG_ADMIN_TOKEN` is an `auth.omg.dev` JWT for an allowlisted
|
|
62
62
|
admin — rotate it in server env, no client rebuild. Options:
|
|
@@ -108,7 +108,6 @@ mountAdminConsole(document.getElementById("root")!, {
|
|
|
108
108
|
per-plan rule → rollout % (stable hash) → global default.
|
|
109
109
|
- **Users** (read-only) — searchable account list with sessions, app counts,
|
|
110
110
|
flag override counts, and on-demand billing balance for the selected user.
|
|
111
|
-
- **Reports** (triage) — user-submitted bug reports with recovery context.
|
|
112
111
|
- **Pricing** (read-only) — live plan→Stripe projection + the model catalog.
|
|
113
112
|
Pricing is code-defined (`defineBilling`) and reconciled by the orchestrator;
|
|
114
113
|
this is a window, not an editor.
|
|
@@ -119,7 +118,6 @@ Talks to the control-plane custom functions:
|
|
|
119
118
|
|
|
120
119
|
- `POST /api/flags/{listFlags,upsert,setArchived,removeFlag,listOverrides,setOverride,removeOverride,findUsers,evaluate}`
|
|
121
120
|
- `POST /api/users/{listUsers,findUsers}`
|
|
122
|
-
- `POST /api/bugReports/{listReports,getReport,setStatus,addNote,removeReport}`
|
|
123
121
|
- `POST /api/adminBilling/{planVersions,models,userBalance}`
|
|
124
122
|
|
|
125
123
|
`flags.evaluate` is the consumer-facing endpoint (non-admin = self only) that
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export { AdminClient, AdminConsole, AdminError, BrainRoutingPanel, FlagsPanel, PricingPanel,
|
|
1
|
+
import { a as UsersPanel, c as STYLE_ID, i as PricingPanel, l as ensureStyles, n as BrainRoutingPanel, o as AdminClient, r as FlagsPanel, s as AdminError, t as AdminConsole } from "./react-6ZXjCUwb.mjs";
|
|
2
|
+
export { AdminClient, AdminConsole, AdminError, BrainRoutingPanel, FlagsPanel, PricingPanel, STYLE_ID, UsersPanel, ensureStyles };
|
|
@@ -443,8 +443,8 @@ a.vadm-flow-node:hover { border-color: var(--vadm-accent); }
|
|
|
443
443
|
`;
|
|
444
444
|
//#endregion
|
|
445
445
|
//#region src/safe-url.ts
|
|
446
|
-
/** URLs in the admin console can originate from
|
|
447
|
-
*
|
|
446
|
+
/** URLs in the admin console can originate from user-controlled Computers.
|
|
447
|
+
* Only ordinary web links may become clickable operator actions. */
|
|
448
448
|
function safeHref(value) {
|
|
449
449
|
if (typeof value !== "string") return void 0;
|
|
450
450
|
try {
|
|
@@ -537,37 +537,6 @@ var AdminClient = class {
|
|
|
537
537
|
userBalance(userId) {
|
|
538
538
|
return this.rpc("adminBilling", "userBalance", { userId });
|
|
539
539
|
}
|
|
540
|
-
listReports(args) {
|
|
541
|
-
return this.rpc("bugReports", "listReports", args ?? {});
|
|
542
|
-
}
|
|
543
|
-
getReport(id) {
|
|
544
|
-
return this.rpc("bugReports", "getReport", { id });
|
|
545
|
-
}
|
|
546
|
-
setReportStatus(id, status, assignee) {
|
|
547
|
-
return this.rpc("bugReports", "setStatus", {
|
|
548
|
-
id,
|
|
549
|
-
status,
|
|
550
|
-
assignee
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
addReportNote(id, note) {
|
|
554
|
-
return this.rpc("bugReports", "addNote", {
|
|
555
|
-
id,
|
|
556
|
-
note
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
setReportCluster(id, clusterKey) {
|
|
560
|
-
return this.rpc("bugReports", "setCluster", {
|
|
561
|
-
id,
|
|
562
|
-
clusterKey
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
|
-
listClusters() {
|
|
566
|
-
return this.rpc("bugReports", "listClusters", {});
|
|
567
|
-
}
|
|
568
|
-
removeReport(id) {
|
|
569
|
-
return this.rpc("bugReports", "removeReport", { id });
|
|
570
|
-
}
|
|
571
540
|
listImessageBrainRouting(args) {
|
|
572
541
|
return this.rpc("imessageDebug", "listBrainRouting", args ?? {});
|
|
573
542
|
}
|
|
@@ -1230,6 +1199,13 @@ function PricingPanel({ client }) {
|
|
|
1230
1199
|
})
|
|
1231
1200
|
] });
|
|
1232
1201
|
}
|
|
1202
|
+
function fmtTime(ms) {
|
|
1203
|
+
try {
|
|
1204
|
+
return new Date(ms).toLocaleString();
|
|
1205
|
+
} catch {
|
|
1206
|
+
return String(ms);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1233
1209
|
function UsersPanel({ client }) {
|
|
1234
1210
|
const [queryInput, setQueryInput] = useState("");
|
|
1235
1211
|
const [query, setQuery] = useState("");
|
|
@@ -1413,405 +1389,6 @@ function UsersPanel({ client }) {
|
|
|
1413
1389
|
})
|
|
1414
1390
|
] });
|
|
1415
1391
|
}
|
|
1416
|
-
const BUG_STATUSES = [
|
|
1417
|
-
"open",
|
|
1418
|
-
"triaged",
|
|
1419
|
-
"in_progress",
|
|
1420
|
-
"resolved",
|
|
1421
|
-
"wont_fix"
|
|
1422
|
-
];
|
|
1423
|
-
const STATUS_LABEL = {
|
|
1424
|
-
open: "Open",
|
|
1425
|
-
triaged: "Triaged",
|
|
1426
|
-
in_progress: "In progress",
|
|
1427
|
-
resolved: "Resolved",
|
|
1428
|
-
wont_fix: "Won't fix"
|
|
1429
|
-
};
|
|
1430
|
-
const OPEN_STATUSES = new Set([
|
|
1431
|
-
"open",
|
|
1432
|
-
"triaged",
|
|
1433
|
-
"in_progress"
|
|
1434
|
-
]);
|
|
1435
|
-
function fmtTime(ms) {
|
|
1436
|
-
try {
|
|
1437
|
-
return new Date(ms).toLocaleString();
|
|
1438
|
-
} catch {
|
|
1439
|
-
return String(ms);
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
const STORAGE_HOSTS = ["t3.storage.dev"];
|
|
1443
|
-
function safeImageSrc(v) {
|
|
1444
|
-
if (typeof v !== "string") return void 0;
|
|
1445
|
-
try {
|
|
1446
|
-
const u = new URL(v);
|
|
1447
|
-
if (u.protocol !== "http:" && u.protocol !== "https:") return void 0;
|
|
1448
|
-
const host = u.hostname.toLowerCase();
|
|
1449
|
-
return STORAGE_HOSTS.some((h) => host === h || host.endsWith("." + h)) ? v : void 0;
|
|
1450
|
-
} catch {
|
|
1451
|
-
return;
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
function ReportsPanel({ client }) {
|
|
1455
|
-
const [reports, setReports] = useState(null);
|
|
1456
|
-
const [counts, setCounts] = useState({});
|
|
1457
|
-
const [filter, setFilter] = useState("all");
|
|
1458
|
-
const [selectedId, setSelectedId] = useState(null);
|
|
1459
|
-
const [detail, setDetail] = useState(null);
|
|
1460
|
-
const [error, setError] = useState(null);
|
|
1461
|
-
const [busy, setBusy] = useState(false);
|
|
1462
|
-
const [note, setNote] = useState("");
|
|
1463
|
-
const load = useCallback(async () => {
|
|
1464
|
-
try {
|
|
1465
|
-
const res = await client.listReports(filter === "all" ? {} : { status: filter });
|
|
1466
|
-
setReports(res.reports);
|
|
1467
|
-
setCounts(res.counts);
|
|
1468
|
-
setError(null);
|
|
1469
|
-
} catch (e) {
|
|
1470
|
-
setError(e);
|
|
1471
|
-
}
|
|
1472
|
-
}, [client, filter]);
|
|
1473
|
-
useEffect(() => {
|
|
1474
|
-
ensureStyles();
|
|
1475
|
-
load();
|
|
1476
|
-
}, [load]);
|
|
1477
|
-
const openDetail = useCallback(async (id) => {
|
|
1478
|
-
setSelectedId(id);
|
|
1479
|
-
setDetail(null);
|
|
1480
|
-
try {
|
|
1481
|
-
const d = await client.getReport(id);
|
|
1482
|
-
setDetail(d);
|
|
1483
|
-
setNote(d?.adminNotes ?? "");
|
|
1484
|
-
} catch (e) {
|
|
1485
|
-
setError(e);
|
|
1486
|
-
}
|
|
1487
|
-
}, [client]);
|
|
1488
|
-
async function mutate(fn) {
|
|
1489
|
-
setBusy(true);
|
|
1490
|
-
try {
|
|
1491
|
-
await fn();
|
|
1492
|
-
await load();
|
|
1493
|
-
if (selectedId) await openDetail(selectedId);
|
|
1494
|
-
setError(null);
|
|
1495
|
-
} catch (e) {
|
|
1496
|
-
setError(e);
|
|
1497
|
-
} finally {
|
|
1498
|
-
setBusy(false);
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
|
-
const totalOpen = BUG_STATUSES.filter((s) => OPEN_STATUSES.has(s)).reduce((n, s) => n + (counts[s] ?? 0), 0);
|
|
1502
|
-
return /* @__PURE__ */ jsxs("div", { children: [
|
|
1503
|
-
/* @__PURE__ */ jsxs("p", {
|
|
1504
|
-
className: "vadm-sub",
|
|
1505
|
-
children: [
|
|
1506
|
-
"User-submitted bug reports. Each carries the context to ",
|
|
1507
|
-
/* @__PURE__ */ jsx("strong", { children: "recover" }),
|
|
1508
|
-
" — open or fork the exact app version the reporter saw. ",
|
|
1509
|
-
totalOpen,
|
|
1510
|
-
" open."
|
|
1511
|
-
]
|
|
1512
|
-
}),
|
|
1513
|
-
/* @__PURE__ */ jsx(ErrorBanner, { error }),
|
|
1514
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1515
|
-
className: "vadm-row",
|
|
1516
|
-
style: {
|
|
1517
|
-
marginBottom: 12,
|
|
1518
|
-
gap: 6,
|
|
1519
|
-
flexWrap: "wrap"
|
|
1520
|
-
},
|
|
1521
|
-
children: [/* @__PURE__ */ jsx("button", {
|
|
1522
|
-
className: "vadm-tab",
|
|
1523
|
-
"data-active": filter === "all",
|
|
1524
|
-
onClick: () => setFilter("all"),
|
|
1525
|
-
children: "All"
|
|
1526
|
-
}), BUG_STATUSES.map((s) => /* @__PURE__ */ jsxs("button", {
|
|
1527
|
-
className: "vadm-tab",
|
|
1528
|
-
"data-active": filter === s,
|
|
1529
|
-
onClick: () => setFilter(s),
|
|
1530
|
-
children: [
|
|
1531
|
-
STATUS_LABEL[s],
|
|
1532
|
-
" ",
|
|
1533
|
-
counts[s] ? `(${counts[s]})` : ""
|
|
1534
|
-
]
|
|
1535
|
-
}, s))]
|
|
1536
|
-
}),
|
|
1537
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1538
|
-
className: "vadm-spread",
|
|
1539
|
-
style: {
|
|
1540
|
-
alignItems: "flex-start",
|
|
1541
|
-
gap: 16
|
|
1542
|
-
},
|
|
1543
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
1544
|
-
className: "vadm-grow",
|
|
1545
|
-
style: { minWidth: 0 },
|
|
1546
|
-
children: reports === null ? /* @__PURE__ */ jsx("div", {
|
|
1547
|
-
className: "vadm-empty",
|
|
1548
|
-
children: "Loading…"
|
|
1549
|
-
}) : reports.length === 0 ? /* @__PURE__ */ jsx("div", {
|
|
1550
|
-
className: "vadm-empty",
|
|
1551
|
-
children: "No reports."
|
|
1552
|
-
}) : /* @__PURE__ */ jsxs("table", {
|
|
1553
|
-
className: "vadm-table",
|
|
1554
|
-
children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
1555
|
-
/* @__PURE__ */ jsx("th", { children: "Title" }),
|
|
1556
|
-
/* @__PURE__ */ jsx("th", { children: "Sev" }),
|
|
1557
|
-
/* @__PURE__ */ jsx("th", { children: "Status" }),
|
|
1558
|
-
/* @__PURE__ */ jsx("th", { children: "Reporter" }),
|
|
1559
|
-
/* @__PURE__ */ jsx("th", { children: "When" })
|
|
1560
|
-
] }) }), /* @__PURE__ */ jsx("tbody", { children: reports.map((r) => /* @__PURE__ */ jsxs("tr", {
|
|
1561
|
-
onClick: () => openDetail(r.id),
|
|
1562
|
-
style: {
|
|
1563
|
-
cursor: "pointer",
|
|
1564
|
-
fontWeight: r.id === selectedId ? 600 : void 0
|
|
1565
|
-
},
|
|
1566
|
-
children: [
|
|
1567
|
-
/* @__PURE__ */ jsx("td", {
|
|
1568
|
-
style: {
|
|
1569
|
-
maxWidth: 260,
|
|
1570
|
-
overflow: "hidden",
|
|
1571
|
-
textOverflow: "ellipsis",
|
|
1572
|
-
whiteSpace: "nowrap"
|
|
1573
|
-
},
|
|
1574
|
-
children: r.title
|
|
1575
|
-
}),
|
|
1576
|
-
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("span", {
|
|
1577
|
-
className: "vadm-badge",
|
|
1578
|
-
"data-on": r.severity === "critical" || r.severity === "high" ? "true" : void 0,
|
|
1579
|
-
children: r.severity
|
|
1580
|
-
}) }),
|
|
1581
|
-
/* @__PURE__ */ jsx("td", { children: STATUS_LABEL[r.status] }),
|
|
1582
|
-
/* @__PURE__ */ jsxs("td", {
|
|
1583
|
-
className: "vadm-muted",
|
|
1584
|
-
children: [r.reporterEmail ?? "—", r.reporterEmail && !r.reporterUserId && /* @__PURE__ */ jsx("span", {
|
|
1585
|
-
className: "vadm-badge",
|
|
1586
|
-
style: { marginLeft: 6 },
|
|
1587
|
-
title: "Self-reported email — not verified",
|
|
1588
|
-
children: "unverified"
|
|
1589
|
-
})]
|
|
1590
|
-
}),
|
|
1591
|
-
/* @__PURE__ */ jsx("td", {
|
|
1592
|
-
className: "vadm-muted",
|
|
1593
|
-
children: fmtTime(r.createdAt)
|
|
1594
|
-
})
|
|
1595
|
-
]
|
|
1596
|
-
}, r.id)) })]
|
|
1597
|
-
})
|
|
1598
|
-
}), detail && /* @__PURE__ */ jsxs("div", {
|
|
1599
|
-
className: "vadm-detail vadm-card",
|
|
1600
|
-
style: {
|
|
1601
|
-
width: 360,
|
|
1602
|
-
flexShrink: 0
|
|
1603
|
-
},
|
|
1604
|
-
children: [
|
|
1605
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1606
|
-
style: {
|
|
1607
|
-
border: "1px solid #f0d8a8",
|
|
1608
|
-
background: "#fffaf0",
|
|
1609
|
-
borderRadius: 8,
|
|
1610
|
-
padding: "8px 10px",
|
|
1611
|
-
margin: "0 0 12px"
|
|
1612
|
-
},
|
|
1613
|
-
children: [
|
|
1614
|
-
/* @__PURE__ */ jsx("div", {
|
|
1615
|
-
className: "vadm-muted",
|
|
1616
|
-
style: {
|
|
1617
|
-
fontSize: 11,
|
|
1618
|
-
marginBottom: 4,
|
|
1619
|
-
textTransform: "uppercase",
|
|
1620
|
-
letterSpacing: .4
|
|
1621
|
-
},
|
|
1622
|
-
children: "Reported text · untrusted input"
|
|
1623
|
-
}),
|
|
1624
|
-
/* @__PURE__ */ jsx("p", {
|
|
1625
|
-
className: "vadm-h",
|
|
1626
|
-
style: { margin: "0 0 4px" },
|
|
1627
|
-
children: detail.title
|
|
1628
|
-
}),
|
|
1629
|
-
/* @__PURE__ */ jsx("p", {
|
|
1630
|
-
style: {
|
|
1631
|
-
whiteSpace: "pre-wrap",
|
|
1632
|
-
wordBreak: "break-word",
|
|
1633
|
-
margin: 0
|
|
1634
|
-
},
|
|
1635
|
-
children: detail.body || /* @__PURE__ */ jsx("em", {
|
|
1636
|
-
className: "vadm-muted",
|
|
1637
|
-
children: "No description."
|
|
1638
|
-
})
|
|
1639
|
-
})
|
|
1640
|
-
]
|
|
1641
|
-
}),
|
|
1642
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1643
|
-
className: "vadm-field",
|
|
1644
|
-
children: [/* @__PURE__ */ jsx("label", {
|
|
1645
|
-
className: "vadm-label",
|
|
1646
|
-
children: "Status"
|
|
1647
|
-
}), /* @__PURE__ */ jsx("select", {
|
|
1648
|
-
className: "vadm-select",
|
|
1649
|
-
value: detail.status,
|
|
1650
|
-
disabled: busy,
|
|
1651
|
-
onChange: (e) => mutate(() => client.setReportStatus(detail.id, e.target.value)),
|
|
1652
|
-
children: BUG_STATUSES.map((s) => /* @__PURE__ */ jsx("option", {
|
|
1653
|
-
value: s,
|
|
1654
|
-
children: STATUS_LABEL[s]
|
|
1655
|
-
}, s))
|
|
1656
|
-
})]
|
|
1657
|
-
}),
|
|
1658
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1659
|
-
className: "vadm-field",
|
|
1660
|
-
children: [/* @__PURE__ */ jsx("label", {
|
|
1661
|
-
className: "vadm-label",
|
|
1662
|
-
children: "Recover context"
|
|
1663
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
1664
|
-
className: "vadm-mono",
|
|
1665
|
-
style: {
|
|
1666
|
-
fontSize: 12,
|
|
1667
|
-
lineHeight: 1.6
|
|
1668
|
-
},
|
|
1669
|
-
children: [
|
|
1670
|
-
detail.slug ? /* @__PURE__ */ jsxs("div", { children: [
|
|
1671
|
-
"app:",
|
|
1672
|
-
" ",
|
|
1673
|
-
/* @__PURE__ */ jsx("a", {
|
|
1674
|
-
href: `https://app.omg.dev/${detail.slug}`,
|
|
1675
|
-
target: "_blank",
|
|
1676
|
-
rel: "noreferrer",
|
|
1677
|
-
children: detail.slug
|
|
1678
|
-
}),
|
|
1679
|
-
detail.version != null ? ` · v${detail.version}` : ""
|
|
1680
|
-
] }) : /* @__PURE__ */ jsx("div", {
|
|
1681
|
-
className: "vadm-muted",
|
|
1682
|
-
children: "no app linked"
|
|
1683
|
-
}),
|
|
1684
|
-
detail.snapshotId && /* @__PURE__ */ jsxs("div", { children: ["snapshot: ", detail.snapshotId] }),
|
|
1685
|
-
detail.runId && /* @__PURE__ */ jsxs("div", { children: ["run: ", detail.runId] }),
|
|
1686
|
-
detail.pageUrl && /* @__PURE__ */ jsxs("div", {
|
|
1687
|
-
style: {
|
|
1688
|
-
overflow: "hidden",
|
|
1689
|
-
textOverflow: "ellipsis",
|
|
1690
|
-
whiteSpace: "nowrap"
|
|
1691
|
-
},
|
|
1692
|
-
children: [
|
|
1693
|
-
"page:",
|
|
1694
|
-
" ",
|
|
1695
|
-
safeHref(detail.pageUrl) ? /* @__PURE__ */ jsx("a", {
|
|
1696
|
-
href: detail.pageUrl,
|
|
1697
|
-
target: "_blank",
|
|
1698
|
-
rel: "noreferrer",
|
|
1699
|
-
children: detail.pageUrl
|
|
1700
|
-
}) : /* @__PURE__ */ jsx("span", { children: detail.pageUrl })
|
|
1701
|
-
]
|
|
1702
|
-
})
|
|
1703
|
-
]
|
|
1704
|
-
})]
|
|
1705
|
-
}),
|
|
1706
|
-
detail.slug && /* @__PURE__ */ jsxs("div", {
|
|
1707
|
-
className: "vadm-row",
|
|
1708
|
-
style: {
|
|
1709
|
-
gap: 6,
|
|
1710
|
-
marginBottom: 8
|
|
1711
|
-
},
|
|
1712
|
-
children: [/* @__PURE__ */ jsx("a", {
|
|
1713
|
-
className: "vadm-btn vadm-btn-sm",
|
|
1714
|
-
href: `https://app.omg.dev/${detail.slug}`,
|
|
1715
|
-
target: "_blank",
|
|
1716
|
-
rel: "noreferrer",
|
|
1717
|
-
children: "Open app"
|
|
1718
|
-
}), /* @__PURE__ */ jsx("a", {
|
|
1719
|
-
className: "vadm-btn vadm-btn-sm",
|
|
1720
|
-
href: `https://app.omg.dev/${detail.slug}/inspect/data`,
|
|
1721
|
-
target: "_blank",
|
|
1722
|
-
rel: "noreferrer",
|
|
1723
|
-
children: "Inspect"
|
|
1724
|
-
})]
|
|
1725
|
-
}),
|
|
1726
|
-
(() => {
|
|
1727
|
-
const ctx = detail.context;
|
|
1728
|
-
const url = safeImageSrc(ctx?.screenshotUrl) ?? null;
|
|
1729
|
-
return url ? /* @__PURE__ */ jsxs("div", {
|
|
1730
|
-
className: "vadm-field",
|
|
1731
|
-
children: [/* @__PURE__ */ jsx("label", {
|
|
1732
|
-
className: "vadm-label",
|
|
1733
|
-
children: "Screenshot"
|
|
1734
|
-
}), /* @__PURE__ */ jsx("a", {
|
|
1735
|
-
href: url,
|
|
1736
|
-
target: "_blank",
|
|
1737
|
-
rel: "noreferrer",
|
|
1738
|
-
children: /* @__PURE__ */ jsx("img", {
|
|
1739
|
-
src: url,
|
|
1740
|
-
alt: "screenshot",
|
|
1741
|
-
style: {
|
|
1742
|
-
width: "100%",
|
|
1743
|
-
maxHeight: 200,
|
|
1744
|
-
objectFit: "cover",
|
|
1745
|
-
borderRadius: 8,
|
|
1746
|
-
border: "1px solid #eee"
|
|
1747
|
-
}
|
|
1748
|
-
})
|
|
1749
|
-
})]
|
|
1750
|
-
}) : null;
|
|
1751
|
-
})(),
|
|
1752
|
-
detail.context != null && /* @__PURE__ */ jsxs("div", {
|
|
1753
|
-
className: "vadm-field",
|
|
1754
|
-
children: [/* @__PURE__ */ jsx("label", {
|
|
1755
|
-
className: "vadm-label",
|
|
1756
|
-
children: "Diagnostics"
|
|
1757
|
-
}), /* @__PURE__ */ jsx("pre", {
|
|
1758
|
-
className: "vadm-mono",
|
|
1759
|
-
style: {
|
|
1760
|
-
fontSize: 11,
|
|
1761
|
-
maxHeight: 160,
|
|
1762
|
-
overflow: "auto",
|
|
1763
|
-
margin: 0
|
|
1764
|
-
},
|
|
1765
|
-
children: JSON.stringify(detail.context, null, 2)
|
|
1766
|
-
})]
|
|
1767
|
-
}),
|
|
1768
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1769
|
-
className: "vadm-field",
|
|
1770
|
-
children: [
|
|
1771
|
-
/* @__PURE__ */ jsx("label", {
|
|
1772
|
-
className: "vadm-label",
|
|
1773
|
-
children: "Triage note"
|
|
1774
|
-
}),
|
|
1775
|
-
/* @__PURE__ */ jsx("textarea", {
|
|
1776
|
-
className: "vadm-textarea",
|
|
1777
|
-
rows: 3,
|
|
1778
|
-
value: note,
|
|
1779
|
-
onChange: (e) => setNote(e.target.value),
|
|
1780
|
-
disabled: busy
|
|
1781
|
-
}),
|
|
1782
|
-
/* @__PURE__ */ jsx("button", {
|
|
1783
|
-
className: "vadm-btn vadm-btn-sm",
|
|
1784
|
-
disabled: busy || note === (detail.adminNotes ?? ""),
|
|
1785
|
-
onClick: () => mutate(() => client.addReportNote(detail.id, note)),
|
|
1786
|
-
style: { marginTop: 6 },
|
|
1787
|
-
children: "Save note"
|
|
1788
|
-
})
|
|
1789
|
-
]
|
|
1790
|
-
}),
|
|
1791
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1792
|
-
className: "vadm-spread",
|
|
1793
|
-
style: { marginTop: 12 },
|
|
1794
|
-
children: [/* @__PURE__ */ jsxs("span", {
|
|
1795
|
-
className: "vadm-muted",
|
|
1796
|
-
style: { fontSize: 12 },
|
|
1797
|
-
children: [detail.reporterEmail ?? "anonymous", detail.reporterEmail && !detail.reporterUserId && " · unverified"]
|
|
1798
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
1799
|
-
className: "vadm-btn vadm-btn-sm",
|
|
1800
|
-
disabled: busy,
|
|
1801
|
-
onClick: () => {
|
|
1802
|
-
if (confirm("Delete this report?")) mutate(() => client.removeReport(detail.id).then(() => {
|
|
1803
|
-
setDetail(null);
|
|
1804
|
-
setSelectedId(null);
|
|
1805
|
-
}));
|
|
1806
|
-
},
|
|
1807
|
-
children: "Delete"
|
|
1808
|
-
})]
|
|
1809
|
-
})
|
|
1810
|
-
]
|
|
1811
|
-
})]
|
|
1812
|
-
})
|
|
1813
|
-
] });
|
|
1814
|
-
}
|
|
1815
1392
|
function shortId(value) {
|
|
1816
1393
|
if (!value) return "none";
|
|
1817
1394
|
return value.length > 16 ? `${value.slice(0, 8)}…${value.slice(-5)}` : value;
|
|
@@ -2545,11 +2122,6 @@ const DEFAULT_PANELS = [
|
|
|
2545
2122
|
label: "Users",
|
|
2546
2123
|
render: (c) => /* @__PURE__ */ jsx(UsersPanel, { client: c })
|
|
2547
2124
|
},
|
|
2548
|
-
{
|
|
2549
|
-
id: "reports",
|
|
2550
|
-
label: "Reports",
|
|
2551
|
-
render: (c) => /* @__PURE__ */ jsx(ReportsPanel, { client: c })
|
|
2552
|
-
},
|
|
2553
2125
|
{
|
|
2554
2126
|
id: "pricing",
|
|
2555
2127
|
label: "Pricing",
|
|
@@ -2584,4 +2156,4 @@ function AdminConsole({ client, config, panels = DEFAULT_PANELS }) {
|
|
|
2584
2156
|
});
|
|
2585
2157
|
}
|
|
2586
2158
|
//#endregion
|
|
2587
|
-
export {
|
|
2159
|
+
export { UsersPanel as a, STYLE_ID as c, PricingPanel as i, ensureStyles as l, BrainRoutingPanel as n, AdminClient as o, FlagsPanel as r, AdminError as s, AdminConsole as t };
|
package/dist/server.mjs
CHANGED
package/dist/standalone.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { o as AdminClient, t as AdminConsole } from "./react-6ZXjCUwb.mjs";
|
|
2
2
|
import { createElement } from "react";
|
|
3
3
|
import { createRoot } from "react-dom/client";
|
|
4
4
|
//#region src/standalone.tsx
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -102,31 +102,6 @@ export class AdminClient {
|
|
|
102
102
|
return this.rpc<UserBalance>("adminBilling", "userBalance", { userId })
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
// ── bug reports (triage) ───────────────────────────────────────────────────
|
|
106
|
-
listReports(args?: { status?: BugStatus; limit?: number; clusterKey?: string }) {
|
|
107
|
-
return this.rpc<BugReportList>("bugReports", "listReports", args ?? {})
|
|
108
|
-
}
|
|
109
|
-
getReport(id: string) {
|
|
110
|
-
return this.rpc<BugReportDetail | null>("bugReports", "getReport", { id })
|
|
111
|
-
}
|
|
112
|
-
setReportStatus(id: string, status: BugStatus, assignee?: string) {
|
|
113
|
-
return this.rpc<{ ok: true }>("bugReports", "setStatus", { id, status, assignee })
|
|
114
|
-
}
|
|
115
|
-
addReportNote(id: string, note: string) {
|
|
116
|
-
return this.rpc<{ ok: true }>("bugReports", "addNote", { id, note })
|
|
117
|
-
}
|
|
118
|
-
// Dedupe surface: group a report under a root cause (clusterKey=null clears),
|
|
119
|
-
// and roll up clusters so triage can skip a duplicate fixer.
|
|
120
|
-
setReportCluster(id: string, clusterKey: string | null) {
|
|
121
|
-
return this.rpc<{ ok: true; clusterKey: string | null }>("bugReports", "setCluster", { id, clusterKey })
|
|
122
|
-
}
|
|
123
|
-
listClusters() {
|
|
124
|
-
return this.rpc<BugClusterList>("bugReports", "listClusters", {})
|
|
125
|
-
}
|
|
126
|
-
removeReport(id: string) {
|
|
127
|
-
return this.rpc<{ ok: boolean }>("bugReports", "removeReport", { id })
|
|
128
|
-
}
|
|
129
|
-
|
|
130
105
|
// ── iMessage Brain (read-only routing ledger) ─────────────────────────────
|
|
131
106
|
// `steps: true` also returns the per-turn step log (rules evaluated, prompt
|
|
132
107
|
// blocks, tool calls with arguments, tool results, effects) — the waterfall.
|
|
@@ -297,59 +272,3 @@ export interface ImessageBrainMessage {
|
|
|
297
272
|
media?: string[]
|
|
298
273
|
at: number
|
|
299
274
|
}
|
|
300
|
-
|
|
301
|
-
// ── bug reports ───────────────────────────────────────────────────────────────
|
|
302
|
-
|
|
303
|
-
export type BugStatus = "open" | "triaged" | "in_progress" | "resolved" | "wont_fix"
|
|
304
|
-
export type BugSeverity = "low" | "normal" | "high" | "critical"
|
|
305
|
-
|
|
306
|
-
export interface BugReportSummary {
|
|
307
|
-
id: string
|
|
308
|
-
title: string
|
|
309
|
-
severity: BugSeverity
|
|
310
|
-
status: BugStatus
|
|
311
|
-
assignee: string | null
|
|
312
|
-
reporterEmail: string | null
|
|
313
|
-
// null = anonymous app-submitted report → reporterEmail is self-claimed and
|
|
314
|
-
// unverified (the UI marks it). Non-null = dashboard report, email from JWT.
|
|
315
|
-
reporterUserId: string | null
|
|
316
|
-
slug: string | null
|
|
317
|
-
// Root-cause cluster key (null = unclustered). Reports sharing a key are
|
|
318
|
-
// duplicates of one root cause; triage uses it to dedupe.
|
|
319
|
-
clusterKey: string | null
|
|
320
|
-
createdAt: number
|
|
321
|
-
updatedAt: number
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
export interface BugReportList {
|
|
325
|
-
reports: BugReportSummary[]
|
|
326
|
-
counts: Record<string, number>
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
export interface BugCluster {
|
|
330
|
-
clusterKey: string
|
|
331
|
-
total: number
|
|
332
|
-
open: number
|
|
333
|
-
// Reports not yet resolved/wont_fix — non-zero means the root cause is still
|
|
334
|
-
// live, so a new duplicate should attach here instead of opening a new fixer.
|
|
335
|
-
unresolved: number
|
|
336
|
-
latestAt: number
|
|
337
|
-
latestTitle: string | null
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
export interface BugClusterList {
|
|
341
|
-
clusters: BugCluster[]
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
export interface BugReportDetail extends BugReportSummary {
|
|
345
|
-
body: string
|
|
346
|
-
projectId: string | null
|
|
347
|
-
runId: string | null
|
|
348
|
-
snapshotId: string | null
|
|
349
|
-
version: number | null
|
|
350
|
-
pageUrl: string | null
|
|
351
|
-
userAgent: string | null
|
|
352
|
-
adminNotes: string | null
|
|
353
|
-
resolvedAt: number | null
|
|
354
|
-
context: unknown
|
|
355
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @omg-dev/admin — embeddable internal admin console.
|
|
2
2
|
//
|
|
3
|
-
// A panel-registry React console (Feature Flags, Users
|
|
3
|
+
// A panel-registry React console (Feature Flags, Users + read-only
|
|
4
4
|
// Pricing/Billing) that drops into any host as a single <AdminConsole/> — the
|
|
5
5
|
// omg dashboard, the LFG app, or Inspect. Host-agnostic: you pass an AdminClient
|
|
6
6
|
// (control-plane base URL + a bearer-token getter); the console does the rest.
|
|
@@ -13,7 +13,6 @@ export {
|
|
|
13
13
|
AdminConsole,
|
|
14
14
|
FlagsPanel,
|
|
15
15
|
PricingPanel,
|
|
16
|
-
ReportsPanel,
|
|
17
16
|
UsersPanel,
|
|
18
17
|
BrainRoutingPanel,
|
|
19
18
|
AdminClient,
|
|
@@ -37,11 +36,6 @@ export {
|
|
|
37
36
|
type ImessageBrainTrace,
|
|
38
37
|
type ImessageBrainMessage,
|
|
39
38
|
type ValueType,
|
|
40
|
-
type BugStatus,
|
|
41
|
-
type BugSeverity,
|
|
42
|
-
type BugReportSummary,
|
|
43
|
-
type BugReportList,
|
|
44
|
-
type BugReportDetail,
|
|
45
39
|
} from "./client"
|
|
46
40
|
|
|
47
41
|
export { ensureStyles, STYLE_ID } from "./styles"
|
package/src/react.tsx
CHANGED
|
@@ -26,9 +26,6 @@ import {
|
|
|
26
26
|
type ImessageBrainTrace,
|
|
27
27
|
type ImessageBrainTraceStep,
|
|
28
28
|
type ImessageBrainMessage,
|
|
29
|
-
type BugReportSummary,
|
|
30
|
-
type BugReportDetail,
|
|
31
|
-
type BugStatus,
|
|
32
29
|
} from "./client"
|
|
33
30
|
|
|
34
31
|
export type { AdminClientConfig }
|
|
@@ -460,6 +457,14 @@ export function PricingPanel({ client }: { client: AdminClient }) {
|
|
|
460
457
|
)
|
|
461
458
|
}
|
|
462
459
|
|
|
460
|
+
function fmtTime(ms: number): string {
|
|
461
|
+
try {
|
|
462
|
+
return new Date(ms).toLocaleString()
|
|
463
|
+
} catch {
|
|
464
|
+
return String(ms)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
463
468
|
// ── Users panel ────────────────────────────────────────────────────────────
|
|
464
469
|
|
|
465
470
|
export function UsersPanel({ client }: { client: AdminClient }) {
|
|
@@ -623,308 +628,6 @@ export function UsersPanel({ client }: { client: AdminClient }) {
|
|
|
623
628
|
)
|
|
624
629
|
}
|
|
625
630
|
|
|
626
|
-
// ── Reports panel (triage) ─────────────────────────────────────────────────
|
|
627
|
-
|
|
628
|
-
const BUG_STATUSES: BugStatus[] = ["open", "triaged", "in_progress", "resolved", "wont_fix"]
|
|
629
|
-
const STATUS_LABEL: Record<BugStatus, string> = {
|
|
630
|
-
open: "Open",
|
|
631
|
-
triaged: "Triaged",
|
|
632
|
-
in_progress: "In progress",
|
|
633
|
-
resolved: "Resolved",
|
|
634
|
-
wont_fix: "Won't fix",
|
|
635
|
-
}
|
|
636
|
-
const OPEN_STATUSES = new Set<BugStatus>(["open", "triaged", "in_progress"])
|
|
637
|
-
|
|
638
|
-
function fmtTime(ms: number): string {
|
|
639
|
-
try {
|
|
640
|
-
return new Date(ms).toLocaleString()
|
|
641
|
-
} catch {
|
|
642
|
-
return String(ms)
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
// Screenshots come from a report (untrusted). An <img src> to an arbitrary host
|
|
647
|
-
// is a tracking pixel / IP-leak / cookie beacon against the admin. Only render
|
|
648
|
-
// images served from our storage CDN; otherwise show nothing (the report still
|
|
649
|
-
// carries the durable screenshotKey for manual lookup). Mirrors the ingestion
|
|
650
|
-
// allowlist in control-plane/lib/sanitize.ts → storageImageUrl.
|
|
651
|
-
const STORAGE_HOSTS = ["t3.storage.dev"]
|
|
652
|
-
function safeImageSrc(v: unknown): string | undefined {
|
|
653
|
-
if (typeof v !== "string") return undefined
|
|
654
|
-
try {
|
|
655
|
-
const u = new URL(v)
|
|
656
|
-
if (u.protocol !== "http:" && u.protocol !== "https:") return undefined
|
|
657
|
-
const host = u.hostname.toLowerCase()
|
|
658
|
-
return STORAGE_HOSTS.some((h) => host === h || host.endsWith("." + h)) ? v : undefined
|
|
659
|
-
} catch {
|
|
660
|
-
return undefined
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
export function ReportsPanel({ client }: { client: AdminClient }) {
|
|
665
|
-
const [reports, setReports] = useState<BugReportSummary[] | null>(null)
|
|
666
|
-
const [counts, setCounts] = useState<Record<string, number>>({})
|
|
667
|
-
const [filter, setFilter] = useState<BugStatus | "all">("all")
|
|
668
|
-
const [selectedId, setSelectedId] = useState<string | null>(null)
|
|
669
|
-
const [detail, setDetail] = useState<BugReportDetail | null>(null)
|
|
670
|
-
const [error, setError] = useState<unknown>(null)
|
|
671
|
-
const [busy, setBusy] = useState(false)
|
|
672
|
-
const [note, setNote] = useState("")
|
|
673
|
-
|
|
674
|
-
const load = useCallback(async () => {
|
|
675
|
-
try {
|
|
676
|
-
const res = await client.listReports(filter === "all" ? {} : { status: filter })
|
|
677
|
-
setReports(res.reports)
|
|
678
|
-
setCounts(res.counts)
|
|
679
|
-
setError(null)
|
|
680
|
-
} catch (e) {
|
|
681
|
-
setError(e)
|
|
682
|
-
}
|
|
683
|
-
}, [client, filter])
|
|
684
|
-
|
|
685
|
-
useEffect(() => {
|
|
686
|
-
ensureStyles()
|
|
687
|
-
void load()
|
|
688
|
-
}, [load])
|
|
689
|
-
|
|
690
|
-
const openDetail = useCallback(
|
|
691
|
-
async (id: string) => {
|
|
692
|
-
setSelectedId(id)
|
|
693
|
-
setDetail(null)
|
|
694
|
-
try {
|
|
695
|
-
const d = await client.getReport(id)
|
|
696
|
-
setDetail(d)
|
|
697
|
-
setNote(d?.adminNotes ?? "")
|
|
698
|
-
} catch (e) {
|
|
699
|
-
setError(e)
|
|
700
|
-
}
|
|
701
|
-
},
|
|
702
|
-
[client],
|
|
703
|
-
)
|
|
704
|
-
|
|
705
|
-
async function mutate(fn: () => Promise<unknown>) {
|
|
706
|
-
setBusy(true)
|
|
707
|
-
try {
|
|
708
|
-
await fn()
|
|
709
|
-
await load()
|
|
710
|
-
if (selectedId) await openDetail(selectedId)
|
|
711
|
-
setError(null)
|
|
712
|
-
} catch (e) {
|
|
713
|
-
setError(e)
|
|
714
|
-
} finally {
|
|
715
|
-
setBusy(false)
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
const totalOpen = BUG_STATUSES.filter((s) => OPEN_STATUSES.has(s)).reduce((n, s) => n + (counts[s] ?? 0), 0)
|
|
720
|
-
|
|
721
|
-
return (
|
|
722
|
-
<div>
|
|
723
|
-
<p className="vadm-sub">
|
|
724
|
-
User-submitted bug reports. Each carries the context to <strong>recover</strong> — open or fork the exact app
|
|
725
|
-
version the reporter saw. {totalOpen} open.
|
|
726
|
-
</p>
|
|
727
|
-
<ErrorBanner error={error} />
|
|
728
|
-
|
|
729
|
-
<div className="vadm-row" style={{ marginBottom: 12, gap: 6, flexWrap: "wrap" }}>
|
|
730
|
-
<button className="vadm-tab" data-active={filter === "all"} onClick={() => setFilter("all")}>
|
|
731
|
-
All
|
|
732
|
-
</button>
|
|
733
|
-
{BUG_STATUSES.map((s) => (
|
|
734
|
-
<button key={s} className="vadm-tab" data-active={filter === s} onClick={() => setFilter(s)}>
|
|
735
|
-
{STATUS_LABEL[s]} {counts[s] ? `(${counts[s]})` : ""}
|
|
736
|
-
</button>
|
|
737
|
-
))}
|
|
738
|
-
</div>
|
|
739
|
-
|
|
740
|
-
<div className="vadm-spread" style={{ alignItems: "flex-start", gap: 16 }}>
|
|
741
|
-
{/* List */}
|
|
742
|
-
<div className="vadm-grow" style={{ minWidth: 0 }}>
|
|
743
|
-
{reports === null ? (
|
|
744
|
-
<div className="vadm-empty">Loading…</div>
|
|
745
|
-
) : reports.length === 0 ? (
|
|
746
|
-
<div className="vadm-empty">No reports.</div>
|
|
747
|
-
) : (
|
|
748
|
-
<table className="vadm-table">
|
|
749
|
-
<thead>
|
|
750
|
-
<tr>
|
|
751
|
-
<th>Title</th>
|
|
752
|
-
<th>Sev</th>
|
|
753
|
-
<th>Status</th>
|
|
754
|
-
<th>Reporter</th>
|
|
755
|
-
<th>When</th>
|
|
756
|
-
</tr>
|
|
757
|
-
</thead>
|
|
758
|
-
<tbody>
|
|
759
|
-
{reports.map((r) => (
|
|
760
|
-
<tr
|
|
761
|
-
key={r.id}
|
|
762
|
-
onClick={() => openDetail(r.id)}
|
|
763
|
-
style={{ cursor: "pointer", fontWeight: r.id === selectedId ? 600 : undefined }}
|
|
764
|
-
>
|
|
765
|
-
<td style={{ maxWidth: 260, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
|
766
|
-
{r.title}
|
|
767
|
-
</td>
|
|
768
|
-
<td>
|
|
769
|
-
<span className="vadm-badge" data-on={r.severity === "critical" || r.severity === "high" ? "true" : undefined}>
|
|
770
|
-
{r.severity}
|
|
771
|
-
</span>
|
|
772
|
-
</td>
|
|
773
|
-
<td>{STATUS_LABEL[r.status]}</td>
|
|
774
|
-
<td className="vadm-muted">
|
|
775
|
-
{r.reporterEmail ?? "—"}
|
|
776
|
-
{r.reporterEmail && !r.reporterUserId && (
|
|
777
|
-
<span className="vadm-badge" style={{ marginLeft: 6 }} title="Self-reported email — not verified">
|
|
778
|
-
unverified
|
|
779
|
-
</span>
|
|
780
|
-
)}
|
|
781
|
-
</td>
|
|
782
|
-
<td className="vadm-muted">{fmtTime(r.createdAt)}</td>
|
|
783
|
-
</tr>
|
|
784
|
-
))}
|
|
785
|
-
</tbody>
|
|
786
|
-
</table>
|
|
787
|
-
)}
|
|
788
|
-
</div>
|
|
789
|
-
|
|
790
|
-
{/* Detail */}
|
|
791
|
-
{detail && (
|
|
792
|
-
<div className="vadm-detail vadm-card" style={{ width: 360, flexShrink: 0 }}>
|
|
793
|
-
{/* The title + body are UNTRUSTED visitor text (potential prompt
|
|
794
|
-
injection if pasted into an LLM). Fence them visually so a
|
|
795
|
-
triager never mistakes embedded "instructions" for system copy.
|
|
796
|
-
React already escapes the content; this is provenance, not XSS. */}
|
|
797
|
-
<div
|
|
798
|
-
style={{
|
|
799
|
-
border: "1px solid #f0d8a8",
|
|
800
|
-
background: "#fffaf0",
|
|
801
|
-
borderRadius: 8,
|
|
802
|
-
padding: "8px 10px",
|
|
803
|
-
margin: "0 0 12px",
|
|
804
|
-
}}
|
|
805
|
-
>
|
|
806
|
-
<div className="vadm-muted" style={{ fontSize: 11, marginBottom: 4, textTransform: "uppercase", letterSpacing: 0.4 }}>
|
|
807
|
-
Reported text · untrusted input
|
|
808
|
-
</div>
|
|
809
|
-
<p className="vadm-h" style={{ margin: "0 0 4px" }}>{detail.title}</p>
|
|
810
|
-
<p style={{ whiteSpace: "pre-wrap", wordBreak: "break-word", margin: 0 }}>
|
|
811
|
-
{detail.body || <em className="vadm-muted">No description.</em>}
|
|
812
|
-
</p>
|
|
813
|
-
</div>
|
|
814
|
-
|
|
815
|
-
<div className="vadm-field">
|
|
816
|
-
<label className="vadm-label">Status</label>
|
|
817
|
-
<select
|
|
818
|
-
className="vadm-select"
|
|
819
|
-
value={detail.status}
|
|
820
|
-
disabled={busy}
|
|
821
|
-
onChange={(e) => mutate(() => client.setReportStatus(detail.id, e.target.value as BugStatus))}
|
|
822
|
-
>
|
|
823
|
-
{BUG_STATUSES.map((s) => (
|
|
824
|
-
<option key={s} value={s}>{STATUS_LABEL[s]}</option>
|
|
825
|
-
))}
|
|
826
|
-
</select>
|
|
827
|
-
</div>
|
|
828
|
-
|
|
829
|
-
<div className="vadm-field">
|
|
830
|
-
<label className="vadm-label">Recover context</label>
|
|
831
|
-
<div className="vadm-mono" style={{ fontSize: 12, lineHeight: 1.6 }}>
|
|
832
|
-
{detail.slug ? (
|
|
833
|
-
<div>
|
|
834
|
-
app:{" "}
|
|
835
|
-
<a href={`https://app.omg.dev/${detail.slug}`} target="_blank" rel="noreferrer">{detail.slug}</a>
|
|
836
|
-
{detail.version != null ? ` · v${detail.version}` : ""}
|
|
837
|
-
</div>
|
|
838
|
-
) : (
|
|
839
|
-
<div className="vadm-muted">no app linked</div>
|
|
840
|
-
)}
|
|
841
|
-
{detail.snapshotId && <div>snapshot: {detail.snapshotId}</div>}
|
|
842
|
-
{detail.runId && <div>run: {detail.runId}</div>}
|
|
843
|
-
{detail.pageUrl && (
|
|
844
|
-
<div style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
|
845
|
-
page:{" "}
|
|
846
|
-
{safeHref(detail.pageUrl) ? (
|
|
847
|
-
<a href={detail.pageUrl} target="_blank" rel="noreferrer">{detail.pageUrl}</a>
|
|
848
|
-
) : (
|
|
849
|
-
<span>{detail.pageUrl}</span>
|
|
850
|
-
)}
|
|
851
|
-
</div>
|
|
852
|
-
)}
|
|
853
|
-
</div>
|
|
854
|
-
</div>
|
|
855
|
-
|
|
856
|
-
{detail.slug && (
|
|
857
|
-
<div className="vadm-row" style={{ gap: 6, marginBottom: 8 }}>
|
|
858
|
-
<a className="vadm-btn vadm-btn-sm" href={`https://app.omg.dev/${detail.slug}`} target="_blank" rel="noreferrer">
|
|
859
|
-
Open app
|
|
860
|
-
</a>
|
|
861
|
-
<a className="vadm-btn vadm-btn-sm" href={`https://app.omg.dev/${detail.slug}/inspect/data`} target="_blank" rel="noreferrer">
|
|
862
|
-
Inspect
|
|
863
|
-
</a>
|
|
864
|
-
</div>
|
|
865
|
-
)}
|
|
866
|
-
|
|
867
|
-
{(() => {
|
|
868
|
-
const ctx = detail.context as { screenshotUrl?: unknown } | null
|
|
869
|
-
const url = safeImageSrc(ctx?.screenshotUrl) ?? null
|
|
870
|
-
return url ? (
|
|
871
|
-
<div className="vadm-field">
|
|
872
|
-
<label className="vadm-label">Screenshot</label>
|
|
873
|
-
<a href={url} target="_blank" rel="noreferrer">
|
|
874
|
-
<img
|
|
875
|
-
src={url}
|
|
876
|
-
alt="screenshot"
|
|
877
|
-
style={{ width: "100%", maxHeight: 200, objectFit: "cover", borderRadius: 8, border: "1px solid #eee" }}
|
|
878
|
-
/>
|
|
879
|
-
</a>
|
|
880
|
-
</div>
|
|
881
|
-
) : null
|
|
882
|
-
})()}
|
|
883
|
-
|
|
884
|
-
{detail.context != null && (
|
|
885
|
-
<div className="vadm-field">
|
|
886
|
-
<label className="vadm-label">Diagnostics</label>
|
|
887
|
-
<pre className="vadm-mono" style={{ fontSize: 11, maxHeight: 160, overflow: "auto", margin: 0 }}>
|
|
888
|
-
{JSON.stringify(detail.context, null, 2)}
|
|
889
|
-
</pre>
|
|
890
|
-
</div>
|
|
891
|
-
)}
|
|
892
|
-
|
|
893
|
-
<div className="vadm-field">
|
|
894
|
-
<label className="vadm-label">Triage note</label>
|
|
895
|
-
<textarea className="vadm-textarea" rows={3} value={note} onChange={(e) => setNote(e.target.value)} disabled={busy} />
|
|
896
|
-
<button
|
|
897
|
-
className="vadm-btn vadm-btn-sm"
|
|
898
|
-
disabled={busy || note === (detail.adminNotes ?? "")}
|
|
899
|
-
onClick={() => mutate(() => client.addReportNote(detail.id, note))}
|
|
900
|
-
style={{ marginTop: 6 }}
|
|
901
|
-
>
|
|
902
|
-
Save note
|
|
903
|
-
</button>
|
|
904
|
-
</div>
|
|
905
|
-
|
|
906
|
-
<div className="vadm-spread" style={{ marginTop: 12 }}>
|
|
907
|
-
<span className="vadm-muted" style={{ fontSize: 12 }}>
|
|
908
|
-
{detail.reporterEmail ?? "anonymous"}
|
|
909
|
-
{detail.reporterEmail && !detail.reporterUserId && " · unverified"}
|
|
910
|
-
</span>
|
|
911
|
-
<button
|
|
912
|
-
className="vadm-btn vadm-btn-sm"
|
|
913
|
-
disabled={busy}
|
|
914
|
-
onClick={() => {
|
|
915
|
-
if (confirm("Delete this report?")) void mutate(() => client.removeReport(detail.id).then(() => { setDetail(null); setSelectedId(null) }))
|
|
916
|
-
}}
|
|
917
|
-
>
|
|
918
|
-
Delete
|
|
919
|
-
</button>
|
|
920
|
-
</div>
|
|
921
|
-
</div>
|
|
922
|
-
)}
|
|
923
|
-
</div>
|
|
924
|
-
</div>
|
|
925
|
-
)
|
|
926
|
-
}
|
|
927
|
-
|
|
928
631
|
// ── iMessage Brain routing panel ────────────────────────────────────────────
|
|
929
632
|
|
|
930
633
|
function shortId(value: string | null): string {
|
|
@@ -1585,7 +1288,6 @@ const DEFAULT_PANELS: PanelDef[] = [
|
|
|
1585
1288
|
{ id: "brain", label: "Chat Brain", render: (c) => <BrainRoutingPanel client={c} /> },
|
|
1586
1289
|
{ id: "flags", label: "Flags", render: (c) => <FlagsPanel client={c} /> },
|
|
1587
1290
|
{ id: "users", label: "Users", render: (c) => <UsersPanel client={c} /> },
|
|
1588
|
-
{ id: "reports", label: "Reports", render: (c) => <ReportsPanel client={c} /> },
|
|
1589
1291
|
{ id: "pricing", label: "Pricing", render: (c) => <PricingPanel client={c} /> },
|
|
1590
1292
|
]
|
|
1591
1293
|
|
package/src/safe-url.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/** URLs in the admin console can originate from
|
|
2
|
-
*
|
|
1
|
+
/** URLs in the admin console can originate from user-controlled Computers.
|
|
2
|
+
* Only ordinary web links may become clickable operator actions. */
|
|
3
3
|
export function safeHref(value: unknown): string | undefined {
|
|
4
4
|
if (typeof value !== "string") return undefined
|
|
5
5
|
try {
|
package/src/server.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface OmgAdminProxyOptions {
|
|
|
30
30
|
// The proxy only forwards the admin surface — never arbitrary control-plane
|
|
31
31
|
// paths — so a leaked mount can't wield the powerful admin token against other
|
|
32
32
|
// endpoints. Keep in sync with the console's AdminClient modules.
|
|
33
|
-
const ALLOWED = ["/api/flags/", "/api/adminBilling/", "/api/
|
|
33
|
+
const ALLOWED = ["/api/flags/", "/api/adminBilling/", "/api/users/", "/api/imessageDebug/"];
|
|
34
34
|
|
|
35
35
|
function json(body: unknown, status: number): Response {
|
|
36
36
|
return new Response(JSON.stringify(body), {
|