@ultimat3/admin 22.15.0 → 24.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/CLAUDE.md +83 -91
  2. package/README.md +282 -89
  3. package/package.json +19 -17
  4. package/src/action-form.tsx +139 -0
  5. package/src/action-gate.ts +141 -68
  6. package/src/action-input.ts +119 -0
  7. package/src/action-label.ts +9 -0
  8. package/src/action-row.ts +37 -0
  9. package/src/actions.tsx +89 -66
  10. package/src/actor.ts +53 -0
  11. package/src/admin.module.scss +298 -0
  12. package/src/admin.ts +187 -23
  13. package/src/audit-pg.ts +207 -0
  14. package/src/audit-schema.ts +47 -0
  15. package/src/audit.ts +109 -22
  16. package/src/authz.ts +11 -0
  17. package/src/batch-bar.tsx +66 -0
  18. package/src/batch-job.ts +152 -0
  19. package/src/batch-matching.ts +87 -0
  20. package/src/batch-queue.ts +90 -0
  21. package/src/batch.ts +396 -0
  22. package/src/catalog-keys.ts +55 -0
  23. package/src/crud-input.ts +104 -0
  24. package/src/crud-outcome.ts +224 -0
  25. package/src/crud.ts +172 -176
  26. package/src/describe.ts +131 -0
  27. package/src/detail.tsx +115 -43
  28. package/src/dev/data.ts +27 -12
  29. package/src/dev/facts.ts +28 -1
  30. package/src/dev/index.ts +1 -0
  31. package/src/dev/panel-cache.ts +8 -3
  32. package/src/dev/panel-db.ts +37 -5
  33. package/src/dev/panel-jobs.ts +8 -0
  34. package/src/dev/panel-routes.ts +10 -0
  35. package/src/dev/panel-timeline.ts +7 -2
  36. package/src/dev/panel.ts +10 -2
  37. package/src/dev/server.ts +16 -11
  38. package/src/dev/sql-runner.ts +31 -0
  39. package/src/entity-columns.ts +24 -5
  40. package/src/errors.ts +92 -5
  41. package/src/fields.ts +63 -4
  42. package/src/form-decode.ts +132 -0
  43. package/src/form.tsx +96 -49
  44. package/src/index.ts +199 -9
  45. package/src/jobs/fleet-repo.ts +155 -0
  46. package/src/jobs/job-actions.ts +172 -0
  47. package/src/jobs/job-entities.ts +118 -0
  48. package/src/jobs/job-labels.ts +108 -0
  49. package/src/jobs/job-repo.ts +137 -0
  50. package/src/jobs/job-resources.ts +211 -0
  51. package/src/jobs/job-where.ts +170 -0
  52. package/src/jobs/operator.ts +34 -0
  53. package/src/jobs/overview-data.ts +129 -0
  54. package/src/jobs/overview-tab.tsx +61 -0
  55. package/src/jobs/overview.tsx +189 -0
  56. package/src/json-text.ts +52 -0
  57. package/src/keyset-seek.ts +189 -0
  58. package/src/layout.tsx +55 -43
  59. package/src/list-columns.tsx +53 -0
  60. package/src/list-filter-bar.tsx +207 -0
  61. package/src/list-filters.ts +242 -0
  62. package/src/list-request.ts +129 -0
  63. package/src/list-scope-tabs.tsx +60 -0
  64. package/src/list-scope.ts +113 -0
  65. package/src/list.tsx +111 -72
  66. package/src/mcp-tools.ts +57 -13
  67. package/src/mcp.ts +112 -38
  68. package/src/mounts.ts +44 -0
  69. package/src/page-guard.tsx +26 -29
  70. package/src/pagination.ts +10 -4
  71. package/src/policy-bridge.ts +115 -18
  72. package/src/registry.ts +140 -9
  73. package/src/related.ts +105 -0
  74. package/src/relations.ts +240 -0
  75. package/src/renderers.tsx +89 -0
  76. package/src/repo-entity.ts +195 -0
  77. package/src/resource-fields.ts +116 -0
  78. package/src/resource-layout.ts +89 -0
  79. package/src/resource-list.ts +194 -0
  80. package/src/resource.ts +129 -79
  81. package/src/route-config.ts +71 -0
  82. package/src/routes.ts +55 -54
  83. package/src/row-scope-write.ts +130 -0
  84. package/src/row-version.ts +20 -0
  85. package/src/screen-action.tsx +333 -0
  86. package/src/screen-frame.tsx +167 -0
  87. package/src/screen-home.tsx +201 -0
  88. package/src/screen-lookup.tsx +157 -0
  89. package/src/screen-resource.tsx +441 -0
  90. package/src/screen-system.tsx +75 -0
  91. package/src/screens.ts +56 -0
  92. package/src/search.ts +22 -10
  93. package/src/stylesheet-scope.ts +8 -0
  94. package/src/test-ctx.ts +31 -0
  95. package/src/validate.ts +3 -0
  96. package/src/widget-value.ts +36 -8
  97. package/src/widgets.tsx +84 -16
@@ -0,0 +1,224 @@
1
+ // The audit half of a CRUD call that did NOT write: a refusal, an input the schema rejected, a
2
+ // row that was not there, a repo that threw. Each leaves its entry and answers the result the
3
+ // caller renders — so `crud.ts` reads as the five steps of each operation and nothing else.
4
+
5
+ import {
6
+ type AuditDraft,
7
+ type AuditEntry,
8
+ type AuditFieldDiff,
9
+ type AuditLog,
10
+ deniedDraft,
11
+ } from './audit';
12
+ import type { AdminDecision } from './authz';
13
+ import type { CrudCtx, CrudResult } from './crud';
14
+ import type { AdminOperation } from './permissions';
15
+ import type { AdminRow } from './registry';
16
+ import type { AdminResource } from './resource';
17
+ import { ROW_CHANGED_REASON } from './row-version';
18
+ import type { ValidationIssue } from './validate';
19
+
20
+ export const deniedEntry = (
21
+ resource: { readonly name: string },
22
+ op: AdminOperation,
23
+ ctx: CrudCtx,
24
+ decision: AdminDecision,
25
+ id: string | null,
26
+ ) =>
27
+ deniedDraft({
28
+ requestId: ctx.requestId,
29
+ actor: ctx.actor,
30
+ operation: op,
31
+ kind: 'operation',
32
+ entity: resource.name,
33
+ entityId: id,
34
+ decision,
35
+ });
36
+
37
+ export async function refuse<Row extends AdminRow>(
38
+ resource: AdminResource<Row>,
39
+ op: AdminOperation,
40
+ ctx: CrudCtx,
41
+ decision: AdminDecision,
42
+ id: string | null,
43
+ confirmationRequired = false,
44
+ ): Promise<CrudResult<Row>> {
45
+ return {
46
+ ok: false,
47
+ kind: 'denied',
48
+ decision,
49
+ confirmationRequired,
50
+ audit: await ctx.audit.append(deniedEntry(resource, op, ctx, decision, id)),
51
+ };
52
+ }
53
+
54
+ /** The reason on a `failed` entry. A key, never the database's message. */
55
+ const WRITE_FAILED_REASON = 'admin.audit.write-failed';
56
+
57
+ /** What every entry of one audited write shares: who, what, on which table, under which grant. */
58
+ export type AuditedEntry = Pick<
59
+ AuditDraft,
60
+ 'requestId' | 'actor' | 'operation' | 'kind' | 'entity' | 'permission'
61
+ >;
62
+
63
+ /**
64
+ * Run a WRITE and append its entry as one unit where the log can make it one, and leave a `failed`
65
+ * entry behind if either throws — the one shape for a CRUD write, an action's handler, a set-based
66
+ * batch and a queued one.
67
+ *
68
+ * A mutation cannot append BEFORE the call the way a read does — that would record a write which
69
+ * may never have happened. So the write and its entry go inside `audit.atomic`: a durable log
70
+ * opens a transaction, so a row never changes without the entry naming who changed it, and an
71
+ * entry never describes a write that rolled back. A sink that throws on the `allowed` entry rolls
72
+ * the write back with it; the log then says `failed`, and so does the caller's error — it used to
73
+ * say `failed` for an action that had committed, and an operator re-ran it.
74
+ *
75
+ * The `failed` entry is written OUTSIDE, after the rollback — inside, it would be rolled back with
76
+ * the write it reports. Nothing about the thrown value is read or rendered (a `catch` binding holds
77
+ * whatever an app threw, and `String()` of a prototype-less object throws from inside the block
78
+ * that owes the auditor its entry); the caller gets it UNCHANGED, and an audit reason is a key.
79
+ */
80
+ export async function atomicallyAudited<T>(
81
+ audit: AuditLog,
82
+ entry: AuditedEntry,
83
+ reasons: { readonly allowed: string; readonly failed: string },
84
+ /** The id of the row the write addresses, for the `failed` entry: `null` for a set or a create. */
85
+ entityId: string | null,
86
+ write: () => Promise<T>,
87
+ /** The diff of what the write did, and the id of the row it left — known only afterwards. */
88
+ written: (value: T) => {
89
+ readonly entityId: string | null;
90
+ readonly diff: readonly AuditFieldDiff[];
91
+ },
92
+ ): Promise<{ readonly value: T; readonly audit: AuditEntry }> {
93
+ try {
94
+ return await audit.atomic(async () => {
95
+ const value = await write();
96
+ const appended = await audit.append({
97
+ ...entry,
98
+ ...written(value),
99
+ outcome: 'allowed',
100
+ reason: reasons.allowed,
101
+ });
102
+ return { value, audit: appended };
103
+ });
104
+ } catch (error) {
105
+ await audit.append({ ...entry, entityId, outcome: 'failed', reason: reasons.failed, diff: [] });
106
+ throw error;
107
+ }
108
+ }
109
+
110
+ /** A repo WRITE of one CRUD operation, through `atomicallyAudited`. */
111
+ export function auditedWrite<T>(
112
+ // Only the NAME is read, so this stays invariance-free: `AdminResource<Row>` at four call
113
+ // sites would need the generic threaded through for nothing.
114
+ resource: { readonly name: string },
115
+ op: AdminOperation,
116
+ ctx: CrudCtx,
117
+ entityId: string | null,
118
+ decision: AdminDecision,
119
+ write: () => Promise<T>,
120
+ written: (value: T) => {
121
+ readonly entityId: string | null;
122
+ readonly diff: readonly AuditFieldDiff[];
123
+ },
124
+ ): Promise<{ readonly value: T; readonly audit: AuditEntry }> {
125
+ return atomicallyAudited(
126
+ ctx.audit,
127
+ {
128
+ requestId: ctx.requestId,
129
+ actor: ctx.actor,
130
+ operation: op,
131
+ kind: 'operation',
132
+ entity: resource.name,
133
+ permission: decision.permission,
134
+ },
135
+ { allowed: decision.reason, failed: WRITE_FAILED_REASON },
136
+ entityId,
137
+ write,
138
+ written,
139
+ );
140
+ }
141
+
142
+ /** The key a write to a row that is not there is logged with. */
143
+ const ROW_MISSING_REASON = 'admin.error.row-missing';
144
+
145
+ export async function missingRow<Row extends AdminRow>(
146
+ resource: AdminResource<Row>,
147
+ op: AdminOperation,
148
+ ctx: CrudCtx,
149
+ id: string,
150
+ decision: AdminDecision,
151
+ ): Promise<CrudResult<Row>> {
152
+ return {
153
+ ok: false,
154
+ kind: 'missing',
155
+ audit: await ctx.audit.append({
156
+ requestId: ctx.requestId,
157
+ actor: ctx.actor,
158
+ operation: op,
159
+ kind: 'operation',
160
+ entity: resource.name,
161
+ entityId: id,
162
+ permission: decision.permission,
163
+ outcome: 'failed',
164
+ reason: ROW_MISSING_REASON,
165
+ diff: [],
166
+ }),
167
+ };
168
+ }
169
+
170
+ /** An update against a version the row no longer has — `crud.ts`'s optimistic check. */
171
+ export async function staleRow<Row extends AdminRow>(
172
+ resource: AdminResource<Row>,
173
+ ctx: CrudCtx,
174
+ id: string,
175
+ decision: AdminDecision,
176
+ row: Row,
177
+ version: string,
178
+ ): Promise<CrudResult<Row>> {
179
+ return {
180
+ ok: false,
181
+ kind: 'stale',
182
+ row,
183
+ version,
184
+ audit: await ctx.audit.append({
185
+ requestId: ctx.requestId,
186
+ actor: ctx.actor,
187
+ operation: 'update',
188
+ kind: 'operation',
189
+ entity: resource.name,
190
+ entityId: id,
191
+ permission: decision.permission,
192
+ outcome: 'failed',
193
+ reason: ROW_CHANGED_REASON,
194
+ diff: [],
195
+ }),
196
+ };
197
+ }
198
+
199
+ export async function invalid<Row extends AdminRow>(
200
+ resource: AdminResource<Row>,
201
+ op: AdminOperation,
202
+ ctx: CrudCtx,
203
+ id: string | null,
204
+ issues: readonly ValidationIssue[],
205
+ decision: AdminDecision,
206
+ ): Promise<CrudResult<Row>> {
207
+ return {
208
+ ok: false,
209
+ kind: 'invalid',
210
+ issues,
211
+ audit: await ctx.audit.append({
212
+ requestId: ctx.requestId,
213
+ actor: ctx.actor,
214
+ operation: op,
215
+ kind: 'operation',
216
+ entity: resource.name,
217
+ entityId: id,
218
+ permission: decision.permission,
219
+ outcome: 'failed',
220
+ reason: 'admin.error.invalid-input',
221
+ diff: [],
222
+ }),
223
+ };
224
+ }