Skip to content
File

Blob: src/client/islands/repo-admin/useRepoAdminActions.ts

typescript151 lines
1import { useState } from "react";
2import { safeReadJson } from "@/client/json";
3import { isJsonObject, type JsonValue } from "@/shared/web";
4 
5export function useRepoAdminActions(owner: string, repo: string) {
6 const [compactionResult, setCompactionResult] = useState<JsonValue | null>(null);
7 const [oidResult, setOidResult] = useState<JsonValue | null>(null);
8 const [stateDump, setStateDump] = useState<JsonValue | null>(null);
9 const [pending, setPending] = useState<Record<string, boolean>>({});
10 
11 function readFlag(value: JsonValue | null, key: string): boolean {
12 return isJsonObject(value) && value[key] === true;
13 }
14 
15 function readErrorMessage(value: JsonValue | null): string {
16 return isJsonObject(value) && typeof value.error === "string" ? value.error : "Unknown error";
17 }
18 
19 async function runAction<T>(key: string, action: () => Promise<T>) {
20 setPending((current) => ({ ...current, [key]: true }));
21 try {
22 return await action();
23 } finally {
24 setPending((current) => ({ ...current, [key]: false }));
25 }
26 }
27 
28 async function startCompaction(dryRun: boolean) {
29 await runAction(dryRun ? "compaction-dry-run" : "compaction-start", async () => {
30 const response = await fetch(`/${owner}/${repo}/admin/compact`, {
31 method: "POST",
32 headers: { "Content-Type": "application/json" },
33 body: JSON.stringify({ dryRun }),
34 });
35 const data = await safeReadJson(response);
36 setCompactionResult(data);
37 if (response.ok && !dryRun) {
38 window.setTimeout(() => window.location.reload(), 2000);
39 }
40 });
41 }
42 
43 async function clearCompaction() {
44 if (!window.confirm("Clear the recorded compaction request for this repository?")) {
45 return;
46 }
47 
48 await runAction("compaction-clear", async () => {
49 const response = await fetch(`/${owner}/${repo}/admin/compact`, { method: "DELETE" });
50 const data = await safeReadJson(response);
51 if (readFlag(data, "ok")) {
52 window.alert("Compaction request cleared successfully");
53 window.location.reload();
54 return;
55 }
56 
57 window.alert(`Error: ${readErrorMessage(data)}`);
58 });
59 }
60 
61 async function removePack(packName: string) {
62 const warning =
63 `Are you sure you want to remove pack: ${packName}?\n\n` +
64 `This deletes the pack file, its index, reference sidecar, and associated metadata.\n\n` +
65 `Only superseded packs should be removed during normal operation. ` +
66 `Active packs may still be referenced until compaction replaces them.`;
67 if (!window.confirm(warning)) {
68 return;
69 }
70 
71 await runAction(`remove-pack:${packName}`, async () => {
72 const response = await fetch(`/${owner}/${repo}/admin/pack/${encodeURIComponent(packName)}`, {
73 method: "DELETE",
74 });
75 const data = await safeReadJson(response);
76 if (readFlag(data, "ok")) {
77 window.alert(
78 `Pack removed successfully:\n- Pack file: ${readFlag(data, "deletedPack") ? "delete requested" : "unchanged"}\n- Index file: ${readFlag(data, "deletedIndex") ? "delete requested" : "unchanged"}\n- Reference sidecar: ${readFlag(data, "deletedRefs") ? "delete requested" : "unchanged"}\n- Metadata: ${readFlag(data, "deletedMetadata") ? "cleaned" : "unchanged"}`
79 );
80 window.location.reload();
81 return;
82 }
83 
84 window.alert(`Error removing pack: ${readErrorMessage(data)}`);
85 });
86 }
87 
88 async function checkOid(debugOid: string) {
89 if (!debugOid || !/^[a-f0-9]{40}$/i.test(debugOid.trim())) {
90 window.alert("Please enter a valid 40-character SHA-1 hash");
91 return;
92 }
93 
94 await runAction("check-oid", async () => {
95 const response = await fetch(`/${owner}/${repo}/admin/debug-oid/${debugOid.trim()}`);
96 setOidResult(await safeReadJson(response));
97 });
98 }
99 
100 async function dumpState() {
101 await runAction("dump-state", async () => {
102 const response = await fetch(`/${owner}/${repo}/admin/debug-state`);
103 setStateDump(await safeReadJson(response));
104 });
105 }
106 
107 async function purgeRepo() {
108 const confirmation = window.prompt(
109 `This action will PERMANENTLY DELETE all repository data.\n\nTo confirm, type exactly: purge-${owner}/${repo}`
110 );
111 if (confirmation !== `purge-${owner}/${repo}`) {
112 if (confirmation !== null) {
113 window.alert("Confirmation text did not match. Action cancelled.");
114 }
115 return;
116 }
117 if (!window.confirm("Final confirmation: Delete this repository forever?")) {
118 return;
119 }
120 
121 await runAction("purge-repo", async () => {
122 const response = await fetch(`/${owner}/${repo}/admin/purge`, {
123 method: "DELETE",
124 headers: { "Content-Type": "application/json" },
125 body: JSON.stringify({ confirm: confirmation }),
126 });
127 const data = await safeReadJson(response);
128 if (readFlag(data, "ok")) {
129 window.alert("Repository has been permanently deleted");
130 window.location.href = `/${owner}`;
131 return;
132 }
133 
134 window.alert(`Error: ${readErrorMessage(data)}`);
135 });
136 }
137 
138 return {
139 compactionResult,
140 oidResult,
141 stateDump,
142 pending,
143 startCompaction,
144 clearCompaction,
145 removePack,
146 checkOid,
147 dumpState,
148 purgeRepo,
149 };
150}