Restructure/frontend editor (#6404)

## Move editor under `frontend/editor/`

Pure restructure: `frontend/` becomes the workspace, `frontend/editor/`
holds
  the PDF editor. 1775 file renames + 40 wiring edits. No logic changes.

  ### Why

`frontend/` is currently the editor — its `src/`, `public/`,
`src-tauri/`,
  config files all sit at the root. Promoting `frontend/` to a
workspace and putting the editor in a sibling folder leaves room for
future
apps to drop in alongside it, sharing one `package.json` /
`node_modules` /
  lint config / Storybook.

  ### What moves

  frontend/
  ├── editor/                ← NEW: everything editor-specific
  │   ├── src/               ← was frontend/src/
  │   ├── public/            ← was frontend/public/
  │   ├── src-tauri/         ← was frontend/src-tauri/
│ ├── index.html, vite.config.ts, vitest.config.ts, playwright.config.ts
  │   ├── tsconfig*.json, tailwind.config.js, postcss.config.js
  │   ├── scripts/
  │   ├── .env, .env.desktop, .env.saas
  │   └── DeveloperGuide.md
├── package.json, package-lock.json, node_modules/ ← workspace install
  ├── eslint.config.mjs, .prettierrc, .prettierignore ← shared tooling
  ├── .gitignore
  └── README.md

  ### Wiring edits (40 files)

  - `.taskfiles/frontend.yml`, `desktop.yml`, `e2e.yml`
  - `build.gradle`, `app/core/build.gradle`
- `eslint.config.mjs`, `frontend/package.json`, `.gitignore`,
`.prettierignore`
  - `docker/frontend/Dockerfile`
  - 8 `.github/workflows/*.yml`, plus `.github/dependabot.yml`,
    `.github/config/.files.yaml`, `.github/labeler-config-srvaroa.yml`
  - `scripts/translations/**`
- Docs: `AGENTS.md`, `CLAUDE.md`, `ADDING_TOOLS.md`,
`DeveloperGuide.md`,
`WINDOWS_SIGNING.md`, `devGuide/HowToAddNewLanguage.md`,
`frontend/README.md`,
    `frontend/editor/DeveloperGuide.md`

Plus 3 renamed + edited: `editor/vite.config.ts` (env path +
node_modules
  walk-up), `editor/scripts/setup-env.mts` (renamed from `.ts` for
`import.meta.url`), `editor/scripts/build-provisioner.mjs` (resolve
src-tauri
  relative to script).

  ### Verification

  | Check | Result |
  |---|---|
  | `task frontend:typecheck:all` (6 variants) | exit 0 |
  | `task frontend:lint` (eslint + dpdm) | exit 0 |
  | `task frontend:format:check` | exit 0 |
  | `task frontend:test` | 657 tests pass, 50 files |
| `task frontend:build:{core,proprietary,saas,desktop,prototypes}` | all
green |
| `task desktop:build` | full Tauri pipeline →
`Stirling-PDF_2.11.0_x64_en-US.msi` |
  | `playwright test --list --project=stubbed` | 172 tests discovered |

`task desktop:build` exercises the heaviest path — Rust + WiX + MSI
bundle
against the moved `editor/src-tauri/`. If anything in the restructure
was
  wrong it wouldn't have built.

  ### Test plan

  - [ ] `frontend-validation.yml` green
  - [ ] `e2e-stubbed.yml` green
  - [ ] `tauri-build.yml` green on at least one platform
  - [ ] `check_toml.yml` runs on a translation-touching PR

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
Reece Browne
2026-05-22 13:40:34 +01:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 48027ee9d6
commit 0a50e765b7
1820 changed files with 300 additions and 226 deletions
@@ -0,0 +1,296 @@
import {
createContext,
useContext,
useEffect,
useState,
ReactNode,
useCallback,
} from "react";
import { springAuth } from "@app/auth/springAuthClient";
import { clearPlatformAuthOnLoginInit } from "@app/extensions/authSessionCleanup";
import type {
Session,
User,
AuthError,
AuthChangeEvent,
} from "@app/auth/springAuthClient";
/**
* Auth Context Type
* Simplified version without SaaS-specific features (credits, subscriptions)
*/
interface AuthContextType {
session: Session | null;
user: User | null;
loading: boolean;
error: AuthError | null;
signOut: () => Promise<void>;
refreshSession: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType>({
session: null,
user: null,
loading: true,
error: null,
signOut: async () => {},
refreshSession: async () => {},
});
/**
* Auth Provider Component
*
* Manages authentication state and provides it to the entire app.
* Integrates with Spring Security + JWT backend.
*/
export function AuthProvider({ children }: { children: ReactNode }) {
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<AuthError | null>(null);
// Debug: Track state transitions
useEffect(() => {
console.log("[Auth] State changed:", {
loading,
hasSession: !!session,
hasError: !!error,
userId: session?.user?.id,
timestamp: new Date().toISOString(),
});
}, [loading, session, error]);
/**
* Refresh current session
*/
const refreshSession = useCallback(async () => {
try {
setLoading(true);
setError(null);
console.debug("[Auth] refreshSession: start", {
path: window.location.pathname,
});
console.debug("[Auth] Refreshing session...");
const { data, error } = await springAuth.refreshSession();
if (error) {
console.error("[Auth] Session refresh error:", error);
setError(error);
setSession(null);
} else {
console.debug("[Auth] Session refreshed successfully");
setSession(data.session);
}
} catch (err) {
console.error("[Auth] Unexpected error during session refresh:", err);
setError(err as AuthError);
} finally {
console.debug("[Auth] refreshSession: done", { hasSession: !!session });
setLoading(false);
}
}, []);
/**
* Sign out user
*/
const signOut = useCallback(async () => {
try {
setError(null);
console.debug("[Auth] Signing out...");
const { error } = await springAuth.signOut();
if (error) {
console.error("[Auth] Sign out error:", error);
setError(error);
} else {
console.debug("[Auth] Signed out successfully");
setSession(null);
}
} catch (err) {
console.error("[Auth] Unexpected error during sign out:", err);
setError(err as AuthError);
}
}, []);
/**
* Initialize auth on mount
*/
useEffect(() => {
let mounted = true;
const mountId = Math.random().toString(36).substring(7);
console.log(`[Auth:${mountId}] 🔵 AuthProvider mounted`);
const initializeAuth = async () => {
try {
console.debug(`[Auth:${mountId}] Initializing auth...`);
console.debug(
`[Auth:${mountId}] Path: ${window.location.pathname} Search: ${window.location.search}`,
);
// Clear any platform-specific cached auth on login page init.
if (
typeof window !== "undefined" &&
window.location.pathname.startsWith("/login")
) {
await clearPlatformAuthOnLoginInit();
}
// Skip config check entirely - let the app handle login state
// The config will be fetched by useAppConfig when needed
const { data, error } = await springAuth.getSession();
if (!mounted) return;
if (error) {
console.error("[Auth] Initial session error:", error);
setError(error);
} else {
console.debug("[Auth] Initial session loaded:", {
hasSession: !!data.session,
userId: data.session?.user?.id,
email: data.session?.user?.email,
});
setSession(data.session);
}
} catch (err) {
console.error(
"[Auth] Unexpected error during auth initialization:",
err,
);
if (mounted) {
setError(err as AuthError);
}
} finally {
console.debug(
`[Auth:${mountId}] Initialize auth complete. mounted=${mounted}`,
);
if (mounted) {
setLoading(false);
}
}
};
initializeAuth();
// Listen for jwt-available event (triggered by desktop auth or other sources)
const handleJwtAvailable = () => {
console.log(`[Auth:${mountId}] ════════════════════════════════════`);
console.log(`[Auth:${mountId}] 🔄 JWT available event received`);
console.log(
`[Auth:${mountId}] Current state: loading=${loading}, hasSession=${!!session}`,
);
console.log(
`[Auth:${mountId}] Setting loading=true to stabilize auth state`,
);
setLoading(true); // Prevent unstable renders during auth state transition
setError(null);
console.log(`[Auth:${mountId}] Refreshing session...`);
void initializeAuth();
};
window.addEventListener("jwt-available", handleJwtAvailable);
// Subscribe to auth state changes
const {
data: { subscription },
} = springAuth.onAuthStateChange(
async (event: AuthChangeEvent, newSession: Session | null) => {
if (!mounted) {
console.log(
`[Auth:${mountId}] ⚠️ Auth state change ignored (unmounted): ${event}`,
);
return;
}
console.log(`[Auth:${mountId}] ════════════════════════════════════`);
console.log(`[Auth:${mountId}] 📢 Auth state change event: ${event}`);
console.log(`[Auth:${mountId}] Has session: ${!!newSession}`);
console.log(
`[Auth:${mountId}] User: ${newSession?.user?.email || "none"}`,
);
console.log(`[Auth:${mountId}] Timestamp: ${new Date().toISOString()}`);
// Schedule state update
setTimeout(() => {
if (mounted) {
console.log(
`[Auth:${mountId}] Applying session update (event: ${event})`,
);
setSession(newSession);
setError(null);
// Handle specific events
if (event === "SIGNED_OUT") {
console.log(
`[Auth:${mountId}] ✓ User signed out, session cleared`,
);
} else if (event === "SIGNED_IN") {
console.log(`[Auth:${mountId}] ✓ User signed in successfully`);
} else if (event === "TOKEN_REFRESHED") {
console.log(`[Auth:${mountId}] ✓ Token refreshed`);
} else if (event === "USER_UPDATED") {
console.log(`[Auth:${mountId}] ✓ User updated`);
}
} else {
console.log(
`[Auth:${mountId}] ⚠️ Session update skipped (unmounted during timeout)`,
);
}
}, 0);
},
);
return () => {
console.log(`[Auth:${mountId}] 🔴 AuthProvider unmounting`);
mounted = false;
window.removeEventListener("jwt-available", handleJwtAvailable);
subscription.unsubscribe();
};
}, []);
const value: AuthContextType = {
session,
user: session?.user ?? null,
loading,
error,
signOut,
refreshSession,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
/**
* Hook to access auth context
* Must be used within AuthProvider
*/
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}
/**
* Debug hook to expose auth state for debugging
* Can be used in development to monitor auth state
*/
export function useAuthDebug() {
const auth = useAuth();
useEffect(() => {
console.debug("[Auth Debug] Current auth state:", {
hasSession: !!auth.session,
hasUser: !!auth.user,
loading: auth.loading,
hasError: !!auth.error,
userId: auth.user?.id,
email: auth.user?.email,
});
}, [auth.session, auth.user, auth.loading, auth.error]);
return auth;
}