@meith/web 0.36.1 → 0.37.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.
- package/app/admin/system/page.tsx +241 -164
- package/app/fixtures/page.tsx +30 -0
- package/app/layout.tsx +0 -2
- package/next.config.mjs +1 -1
- package/package.json +50 -50
- package/src/components/admin/system-run-details.tsx +72 -0
- package/src/server/auth-config.ts +2 -13
- package/src/server/board-latest.tsx +5 -3
- package/src/server/container.ts +16 -38
- package/src/server/federation-actions.ts +0 -3
- package/src/server/fixture-activity-repo.ts +127 -0
- package/src/server/fixture-content.json +3045 -0
- package/src/server/navigation.ts +6 -2
- package/src/server/plugin-routes.ts +1 -2
- package/src/server/request-fingerprint.ts +2 -15
- package/src/server/seed-board.ts +180 -29
- package/src/server/stats.ts +5 -4
- package/src/server/syndication.ts +1 -3
- package/src/server/two-factor-actions.ts +0 -3
- package/src/server/upgrade-notice.ts +1 -1
- package/src/server/user-admin-actions.ts +0 -4
- package/src/server/usercp-actions.ts +0 -5
- package/src/theme/contract.fixture.ts +6 -6
- package/src/theme/gallery.fixture.tsx +412 -0
- package/src/theme/preview-boundary.tsx +23 -0
- package/src/theme/preview.fixture.ts +304 -0
- package/src/view/system-run-detail.ts +33 -0
- package/app/demo/stripe/[[...path]]/route.ts +0 -27
- package/src/components/shell/demo-banner.tsx +0 -41
- package/src/server/demo-stripe.ts +0 -104
- package/src/server/demo-uploads.ts +0 -17
- package/src/server/demo.ts +0 -59
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Metadata } from 'next'
|
|
2
2
|
|
|
3
3
|
import type { TaskHealthStatus } from '@meith/tasks'
|
|
4
|
-
import { cn } from '@meith/ui'
|
|
5
4
|
|
|
6
5
|
import {
|
|
7
6
|
ApplyMigrationsForm,
|
|
@@ -12,6 +11,7 @@ import {
|
|
|
12
11
|
ReindexSearchForm,
|
|
13
12
|
RetryJobForm,
|
|
14
13
|
} from '@/components/admin/system-forms'
|
|
14
|
+
import { SystemRunDetails } from '@/components/admin/system-run-details'
|
|
15
15
|
import { PANEL_CARD } from '@/components/shell/panel-list'
|
|
16
16
|
import { PanelPage } from '@/components/shell/panel-page'
|
|
17
17
|
import { adminPageContext } from '@/server/admin'
|
|
@@ -35,9 +35,14 @@ export async function generateMetadata(): Promise<Metadata> {
|
|
|
35
35
|
return { title: await tr('page.system-health') }
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export default async function AdminSystemPage(
|
|
38
|
+
export default async function AdminSystemPage({
|
|
39
|
+
searchParams,
|
|
40
|
+
}: {
|
|
41
|
+
searchParams: Promise<{ maintenance?: string }>
|
|
42
|
+
}) {
|
|
39
43
|
if ((await adminPageContext()) === null) return null
|
|
40
44
|
|
|
45
|
+
const maintenanceOpen = (await searchParams).maintenance === '1'
|
|
41
46
|
const now = new Date()
|
|
42
47
|
const [translator, view, upgradeNotice] = await Promise.all([
|
|
43
48
|
getTranslator(),
|
|
@@ -55,12 +60,19 @@ export default async function AdminSystemPage() {
|
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
const { mail, scheduler, volumes, legacyPasswordHashes } = view
|
|
63
|
+
const taskTitles = new Map(scheduler.tasks.map((task) => [task.key, task.titleKey]))
|
|
64
|
+
const taskCounts = Object.entries(TASK_STATUS_KEYS).map(([status, key]) => ({
|
|
65
|
+
status,
|
|
66
|
+
label: translator.t(key),
|
|
67
|
+
count: scheduler.tasks.filter((task) => task.status === status).length,
|
|
68
|
+
}))
|
|
58
69
|
|
|
59
70
|
return (
|
|
60
71
|
<PanelPage
|
|
61
72
|
title={await tr('page.system-health')}
|
|
62
73
|
lede={await tr('page.what-board-does-schedule-whether')}
|
|
63
|
-
|
|
74
|
+
width="wide"
|
|
75
|
+
gap="normal"
|
|
64
76
|
>
|
|
65
77
|
{scheduler.schedulerStopped && (
|
|
66
78
|
<section
|
|
@@ -129,116 +141,6 @@ export default async function AdminSystemPage() {
|
|
|
129
141
|
</section>
|
|
130
142
|
)}
|
|
131
143
|
|
|
132
|
-
<section className={PANEL_CARD}>
|
|
133
|
-
<h2 className="font-heading text-lg font-semibold">{translator.t('adminSystem.mail')}</h2>
|
|
134
|
-
<p className="text-sm">
|
|
135
|
-
{mail.summary}
|
|
136
|
-
{!mail.sends && (
|
|
137
|
-
<span className="text-muted-foreground">
|
|
138
|
-
{' '}
|
|
139
|
-
{translator.t('adminSystem.mailDoesNotSend')}
|
|
140
|
-
</span>
|
|
141
|
-
)}{' '}
|
|
142
|
-
{translator.t('adminSystem.activationMethod')} <code>{mail.activationMethod}</code>{' '}
|
|
143
|
-
{translator.t('adminSystem.configuredFrom')}{' '}
|
|
144
|
-
<code>
|
|
145
|
-
{mail.source === 'environment'
|
|
146
|
-
? translator.t('adminSystem.environment')
|
|
147
|
-
: translator.t('adminSystem.boardSettings')}
|
|
148
|
-
</code>
|
|
149
|
-
</p>
|
|
150
|
-
{mail.source === 'board' && (
|
|
151
|
-
<p className="text-sm text-muted-foreground">
|
|
152
|
-
{translator.t('adminSystem.changeMailBefore')}{' '}
|
|
153
|
-
<a href="/admin/settings?group=mail" className="underline">
|
|
154
|
-
{translator.t('page.board-settings')}
|
|
155
|
-
</a>
|
|
156
|
-
{translator.t('adminSystem.changeMailEnd')}
|
|
157
|
-
</p>
|
|
158
|
-
)}
|
|
159
|
-
<p className="text-sm text-muted-foreground">{translator.t('adminSystem.mailSchedule')}</p>
|
|
160
|
-
</section>
|
|
161
|
-
|
|
162
|
-
<section className={PANEL_CARD}>
|
|
163
|
-
<h2 className="font-heading text-lg font-semibold">{await tr('page.scheduled-tasks')}</h2>
|
|
164
|
-
{scheduler.tasks.length === 0 ? (
|
|
165
|
-
<p className="text-sm text-muted-foreground">{translator.t('adminSystem.noTasks')}</p>
|
|
166
|
-
) : (
|
|
167
|
-
<ul className="flex flex-col divide-y divide-border">
|
|
168
|
-
{scheduler.tasks.map((task) => (
|
|
169
|
-
<li
|
|
170
|
-
key={task.key}
|
|
171
|
-
className="flex flex-wrap items-center justify-between gap-x-3 gap-y-2 py-3 first:pt-0 last:pb-0"
|
|
172
|
-
>
|
|
173
|
-
<span className="flex min-w-0 flex-col">
|
|
174
|
-
<span className="truncate text-sm font-medium">
|
|
175
|
-
{task.titleKey === undefined ? task.key : translator.t(task.titleKey)}
|
|
176
|
-
</span>
|
|
177
|
-
{task.descriptionKey !== undefined && (
|
|
178
|
-
<span className="text-xs text-muted-foreground">
|
|
179
|
-
{translator.t(task.descriptionKey)}
|
|
180
|
-
</span>
|
|
181
|
-
)}
|
|
182
|
-
<span className="truncate text-xs text-muted-foreground">
|
|
183
|
-
<code>{task.key}</code> ·{' '}
|
|
184
|
-
{translator.t('adminSystem.taskInterval', { seconds: task.intervalSeconds })} ·{' '}
|
|
185
|
-
{task.lastRunAt === null
|
|
186
|
-
? translator.t('adminSystem.neverRun')
|
|
187
|
-
: translator.t('adminSystem.taskLastRun', {
|
|
188
|
-
time: formatTime(task.lastRunAt, now, translator).label,
|
|
189
|
-
})}
|
|
190
|
-
{task.consecutiveFailures > 0 &&
|
|
191
|
-
` · ${translator.t('adminSystem.taskFailures', {
|
|
192
|
-
count: task.consecutiveFailures,
|
|
193
|
-
})}`}
|
|
194
|
-
</span>
|
|
195
|
-
</span>
|
|
196
|
-
<span
|
|
197
|
-
className={
|
|
198
|
-
task.status === 'healthy' ||
|
|
199
|
-
task.status === 'running' ||
|
|
200
|
-
task.status === 'disabled'
|
|
201
|
-
? 'shrink-0 text-xs text-muted-foreground'
|
|
202
|
-
: 'shrink-0 text-xs font-medium text-destructive'
|
|
203
|
-
}
|
|
204
|
-
>
|
|
205
|
-
{translator.t(TASK_STATUS_KEYS[task.status])}
|
|
206
|
-
</span>
|
|
207
|
-
</li>
|
|
208
|
-
))}
|
|
209
|
-
</ul>
|
|
210
|
-
)}
|
|
211
|
-
</section>
|
|
212
|
-
|
|
213
|
-
<section className={PANEL_CARD}>
|
|
214
|
-
<h2 className="font-heading text-lg font-semibold">{await tr('page.recent-runs')}</h2>
|
|
215
|
-
{view.runs.length === 0 ? (
|
|
216
|
-
<p className="text-sm text-muted-foreground">{await tr('page.nothing-has-run-yet')}</p>
|
|
217
|
-
) : (
|
|
218
|
-
<ul className="flex flex-col gap-1 text-sm">
|
|
219
|
-
{view.runs.map((run, index) => (
|
|
220
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: the tiebreaker between two runs of one task recorded at the same instant
|
|
221
|
-
<li key={`${run.taskKey}:${run.ranAt.toISOString()}:${index}`}>
|
|
222
|
-
<code className="text-xs">{run.taskKey}</code>{' '}
|
|
223
|
-
<span className="text-muted-foreground">
|
|
224
|
-
{run.succeeded
|
|
225
|
-
? translator.t('adminSystem.taskRunOk')
|
|
226
|
-
: translator.t('adminSystem.taskRunFailed')}
|
|
227
|
-
{run.durationMs !== null && ` · ${run.durationMs}ms`} ·{' '}
|
|
228
|
-
<time dateTime={run.ranAt.toISOString()}>
|
|
229
|
-
{formatTime(run.ranAt, now, translator).label}
|
|
230
|
-
</time>
|
|
231
|
-
{run.detail !== null && ` · ${run.detail}`}
|
|
232
|
-
</span>
|
|
233
|
-
{run.error !== null && (
|
|
234
|
-
<span className="block text-xs text-destructive">{run.error}</span>
|
|
235
|
-
)}
|
|
236
|
-
</li>
|
|
237
|
-
))}
|
|
238
|
-
</ul>
|
|
239
|
-
)}
|
|
240
|
-
</section>
|
|
241
|
-
|
|
242
144
|
<section className={PANEL_CARD}>
|
|
243
145
|
<h2 className="font-heading text-lg font-semibold">
|
|
244
146
|
{translator.t('adminSystem.volumes')}
|
|
@@ -255,65 +157,201 @@ export default async function AdminSystemPage() {
|
|
|
255
157
|
</ul>
|
|
256
158
|
</section>
|
|
257
159
|
|
|
258
|
-
|
|
160
|
+
<div className="grid items-start gap-4 xl:grid-cols-2">
|
|
259
161
|
<section className={PANEL_CARD}>
|
|
260
|
-
<h2 className="font-heading text-lg font-semibold">
|
|
261
|
-
{translator.t('adminSystem.legacyPasswords')}
|
|
262
|
-
</h2>
|
|
162
|
+
<h2 className="font-heading text-lg font-semibold">{translator.t('adminSystem.mail')}</h2>
|
|
263
163
|
<p className="text-sm">
|
|
264
|
-
{
|
|
164
|
+
{mail.summary}
|
|
165
|
+
{!mail.sends && (
|
|
166
|
+
<span className="text-muted-foreground">
|
|
167
|
+
{' '}
|
|
168
|
+
{translator.t('adminSystem.mailDoesNotSend')}
|
|
169
|
+
</span>
|
|
170
|
+
)}{' '}
|
|
171
|
+
{translator.t('adminSystem.activationMethod')} <code>{mail.activationMethod}</code>{' '}
|
|
172
|
+
{translator.t('adminSystem.configuredFrom')}{' '}
|
|
173
|
+
<code>
|
|
174
|
+
{mail.source === 'environment'
|
|
175
|
+
? translator.t('adminSystem.environment')
|
|
176
|
+
: translator.t('adminSystem.boardSettings')}
|
|
177
|
+
</code>
|
|
265
178
|
</p>
|
|
179
|
+
{mail.source === 'board' && (
|
|
180
|
+
<p className="text-sm text-muted-foreground">
|
|
181
|
+
{translator.t('adminSystem.changeMailBefore')}{' '}
|
|
182
|
+
<a href="/admin/settings?group=mail" className="underline">
|
|
183
|
+
{translator.t('page.board-settings')}
|
|
184
|
+
</a>
|
|
185
|
+
{translator.t('adminSystem.changeMailEnd')}
|
|
186
|
+
</p>
|
|
187
|
+
)}
|
|
266
188
|
<p className="text-sm text-muted-foreground">
|
|
267
|
-
{translator.t('adminSystem.
|
|
189
|
+
{translator.t('adminSystem.mailSchedule')}
|
|
268
190
|
</p>
|
|
269
191
|
</section>
|
|
270
|
-
)}
|
|
271
192
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
</p>
|
|
279
|
-
{upgradeNotice === null ? (
|
|
280
|
-
<p className="text-sm text-muted-foreground">
|
|
281
|
-
{translator.t('adminSystem.migrationsUpToDate')}
|
|
193
|
+
<section className={PANEL_CARD}>
|
|
194
|
+
<h2 className="font-heading text-lg font-semibold">
|
|
195
|
+
{translator.t('adminSystem.migrations')}
|
|
196
|
+
</h2>
|
|
197
|
+
<p className="text-sm">
|
|
198
|
+
{translator.t('adminSystem.runningVersion')} <code>{CODE_VERSION}</code>.
|
|
282
199
|
</p>
|
|
283
|
-
|
|
284
|
-
<>
|
|
285
|
-
<p className="text-sm text-destructive">{upgradeNotice}</p>
|
|
200
|
+
{upgradeNotice === null ? (
|
|
286
201
|
<p className="text-sm text-muted-foreground">
|
|
287
|
-
{translator.t('adminSystem.
|
|
202
|
+
{translator.t('adminSystem.migrationsUpToDate')}
|
|
288
203
|
</p>
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
204
|
+
) : (
|
|
205
|
+
<>
|
|
206
|
+
<p className="text-sm text-destructive">{upgradeNotice}</p>
|
|
207
|
+
<p className="text-sm text-muted-foreground">
|
|
208
|
+
{translator.t('adminSystem.migrationsHint')}
|
|
209
|
+
</p>
|
|
210
|
+
<ApplyMigrationsForm copy={copy} />
|
|
211
|
+
</>
|
|
212
|
+
)}
|
|
213
|
+
</section>
|
|
214
|
+
</div>
|
|
293
215
|
|
|
294
216
|
<section className={PANEL_CARD}>
|
|
295
|
-
<h2 className="font-heading text-lg font-semibold">
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
217
|
+
<h2 className="font-heading text-lg font-semibold">{await tr('page.scheduled-tasks')}</h2>
|
|
218
|
+
<p className="text-sm text-muted-foreground">
|
|
219
|
+
{translator.t('adminSystem.taskCount', { count: scheduler.tasks.length })}
|
|
220
|
+
</p>
|
|
221
|
+
<ul className="flex flex-wrap gap-x-4 gap-y-2 text-sm">
|
|
222
|
+
{taskCounts
|
|
223
|
+
.filter(({ count }) => count > 0)
|
|
224
|
+
.map(({ status, label, count }) => (
|
|
225
|
+
<li
|
|
226
|
+
key={status}
|
|
227
|
+
className={
|
|
228
|
+
status === 'stale' || status === 'failing'
|
|
229
|
+
? 'font-medium text-destructive'
|
|
230
|
+
: 'text-muted-foreground'
|
|
231
|
+
}
|
|
232
|
+
>
|
|
233
|
+
{translator.t('adminSystem.tasksInState', { count, status: label })}
|
|
310
234
|
</li>
|
|
311
235
|
))}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
236
|
+
</ul>
|
|
237
|
+
<details open={scheduler.stale > 0 || scheduler.failing > 0}>
|
|
238
|
+
<summary className="cursor-pointer text-sm font-medium">
|
|
239
|
+
{translator.t('adminSystem.showTasks')}
|
|
240
|
+
</summary>
|
|
241
|
+
{scheduler.tasks.length === 0 ? (
|
|
242
|
+
<p className="text-sm text-muted-foreground">{translator.t('adminSystem.noTasks')}</p>
|
|
243
|
+
) : (
|
|
244
|
+
<ul className="grid gap-x-6 sm:grid-cols-2">
|
|
245
|
+
{scheduler.tasks.map((task) => (
|
|
246
|
+
<li
|
|
247
|
+
key={task.key}
|
|
248
|
+
className="flex min-w-0 items-start justify-between gap-3 border-t border-border py-3"
|
|
249
|
+
>
|
|
250
|
+
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
|
251
|
+
<span className="text-sm font-medium [overflow-wrap:anywhere]">
|
|
252
|
+
{task.titleKey === undefined ? task.key : translator.t(task.titleKey)}
|
|
253
|
+
</span>
|
|
254
|
+
<span className="text-xs text-muted-foreground">
|
|
255
|
+
{translator.t('adminSystem.taskInterval', { seconds: task.intervalSeconds })}{' '}
|
|
256
|
+
·{' '}
|
|
257
|
+
{task.lastRunAt === null
|
|
258
|
+
? translator.t('adminSystem.neverRun')
|
|
259
|
+
: translator.t('adminSystem.taskLastRun', {
|
|
260
|
+
time: formatTime(task.lastRunAt, now, translator).label,
|
|
261
|
+
})}
|
|
262
|
+
{task.consecutiveFailures > 0 &&
|
|
263
|
+
` · ${translator.t('adminSystem.taskFailures', {
|
|
264
|
+
count: task.consecutiveFailures,
|
|
265
|
+
})}`}
|
|
266
|
+
</span>
|
|
267
|
+
<details className="text-xs text-muted-foreground">
|
|
268
|
+
<summary className="cursor-pointer">
|
|
269
|
+
{translator.t('adminSystem.taskDetails')}
|
|
270
|
+
</summary>
|
|
271
|
+
<div className="mt-2 flex flex-col gap-1 [overflow-wrap:anywhere]">
|
|
272
|
+
<code>{task.key}</code>
|
|
273
|
+
{task.descriptionKey !== undefined && (
|
|
274
|
+
<p>{translator.t(task.descriptionKey)}</p>
|
|
275
|
+
)}
|
|
276
|
+
</div>
|
|
277
|
+
</details>
|
|
278
|
+
</div>
|
|
279
|
+
<span
|
|
280
|
+
className={
|
|
281
|
+
task.status === 'healthy' ||
|
|
282
|
+
task.status === 'running' ||
|
|
283
|
+
task.status === 'disabled'
|
|
284
|
+
? 'shrink-0 text-xs text-muted-foreground'
|
|
285
|
+
: 'shrink-0 text-xs font-medium text-destructive'
|
|
286
|
+
}
|
|
287
|
+
>
|
|
288
|
+
{translator.t(TASK_STATUS_KEYS[task.status])}
|
|
289
|
+
</span>
|
|
290
|
+
</li>
|
|
291
|
+
))}
|
|
292
|
+
</ul>
|
|
293
|
+
)}
|
|
294
|
+
</details>
|
|
315
295
|
</section>
|
|
316
296
|
|
|
297
|
+
<section className={PANEL_CARD}>
|
|
298
|
+
<h2 className="font-heading text-lg font-semibold">{await tr('page.recent-runs')}</h2>
|
|
299
|
+
<details open={view.runs.some((run) => !run.succeeded)}>
|
|
300
|
+
<summary className="cursor-pointer text-sm font-medium">
|
|
301
|
+
{translator.t('adminSystem.showRuns')}
|
|
302
|
+
<span className="ml-2 font-normal text-muted-foreground">
|
|
303
|
+
{translator.t('adminSystem.runCount', { count: view.runs.length })}
|
|
304
|
+
</span>
|
|
305
|
+
</summary>
|
|
306
|
+
{view.runs.length === 0 ? (
|
|
307
|
+
<p className="text-sm text-muted-foreground">{await tr('page.nothing-has-run-yet')}</p>
|
|
308
|
+
) : (
|
|
309
|
+
<ul className="mt-3 flex flex-col divide-y divide-border text-sm">
|
|
310
|
+
{view.runs.map((run, index) => (
|
|
311
|
+
<li
|
|
312
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: the tiebreaker between two runs of one task recorded at the same instant
|
|
313
|
+
key={`${run.taskKey}:${run.ranAt.toISOString()}:${index}`}
|
|
314
|
+
className="min-w-0 py-3 [overflow-wrap:anywhere]"
|
|
315
|
+
>
|
|
316
|
+
<span className="font-medium">
|
|
317
|
+
{taskTitles.get(run.taskKey) === undefined
|
|
318
|
+
? run.taskKey
|
|
319
|
+
: translator.t(taskTitles.get(run.taskKey)!)}
|
|
320
|
+
</span>{' '}
|
|
321
|
+
<span className="text-muted-foreground">
|
|
322
|
+
{run.succeeded
|
|
323
|
+
? translator.t('adminSystem.taskRunOk')
|
|
324
|
+
: translator.t('adminSystem.taskRunFailed')}
|
|
325
|
+
{run.durationMs !== null && ` · ${run.durationMs}ms`} ·{' '}
|
|
326
|
+
<time dateTime={run.ranAt.toISOString()}>
|
|
327
|
+
{formatTime(run.ranAt, now, translator).label}
|
|
328
|
+
</time>
|
|
329
|
+
</span>
|
|
330
|
+
<SystemRunDetails detail={run.detail} translator={translator} />
|
|
331
|
+
{run.error !== null && (
|
|
332
|
+
<span className="block text-xs text-destructive">{run.error}</span>
|
|
333
|
+
)}
|
|
334
|
+
</li>
|
|
335
|
+
))}
|
|
336
|
+
</ul>
|
|
337
|
+
)}
|
|
338
|
+
</details>
|
|
339
|
+
</section>
|
|
340
|
+
|
|
341
|
+
{legacyPasswordHashes > 0 && (
|
|
342
|
+
<section className={PANEL_CARD}>
|
|
343
|
+
<h2 className="font-heading text-lg font-semibold">
|
|
344
|
+
{translator.t('adminSystem.legacyPasswords')}
|
|
345
|
+
</h2>
|
|
346
|
+
<p className="text-sm">
|
|
347
|
+
{translator.t('adminSystem.legacyPasswordsCount', { count: legacyPasswordHashes })}
|
|
348
|
+
</p>
|
|
349
|
+
<p className="text-sm text-muted-foreground">
|
|
350
|
+
{translator.t('adminSystem.legacyPasswordsHint')}
|
|
351
|
+
</p>
|
|
352
|
+
</section>
|
|
353
|
+
)}
|
|
354
|
+
|
|
317
355
|
<section className={PANEL_CARD}>
|
|
318
356
|
<h2 className="font-heading text-lg font-semibold">{await tr('page.search-index')}</h2>
|
|
319
357
|
<p className="text-sm text-muted-foreground">
|
|
@@ -332,18 +370,57 @@ export default async function AdminSystemPage() {
|
|
|
332
370
|
<ReindexSearchForm pending={view.searchIndex.pending} copy={copy} />
|
|
333
371
|
</section>
|
|
334
372
|
|
|
335
|
-
<section className={
|
|
373
|
+
<section id="maintenance" className={PANEL_CARD}>
|
|
336
374
|
<h2 className="font-heading text-lg font-semibold">
|
|
337
375
|
{translator.t('adminSystem.maintenance')}
|
|
338
376
|
</h2>
|
|
339
|
-
<
|
|
340
|
-
{
|
|
341
|
-
|
|
377
|
+
<a
|
|
378
|
+
href={
|
|
379
|
+
maintenanceOpen
|
|
380
|
+
? '/admin/system#maintenance'
|
|
381
|
+
: '/admin/system?maintenance=1#maintenance'
|
|
382
|
+
}
|
|
383
|
+
className="w-fit text-sm font-medium underline underline-offset-4"
|
|
384
|
+
>
|
|
385
|
+
{translator.t(
|
|
386
|
+
maintenanceOpen ? 'adminSystem.closeMaintenance' : 'adminSystem.openMaintenance',
|
|
387
|
+
)}
|
|
388
|
+
</a>
|
|
389
|
+
{maintenanceOpen && (
|
|
390
|
+
<div className="flex flex-col gap-6">
|
|
391
|
+
<section className="flex flex-col gap-3">
|
|
392
|
+
<h3 className="font-heading font-semibold">{translator.t('adminSystem.recount')}</h3>
|
|
393
|
+
<p className="text-sm text-muted-foreground">
|
|
394
|
+
{translator.t('adminSystem.recountHint')}
|
|
395
|
+
</p>
|
|
396
|
+
{view.recount.length > 0 && (
|
|
397
|
+
<ul className="flex flex-col gap-1 text-xs text-muted-foreground">
|
|
398
|
+
{view.recount.map((row) => (
|
|
399
|
+
<li key={row.id}>
|
|
400
|
+
{translator.t('adminSystem.recountProgress', {
|
|
401
|
+
id: row.id,
|
|
402
|
+
phase: row.phase,
|
|
403
|
+
cursor: row.cursor,
|
|
404
|
+
passes: row.passes,
|
|
405
|
+
corrected: row.corrected,
|
|
406
|
+
})}
|
|
407
|
+
</li>
|
|
408
|
+
))}
|
|
409
|
+
</ul>
|
|
410
|
+
)}
|
|
411
|
+
<RecountForm copy={copy} />
|
|
412
|
+
</section>
|
|
413
|
+
|
|
414
|
+
<p className="text-sm text-muted-foreground">
|
|
415
|
+
{translator.t('adminSystem.maintenanceHint')}
|
|
416
|
+
</p>
|
|
342
417
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
418
|
+
<PruneSessionsForm prunable={view.prunableSessions} copy={copy} />
|
|
419
|
+
<PruneTokensForm copy={copy} />
|
|
420
|
+
<ClearCacheForm copy={copy} />
|
|
421
|
+
<RetryJobForm copy={copy} />
|
|
422
|
+
</div>
|
|
423
|
+
)}
|
|
347
424
|
</section>
|
|
348
425
|
</PanelPage>
|
|
349
426
|
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Metadata } from 'next'
|
|
2
|
+
import { notFound } from 'next/navigation'
|
|
3
|
+
|
|
4
|
+
import { env } from '@meith/core'
|
|
5
|
+
import { isSlotName } from '@meith/theme-kit'
|
|
6
|
+
|
|
7
|
+
import { getTranslator } from '@/server/i18n'
|
|
8
|
+
import { FixtureGallery } from '@/theme/gallery.fixture'
|
|
9
|
+
import { fixtureVariants } from '@/theme/preview.fixture'
|
|
10
|
+
|
|
11
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
12
|
+
return {
|
|
13
|
+
title: (await getTranslator()).t('nav.themeFixtures'),
|
|
14
|
+
robots: { index: false, follow: false },
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default async function FixturesPage({
|
|
19
|
+
searchParams,
|
|
20
|
+
}: {
|
|
21
|
+
searchParams: Promise<Record<string, string | string[] | undefined>>
|
|
22
|
+
}) {
|
|
23
|
+
if (env.DATA_SOURCE !== 'fixture') notFound()
|
|
24
|
+
const query = await searchParams
|
|
25
|
+
const name = query.slot ?? 'BoardIndex'
|
|
26
|
+
if (typeof name !== 'string' || !isSlotName(name)) notFound()
|
|
27
|
+
const variant = query.variant ?? 'default'
|
|
28
|
+
if (typeof variant !== 'string' || !fixtureVariants(name).includes(variant)) notFound()
|
|
29
|
+
return <FixtureGallery name={name} variant={variant} />
|
|
30
|
+
}
|
package/app/layout.tsx
CHANGED
|
@@ -9,7 +9,6 @@ import { NavTabsEnhancer } from '@meith/ui/nav-tabs-enhancer'
|
|
|
9
9
|
|
|
10
10
|
import { CopyProvider } from '@/components/shell/copy'
|
|
11
11
|
import { CrashNoticeProvider } from '@/components/shell/crash-notice'
|
|
12
|
-
import { DemoBanner } from '@/components/shell/demo-banner'
|
|
13
12
|
import { GroupNameStyle } from '@/components/shell/group-name-style'
|
|
14
13
|
import { ServiceWorkerRegistrar } from '@/components/shell/service-worker'
|
|
15
14
|
import { ThemeRuntimeStyle } from '@/components/shell/theme-runtime-style'
|
|
@@ -110,7 +109,6 @@ export default async function RootLayout({
|
|
|
110
109
|
</head>
|
|
111
110
|
<body className="font-sans antialiased">
|
|
112
111
|
<NavTabsEnhancer />
|
|
113
|
-
<DemoBanner />
|
|
114
112
|
<CrashNoticeProvider
|
|
115
113
|
notice={notice}
|
|
116
114
|
requestId={currentRequestId() ?? null}
|
package/next.config.mjs
CHANGED
|
@@ -63,7 +63,6 @@ const nextConfig = {
|
|
|
63
63
|
'@meith/board-digest',
|
|
64
64
|
'@meith/core',
|
|
65
65
|
'@meith/db',
|
|
66
|
-
'@meith/demo',
|
|
67
66
|
'@meith/drafts',
|
|
68
67
|
'@meith/drivers',
|
|
69
68
|
'@meith/events',
|
|
@@ -78,6 +77,7 @@ const nextConfig = {
|
|
|
78
77
|
'@meith/messages',
|
|
79
78
|
'@meith/moderation',
|
|
80
79
|
'@meith/notifications',
|
|
80
|
+
'@meith/plugin-awards',
|
|
81
81
|
'@meith/plugin-calendar',
|
|
82
82
|
'@meith/plugin-dues',
|
|
83
83
|
'@meith/plugin-kit',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meith/web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
4
4
|
"description": "The board itself: the Next.js app, and the forum-web bin that materializes it into an external board workspace.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -43,55 +43,55 @@
|
|
|
43
43
|
"react-dom": "19.2.8",
|
|
44
44
|
"tailwindcss": "^4.3.3",
|
|
45
45
|
"typescript": "7.0.2",
|
|
46
|
-
"@meith/accounts": "0.
|
|
47
|
-
"@meith/admin": "0.
|
|
48
|
-
"@meith/antispam": "0.
|
|
49
|
-
"@meith/api": "0.
|
|
50
|
-
"@meith/attachments": "0.
|
|
51
|
-
"@meith/authorization": "0.
|
|
52
|
-
"@meith/avatars": "0.
|
|
53
|
-
"@meith/backup": "0.
|
|
54
|
-
"@meith/board-digest": "0.
|
|
55
|
-
"@meith/core": "0.
|
|
56
|
-
"@meith/db": "0.
|
|
57
|
-
"@meith/
|
|
58
|
-
"@meith/
|
|
59
|
-
"@meith/
|
|
60
|
-
"@meith/
|
|
61
|
-
"@meith/
|
|
62
|
-
"@meith/groups": "0.
|
|
63
|
-
"@meith/
|
|
64
|
-
"@meith/
|
|
65
|
-
"@meith/
|
|
66
|
-
"@meith/
|
|
67
|
-
"@meith/
|
|
68
|
-
"@meith/
|
|
69
|
-
"@meith/
|
|
70
|
-
"@meith/
|
|
71
|
-
"@meith/
|
|
72
|
-
"@meith/plugin-calendar": "0.
|
|
73
|
-
"@meith/plugin-dues": "0.
|
|
74
|
-
"@meith/plugin-kit": "0.
|
|
75
|
-
"@meith/polls": "0.
|
|
76
|
-
"@meith/posts": "0.
|
|
77
|
-
"@meith/profile-fields": "0.
|
|
78
|
-
"@meith/relations": "0.
|
|
79
|
-
"@meith/reputation": "0.
|
|
80
|
-
"@meith/runtime": "0.
|
|
81
|
-
"@meith/search": "0.
|
|
82
|
-
"@meith/settings": "0.
|
|
83
|
-
"@meith/signatures": "0.
|
|
84
|
-
"@meith/subscriptions": "0.
|
|
85
|
-
"@meith/
|
|
86
|
-
"@meith/
|
|
87
|
-
"@meith/theme-default": "0.
|
|
88
|
-
"@meith/theme-kit": "0.
|
|
89
|
-
"@meith/theme-midnight": "0.
|
|
90
|
-
"@meith/theme-phasebook": "0.
|
|
91
|
-
"@meith/theme-raidframe": "0.
|
|
92
|
-
"@meith/threads": "0.
|
|
93
|
-
"@meith/ui": "0.
|
|
94
|
-
"@meith/upgrade": "0.
|
|
46
|
+
"@meith/accounts": "0.37.0",
|
|
47
|
+
"@meith/admin": "0.37.0",
|
|
48
|
+
"@meith/antispam": "0.37.0",
|
|
49
|
+
"@meith/api": "0.37.0",
|
|
50
|
+
"@meith/attachments": "0.37.0",
|
|
51
|
+
"@meith/authorization": "0.37.0",
|
|
52
|
+
"@meith/avatars": "0.37.0",
|
|
53
|
+
"@meith/backup": "0.37.0",
|
|
54
|
+
"@meith/board-digest": "0.37.0",
|
|
55
|
+
"@meith/core": "0.37.0",
|
|
56
|
+
"@meith/db": "0.37.0",
|
|
57
|
+
"@meith/drafts": "0.37.0",
|
|
58
|
+
"@meith/drivers": "0.37.0",
|
|
59
|
+
"@meith/events": "0.37.0",
|
|
60
|
+
"@meith/forums": "0.37.0",
|
|
61
|
+
"@meith/i18n": "0.37.0",
|
|
62
|
+
"@meith/groups": "0.37.0",
|
|
63
|
+
"@meith/import": "0.37.0",
|
|
64
|
+
"@meith/install": "0.37.0",
|
|
65
|
+
"@meith/mail": "0.37.0",
|
|
66
|
+
"@meith/markdown": "0.37.0",
|
|
67
|
+
"@meith/marketplace": "0.37.0",
|
|
68
|
+
"@meith/messages": "0.37.0",
|
|
69
|
+
"@meith/moderation": "0.37.0",
|
|
70
|
+
"@meith/notifications": "0.37.0",
|
|
71
|
+
"@meith/plugin-awards": "0.37.0",
|
|
72
|
+
"@meith/plugin-calendar": "0.37.0",
|
|
73
|
+
"@meith/plugin-dues": "0.37.0",
|
|
74
|
+
"@meith/plugin-kit": "0.37.0",
|
|
75
|
+
"@meith/polls": "0.37.0",
|
|
76
|
+
"@meith/posts": "0.37.0",
|
|
77
|
+
"@meith/profile-fields": "0.37.0",
|
|
78
|
+
"@meith/relations": "0.37.0",
|
|
79
|
+
"@meith/reputation": "0.37.0",
|
|
80
|
+
"@meith/runtime": "0.37.0",
|
|
81
|
+
"@meith/search": "0.37.0",
|
|
82
|
+
"@meith/settings": "0.37.0",
|
|
83
|
+
"@meith/signatures": "0.37.0",
|
|
84
|
+
"@meith/subscriptions": "0.37.0",
|
|
85
|
+
"@meith/theme-clubhouse": "0.37.0",
|
|
86
|
+
"@meith/tasks": "0.37.0",
|
|
87
|
+
"@meith/theme-default": "0.37.0",
|
|
88
|
+
"@meith/theme-kit": "0.37.0",
|
|
89
|
+
"@meith/theme-midnight": "0.37.0",
|
|
90
|
+
"@meith/theme-phasebook": "0.37.0",
|
|
91
|
+
"@meith/theme-raidframe": "0.37.0",
|
|
92
|
+
"@meith/threads": "0.37.0",
|
|
93
|
+
"@meith/ui": "0.37.0",
|
|
94
|
+
"@meith/upgrade": "0.37.0"
|
|
95
95
|
},
|
|
96
96
|
"scripts": {
|
|
97
97
|
"dev": "next dev",
|