@meith/web 0.27.0 → 0.28.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.
|
@@ -4,6 +4,7 @@ import type { TaskHealthStatus } from '@meith/tasks'
|
|
|
4
4
|
import { cn } from '@meith/ui'
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
+
ApplyMigrationsForm,
|
|
7
8
|
ClearCacheForm,
|
|
8
9
|
PruneSessionsForm,
|
|
9
10
|
PruneTokensForm,
|
|
@@ -16,6 +17,7 @@ import { PanelPage } from '@/components/shell/panel-page'
|
|
|
16
17
|
import { adminPageContext } from '@/server/admin'
|
|
17
18
|
import { getTranslator, tr } from '@/server/i18n'
|
|
18
19
|
import { buildSystemHealthView } from '@/server/system-admin'
|
|
20
|
+
import { CODE_VERSION, pendingUpgradeNotice } from '@/server/upgrade-notice'
|
|
19
21
|
import { systemFormsCopy } from '@/view/admin-panel-copy'
|
|
20
22
|
import { formatTime } from '@/view/time'
|
|
21
23
|
|
|
@@ -36,7 +38,11 @@ export default async function AdminSystemPage() {
|
|
|
36
38
|
if ((await adminPageContext()) === null) return null
|
|
37
39
|
|
|
38
40
|
const now = new Date()
|
|
39
|
-
const [translator, view] = await Promise.all([
|
|
41
|
+
const [translator, view, upgradeNotice] = await Promise.all([
|
|
42
|
+
getTranslator(),
|
|
43
|
+
buildSystemHealthView(now),
|
|
44
|
+
pendingUpgradeNotice(),
|
|
45
|
+
])
|
|
40
46
|
const copy = systemFormsCopy(translator)
|
|
41
47
|
|
|
42
48
|
if (view === null) {
|
|
@@ -246,6 +252,28 @@ export default async function AdminSystemPage() {
|
|
|
246
252
|
</ul>
|
|
247
253
|
</section>
|
|
248
254
|
|
|
255
|
+
<section className={PANEL_CARD}>
|
|
256
|
+
<h2 className="font-heading text-lg font-semibold">
|
|
257
|
+
{translator.t('adminSystem.migrations')}
|
|
258
|
+
</h2>
|
|
259
|
+
<p className="text-sm">
|
|
260
|
+
{translator.t('adminSystem.runningVersion')} <code>{CODE_VERSION}</code>.
|
|
261
|
+
</p>
|
|
262
|
+
{upgradeNotice === null ? (
|
|
263
|
+
<p className="text-sm text-muted-foreground">
|
|
264
|
+
{translator.t('adminSystem.migrationsUpToDate')}
|
|
265
|
+
</p>
|
|
266
|
+
) : (
|
|
267
|
+
<>
|
|
268
|
+
<p className="text-sm text-destructive">{upgradeNotice}</p>
|
|
269
|
+
<p className="text-sm text-muted-foreground">
|
|
270
|
+
{translator.t('adminSystem.migrationsHint')}
|
|
271
|
+
</p>
|
|
272
|
+
<ApplyMigrationsForm copy={copy} />
|
|
273
|
+
</>
|
|
274
|
+
)}
|
|
275
|
+
</section>
|
|
276
|
+
|
|
249
277
|
<section className={PANEL_CARD}>
|
|
250
278
|
<h2 className="font-heading text-lg font-semibold">
|
|
251
279
|
{translator.t('adminSystem.recount')}
|
|
@@ -167,8 +167,8 @@ export default async function AdminThemesPage() {
|
|
|
167
167
|
</p>
|
|
168
168
|
<p className="text-muted-foreground">
|
|
169
169
|
{translator.t('adminThemes.deployBefore')} <em>exist</em>
|
|
170
|
-
{translator.t('adminThemes.deployAfterExist')}
|
|
171
|
-
{translator.t('adminThemes.deployAfterAdd')}{' '}
|
|
170
|
+
{translator.t('adminThemes.deployAfterExist')}{' '}
|
|
171
|
+
<code className="text-xs">npm install</code> {translator.t('adminThemes.deployAfterAdd')}{' '}
|
|
172
172
|
<code className="text-xs">meith.config.ts</code>
|
|
173
173
|
{translator.t('adminThemes.deployAfterConfig')}{' '}
|
|
174
174
|
<code className="text-xs">defaultTheme</code> {translator.t('adminThemes.deployEnd')}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meith/web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.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,53 +43,53 @@
|
|
|
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/core": "0.
|
|
54
|
-
"@meith/db": "0.
|
|
55
|
-
"@meith/demo": "0.
|
|
56
|
-
"@meith/drafts": "0.
|
|
57
|
-
"@meith/drivers": "0.
|
|
58
|
-
"@meith/events": "0.
|
|
59
|
-
"@meith/forums": "0.
|
|
60
|
-
"@meith/
|
|
61
|
-
"@meith/
|
|
62
|
-
"@meith/import": "0.
|
|
63
|
-
"@meith/install": "0.
|
|
64
|
-
"@meith/mail": "0.
|
|
65
|
-
"@meith/markdown": "0.
|
|
66
|
-
"@meith/marketplace": "0.
|
|
67
|
-
"@meith/messages": "0.
|
|
68
|
-
"@meith/moderation": "0.
|
|
69
|
-
"@meith/notifications": "0.
|
|
70
|
-
"@meith/plugin-calendar": "0.
|
|
71
|
-
"@meith/plugin-dues": "0.
|
|
72
|
-
"@meith/plugin-kit": "0.
|
|
73
|
-
"@meith/polls": "0.
|
|
74
|
-
"@meith/posts": "0.
|
|
75
|
-
"@meith/profile-fields": "0.
|
|
76
|
-
"@meith/relations": "0.
|
|
77
|
-
"@meith/reputation": "0.
|
|
78
|
-
"@meith/runtime": "0.
|
|
79
|
-
"@meith/search": "0.
|
|
80
|
-
"@meith/settings": "0.
|
|
81
|
-
"@meith/signatures": "0.
|
|
82
|
-
"@meith/subscriptions": "0.
|
|
83
|
-
"@meith/tasks": "0.
|
|
84
|
-
"@meith/theme-clubhouse": "0.
|
|
85
|
-
"@meith/theme-default": "0.
|
|
86
|
-
"@meith/theme-kit": "0.
|
|
87
|
-
"@meith/theme-midnight": "0.
|
|
88
|
-
"@meith/theme-phasebook": "0.
|
|
89
|
-
"@meith/theme-raidframe": "0.
|
|
90
|
-
"@meith/threads": "0.
|
|
91
|
-
"@meith/ui": "0.
|
|
92
|
-
"@meith/upgrade": "0.
|
|
46
|
+
"@meith/accounts": "0.28.0",
|
|
47
|
+
"@meith/admin": "0.28.0",
|
|
48
|
+
"@meith/antispam": "0.28.0",
|
|
49
|
+
"@meith/api": "0.28.0",
|
|
50
|
+
"@meith/attachments": "0.28.0",
|
|
51
|
+
"@meith/authorization": "0.28.0",
|
|
52
|
+
"@meith/avatars": "0.28.0",
|
|
53
|
+
"@meith/core": "0.28.0",
|
|
54
|
+
"@meith/db": "0.28.0",
|
|
55
|
+
"@meith/demo": "0.28.0",
|
|
56
|
+
"@meith/drafts": "0.28.0",
|
|
57
|
+
"@meith/drivers": "0.28.0",
|
|
58
|
+
"@meith/events": "0.28.0",
|
|
59
|
+
"@meith/forums": "0.28.0",
|
|
60
|
+
"@meith/i18n": "0.28.0",
|
|
61
|
+
"@meith/groups": "0.28.0",
|
|
62
|
+
"@meith/import": "0.28.0",
|
|
63
|
+
"@meith/install": "0.28.0",
|
|
64
|
+
"@meith/mail": "0.28.0",
|
|
65
|
+
"@meith/markdown": "0.28.0",
|
|
66
|
+
"@meith/marketplace": "0.28.0",
|
|
67
|
+
"@meith/messages": "0.28.0",
|
|
68
|
+
"@meith/moderation": "0.28.0",
|
|
69
|
+
"@meith/notifications": "0.28.0",
|
|
70
|
+
"@meith/plugin-calendar": "0.28.0",
|
|
71
|
+
"@meith/plugin-dues": "0.28.0",
|
|
72
|
+
"@meith/plugin-kit": "0.28.0",
|
|
73
|
+
"@meith/polls": "0.28.0",
|
|
74
|
+
"@meith/posts": "0.28.0",
|
|
75
|
+
"@meith/profile-fields": "0.28.0",
|
|
76
|
+
"@meith/relations": "0.28.0",
|
|
77
|
+
"@meith/reputation": "0.28.0",
|
|
78
|
+
"@meith/runtime": "0.28.0",
|
|
79
|
+
"@meith/search": "0.28.0",
|
|
80
|
+
"@meith/settings": "0.28.0",
|
|
81
|
+
"@meith/signatures": "0.28.0",
|
|
82
|
+
"@meith/subscriptions": "0.28.0",
|
|
83
|
+
"@meith/tasks": "0.28.0",
|
|
84
|
+
"@meith/theme-clubhouse": "0.28.0",
|
|
85
|
+
"@meith/theme-default": "0.28.0",
|
|
86
|
+
"@meith/theme-kit": "0.28.0",
|
|
87
|
+
"@meith/theme-midnight": "0.28.0",
|
|
88
|
+
"@meith/theme-phasebook": "0.28.0",
|
|
89
|
+
"@meith/theme-raidframe": "0.28.0",
|
|
90
|
+
"@meith/threads": "0.28.0",
|
|
91
|
+
"@meith/ui": "0.28.0",
|
|
92
|
+
"@meith/upgrade": "0.28.0"
|
|
93
93
|
},
|
|
94
94
|
"scripts": {
|
|
95
95
|
"dev": "next dev",
|
|
@@ -4,6 +4,7 @@ import { useActionState } from 'react'
|
|
|
4
4
|
|
|
5
5
|
import { EMPTY_STATE } from '@/server/auth-form-state'
|
|
6
6
|
import {
|
|
7
|
+
applyUpgradeAction,
|
|
7
8
|
clearCacheAction,
|
|
8
9
|
pruneSessionsAction,
|
|
9
10
|
pruneTokensAction,
|
|
@@ -42,6 +43,27 @@ export function PruneSessionsForm({ prunable, copy }: { prunable: number; copy:
|
|
|
42
43
|
)
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
export function ApplyMigrationsForm({ copy }: { copy: Copy }) {
|
|
47
|
+
const [state, action] = useActionState(applyUpgradeAction, EMPTY_STATE)
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<form action={action} className="flex flex-col gap-2">
|
|
51
|
+
<FormError message={state.error} />
|
|
52
|
+
{state.notice === 'upgraded' && (
|
|
53
|
+
<Result>
|
|
54
|
+
{formatFromCopy(copy, 'adminPanel.system.upgradeApplied', {
|
|
55
|
+
core: state.values?.core ?? '0',
|
|
56
|
+
plugins: state.values?.plugins === '' ? '—' : (state.values?.plugins ?? '—'),
|
|
57
|
+
})}
|
|
58
|
+
</Result>
|
|
59
|
+
)}
|
|
60
|
+
<div>
|
|
61
|
+
<SubmitButton>{fromCopy(copy, 'adminPanel.system.applyMigrations')}</SubmitButton>
|
|
62
|
+
</div>
|
|
63
|
+
</form>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
45
67
|
export function PruneTokensForm({ copy }: { copy: Copy }) {
|
|
46
68
|
const [state, action] = useActionState(pruneTokensAction, EMPTY_STATE)
|
|
47
69
|
|
|
@@ -6,12 +6,13 @@ import { NotFoundError, ValidationError } from '@meith/core'
|
|
|
6
6
|
import { drivers } from '@meith/drivers'
|
|
7
7
|
import { msg } from '@meith/i18n'
|
|
8
8
|
|
|
9
|
-
import { recordAdminAction, requireAdmin } from './admin'
|
|
9
|
+
import { recordAdminAction, requireAdmin, requireFreshAdmin } from './admin'
|
|
10
10
|
import type { FormState } from './auth-form-state'
|
|
11
11
|
import { clearableTag } from './cache-targets'
|
|
12
12
|
import { formStateReporter } from './form-state-reporter'
|
|
13
13
|
import { requireSearch } from './search'
|
|
14
14
|
import { requireMaintenance, requireRecount } from './system-admin'
|
|
15
|
+
import { applyPendingUpgrade } from './upgrade-notice'
|
|
15
16
|
|
|
16
17
|
const SWEEP_LIMIT = 5_000
|
|
17
18
|
|
|
@@ -89,6 +90,28 @@ export async function reindexSearchAction(): Promise<FormState> {
|
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
export async function applyUpgradeAction(): Promise<FormState> {
|
|
94
|
+
try {
|
|
95
|
+
await requireFreshAdmin()
|
|
96
|
+
|
|
97
|
+
const result = await applyPendingUpgrade()
|
|
98
|
+
|
|
99
|
+
refreshSystemScreen()
|
|
100
|
+
revalidatePath('/admin')
|
|
101
|
+
await recordAdminAction({
|
|
102
|
+
action: 'system.upgrade_applied',
|
|
103
|
+
detail: { coreMigrations: result.coreMigrations, plugins: result.plugins },
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
notice: 'upgraded',
|
|
108
|
+
values: { core: String(result.coreMigrations), plugins: result.plugins.join(', ') },
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
return toFormState(err)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
92
115
|
export async function clearCacheAction(_prev: FormState, form: FormData): Promise<FormState> {
|
|
93
116
|
try {
|
|
94
117
|
await requireAdmin()
|
|
@@ -1,12 +1,94 @@
|
|
|
1
1
|
import 'server-only'
|
|
2
2
|
|
|
3
|
-
import { env, logger } from '@meith/core'
|
|
4
|
-
import {
|
|
3
|
+
import { env, logger, ValidationError } from '@meith/core'
|
|
4
|
+
import {
|
|
5
|
+
appliedPluginMigrations,
|
|
6
|
+
applyPluginMigration,
|
|
7
|
+
getDb,
|
|
8
|
+
PostgresNavigationRepository,
|
|
9
|
+
readVersion,
|
|
10
|
+
recordVersion,
|
|
11
|
+
runMigrations,
|
|
12
|
+
} from '@meith/db'
|
|
13
|
+
import { msg } from '@meith/i18n'
|
|
14
|
+
import { pluginNavigationPlacements } from '@meith/plugin-kit'
|
|
15
|
+
import { runPluginLifecycle } from '@meith/runtime'
|
|
5
16
|
import { planUpgrade, type UpgradeState, upgradeNotice } from '@meith/upgrade'
|
|
6
17
|
|
|
7
18
|
import { activeDefinitions } from './plugin-host'
|
|
8
19
|
|
|
9
|
-
export const CODE_VERSION = '0.
|
|
20
|
+
export const CODE_VERSION = '0.28.0'
|
|
21
|
+
|
|
22
|
+
export interface UpgradeApplied {
|
|
23
|
+
readonly coreMigrations: number
|
|
24
|
+
readonly plugins: readonly string[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function applyPendingUpgrade(): Promise<UpgradeApplied> {
|
|
28
|
+
const db = getDb()
|
|
29
|
+
const definitions = activeDefinitions()
|
|
30
|
+
|
|
31
|
+
const applied: Record<string, readonly string[]> = {}
|
|
32
|
+
for (const plugin of definitions) {
|
|
33
|
+
applied[plugin.key] = await appliedPluginMigrations(db, plugin.key)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const state: UpgradeState = {
|
|
37
|
+
recordedVersion: (await readVersion(db, 'core')) ?? CODE_VERSION,
|
|
38
|
+
codeVersion: CODE_VERSION,
|
|
39
|
+
pendingCoreMigrations: [],
|
|
40
|
+
plugins: definitions.map((plugin) => ({
|
|
41
|
+
key: plugin.key,
|
|
42
|
+
version: plugin.version,
|
|
43
|
+
dependsOn: plugin.dependsOn ?? [],
|
|
44
|
+
migrationIds: (plugin.migrations ?? []).map((migration) => migration.id),
|
|
45
|
+
})),
|
|
46
|
+
appliedPluginMigrations: applied,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const plan = planUpgrade(state)
|
|
50
|
+
if (plan.refusal !== null || plan.orderFailure !== null) {
|
|
51
|
+
const notice = upgradeNotice(plan, state)
|
|
52
|
+
throw notice === null
|
|
53
|
+
? new ValidationError(msg('error.app.board-cannot-be-upgraded'))
|
|
54
|
+
: new ValidationError(notice)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const fresh = new Set<string>()
|
|
58
|
+
for (const plugin of definitions) {
|
|
59
|
+
if ((await readVersion(db, `plugin:${plugin.key}`)) === null) fresh.add(plugin.key)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const coreMigrations = await runMigrations()
|
|
63
|
+
|
|
64
|
+
const touched: string[] = []
|
|
65
|
+
for (const definition of definitions) {
|
|
66
|
+
let changed = false
|
|
67
|
+
for (const migration of definition.migrations ?? []) {
|
|
68
|
+
const ran = await applyPluginMigration(db, definition.key, migration.id, migration.statements)
|
|
69
|
+
if (ran) changed = true
|
|
70
|
+
}
|
|
71
|
+
if (fresh.has(definition.key) && definition.onInstall !== undefined) {
|
|
72
|
+
const { ran } = await runPluginLifecycle({ db, plugin: definition, phase: 'install' })
|
|
73
|
+
if (ran) changed = true
|
|
74
|
+
}
|
|
75
|
+
await recordVersion(db, `plugin:${definition.key}`, definition.version)
|
|
76
|
+
if (changed) touched.push(definition.key)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await new PostgresNavigationRepository(db).syncPluginItems(
|
|
80
|
+
pluginNavigationPlacements(definitions).map((item) => ({
|
|
81
|
+
key: item.key,
|
|
82
|
+
href: item.href,
|
|
83
|
+
audience: item.audience,
|
|
84
|
+
parentKey: item.parentKey,
|
|
85
|
+
})),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
await recordVersion(db, 'core', CODE_VERSION)
|
|
89
|
+
|
|
90
|
+
return { coreMigrations, plugins: touched }
|
|
91
|
+
}
|
|
10
92
|
|
|
11
93
|
export async function pendingUpgradeNotice(): Promise<string | null> {
|
|
12
94
|
if (env.DATA_SOURCE !== 'postgres') return null
|
|
@@ -60,6 +60,7 @@ export function systemFormsCopy(t: Translator = untranslated()): Readonly<Record
|
|
|
60
60
|
'adminPanel.system.jobIdHint',
|
|
61
61
|
'adminPanel.system.retry',
|
|
62
62
|
'adminPanel.system.nothingToIndex',
|
|
63
|
+
'adminPanel.system.applyMigrations',
|
|
63
64
|
],
|
|
64
65
|
t,
|
|
65
66
|
),
|
|
@@ -73,6 +74,7 @@ export function systemFormsCopy(t: Translator = untranslated()): Readonly<Record
|
|
|
73
74
|
'adminPanel.system.indexFinished',
|
|
74
75
|
'adminPanel.system.indexMore',
|
|
75
76
|
'adminPanel.system.indexNext',
|
|
77
|
+
'adminPanel.system.upgradeApplied',
|
|
76
78
|
],
|
|
77
79
|
t,
|
|
78
80
|
),
|