@polderlabs/bizar 4.5.2 → 4.7.1

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 (123) hide show
  1. package/bizar-dash/dist/assets/main-DHZmbnxQ.js +361 -0
  2. package/bizar-dash/dist/assets/main-DHZmbnxQ.js.map +1 -0
  3. package/bizar-dash/dist/assets/main-DX_Jh8Wc.css +1 -0
  4. package/bizar-dash/dist/assets/{mobile-lbH6szyX.js → mobile-BK8-ythT.js} +18 -18
  5. package/bizar-dash/dist/assets/mobile-BK8-ythT.js.map +1 -0
  6. package/bizar-dash/dist/assets/{mobile-BRhoDOUz.js → mobile-Chvf9u_B.js} +1 -1
  7. package/bizar-dash/dist/assets/{mobile-BRhoDOUz.js.map → mobile-Chvf9u_B.js.map} +1 -1
  8. package/bizar-dash/dist/index.html +3 -3
  9. package/bizar-dash/dist/mobile.html +2 -2
  10. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  11. package/bizar-dash/skills/publishing/SKILL.md +146 -0
  12. package/bizar-dash/src/server/api.mjs +8 -0
  13. package/bizar-dash/src/server/backup-store.mjs +525 -0
  14. package/bizar-dash/src/server/digest-store.mjs +558 -0
  15. package/bizar-dash/src/server/lib/rate-limit.mjs +122 -0
  16. package/bizar-dash/src/server/logger.mjs +71 -0
  17. package/bizar-dash/src/server/memory-lightrag.mjs +45 -6
  18. package/bizar-dash/src/server/metrics.mjs +193 -0
  19. package/bizar-dash/src/server/routes/backup.mjs +112 -0
  20. package/bizar-dash/src/server/routes/chat.mjs +20 -3
  21. package/bizar-dash/src/server/routes/digests.mjs +82 -0
  22. package/bizar-dash/src/server/routes/lightrag.mjs +3 -2
  23. package/bizar-dash/src/server/routes/memory.mjs +5 -4
  24. package/bizar-dash/src/server/routes/misc.mjs +2 -1
  25. package/bizar-dash/src/server/routes/overview.mjs +2 -1
  26. package/bizar-dash/src/server/routes-v2/events.mjs +14 -0
  27. package/bizar-dash/src/server/schedules-runner.mjs +126 -0
  28. package/bizar-dash/src/server/server.mjs +79 -0
  29. package/bizar-dash/src/web/App.tsx +8 -1
  30. package/bizar-dash/src/web/components/BackupRestore.tsx +330 -0
  31. package/bizar-dash/src/web/components/SearchModal.tsx +6 -3
  32. package/bizar-dash/src/web/components/VirtualList.tsx +53 -0
  33. package/bizar-dash/src/web/components/chat/ChatThread.tsx +17 -11
  34. package/bizar-dash/src/web/components/chat/Composer.tsx +2 -0
  35. package/bizar-dash/src/web/hooks/useI18n.ts +13 -0
  36. package/bizar-dash/src/web/lib/i18n.ts +25 -0
  37. package/bizar-dash/src/web/locales/en.json +52 -0
  38. package/bizar-dash/src/web/main.tsx +5 -0
  39. package/bizar-dash/src/web/styles/main.css +70 -8
  40. package/bizar-dash/src/web/views/Activity.tsx +35 -18
  41. package/bizar-dash/src/web/views/Agents.tsx +57 -42
  42. package/bizar-dash/src/web/views/Artifacts.tsx +38 -25
  43. package/bizar-dash/src/web/views/Chat.tsx +8 -0
  44. package/bizar-dash/src/web/views/History.tsx +94 -76
  45. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +21 -4
  46. package/bizar-dash/src/web/views/Mods.tsx +30 -17
  47. package/bizar-dash/src/web/views/Overview.tsx +19 -9
  48. package/bizar-dash/src/web/views/Providers.tsx +16 -16
  49. package/bizar-dash/src/web/views/Schedules.tsx +33 -15
  50. package/bizar-dash/src/web/views/Settings.tsx +97 -1745
  51. package/bizar-dash/src/web/views/Skills.tsx +4 -1
  52. package/bizar-dash/src/web/views/Tasks.tsx +11 -2
  53. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +8 -2
  54. package/bizar-dash/src/web/views/memory/LightragPanel.tsx +3 -0
  55. package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +12 -4
  56. package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +3 -0
  57. package/bizar-dash/src/web/views/settings/ActivitySection.tsx +205 -0
  58. package/bizar-dash/src/web/views/settings/AgentSection.tsx +294 -0
  59. package/bizar-dash/src/web/views/settings/AuthSection.tsx +159 -0
  60. package/bizar-dash/src/web/views/settings/BackupSection.tsx +16 -0
  61. package/bizar-dash/src/web/views/settings/EnvVarsSection.tsx +16 -0
  62. package/bizar-dash/src/web/views/settings/GeneralSection.tsx +105 -0
  63. package/bizar-dash/src/web/views/settings/HeadroomSection.tsx +39 -0
  64. package/bizar-dash/src/web/views/settings/MemorySection.tsx +16 -0
  65. package/bizar-dash/src/web/views/settings/NetworkSection.tsx +87 -0
  66. package/bizar-dash/src/web/views/settings/NotificationsSection.tsx +34 -0
  67. package/bizar-dash/src/web/views/settings/ProvidersSection.tsx +16 -0
  68. package/bizar-dash/src/web/views/settings/SkillsSection.tsx +16 -0
  69. package/bizar-dash/src/web/views/settings/SystemLlmSection.tsx +81 -0
  70. package/bizar-dash/src/web/views/settings/ThemeSection.tsx +168 -0
  71. package/bizar-dash/src/web/views/settings/UpdatesSection.tsx +256 -0
  72. package/bizar-dash/tests/a11y.test.tsx +206 -0
  73. package/bizar-dash/tests/backup-restore.test.mjs +217 -0
  74. package/bizar-dash/tests/backup-restore.test.tsx +123 -0
  75. package/bizar-dash/tests/backup-store.test.mjs +300 -0
  76. package/bizar-dash/tests/cli-bugfixes.test.mjs +4 -4
  77. package/bizar-dash/tests/cli-error-visibility.test.mjs +153 -0
  78. package/bizar-dash/tests/cli-refactor.test.mjs +184 -0
  79. package/bizar-dash/tests/components/Button.test.tsx +41 -0
  80. package/bizar-dash/tests/components/Card.test.tsx +42 -0
  81. package/bizar-dash/tests/components/Modal.test.tsx +104 -0
  82. package/bizar-dash/tests/components/Spinner.test.tsx +32 -0
  83. package/bizar-dash/tests/components/StatusBadge.test.tsx +35 -0
  84. package/bizar-dash/tests/components/Toast.test.tsx +108 -0
  85. package/bizar-dash/tests/digest-generation.test.mjs +191 -0
  86. package/bizar-dash/tests/digest-store.test.mjs +264 -0
  87. package/bizar-dash/tests/hooks/useModal.test.tsx +84 -0
  88. package/bizar-dash/tests/hooks/useToast.test.tsx +50 -0
  89. package/bizar-dash/tests/lib/i18n.test.ts +46 -0
  90. package/bizar-dash/tests/lib/utils.test.ts +194 -0
  91. package/bizar-dash/tests/logger.test.mjs +207 -0
  92. package/bizar-dash/tests/metrics.test.mjs +183 -0
  93. package/bizar-dash/tests/rate-limit.test.mjs +298 -0
  94. package/bizar-dash/tests/server-bugfixes.test.mjs +2 -2
  95. package/bizar-dash/tests/setup.ts +7 -0
  96. package/bizar-dash/vitest.config.ts +13 -0
  97. package/cli/artifact-cli.mjs +605 -0
  98. package/cli/artifact-render.mjs +621 -0
  99. package/cli/artifact-server.mjs +847 -0
  100. package/cli/artifact.mjs +38 -2096
  101. package/cli/bg.mjs +5 -13
  102. package/cli/bin.mjs +221 -1350
  103. package/cli/commands/artifact.mjs +20 -0
  104. package/cli/commands/dash.mjs +160 -0
  105. package/cli/commands/headroom.mjs +204 -0
  106. package/cli/commands/install.mjs +169 -0
  107. package/cli/commands/memory.mjs +25 -0
  108. package/cli/commands/minimax.mjs +285 -0
  109. package/cli/commands/mod.mjs +185 -0
  110. package/cli/commands/service.mjs +65 -0
  111. package/cli/commands/usage.mjs +109 -0
  112. package/cli/commands/util.mjs +459 -0
  113. package/cli/digest.mjs +149 -0
  114. package/cli/doctor.mjs +1 -13
  115. package/cli/provision.mjs +2 -13
  116. package/cli/service-controller.mjs +1 -11
  117. package/cli/service.mjs +1 -11
  118. package/cli/utils.mjs +41 -1
  119. package/package.json +6 -1
  120. package/bizar-dash/dist/assets/main-B4OfGAwz.js +0 -361
  121. package/bizar-dash/dist/assets/main-B4OfGAwz.js.map +0 -1
  122. package/bizar-dash/dist/assets/main-DAlLdW8I.css +0 -1
  123. package/bizar-dash/dist/assets/mobile-lbH6szyX.js.map +0 -1
@@ -191,13 +191,16 @@ function SkillsInner({ snapshot, refreshSnapshot }: Props) {
191
191
  </div>
192
192
  <div className="view-actions">
193
193
  <div className="search-input">
194
- <Search size={14} />
194
+ <Search size={14} aria-hidden />
195
+ <label htmlFor="skills-search" className="sr-only">Search skills</label>
195
196
  <input
197
+ id="skills-search"
196
198
  className="input"
197
199
  type="text"
198
200
  placeholder="Search skills…"
199
201
  value={searchQ}
200
202
  onChange={(e) => setSearchQ(e.target.value)}
203
+ aria-label="Search skills"
201
204
  />
202
205
  {searchQ && (
203
206
  <button
@@ -85,6 +85,7 @@ function TasksInner({ snapshot, refreshSnapshot, setActiveTab }: Props) {
85
85
  const [priorityFilter, setPriorityFilter] = useState<string>('');
86
86
  const [showBacklog, setShowBacklog] = useState(false);
87
87
  const [tick, setTick] = useState(0); // for live timer re-render
88
+ const [statusAnnouncement, setStatusAnnouncement] = useState('');
88
89
 
89
90
  const reload = async () => {
90
91
  try {
@@ -146,6 +147,7 @@ function TasksInner({ snapshot, refreshSnapshot, setActiveTab }: Props) {
146
147
  setTasks((cur) =>
147
148
  cur.map((x) => (x.id === taskId ? { ...x, status: newStatus } : x)),
148
149
  );
150
+ setStatusAnnouncement(`Task ${t.title} moved to ${newStatus}.`);
149
151
  try {
150
152
  await api.patch(`/tasks/${encodeURIComponent(taskId)}/status`, {
151
153
  status: newStatus,
@@ -155,6 +157,7 @@ function TasksInner({ snapshot, refreshSnapshot, setActiveTab }: Props) {
155
157
  setTasks((cur) =>
156
158
  cur.map((x) => (x.id === taskId ? { ...x, status: prev } : x)),
157
159
  );
160
+ setStatusAnnouncement(`Failed to move task ${t.title}: ${(err as Error).message}`);
158
161
  toast.error(`Move failed: ${(err as Error).message}`);
159
162
  }
160
163
  };
@@ -217,6 +220,10 @@ function TasksInner({ snapshot, refreshSnapshot, setActiveTab }: Props) {
217
220
 
218
221
  return (
219
222
  <div className="view view-tasks">
223
+ {/* Hidden live region for screen-reader announcements of status changes. */}
224
+ <div className="sr-only" role="status" aria-live="polite">
225
+ {statusAnnouncement}
226
+ </div>
220
227
  <header className="view-header">
221
228
  <div className="view-header-text">
222
229
  <h2 className="view-title">
@@ -232,13 +239,14 @@ function TasksInner({ snapshot, refreshSnapshot, setActiveTab }: Props) {
232
239
  <div className="tasks-toolbar">
233
240
  <div className="tasks-toolbar-group">
234
241
  <div className="search-input" style={{ width: 200 }}>
235
- <Search size={12} />
242
+ <Search size={12} aria-hidden />
236
243
  <input
237
244
  className="input"
238
245
  type="text"
239
246
  placeholder="Search…"
240
247
  value={filter}
241
248
  onChange={(e) => setFilter(e.target.value)}
249
+ aria-label="Search tasks"
242
250
  />
243
251
  {filter && (
244
252
  <button
@@ -256,8 +264,9 @@ function TasksInner({ snapshot, refreshSnapshot, setActiveTab }: Props) {
256
264
  <div className="tasks-toolbar-divider" />
257
265
 
258
266
  <div className="tasks-toolbar-group">
259
- <span className="tasks-toolbar-label">Priority</span>
267
+ <label className="tasks-toolbar-label" htmlFor="tasks-priority-filter">Priority</label>
260
268
  <select
269
+ id="tasks-priority-filter"
261
270
  className="select select-sm"
262
271
  value={priorityFilter}
263
272
  onChange={(e) => setPriorityFilter(e.target.value)}
@@ -274,8 +274,14 @@ function Row({
274
274
  if (inline) {
275
275
  return (
276
276
  <div className="memory-config-row-inline">
277
- <div className="memory-config-row-label">{label}</div>
278
- <div className="memory-config-row-control">{children}</div>
277
+ <div className="memory-config-row-label" id={`cfg-${label.replace(/\s+/g, '-').toLowerCase()}`}>{label}</div>
278
+ <div
279
+ className="memory-config-row-control"
280
+ role="group"
281
+ aria-labelledby={`cfg-${label.replace(/\s+/g, '-').toLowerCase()}`}
282
+ >
283
+ {children}
284
+ </div>
279
285
  </div>
280
286
  );
281
287
  }
@@ -216,7 +216,9 @@ export function LightragPanel({ refreshKey }: Props) {
216
216
  </CardTitle>
217
217
  <CardMeta>Send a natural-language query to the LightRAG index.</CardMeta>
218
218
  <div className="memory-search-row">
219
+ <label htmlFor="lightrag-query" className="sr-only">Search LightRAG</label>
219
220
  <input
221
+ id="lightrag-query"
220
222
  type="text"
221
223
  className="input"
222
224
  placeholder="e.g. how does the memory service write notes?"
@@ -226,6 +228,7 @@ export function LightragPanel({ refreshKey }: Props) {
226
228
  if (e.key === 'Enter') onQuery();
227
229
  }}
228
230
  disabled={busy === 'query'}
231
+ aria-label="Search LightRAG"
229
232
  />
230
233
  <Button variant="primary" onClick={onQuery} disabled={busy === 'query' || !query.trim()}>
231
234
  {busy === 'query' ? <Loader2 size={12} className="memory-spin" /> : <SearchIcon size={12} />}
@@ -222,13 +222,16 @@ export function ObsidianPanel({ refreshKey }: Props) {
222
222
  <div className="memory-obsidian-list-col">
223
223
  <div className="memory-list-toolbar">
224
224
  <div className="search-input">
225
- <SearchIcon size={14} />
225
+ <SearchIcon size={14} aria-hidden />
226
+ <label htmlFor="obsidian-search" className="sr-only">Search notes</label>
226
227
  <input
228
+ id="obsidian-search"
227
229
  type="text"
228
230
  className="input"
229
231
  placeholder="Search notes…"
230
232
  value={searchQ}
231
233
  onChange={(e) => setSearchQ(e.target.value)}
234
+ aria-label="Search notes"
232
235
  />
233
236
  {searchQ && (
234
237
  <button
@@ -519,8 +522,9 @@ function NoteEditor({
519
522
 
520
523
  return (
521
524
  <div className="memory-editor-modal">
522
- <label className="field-label">Path</label>
525
+ <label className="field-label" htmlFor="obsidian-note-path">Path</label>
523
526
  <input
527
+ id="obsidian-note-path"
524
528
  type="text"
525
529
  className="input mono"
526
530
  value={relPath}
@@ -528,19 +532,23 @@ function NoteEditor({
528
532
  disabled={mode === 'edit'}
529
533
  placeholder="notes/example.md"
530
534
  />
531
- <label className="field-label" style={{ marginTop: 12 }}>Frontmatter (YAML)</label>
535
+ <label className="field-label" htmlFor="obsidian-note-frontmatter" style={{ marginTop: 12 }}>Frontmatter (YAML)</label>
532
536
  <textarea
537
+ id="obsidian-note-frontmatter"
533
538
  className="memory-editor-textarea mono text-sm"
534
539
  rows={6}
535
540
  value={frontmatterYaml}
536
541
  onChange={(e) => setFrontmatterYaml(e.target.value)}
542
+ aria-label="Frontmatter YAML"
537
543
  />
538
- <label className="field-label" style={{ marginTop: 12 }}>Body (Markdown)</label>
544
+ <label className="field-label" htmlFor="obsidian-note-body" style={{ marginTop: 12 }}>Body (Markdown)</label>
539
545
  <textarea
546
+ id="obsidian-note-body"
540
547
  className="memory-editor-textarea mono text-sm"
541
548
  rows={14}
542
549
  value={body}
543
550
  onChange={(e) => setBody(e.target.value)}
551
+ aria-label="Body markdown"
544
552
  />
545
553
  <div className="modal-footer-actions" style={{ marginTop: 12 }}>
546
554
  <Button variant="ghost" onClick={onCancel} disabled={busy}>Cancel</Button>
@@ -77,7 +77,9 @@ export function SemanticSearchPanel({ refreshKey }: Props) {
77
77
  </CardTitle>
78
78
  <CardMeta>Query both LightRAG (graph-based) and the Obsidian vault in one shot.</CardMeta>
79
79
  <div className="memory-search-row">
80
+ <label htmlFor="semantic-search-input" className="sr-only">Semantic search</label>
80
81
  <input
82
+ id="semantic-search-input"
81
83
  type="text"
82
84
  className="input"
83
85
  placeholder="e.g. how are notes indexed?"
@@ -87,6 +89,7 @@ export function SemanticSearchPanel({ refreshKey }: Props) {
87
89
  if (e.key === 'Enter') onSearch(q);
88
90
  }}
89
91
  disabled={loading}
92
+ aria-label="Semantic search query"
90
93
  />
91
94
  <Button variant="primary" onClick={() => onSearch(q)} disabled={loading || !q.trim()}>
92
95
  {loading ? <Loader2 size={12} className="memory-spin" /> : <SearchIcon size={12} />}
@@ -0,0 +1,205 @@
1
+ // src/web/views/settings/ActivitySection.tsx
2
+ import React, { useEffect, useState, useCallback } from 'react';
3
+ import { Activity, Search, Eye, RefreshCw, Info } from 'lucide-react';
4
+ import { Card, CardTitle, CardMeta } from '../../components/Card';
5
+ import { Button } from '../../components/Button';
6
+ import { useToast } from '../../components/Toast';
7
+ import { api } from '../../lib/api';
8
+ import { cn } from '../../lib/utils';
9
+ import type { Settings } from '../../lib/types';
10
+
11
+ type Props = {
12
+ about: Settings['about'];
13
+ };
14
+
15
+ function ActivityLogCard() {
16
+ const toast = useToast();
17
+ const [items, setItems] = useState<Array<{ kind?: string; ts?: string; slug?: string; message?: string }>>([]);
18
+ const [hidden, setHidden] = useState<Set<string>>(new Set());
19
+ const [loading, setLoading] = useState(true);
20
+ const [filter, setFilter] = useState('');
21
+ const [showHidden, setShowHidden] = useState(true);
22
+
23
+ const refresh = useCallback(async () => {
24
+ setLoading(true);
25
+ try {
26
+ const [log, hid] = await Promise.all([
27
+ api.get<{ items: Array<{ kind?: string; ts?: string; slug?: string; message?: string }> }>('/activity'),
28
+ api.get<{ hidden: string[] }>('/activity/hidden'),
29
+ ]);
30
+ setItems(Array.isArray(log.items) ? log.items : []);
31
+ setHidden(new Set(hid.hidden || []));
32
+ } catch (err) {
33
+ toast.error(`Load failed: ${(err as Error).message}`);
34
+ } finally {
35
+ setLoading(false);
36
+ }
37
+ }, [toast]);
38
+
39
+ useEffect(() => { refresh(); }, [refresh]);
40
+
41
+ const keyOf = (it: { kind?: string; ts?: string; slug?: string }, idx: number) => {
42
+ const k = `${it.kind || ''}|${it.ts || ''}|${it.slug || ''}|${idx}`;
43
+ let h = 0;
44
+ for (let i = 0; i < k.length; i++) h = ((h << 5) - h + k.charCodeAt(i)) | 0;
45
+ return Math.abs(h).toString(16).padStart(8, '0').slice(0, 16);
46
+ };
47
+
48
+ const onRestoreAll = async () => {
49
+ try {
50
+ await api.del('/activity/hide');
51
+ setHidden(new Set());
52
+ toast.success('All hidden activity restored.');
53
+ } catch (err) {
54
+ toast.error(`Restore failed: ${(err as Error).message}`);
55
+ }
56
+ };
57
+
58
+ const onRestoreOne = async (key: string) => {
59
+ try {
60
+ await api.del(`/activity/hide/${encodeURIComponent(key)}`);
61
+ const next = new Set(hidden);
62
+ next.delete(key);
63
+ setHidden(next);
64
+ } catch (err) {
65
+ toast.error(`Restore failed: ${(err as Error).message}`);
66
+ }
67
+ };
68
+
69
+ const filtered = items.filter((it, idx) => {
70
+ const k = keyOf(it, idx);
71
+ if (!showHidden && hidden.has(k)) return false;
72
+ if (!filter) return true;
73
+ const q = filter.toLowerCase();
74
+ return [it.kind, it.slug, it.message].some((v) => typeof v === 'string' && v.toLowerCase().includes(q));
75
+ });
76
+
77
+ return (
78
+ <Card id="settings-activity-log" data-section="activity-log">
79
+ <CardTitle>
80
+ <Activity size={14} /> Activity log
81
+ <span className="muted" style={{ fontWeight: 400, marginLeft: 8, fontSize: 12 }}>
82
+ {items.length} total · {hidden.size} hidden
83
+ </span>
84
+ <Button variant="ghost" size="sm" style={{ marginLeft: 'auto' }} onClick={refresh} title="Reload">
85
+ <RefreshCw size={12} />
86
+ </Button>
87
+ </CardTitle>
88
+ <CardMeta>
89
+ Full history from <code>~/.bizar/activity.log</code>. Hiding an item in the Overview only hides it there — the entry stays here.
90
+ </CardMeta>
91
+
92
+ <div className="activity-log-toolbar">
93
+ <div className="activity-log-search">
94
+ <Search size={12} />
95
+ <input
96
+ type="text"
97
+ className="input"
98
+ placeholder="Filter by kind, slug, or message…"
99
+ value={filter}
100
+ onChange={(e) => setFilter(e.target.value)}
101
+ />
102
+ </div>
103
+ <label className="activity-log-toggle">
104
+ <input
105
+ type="checkbox"
106
+ checked={showHidden}
107
+ onChange={(e) => setShowHidden(e.target.checked)}
108
+ />
109
+ Show hidden
110
+ </label>
111
+ <Button
112
+ variant="ghost"
113
+ size="sm"
114
+ disabled={hidden.size === 0}
115
+ onClick={onRestoreAll}
116
+ title="Restore all hidden items to the Overview"
117
+ >
118
+ <Eye size={12} /> Restore all
119
+ </Button>
120
+ </div>
121
+
122
+ {loading ? (
123
+ <div className="muted" style={{ padding: '12px 0', fontSize: 12 }}>Loading…</div>
124
+ ) : filtered.length === 0 ? (
125
+ <div className="muted" style={{ padding: '12px 0', fontSize: 12 }}>
126
+ {items.length === 0 ? 'No activity yet.' : 'No items match the current filter.'}
127
+ </div>
128
+ ) : (
129
+ <div className="activity-log-table-wrap">
130
+ <table className="activity-log-table">
131
+ <thead>
132
+ <tr>
133
+ <th>Kind</th>
134
+ <th>Detail</th>
135
+ <th>Time</th>
136
+ <th></th>
137
+ </tr>
138
+ </thead>
139
+ <tbody>
140
+ {filtered.slice(0, 200).map((it, idx) => {
141
+ const k = keyOf(it, idx);
142
+ const isHidden = hidden.has(k);
143
+ return (
144
+ <tr key={`${it.ts}-${idx}`} className={cn(isHidden && 'activity-log-row-hidden')}>
145
+ <td className="activity-log-kind">{it.kind || 'activity'}</td>
146
+ <td className="activity-log-detail">
147
+ {it.message || it.slug || '—'}
148
+ </td>
149
+ <td className="activity-log-time mono">
150
+ {it.ts ? new Date(it.ts).toLocaleString() : '—'}
151
+ </td>
152
+ <td>
153
+ {isHidden ? (
154
+ <Button variant="ghost" size="sm" onClick={() => onRestoreOne(k)} title="Restore to Overview">
155
+ <Eye size={12} />
156
+ </Button>
157
+ ) : (
158
+ <span className="activity-log-state-tag">shown</span>
159
+ )}
160
+ </td>
161
+ </tr>
162
+ );
163
+ })}
164
+ </tbody>
165
+ </table>
166
+ {filtered.length > 200 && (
167
+ <div className="muted" style={{ fontSize: 11, padding: '8px 0' }}>
168
+ Showing first 200 of {filtered.length}.
169
+ </div>
170
+ )}
171
+ </div>
172
+ )}
173
+ </Card>
174
+ );
175
+ }
176
+
177
+ function AboutCard({ about }: Props) {
178
+ return (
179
+ <Card id="settings-about" data-section="about">
180
+ <CardTitle><Info size={14} /> About</CardTitle>
181
+ <CardMeta>Build metadata.</CardMeta>
182
+ <dl className="about-table">
183
+ <dt>Version</dt>
184
+ <dd className="mono">{about?.version || '—'}</dd>
185
+ <dt>Homepage</dt>
186
+ <dd>
187
+ <a href={about?.homepage || 'https://github.com/DrB0rk/BizarHarness'} target="_blank" rel="noopener noreferrer">
188
+ {about?.homepage || 'https://github.com/DrB0rk/BizarHarness'}
189
+ </a>
190
+ </dd>
191
+ <dt>License</dt>
192
+ <dd>{about?.license || 'MIT'}</dd>
193
+ </dl>
194
+ </Card>
195
+ );
196
+ }
197
+
198
+ export function ActivitySection({ about }: Props) {
199
+ return (
200
+ <>
201
+ <ActivityLogCard />
202
+ <AboutCard about={about} />
203
+ </>
204
+ );
205
+ }