Compare commits

..

3 Commits

Author SHA1 Message Date
MangoPig
8b169c11c0 Fix: Make bootstrap state backend authoritative 2026-06-19 20:24:24 +01:00
MangoPig
35c1a861f5 Merge branch 'Features/Backend/Bootstrap-Reset' 2026-06-19 19:57:46 +01:00
MangoPig
27101bbdd6 Feat: Add development bootstrap reset 2026-06-19 19:57:44 +01:00

View File

@@ -1,6 +1,6 @@
// Path: Frontend/src/components/workspace-home/WorkspaceHome/WorkspaceHome.tsx
import { For, Show, createEffect, createMemo, createSignal, onMount, type JSX } from "solid-js";
import { For, Show, createEffect, createMemo, createSignal, type JSX } from "solid-js";
import { Portal } from "solid-js/web";
import { createStore } from "solid-js/store";
import { resolveAPIBase } from "../../../lib/api";
@@ -44,9 +44,6 @@ const bootstrapStepDefinitions: readonly BootstrapStepDefinition[] = [
},
];
const bootstrapCompletionStorageKey = "moku.bootstrap.completed";
const isDevelopmentEnvironment = import.meta.env.DEV;
const defaultInstanceForm = {
protocol: "http",
access: "local",
@@ -58,7 +55,7 @@ const defaultModeForm = {
} as const;
const defaultAdminForm = {
displayName: "First admin",
displayName: "Admin",
email: "admin@example.com",
password: "",
} as const;
@@ -83,27 +80,6 @@ const initialSubmissionState = (): BootstrapSubmissionState => ({
error: "",
});
const readBootstrapCompletion = (): boolean => {
if (typeof window === "undefined") {
return false;
}
return window.localStorage.getItem(bootstrapCompletionStorageKey) === "true";
};
const writeBootstrapCompletion = (isComplete: boolean): void => {
if (typeof window === "undefined") {
return;
}
if (isComplete) {
window.localStorage.setItem(bootstrapCompletionStorageKey, "true");
return;
}
window.localStorage.removeItem(bootstrapCompletionStorageKey);
};
const readResponseBody = async (response: Response): Promise<unknown> => {
const raw = await response.text();
@@ -140,13 +116,6 @@ export const WorkspaceHome = (props: WorkspaceHomeProps): JSX.Element => {
const [isWizardOpen, setIsWizardOpen] = createSignal(false);
const [currentStepIndex, setCurrentStepIndex] = createSignal(0);
onMount(() => {
const isComplete = readBootstrapCompletion();
setIsBootstrapComplete(isComplete);
setIsWizardOpen(!isComplete);
setIsBootstrapStateResolved(true);
});
createEffect(() => {
if (modeForm.mode === "personal") {
setStructureForm("departmentName", personalStructureDefaults.departmentName);
@@ -164,29 +133,28 @@ export const WorkspaceHome = (props: WorkspaceHomeProps): JSX.Element => {
});
createEffect(() => {
if (!isBootstrapStateResolved()) {
const shellStatus = appShellData.status();
if (shellStatus === "idle" || shellStatus === "loading") {
return;
}
if (appShellData.status() !== "success") {
if (shellStatus !== "success") {
return;
}
const installation = appShellData.installation();
const installationAccessor = appShellData.installation;
const installation = typeof installationAccessor === "function" ? installationAccessor() : undefined;
const isPersistedBootstrap = installation?.isBootstrapped ?? false;
const wasComplete = isBootstrapComplete();
if (isPersistedBootstrap) {
return;
setIsBootstrapComplete(isPersistedBootstrap);
setIsWizardOpen(!isPersistedBootstrap);
setIsBootstrapStateResolved(true);
if (!isPersistedBootstrap && wasComplete) {
resetWizardState();
}
if (!isBootstrapComplete() && isWizardOpen()) {
return;
}
writeBootstrapCompletion(false);
setIsBootstrapComplete(false);
setIsWizardOpen(true);
resetWizardState();
});
const sidebarToggleLabel = (): string =>
@@ -278,9 +246,13 @@ export const WorkspaceHome = (props: WorkspaceHomeProps): JSX.Element => {
if (isLastStep()) {
await appShellData.reload();
writeBootstrapCompletion(true);
setIsBootstrapComplete(true);
setIsWizardOpen(false);
const installationAccessor = appShellData.installation;
const installation = typeof installationAccessor === "function" ? installationAccessor() : undefined;
const isPersistedBootstrap = installation?.isBootstrapped ?? false;
setIsBootstrapComplete(isPersistedBootstrap);
setIsWizardOpen(!isPersistedBootstrap);
setIsBootstrapStateResolved(true);
return;
}
@@ -292,32 +264,6 @@ export const WorkspaceHome = (props: WorkspaceHomeProps): JSX.Element => {
void submitCurrentStep();
};
const handleDevelopmentReset = async (): Promise<void> => {
const response = await fetch(`${apiBase()}/dev/bootstrap/reset`, {
method: "POST",
headers: {
Accept: "application/json",
},
});
const data = await readResponseBody(response);
if (!response.ok) {
throw new Error(
typeof data?.message === "string"
? data.message
: typeof data?.error?.message === "string"
? data.error.message
: "Failed to reset development bootstrap state.",
);
}
writeBootstrapCompletion(false);
setIsBootstrapComplete(false);
setIsWizardOpen(true);
resetWizardState();
await appShellData.reload();
};
const statusLabel = (state: BootstrapSubmissionState): string => {
switch (state.status) {
case "submitting":
@@ -379,20 +325,6 @@ export const WorkspaceHome = (props: WorkspaceHomeProps): JSX.Element => {
</button>
</div>
</Show>
<Show when={isDevelopmentEnvironment && isBootstrapStateResolved()}>
<div class={styles.heroActions}>
<button
type="button"
class={styles.secondaryButton}
data-slot="development-bootstrap-reset"
onClick={() => {
void handleDevelopmentReset();
}}
>
Reset development bootstrap state
</button>
</div>
</Show>
</section>
</main>
@@ -496,13 +428,13 @@ export const WorkspaceHome = (props: WorkspaceHomeProps): JSX.Element => {
<>
<label class={styles.field}>
<span class={styles.fieldLabel}>Display name</span>
<input
type="text"
value={adminForm.displayName}
onInput={(event): void => setAdminForm("displayName", event.currentTarget.value)}
placeholder="First admin"
/>
</label>
<input
type="text"
value={adminForm.displayName}
onInput={(event): void => setAdminForm("displayName", event.currentTarget.value)}
placeholder="Admin"
/>
</label>
<label class={styles.field}>
<span class={styles.fieldLabel}>Email</span>
<input