Skip to content

Commit 52a4007

Browse files
aster-voidclaude
andcommitted
modules/admin: add legacy data migration button
Add a button in admin dashboard to import data from the old utcode.net repository. The migration clones the repo, parses markdown frontmatter, and imports members, articles, and projects into PostgreSQL. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 54d55e9 commit 52a4007

File tree

4 files changed

+449
-3
lines changed

4 files changed

+449
-3
lines changed

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ RUN DATABASE_URL=postgresql://localhost/dummy \
2323
FROM oven/bun:1-slim
2424
WORKDIR /app
2525

26-
# Install sops and age
26+
# Install sops, age, and git (for data migration)
2727
RUN apt-get update && apt-get install -y --no-install-recommends \
28-
curl ca-certificates \
28+
curl ca-certificates git \
2929
&& curl -LO https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64 \
3030
&& mv sops-v3.9.4.linux.amd64 /usr/local/bin/sops \
3131
&& chmod +x /usr/local/bin/sops \

src/lib/components/admin-dashboard/QuickActions.svelte

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,45 @@
11
<script lang="ts">
2-
import { Zap, UserPlus, Pencil, Folder } from "lucide-svelte";
2+
import { Zap, UserPlus, Pencil, Folder, DatabaseBackup, Loader2 } from "lucide-svelte";
3+
import { runMigration } from "$lib/data/private/migration.remote";
4+
5+
let migrationState = $state<"idle" | "running" | "success" | "error">("idle");
6+
let migrationResult = $state<string>("");
7+
8+
async function handleMigration() {
9+
if (
10+
!confirm("Import data from the old utcode.net repository?\nExisting entries will be skipped.")
11+
) {
12+
return;
13+
}
14+
15+
migrationState = "running";
16+
migrationResult = "";
17+
18+
try {
19+
const result = await runMigration();
20+
migrationState = "success";
21+
migrationResult = [
22+
`Members: ${result.members.created} created, ${result.members.skipped} skipped`,
23+
`Articles: ${result.articles.created} created, ${result.articles.skipped} skipped`,
24+
`Projects: ${result.projects.created} created, ${result.projects.skipped} skipped`,
25+
].join("\n");
26+
27+
const allErrors = [
28+
...result.members.errors,
29+
...result.articles.errors,
30+
...result.projects.errors,
31+
];
32+
if (allErrors.length > 0) {
33+
migrationResult += `\n\nErrors:\n${allErrors.slice(0, 5).join("\n")}`;
34+
if (allErrors.length > 5) {
35+
migrationResult += `\n... and ${allErrors.length - 5} more`;
36+
}
37+
}
38+
} catch (e) {
39+
migrationState = "error";
40+
migrationResult = e instanceof Error ? e.message : String(e);
41+
}
42+
}
343
</script>
444

545
<section class="animate-fade-slide-in stagger-5">
@@ -31,5 +71,28 @@
3171
<Folder class="h-4 w-4" />
3272
New Project
3373
</a>
74+
<button
75+
onclick={handleMigration}
76+
disabled={migrationState === "running"}
77+
class="btn gap-2 border-base-300 bg-base-100 font-medium transition-all hover:border-warning/30 hover:bg-warning/5 hover:text-warning disabled:opacity-50"
78+
>
79+
{#if migrationState === "running"}
80+
<Loader2 class="h-4 w-4 animate-spin" />
81+
Migrating...
82+
{:else}
83+
<DatabaseBackup class="h-4 w-4" />
84+
Import Legacy Data
85+
{/if}
86+
</button>
3487
</div>
88+
89+
{#if migrationResult}
90+
<div
91+
class="mt-4 rounded-lg p-4 text-sm whitespace-pre-wrap {migrationState === 'success'
92+
? 'bg-success/10 text-success'
93+
: 'bg-error/10 text-error'}"
94+
>
95+
{migrationResult}
96+
</div>
97+
{/if}
3598
</section>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { command } from "$app/server";
2+
import { requireUtCodeMember } from "$lib/server/database/auth.server";
3+
import { runDataMigration, type MigrationResult } from "$lib/server/services/migration.server";
4+
5+
export const runMigration = command(async (): Promise<MigrationResult> => {
6+
await requireUtCodeMember();
7+
return runDataMigration();
8+
});

0 commit comments

Comments
 (0)