diff --git a/frontend/src/account/useAccountDashboard.js b/frontend/src/account/useAccountDashboard.js
new file mode 100644
index 0000000..013b223
--- /dev/null
+++ b/frontend/src/account/useAccountDashboard.js
@@ -0,0 +1,497 @@
+import { computed, onMounted, reactive, ref, watch } from "vue";
+import { auth, authFetch, login } from "@/auth";
+
+/**
+ * Build the Account page dashboard and admin action state.
+ *
+ * WHY: this page coordinates several downstream services plus admin
+ * approval flows, so isolating the orchestration keeps the SFC readable and
+ * gives tests a direct seam for service state behavior.
+ *
+ * @returns {object} reactive service cards, admin state, and event handlers.
+ */
+export function useAccountDashboard() {
+ const mailu = reactive({
+ status: "loading",
+ imap: "mail.bstein.dev:993 (TLS)",
+ smtp: "mail.bstein.dev:587 (STARTTLS)",
+ username: "",
+ currentPassword: "",
+ revealPassword: false,
+ rotating: false,
+ newPassword: "",
+ error: "",
+ });
+
+ const jellyfin = reactive({
+ status: "loading",
+ username: "",
+ syncStatus: "",
+ syncDetail: "",
+ error: "",
+ });
+
+ const vaultwarden = reactive({
+ status: "loading",
+ username: "",
+ syncedAt: "",
+ error: "",
+ });
+
+ const nextcloudMail = reactive({
+ status: "loading",
+ primaryEmail: "",
+ accountCount: "",
+ syncedAt: "",
+ syncing: false,
+ error: "",
+ });
+
+ const wger = reactive({
+ status: "loading",
+ username: "",
+ password: "",
+ passwordUpdatedAt: "",
+ revealPassword: false,
+ resetting: false,
+ error: "",
+ });
+
+ const firefly = reactive({
+ status: "loading",
+ username: "",
+ password: "",
+ passwordUpdatedAt: "",
+ revealPassword: false,
+ resetting: false,
+ error: "",
+ });
+
+ const admin = reactive({
+ enabled: false,
+ loading: false,
+ requests: [],
+ error: "",
+ acting: {},
+ flags: [],
+ flagsLoading: false,
+ notes: {},
+ selectedFlags: {},
+ });
+ const onboardingUrl = ref("/onboarding");
+ const vaultwardenReady = computed(() =>
+ ["ready", "already_present", "active", "grandfathered"].includes(vaultwarden.status),
+ );
+ const vaultwardenDisplayStatus = computed(() => (vaultwardenReady.value ? "ready" : vaultwarden.status));
+ const vaultwardenOrder = computed(() => (vaultwardenReady.value ? 3 : 0));
+
+ const doLogin = () => login("/account");
+
+ const copied = reactive({});
+ const normalizeEmail = (value) => (typeof value === "string" ? value.toLowerCase() : "");
+ onMounted(() => {
+ if (auth.ready && auth.authenticated) {
+ refreshOverview();
+ refreshAdminRequests();
+ refreshAdminFlags();
+ } else {
+ mailu.status = "login required";
+ nextcloudMail.status = "login required";
+ jellyfin.status = "login required";
+ vaultwarden.status = "login required";
+ wger.status = "login required";
+ firefly.status = "login required";
+ }
+ });
+
+ watch(
+ () => [auth.ready, auth.authenticated],
+ ([ready, authenticated]) => {
+ if (!ready) return;
+ if (!authenticated) {
+ mailu.status = "login required";
+ nextcloudMail.status = "login required";
+ jellyfin.status = "login required";
+ vaultwarden.status = "login required";
+ wger.status = "login required";
+ firefly.status = "login required";
+ onboardingUrl.value = "/onboarding";
+ admin.enabled = false;
+ admin.requests = [];
+ admin.flags = [];
+ return;
+ }
+ refreshOverview();
+ refreshAdminRequests();
+ refreshAdminFlags();
+ },
+ { immediate: false },
+ );
+
+ async function refreshOverview() {
+ mailu.error = "";
+ jellyfin.error = "";
+ vaultwarden.error = "";
+ nextcloudMail.error = "";
+ wger.error = "";
+ firefly.error = "";
+ try {
+ const resp = await authFetch("/api/account/overview", {
+ headers: { Accept: "application/json" },
+ cache: "no-store",
+ });
+ if (!resp.ok) {
+ const data = await resp.json().catch(() => ({}));
+ throw new Error(data?.error || `status ${resp.status}`);
+ }
+ const data = await resp.json();
+ mailu.status = data.mailu?.status || "ready";
+ mailu.username = normalizeEmail(data.mailu?.username) || normalizeEmail(auth.email) || auth.username;
+ mailu.currentPassword = data.mailu?.app_password || "";
+ nextcloudMail.status = data.nextcloud_mail?.status || "unknown";
+ nextcloudMail.primaryEmail = normalizeEmail(data.nextcloud_mail?.primary_email) || "";
+ nextcloudMail.accountCount = data.nextcloud_mail?.account_count || "";
+ nextcloudMail.syncedAt = data.nextcloud_mail?.synced_at || "";
+ wger.status = data.wger?.status || "unknown";
+ wger.username = normalizeEmail(data.wger?.username) || mailu.username || auth.username;
+ wger.password = data.wger?.password || "";
+ wger.passwordUpdatedAt = data.wger?.password_updated_at || "";
+ firefly.status = data.firefly?.status || "unknown";
+ firefly.username = normalizeEmail(data.firefly?.username) || mailu.username || auth.username;
+ firefly.password = data.firefly?.password || "";
+ firefly.passwordUpdatedAt = data.firefly?.password_updated_at || "";
+ vaultwarden.status = data.vaultwarden?.status || "unknown";
+ vaultwarden.username = normalizeEmail(data.vaultwarden?.username) || mailu.username || auth.username;
+ vaultwarden.syncedAt = data.vaultwarden?.synced_at || "";
+ jellyfin.status = data.jellyfin?.status || "ready";
+ jellyfin.username = data.jellyfin?.username || auth.username;
+ jellyfin.syncStatus = data.jellyfin?.sync_status || "";
+ jellyfin.syncDetail = data.jellyfin?.sync_detail || "";
+ onboardingUrl.value = data.onboarding_url || "/onboarding";
+ } catch (err) {
+ mailu.status = "unavailable";
+ nextcloudMail.status = "unavailable";
+ wger.status = "unavailable";
+ firefly.status = "unavailable";
+ vaultwarden.status = "unavailable";
+ jellyfin.status = "unavailable";
+ jellyfin.syncStatus = "";
+ jellyfin.syncDetail = "";
+ onboardingUrl.value = "/onboarding";
+ const message = err?.message ? `Failed to load account status (${err.message})` : "Failed to load account status.";
+ mailu.error = message;
+ nextcloudMail.error = message;
+ wger.error = message;
+ firefly.error = message;
+ vaultwarden.error = message;
+ jellyfin.error = message;
+ }
+ }
+
+ async function refreshAdminRequests() {
+ if (!auth.authenticated) {
+ admin.enabled = false;
+ admin.requests = [];
+ return;
+ }
+ admin.error = "";
+ admin.loading = true;
+ try {
+ const resp = await authFetch("/api/admin/access/requests", {
+ headers: { Accept: "application/json" },
+ cache: "no-store",
+ });
+ if (resp.status === 403) {
+ admin.enabled = false;
+ admin.requests = [];
+ return;
+ }
+ if (!resp.ok) throw new Error(`status ${resp.status}`);
+ const data = await resp.json();
+ admin.enabled = true;
+ admin.requests = Array.isArray(data.requests) ? data.requests : [];
+ for (const req of admin.requests) {
+ if (!req?.username) continue;
+ if (!(req.username in admin.notes)) admin.notes[req.username] = "";
+ if (!(req.username in admin.selectedFlags)) admin.selectedFlags[req.username] = [];
+ }
+ } catch (err) {
+ admin.enabled = false;
+ admin.requests = [];
+ admin.error = err.message || "Failed to load access requests.";
+ } finally {
+ admin.loading = false;
+ }
+ }
+
+ async function refreshAdminFlags() {
+ if (!auth.authenticated) {
+ admin.flags = [];
+ admin.flagsLoading = false;
+ return;
+ }
+ admin.flagsLoading = true;
+ try {
+ const resp = await authFetch("/api/admin/access/flags", {
+ headers: { Accept: "application/json" },
+ cache: "no-store",
+ });
+ if (resp.status === 403) {
+ admin.flags = [];
+ return;
+ }
+ if (!resp.ok) throw new Error(`status ${resp.status}`);
+ const data = await resp.json();
+ admin.flags = Array.isArray(data.flags) ? data.flags : [];
+ } catch (err) {
+ admin.flags = [];
+ admin.error = admin.error || err.message || "Failed to load access flags.";
+ } finally {
+ admin.flagsLoading = false;
+ }
+ }
+
+ function hasFlag(username, flag) {
+ const selected = admin.selectedFlags[username];
+ return Array.isArray(selected) && selected.includes(flag);
+ }
+
+ function formatName(req) {
+ if (!req) return "unknown";
+ const parts = [];
+ if (req.first_name && String(req.first_name).trim()) {
+ parts.push(String(req.first_name).trim());
+ }
+ if (req.last_name && String(req.last_name).trim()) {
+ parts.push(String(req.last_name).trim());
+ }
+ return parts.length ? parts.join(" ") : "unknown";
+ }
+
+ function formatActionError(err, fallback) {
+ const message = err?.message || "";
+ if (!message) return fallback;
+ const normalized = message.toLowerCase();
+ if (normalized.includes("ariadne unavailable") || normalized.includes("status 502") || normalized.includes("status 503")) {
+ return "Ariadne is busy. Please try again in a moment.";
+ }
+ return message;
+ }
+
+ function toggleFlag(username, flag, event) {
+ const checked = Boolean(event?.target?.checked);
+ const selected = Array.isArray(admin.selectedFlags[username]) ? [...admin.selectedFlags[username]] : [];
+ const next = checked ? Array.from(new Set([...selected, flag])) : selected.filter((item) => item !== flag);
+ admin.selectedFlags[username] = next;
+ }
+
+ async function rotateMailu() {
+ mailu.error = "";
+ mailu.newPassword = "";
+ mailu.rotating = true;
+ try {
+ const resp = await authFetch("/api/account/mailu/rotate", { method: "POST" });
+ const data = await resp.json().catch(() => ({}));
+ if (!resp.ok) throw new Error(data.error || `status ${resp.status}`);
+ mailu.newPassword = data.password || "";
+ if (mailu.newPassword) {
+ mailu.currentPassword = mailu.newPassword;
+ mailu.revealPassword = true;
+ }
+ const syncEnabled = Boolean(data.sync_enabled);
+ const syncOk = Boolean(data.sync_ok);
+ const syncError = data.sync_error || "";
+ if (!syncEnabled) {
+ mailu.status = "updated";
+ mailu.error = "Mail sync is not configured; password may not take effect until an admin sync runs.";
+ } else if (!syncOk) {
+ mailu.status = "sync pending";
+ mailu.error = syncError || "Mail sync did not confirm success yet. Try again in a moment.";
+ } else {
+ mailu.status = "updated";
+ }
+ await refreshOverview();
+ } catch (err) {
+ mailu.error = formatActionError(err, "Rotation failed");
+ } finally {
+ mailu.rotating = false;
+ }
+ }
+
+ async function resetWger() {
+ wger.error = "";
+ wger.resetting = true;
+ try {
+ const resp = await authFetch("/api/account/wger/reset", { method: "POST" });
+ const data = await resp.json().catch(() => ({}));
+ if (!resp.ok) throw new Error(data.error || `status ${resp.status}`);
+ if (data.password) {
+ wger.password = data.password;
+ wger.revealPassword = true;
+ }
+ await refreshOverview();
+ } catch (err) {
+ wger.error = formatActionError(err, "Reset failed");
+ } finally {
+ wger.resetting = false;
+ }
+ }
+
+ async function resetFirefly() {
+ firefly.error = "";
+ firefly.resetting = true;
+ try {
+ const resp = await authFetch("/api/account/firefly/reset", { method: "POST" });
+ const data = await resp.json().catch(() => ({}));
+ if (!resp.ok) throw new Error(data.error || `status ${resp.status}`);
+ if (data.password) {
+ firefly.password = data.password;
+ firefly.revealPassword = true;
+ }
+ await refreshOverview();
+ } catch (err) {
+ firefly.error = formatActionError(err, "Reset failed");
+ } finally {
+ firefly.resetting = false;
+ }
+ }
+
+ async function syncNextcloudMail() {
+ nextcloudMail.error = "";
+ nextcloudMail.syncing = true;
+ try {
+ const resp = await authFetch("/api/account/nextcloud/mail/sync", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ wait: true }),
+ });
+ const data = await resp.json().catch(() => ({}));
+ if (!resp.ok) throw new Error(data.error || `status ${resp.status}`);
+ await refreshOverview();
+ } catch (err) {
+ const message = formatActionError(err, "Sync failed");
+ if (message.toLowerCase().includes("ariadne is busy")) {
+ nextcloudMail.error = "Ariadne is busy. Refresh in a moment; the sync may have completed.";
+ } else {
+ nextcloudMail.error = message;
+ }
+ } finally {
+ nextcloudMail.syncing = false;
+ }
+ }
+
+ function fallbackCopy(text) {
+ const textarea = document.createElement("textarea");
+ textarea.value = text;
+ textarea.setAttribute("readonly", "");
+ textarea.style.position = "fixed";
+ textarea.style.top = "-9999px";
+ textarea.style.left = "-9999px";
+ document.body.appendChild(textarea);
+ textarea.select();
+ textarea.setSelectionRange(0, textarea.value.length);
+ document.execCommand("copy");
+ document.body.removeChild(textarea);
+ }
+
+ async function approve(username) {
+ admin.error = "";
+ admin.acting[username] = true;
+ try {
+ const flags = Array.isArray(admin.selectedFlags[username]) ? admin.selectedFlags[username] : [];
+ const note = (admin.notes[username] || "").trim();
+ const payload = {};
+ if (flags.length) payload.flags = flags;
+ if (note) payload.note = note;
+ const resp = await authFetch(`/api/admin/access/requests/${encodeURIComponent(username)}/approve`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ });
+ if (!resp.ok) {
+ const data = await resp.json().catch(() => ({}));
+ throw new Error(data.error || `status ${resp.status}`);
+ }
+ await refreshAdminRequests();
+ } catch (err) {
+ admin.error = err.message || "Approve failed";
+ } finally {
+ admin.acting[username] = false;
+ }
+ }
+
+ async function deny(username) {
+ admin.error = "";
+ admin.acting[username] = true;
+ try {
+ const note = (admin.notes[username] || "").trim();
+ const payload = note ? { note } : {};
+ const resp = await authFetch(`/api/admin/access/requests/${encodeURIComponent(username)}/deny`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ });
+ if (!resp.ok) {
+ const data = await resp.json().catch(() => ({}));
+ throw new Error(data.error || `status ${resp.status}`);
+ }
+ await refreshAdminRequests();
+ } catch (err) {
+ admin.error = err.message || "Deny failed";
+ } finally {
+ admin.acting[username] = false;
+ }
+ }
+
+ async function copy(key, text) {
+ if (!text) return;
+ try {
+ if (navigator?.clipboard?.writeText) {
+ await navigator.clipboard.writeText(text);
+ } else {
+ fallbackCopy(text);
+ }
+ copied[key] = true;
+ window.setTimeout(() => {
+ copied[key] = false;
+ }, 1500);
+ } catch {
+ try {
+ fallbackCopy(text);
+ copied[key] = true;
+ window.setTimeout(() => {
+ copied[key] = false;
+ }, 1500);
+ } catch {
+ // ignore
+ }
+ }
+ }
+
+ return {
+ auth,
+ mailu,
+ jellyfin,
+ vaultwarden,
+ nextcloudMail,
+ wger,
+ firefly,
+ admin,
+ onboardingUrl,
+ vaultwardenReady,
+ vaultwardenDisplayStatus,
+ vaultwardenOrder,
+ doLogin,
+ copied,
+ hasFlag,
+ formatName,
+ toggleFlag,
+ rotateMailu,
+ resetWger,
+ resetFirefly,
+ syncNextcloudMail,
+ approve,
+ deny,
+ copy,
+ };
+}
diff --git a/frontend/src/views/AccountView.vue b/frontend/src/views/AccountView.vue
index 0617aab..08afc78 100644
--- a/frontend/src/views/AccountView.vue
+++ b/frontend/src/views/AccountView.vue
@@ -418,465 +418,34 @@