@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 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**, **Reports**, and read-only **Pricing/Billing**)
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/*`, `/api/bugReports/*`,
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 ReportsPanel, c as AdminError, i as PricingPanel, l as STYLE_ID, n as BrainRoutingPanel, o as UsersPanel, r as FlagsPanel, s as AdminClient, t as AdminConsole, u as ensureStyles } from "./react-BNHn5Pe0.mjs";
2
- export { AdminClient, AdminConsole, AdminError, BrainRoutingPanel, FlagsPanel, PricingPanel, ReportsPanel, STYLE_ID, UsersPanel, ensureStyles };
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 reports or user-controlled
447
- * Computers. Only ordinary web links may become clickable operator actions. */
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 { ReportsPanel as a, AdminError as c, PricingPanel as i, STYLE_ID as l, BrainRoutingPanel as n, UsersPanel as o, FlagsPanel as r, AdminClient as s, AdminConsole as t, ensureStyles as u };
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
@@ -2,7 +2,6 @@
2
2
  const ALLOWED = [
3
3
  "/api/flags/",
4
4
  "/api/adminBilling/",
5
- "/api/bugReports/",
6
5
  "/api/users/",
7
6
  "/api/imessageDebug/"
8
7
  ];
@@ -1,4 +1,4 @@
1
- import { s as AdminClient, t as AdminConsole } from "./react-BNHn5Pe0.mjs";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omg-dev/admin",
3
- "version": "0.4.29",
3
+ "version": "0.4.31",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
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, Reports + read-only
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 reports or user-controlled
2
- * Computers. Only ordinary web links may become clickable operator actions. */
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/bugReports/", "/api/users/", "/api/imessageDebug/"];
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), {