@omg-dev/admin 0.4.24
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 +126 -0
- package/dist/index.mjs +2 -0
- package/dist/react-COCR3YSL.mjs +1603 -0
- package/dist/server.mjs +50 -0
- package/dist/standalone.mjs +11 -0
- package/package.json +47 -0
- package/src/client.ts +278 -0
- package/src/index.ts +43 -0
- package/src/react.tsx +983 -0
- package/src/server.ts +83 -0
- package/src/standalone.tsx +25 -0
- package/src/styles.ts +200 -0
package/dist/server.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//#region src/server.ts
|
|
2
|
+
const ALLOWED = [
|
|
3
|
+
"/api/flags/",
|
|
4
|
+
"/api/adminBilling/",
|
|
5
|
+
"/api/bugReports/",
|
|
6
|
+
"/api/users/"
|
|
7
|
+
];
|
|
8
|
+
function json(body, status) {
|
|
9
|
+
return new Response(JSON.stringify(body), {
|
|
10
|
+
status,
|
|
11
|
+
headers: {
|
|
12
|
+
"content-type": "application/json",
|
|
13
|
+
"cache-control": "no-store"
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function createOmgAdminProxy(opts) {
|
|
18
|
+
const target = (opts.target ?? "https://backend.omg.dev").replace(/\/$/, "");
|
|
19
|
+
const prefix = opts.prefix ?? "/_omg";
|
|
20
|
+
return async function handle(req) {
|
|
21
|
+
const url = new URL(req.url);
|
|
22
|
+
if (url.pathname !== prefix && !url.pathname.startsWith(prefix + "/")) return null;
|
|
23
|
+
if (!opts.token) return json({ error: "omg admin token not configured" }, 503);
|
|
24
|
+
const upstreamPath = url.pathname.slice(prefix.length);
|
|
25
|
+
if (!ALLOWED.some((p) => upstreamPath.startsWith(p))) return json({ error: "forbidden path" }, 403);
|
|
26
|
+
const init = {
|
|
27
|
+
method: req.method,
|
|
28
|
+
headers: {
|
|
29
|
+
"content-type": req.headers.get("content-type") ?? "application/json",
|
|
30
|
+
authorization: `Bearer ${opts.token}`
|
|
31
|
+
},
|
|
32
|
+
signal: AbortSignal.timeout(3e4)
|
|
33
|
+
};
|
|
34
|
+
if (req.method !== "GET" && req.method !== "HEAD") init.body = await req.text();
|
|
35
|
+
try {
|
|
36
|
+
const r = await fetch(`${target}${upstreamPath}${url.search}`, init);
|
|
37
|
+
return new Response(r.body, {
|
|
38
|
+
status: r.status,
|
|
39
|
+
headers: {
|
|
40
|
+
"content-type": r.headers.get("content-type") ?? "application/json",
|
|
41
|
+
"cache-control": "no-store"
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
} catch {
|
|
45
|
+
return json({ error: "omg control-plane unreachable" }, 502);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { createOmgAdminProxy };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { o as AdminClient, t as AdminConsole } from "./react-COCR3YSL.mjs";
|
|
2
|
+
import { createElement } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
//#region src/standalone.tsx
|
|
5
|
+
function mountAdminConsole(el, config) {
|
|
6
|
+
const root = createRoot(el);
|
|
7
|
+
root.render(createElement(AdminConsole, { client: new AdminClient(config) }));
|
|
8
|
+
return root;
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { AdminClient, AdminConsole, mountAdminConsole };
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@omg-dev/admin",
|
|
3
|
+
"version": "0.4.24",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"default": "./dist/index.mjs"
|
|
9
|
+
},
|
|
10
|
+
"./standalone": {
|
|
11
|
+
"types": "./src/standalone.tsx",
|
|
12
|
+
"default": "./dist/standalone.mjs"
|
|
13
|
+
},
|
|
14
|
+
"./server": {
|
|
15
|
+
"types": "./src/server.ts",
|
|
16
|
+
"default": "./dist/server.mjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"react": "^18 || ^19",
|
|
21
|
+
"react-dom": "^18 || ^19"
|
|
22
|
+
},
|
|
23
|
+
"peerDependenciesMeta": {
|
|
24
|
+
"react-dom": {
|
|
25
|
+
"optional": true
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/react": "^19.2.14",
|
|
30
|
+
"@types/react-dom": "^19.2.3",
|
|
31
|
+
"typescript": "^5.4.5"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/BennyKok/vibes.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://docs.omg.dev",
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"src"
|
|
42
|
+
],
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public",
|
|
45
|
+
"registry": "https://registry.npmjs.org/"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
// @omg-dev/admin — host-agnostic control-plane RPC client.
|
|
2
|
+
//
|
|
3
|
+
// The admin console is embedded in MORE than one host (the omg dashboard, the
|
|
4
|
+
// LFG app, later Inspect), so it never assumes a base URL or an auth provider.
|
|
5
|
+
// The host supplies an `AdminClientConfig`: where the control-plane lives
|
|
6
|
+
// (`apiBase`) and how to get a bearer token (`getToken`). Every call is
|
|
7
|
+
// POST {apiBase}/api/<module>/<name> with the args object as the JSON body and
|
|
8
|
+
// `Authorization: Bearer <token>` — the same wire the dashboard's own client
|
|
9
|
+
// uses, minus the Convex-shape bridging the console doesn't need.
|
|
10
|
+
|
|
11
|
+
export interface AdminClientConfig {
|
|
12
|
+
/** Control-plane origin, e.g. "https://backend.omg.dev" (no trailing slash). */
|
|
13
|
+
apiBase: string
|
|
14
|
+
/** Returns a fresh bearer JWT (or null when signed out). Awaited per call. */
|
|
15
|
+
getToken: () => string | null | Promise<string | null>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class AdminError extends Error {
|
|
19
|
+
status: number
|
|
20
|
+
data: unknown
|
|
21
|
+
constructor(message: string, status: number, data: unknown) {
|
|
22
|
+
super(message)
|
|
23
|
+
this.name = "AdminError"
|
|
24
|
+
this.status = status
|
|
25
|
+
this.data = data
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class AdminClient {
|
|
30
|
+
private apiBase: string
|
|
31
|
+
private getToken: AdminClientConfig["getToken"]
|
|
32
|
+
|
|
33
|
+
constructor(config: AdminClientConfig) {
|
|
34
|
+
this.apiBase = config.apiBase.replace(/\/$/, "")
|
|
35
|
+
this.getToken = config.getToken
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private async rpc<T>(module: string, name: string, args?: Record<string, unknown>): Promise<T> {
|
|
39
|
+
const token = await this.getToken()
|
|
40
|
+
const headers: Record<string, string> = { "content-type": "application/json" }
|
|
41
|
+
if (token) headers.authorization = `Bearer ${token}`
|
|
42
|
+
const res = await fetch(`${this.apiBase}/api/${module}/${name}`, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers,
|
|
45
|
+
body: JSON.stringify(args ?? {}),
|
|
46
|
+
})
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
let message = `${module}.${name} failed (${res.status})`
|
|
49
|
+
let payload: unknown = null
|
|
50
|
+
try {
|
|
51
|
+
payload = await res.json()
|
|
52
|
+
if (payload && typeof payload === "object" && "error" in payload) {
|
|
53
|
+
message = String((payload as { error: unknown }).error)
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
/* non-JSON error body */
|
|
57
|
+
}
|
|
58
|
+
throw new AdminError(message, res.status, payload)
|
|
59
|
+
}
|
|
60
|
+
if (res.status === 204) return undefined as T
|
|
61
|
+
const text = await res.text()
|
|
62
|
+
return (text ? JSON.parse(text) : undefined) as T
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── flags ────────────────────────────────────────────────────────────────
|
|
66
|
+
listFlags() {
|
|
67
|
+
return this.rpc<FlagDef[]>("flags", "listFlags")
|
|
68
|
+
}
|
|
69
|
+
upsertFlag(flag: FlagUpsert) {
|
|
70
|
+
return this.rpc<{ ok: true; key: string }>("flags", "upsert", flag as unknown as Record<string, unknown>)
|
|
71
|
+
}
|
|
72
|
+
setFlagArchived(key: string, archived: boolean) {
|
|
73
|
+
return this.rpc<{ ok: true }>("flags", "setArchived", { key, archived })
|
|
74
|
+
}
|
|
75
|
+
removeFlag(key: string) {
|
|
76
|
+
return this.rpc<{ ok: true }>("flags", "removeFlag", { key })
|
|
77
|
+
}
|
|
78
|
+
listOverrides(flagKey: string) {
|
|
79
|
+
return this.rpc<FlagOverride[]>("flags", "listOverrides", { flagKey })
|
|
80
|
+
}
|
|
81
|
+
setOverride(o: { flagKey: string; userId: string; enabled: boolean; value?: unknown; note?: string }) {
|
|
82
|
+
return this.rpc<{ ok: true }>("flags", "setOverride", o as Record<string, unknown>)
|
|
83
|
+
}
|
|
84
|
+
removeOverride(flagKey: string, userId: string) {
|
|
85
|
+
return this.rpc<{ ok: true }>("flags", "removeOverride", { flagKey, userId })
|
|
86
|
+
}
|
|
87
|
+
findUsers(query: string) {
|
|
88
|
+
return this.rpc<UserHit[]>("users", "findUsers", { query })
|
|
89
|
+
}
|
|
90
|
+
listUsers(args?: { query?: string; limit?: number; offset?: number }) {
|
|
91
|
+
return this.rpc<AdminUserList>("users", "listUsers", args ?? {})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── billing (read-only) ────────────────────────────────────────────────────
|
|
95
|
+
planVersions() {
|
|
96
|
+
return this.rpc<PlanVersion[]>("adminBilling", "planVersions")
|
|
97
|
+
}
|
|
98
|
+
models() {
|
|
99
|
+
return this.rpc<ModelInfo[]>("adminBilling", "models")
|
|
100
|
+
}
|
|
101
|
+
userBalance(userId: string) {
|
|
102
|
+
return this.rpc<UserBalance>("adminBilling", "userBalance", { userId })
|
|
103
|
+
}
|
|
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
|
+
|
|
131
|
+
// ── shared types (mirror control-plane function returns) ──────────────────────
|
|
132
|
+
|
|
133
|
+
export type ValueType = "bool" | "json"
|
|
134
|
+
|
|
135
|
+
export interface FlagDef {
|
|
136
|
+
key: string
|
|
137
|
+
description: string
|
|
138
|
+
valueType: ValueType
|
|
139
|
+
enabled: boolean
|
|
140
|
+
defaultValue: unknown
|
|
141
|
+
rolloutPercent: number | null
|
|
142
|
+
planRules: Record<string, unknown> | null
|
|
143
|
+
archivedAt: number | null
|
|
144
|
+
overrideCount: number
|
|
145
|
+
createdAt: number
|
|
146
|
+
updatedAt: number
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface FlagUpsert {
|
|
150
|
+
key: string
|
|
151
|
+
description?: string
|
|
152
|
+
valueType?: ValueType
|
|
153
|
+
enabled?: boolean
|
|
154
|
+
defaultValue?: unknown
|
|
155
|
+
rolloutPercent?: number | null
|
|
156
|
+
planRules?: Record<string, unknown> | null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface FlagOverride {
|
|
160
|
+
flagKey: string
|
|
161
|
+
userId: string
|
|
162
|
+
userEmail: string | null
|
|
163
|
+
userName: string | null
|
|
164
|
+
enabled: boolean
|
|
165
|
+
value: unknown
|
|
166
|
+
note: string
|
|
167
|
+
updatedAt: number
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface UserHit {
|
|
171
|
+
id: string
|
|
172
|
+
email: string
|
|
173
|
+
name: string
|
|
174
|
+
image: string | null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface AdminUser {
|
|
178
|
+
id: string
|
|
179
|
+
email: string
|
|
180
|
+
name: string
|
|
181
|
+
image: string | null
|
|
182
|
+
emailVerified: boolean
|
|
183
|
+
createdAt: number
|
|
184
|
+
updatedAt: number
|
|
185
|
+
activeSessionCount: number
|
|
186
|
+
projectCount: number
|
|
187
|
+
ownedProjectCount: number
|
|
188
|
+
flagOverrideCount: number
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface AdminUserList {
|
|
192
|
+
users: AdminUser[]
|
|
193
|
+
total: number
|
|
194
|
+
limit: number
|
|
195
|
+
offset: number
|
|
196
|
+
query: string
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface PlanVersion {
|
|
200
|
+
planKey: string
|
|
201
|
+
version: number
|
|
202
|
+
status: string
|
|
203
|
+
priceUsd: number
|
|
204
|
+
interval: string
|
|
205
|
+
externalProductId: string
|
|
206
|
+
externalPriceId: string
|
|
207
|
+
createdAt: number
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface ModelInfo {
|
|
211
|
+
id: string
|
|
212
|
+
name?: string
|
|
213
|
+
provider?: string
|
|
214
|
+
[k: string]: unknown
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface UserBalance {
|
|
218
|
+
exists: boolean
|
|
219
|
+
plan: string
|
|
220
|
+
balanceUsd: number
|
|
221
|
+
appCreditsUsd: number
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── bug reports ───────────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
export type BugStatus = "open" | "triaged" | "in_progress" | "resolved" | "wont_fix"
|
|
227
|
+
export type BugSeverity = "low" | "normal" | "high" | "critical"
|
|
228
|
+
|
|
229
|
+
export interface BugReportSummary {
|
|
230
|
+
id: string
|
|
231
|
+
title: string
|
|
232
|
+
severity: BugSeverity
|
|
233
|
+
status: BugStatus
|
|
234
|
+
assignee: string | null
|
|
235
|
+
reporterEmail: string | null
|
|
236
|
+
// null = anonymous app-submitted report → reporterEmail is self-claimed and
|
|
237
|
+
// unverified (the UI marks it). Non-null = dashboard report, email from JWT.
|
|
238
|
+
reporterUserId: string | null
|
|
239
|
+
slug: string | null
|
|
240
|
+
// Root-cause cluster key (null = unclustered). Reports sharing a key are
|
|
241
|
+
// duplicates of one root cause; triage uses it to dedupe.
|
|
242
|
+
clusterKey: string | null
|
|
243
|
+
createdAt: number
|
|
244
|
+
updatedAt: number
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface BugReportList {
|
|
248
|
+
reports: BugReportSummary[]
|
|
249
|
+
counts: Record<string, number>
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export interface BugCluster {
|
|
253
|
+
clusterKey: string
|
|
254
|
+
total: number
|
|
255
|
+
open: number
|
|
256
|
+
// Reports not yet resolved/wont_fix — non-zero means the root cause is still
|
|
257
|
+
// live, so a new duplicate should attach here instead of opening a new fixer.
|
|
258
|
+
unresolved: number
|
|
259
|
+
latestAt: number
|
|
260
|
+
latestTitle: string | null
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export interface BugClusterList {
|
|
264
|
+
clusters: BugCluster[]
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export interface BugReportDetail extends BugReportSummary {
|
|
268
|
+
body: string
|
|
269
|
+
projectId: string | null
|
|
270
|
+
runId: string | null
|
|
271
|
+
snapshotId: string | null
|
|
272
|
+
version: number | null
|
|
273
|
+
pageUrl: string | null
|
|
274
|
+
userAgent: string | null
|
|
275
|
+
adminNotes: string | null
|
|
276
|
+
resolvedAt: number | null
|
|
277
|
+
context: unknown
|
|
278
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// @omg-dev/admin — embeddable internal admin console.
|
|
2
|
+
//
|
|
3
|
+
// A panel-registry React console (Feature Flags, Users, Reports + read-only
|
|
4
|
+
// Pricing/Billing) that drops into any host as a single <AdminConsole/> — the
|
|
5
|
+
// omg dashboard, the LFG app, or Inspect. Host-agnostic: you pass an AdminClient
|
|
6
|
+
// (control-plane base URL + a bearer-token getter); the console does the rest.
|
|
7
|
+
// Self-styled (no Tailwind dependency), and every endpoint it calls is
|
|
8
|
+
// admin-gated server-side.
|
|
9
|
+
//
|
|
10
|
+
// Standalone usage (its own app) lives in "@omg-dev/admin/standalone".
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
AdminConsole,
|
|
14
|
+
FlagsPanel,
|
|
15
|
+
PricingPanel,
|
|
16
|
+
ReportsPanel,
|
|
17
|
+
UsersPanel,
|
|
18
|
+
AdminClient,
|
|
19
|
+
type AdminConsoleProps,
|
|
20
|
+
type AdminClientConfig,
|
|
21
|
+
type PanelDef,
|
|
22
|
+
} from "./react"
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
AdminError,
|
|
26
|
+
type FlagDef,
|
|
27
|
+
type FlagUpsert,
|
|
28
|
+
type FlagOverride,
|
|
29
|
+
type PlanVersion,
|
|
30
|
+
type ModelInfo,
|
|
31
|
+
type UserBalance,
|
|
32
|
+
type UserHit,
|
|
33
|
+
type AdminUser,
|
|
34
|
+
type AdminUserList,
|
|
35
|
+
type ValueType,
|
|
36
|
+
type BugStatus,
|
|
37
|
+
type BugSeverity,
|
|
38
|
+
type BugReportSummary,
|
|
39
|
+
type BugReportList,
|
|
40
|
+
type BugReportDetail,
|
|
41
|
+
} from "./client"
|
|
42
|
+
|
|
43
|
+
export { ensureStyles, STYLE_ID } from "./styles"
|