@cosmicdrift/kumiko-bundled-features 0.224.2 → 0.226.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 (30) hide show
  1. package/package.json +11 -9
  2. package/src/admin-shell/__tests__/admin-shell.boot.test.ts +66 -0
  3. package/src/admin-shell/feature.ts +30 -3
  4. package/src/admin-shell/i18n.ts +2 -0
  5. package/src/cap-overview/__tests__/cap-overview-security.integration.test.ts +207 -0
  6. package/src/cap-overview/__tests__/cap-overview.boot.test.ts +120 -0
  7. package/src/cap-overview/__tests__/usage-math.test.ts +60 -0
  8. package/src/cap-overview/changes.json +8 -0
  9. package/src/cap-overview/constants.ts +26 -0
  10. package/src/cap-overview/feature.ts +77 -0
  11. package/src/cap-overview/handlers/caps-usage.query.ts +99 -0
  12. package/src/cap-overview/handlers/tenant-caps-list.query.ts +230 -0
  13. package/src/cap-overview/handlers/tenant-options.query.ts +33 -0
  14. package/src/cap-overview/i18n.ts +26 -0
  15. package/src/cap-overview/index.ts +20 -0
  16. package/src/cap-overview/screens.ts +101 -0
  17. package/src/cap-overview/types.ts +58 -0
  18. package/src/cap-overview/usage-math.ts +32 -0
  19. package/src/cap-overview/web/__tests__/cap-cards-panel.test.tsx +244 -0
  20. package/src/cap-overview/web/__tests__/cap-usage-bar.test.tsx +76 -0
  21. package/src/cap-overview/web/cap-cards-panel.tsx +109 -0
  22. package/src/cap-overview/web/cap-usage-bar.tsx +47 -0
  23. package/src/cap-overview/web/cap-usage-cell.tsx +20 -0
  24. package/src/cap-overview/web/client-plugin.tsx +27 -0
  25. package/src/cap-overview/web/index.ts +10 -0
  26. package/src/form-draft/__tests__/cleanup.integration.test.ts +51 -20
  27. package/src/notes-history/__tests__/notes-section.test.tsx +22 -11
  28. package/src/notes-history/web/notes-section.tsx +2 -2
  29. package/src/sessions/handlers/detail.query.ts +11 -0
  30. package/src/sessions/handlers/list.query.ts +14 -0
@@ -131,12 +131,12 @@ describe("NotesSection — entry display", () => {
131
131
  test("note body and meta line render as two distinct nodes, not one run-on line", async () => {
132
132
  const view = renderSection();
133
133
  await waitFor(() => {
134
- if (view.queryByTestId("notes-section-row-note-1-body") === null) {
134
+ if (view.queryByTestId("notes-section-body-note-1") === null) {
135
135
  throw new Error("row not rendered yet");
136
136
  }
137
137
  });
138
- const bodyEl = view.getByTestId("notes-section-row-note-1-body");
139
- const metaEl = view.getByTestId("notes-section-row-note-1-meta");
138
+ const bodyEl = view.getByTestId("notes-section-body-note-1");
139
+ const metaEl = view.getByTestId("notes-section-meta-note-1");
140
140
  expect(bodyEl).not.toBe(metaEl);
141
141
  expect(bodyEl.textContent).toBe(NOTE_BODY);
142
142
  expect(metaEl.textContent).not.toContain(NOTE_BODY);
@@ -145,14 +145,25 @@ describe("NotesSection — entry display", () => {
145
145
  expect(row.className).toContain("flex-col");
146
146
  });
147
147
 
148
+ test("the row testid prefix matches exactly one node per note, not three", async () => {
149
+ const view = renderSection();
150
+ await waitFor(() => {
151
+ if (view.queryByTestId("notes-section-row-note-1") === null) {
152
+ throw new Error("rows not rendered yet");
153
+ }
154
+ });
155
+ const matches = view.container.querySelectorAll('[data-testid^="notes-section-row-"]');
156
+ expect(matches.length).toBe(NOTE_ROWS.length);
157
+ });
158
+
148
159
  test("a stamped authorName renders as the author, never the raw authorId", async () => {
149
160
  const view = renderSection();
150
161
  await waitFor(() => {
151
- if (view.queryByTestId("notes-section-row-note-1-meta") === null) {
162
+ if (view.queryByTestId("notes-section-meta-note-1") === null) {
152
163
  throw new Error("row not rendered yet");
153
164
  }
154
165
  });
155
- const metaEl = view.getByTestId("notes-section-row-note-1-meta");
166
+ const metaEl = view.getByTestId("notes-section-meta-note-1");
156
167
  expect(metaEl.textContent).not.toContain(AUTHOR_ID);
157
168
  expect(metaEl.textContent).toContain(AUTHOR_NAME);
158
169
  });
@@ -160,22 +171,22 @@ describe("NotesSection — entry display", () => {
160
171
  test("a null authorName (legacy row) renders the placeholder", async () => {
161
172
  const view = renderSection();
162
173
  await waitFor(() => {
163
- if (view.queryByTestId("notes-section-row-note-2-meta") === null) {
174
+ if (view.queryByTestId("notes-section-meta-note-2") === null) {
164
175
  throw new Error("row not rendered yet");
165
176
  }
166
177
  });
167
- const metaEl = view.getByTestId("notes-section-row-note-2-meta");
178
+ const metaEl = view.getByTestId("notes-section-meta-note-2");
168
179
  expect(metaEl.textContent).toContain("Unknown author");
169
180
  });
170
181
 
171
182
  test("a shredded authorName sentinel renders the placeholder, never [[erased]]", async () => {
172
183
  const view = renderSection();
173
184
  await waitFor(() => {
174
- if (view.queryByTestId("notes-section-row-note-3-meta") === null) {
185
+ if (view.queryByTestId("notes-section-meta-note-3") === null) {
175
186
  throw new Error("row not rendered yet");
176
187
  }
177
188
  });
178
- const metaEl = view.getByTestId("notes-section-row-note-3-meta");
189
+ const metaEl = view.getByTestId("notes-section-meta-note-3");
179
190
  expect(metaEl.textContent).toContain("Unknown author");
180
191
  expect(metaEl.textContent).not.toContain(PII_ERASED_SENTINEL);
181
192
  });
@@ -183,11 +194,11 @@ describe("NotesSection — entry display", () => {
183
194
  test("date renders formatted, never the raw ISO timestamp", async () => {
184
195
  const view = renderSection();
185
196
  await waitFor(() => {
186
- if (view.queryByTestId("notes-section-row-note-1-meta") === null) {
197
+ if (view.queryByTestId("notes-section-meta-note-1") === null) {
187
198
  throw new Error("row not rendered yet");
188
199
  }
189
200
  });
190
- const metaEl = view.getByTestId("notes-section-row-note-1-meta");
201
+ const metaEl = view.getByTestId("notes-section-meta-note-1");
191
202
  expect(metaEl.textContent).not.toContain(INSERTED_AT);
192
203
  expect(metaEl.textContent).toContain(formatWhen(INSERTED_AT));
193
204
  });
@@ -149,8 +149,8 @@ export function NotesSection({
149
149
  data-testid={`notes-section-row-${n.id}`}
150
150
  className="flex flex-col gap-1 rounded-md border p-2"
151
151
  >
152
- <Text testId={`notes-section-row-${n.id}-body`}>{n.body}</Text>
153
- <Text variant="small" testId={`notes-section-row-${n.id}-meta`}>
152
+ <Text testId={`notes-section-body-${n.id}`}>{n.body}</Text>
153
+ <Text variant="small" testId={`notes-section-meta-${n.id}`}>
154
154
  {t("notesHistory.section.meta", {
155
155
  author: displayAuthorName(n.authorName, t("notesHistory.section.authorUnknown")),
156
156
  date: formatWhen(n.insertedAt),
@@ -6,10 +6,21 @@ import { userSessionTable } from "../schema/user-session";
6
6
 
7
7
  // Admin single-session inspector — mirrors list.query's decrypt handling for
8
8
  // the one-row case. ctx.db (TenantDb) applies tenant-scoping automatically.
9
+ const sessionRowSchema = z.object({
10
+ id: z.string(),
11
+ userId: z.string(),
12
+ createdAt: z.unknown(),
13
+ expiresAt: z.unknown(),
14
+ revokedAt: z.unknown(),
15
+ ip: z.string().nullable(),
16
+ userAgent: z.string().nullable(),
17
+ });
18
+
9
19
  export const detailQuery = defineQueryHandler({
10
20
  name: "user-session:detail",
11
21
  schema: z.object({ id: z.uuid() }),
12
22
  access: { roles: access.admin },
23
+ outputSchema: sessionRowSchema.nullable(),
13
24
  handler: async (query, ctx) => {
14
25
  const row = await fetchOne<{
15
26
  id: string;
@@ -33,6 +33,20 @@ export const listQuery = definePagedQueryHandler({
33
33
  sortDirection: z.enum(["asc", "desc"]).optional(),
34
34
  }),
35
35
  access: { roles: access.admin },
36
+ outputSchema: z.object({
37
+ rows: z.array(
38
+ z.object({
39
+ id: z.string(),
40
+ userId: z.string(),
41
+ createdAt: z.unknown(),
42
+ expiresAt: z.unknown(),
43
+ revokedAt: z.unknown(),
44
+ ip: z.string().nullable(),
45
+ userAgent: z.string().nullable(),
46
+ }),
47
+ ),
48
+ nextCursor: z.string().nullable(),
49
+ }),
36
50
  handler: async (query, ctx) => {
37
51
  const requestedSort = query.payload.sort;
38
52
  const sortColumn: SortableColumn =