<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
import { api, type BackendSummary, type DirectoryItem, type FileDetailResponse, type JobSummary, type PlacementPolicySummary } from "./lib/api";
import { clearAccessToken, getAccessToken, setAccessToken } from "./lib/auth";
import { formatBackendType, formatBytes, formatDate, titleCase } from "./lib/format";
import { uploadFileToDirectory } from "./lib/uploads";

type RouteName = "login" | "files" | "recycle" | "storage" | "policies" | "jobs";
type DialogMode = "create" | "rename" | "move" | "delete" | null;
type ViewMode = "list" | "grid";
type BackendType = "local_directory" | "s3" | "webdav";
type StabilityClass = "local" | "stable" | "opportunistic";

const CONFIG_TEMPLATES: Record<BackendType, string> = {
  local_directory: JSON.stringify({ base_path: "./.iron-storage/archive" }, null, 2),
  s3: JSON.stringify(
    {
      bucket: "iron",
      region: "us-east-1",
      endpoint_url: "http://127.0.0.1:9000",
      access_key_id: "",
      secret_access_key: "",
      prefix: "iron",
      force_path_style: true,
    },
    null,
    2,
  ),
  webdav: JSON.stringify(
    {
      endpoint_url: "http://127.0.0.1:5244/dav",
      username: "iron",
      password: "",
      root_path: "iron",
      verify_ssl: true,
    },
    null,
    2,
  ),
};

const route = ref(parseRoute());
const userInitial = ref("A");
const loginUsername = ref("admin");
const loginPassword = ref("changeme-iron");
const loginError = ref("");
const isLoginPending = ref(false);
const search = ref(new URLSearchParams(window.location.search).get("q") ?? "");
const toast = ref("");

const directoryId = computed(() => route.value.directoryId ?? "dir_root");
const directoryName = ref("Personal");
const parentStack = ref<Array<{ id: string; name: string }>>([{ id: "dir_root", name: "Personal" }]);
const directoryItems = ref<DirectoryItem[]>([]);
const selectedItem = ref<DirectoryItem | null>(null);
const sortBy = ref<"name" | "updated" | "size">("name");
const viewMode = ref<ViewMode>("list");
const fileInput = ref<HTMLInputElement | null>(null);
const page = ref(1);
const pageSize = ref(8);
const openMenuId = ref<string | null>(null);
const dialogMode = ref<DialogMode>(null);
const fieldValue = ref("");
const moveTargetId = ref("dir_root");
const fileDetail = ref<FileDetailResponse | null>(null);
const previewUrl = ref("");
const previewText = ref("");
const isDetailDrawerOpen = ref(false);

const recycleItems = ref<Array<DirectoryItem & { deleted_at?: string }>>([]);
const recyclePage = ref(1);
const recyclePageSize = ref(8);
const backends = ref<BackendSummary[]>([]);
const backendPage = ref(1);
const backendPageSize = ref(8);
const isCreateBackendOpen = ref(false);
const editingBackend = ref<BackendSummary | null>(null);
const backendType = ref<BackendType>("s3");
const backendName = ref("s3-main");
const backendStabilityClass = ref<StabilityClass>("stable");
const backendReadPriority = ref(80);
const backendWritePriority = ref(80);
const backendConfigText = ref(CONFIG_TEMPLATES.s3);
const backendFormError = ref("");
const editName = ref("");
const editStabilityClass = ref<StabilityClass>("stable");
const editReadPriority = ref(80);
const editWritePriority = ref(80);
const editConfigText = ref("{}");
const editSecretText = ref("{}");
const editError = ref("");
const policies = ref<PlacementPolicySummary[]>([]);
const selectedPolicyClass = ref<PlacementPolicySummary["file_class"]>("document");
const jobs = ref<JobSummary[]>([]);
const jobsPage = ref(1);
const jobsPageSize = ref(8);
const selectedJob = ref<JobSummary | null>(null);

const currentPolicy = computed(() => policies.value.find((policy) => policy.file_class === selectedPolicyClass.value) ?? null);
const folders = computed(() => filteredItems.value.filter((item) => item.kind === "directory"));
const files = computed(() => filteredItems.value.filter((item) => item.kind === "file"));
const filteredItems = computed(() => {
  const query = search.value.trim().toLowerCase();
  return [...directoryItems.value]
    .filter((item) => (query ? item.name.toLowerCase().includes(query) : true))
    .sort((first, second) => {
      if (sortBy.value === "updated") {
        return second.updated_at.localeCompare(first.updated_at);
      }
      if (sortBy.value === "size") {
        return (second.size_bytes ?? 0) - (first.size_bytes ?? 0);
      }
      if (first.kind !== second.kind) {
        return first.kind === "directory" ? -1 : 1;
      }
      return first.name.localeCompare(second.name);
    });
});
const totalPages = computed(() => Math.max(1, Math.ceil(filteredItems.value.length / pageSize.value)));
const pagedItems = computed(() => filteredItems.value.slice((page.value - 1) * pageSize.value, page.value * pageSize.value));
const fileTotalBytes = computed(() => files.value.reduce((sum, item) => sum + (item.size_bytes ?? 0), 0));
const pageItems = computed(() => getPaginationItems(page.value, totalPages.value));
const moveTargets = computed(() => {
  const entries = [...parentStack.value, ...folders.value.map((folder) => ({ id: folder.id, name: folder.name }))];
  const unique = new Map<string, string>();
  for (const entry of [...entries, { id: "dir_root", name: "Personal" }]) unique.set(entry.id, entry.name);
  if (selectedItem.value?.kind === "directory") unique.delete(selectedItem.value.id);
  return Array.from(unique.entries()).map(([id, name]) => ({ id, name }));
});
const recycleTotalBytes = computed(() => recycleItems.value.reduce((sum, item) => sum + (item.size_bytes ?? 0), 0));
const recycleTotalPages = computed(() => Math.max(1, Math.ceil(recycleItems.value.length / recyclePageSize.value)));
const recyclePageItems = computed(() => getPaginationItems(recyclePage.value, recycleTotalPages.value));
const pagedRecycleItems = computed(() => recycleItems.value.slice((recyclePage.value - 1) * recyclePageSize.value, recyclePage.value * recyclePageSize.value));
const healthyBackendCount = computed(() => backends.value.filter((backend) => backend.last_health_status === "healthy").length);
const enabledBackendCount = computed(() => backends.value.filter((backend) => backend.is_enabled).length);
const backendTotalPages = computed(() => Math.max(1, Math.ceil(backends.value.length / backendPageSize.value)));
const backendPageItems = computed(() => getPaginationItems(backendPage.value, backendTotalPages.value));
const pagedBackends = computed(() => backends.value.slice((backendPage.value - 1) * backendPageSize.value, backendPage.value * backendPageSize.value));
const pendingJobsCount = computed(() => jobs.value.filter((job) => job.status === "queued" || job.status === "pending").length);
const completedJobsCount = computed(() => jobs.value.filter((job) => job.status === "completed").length);
const failedJobsCount = computed(() => jobs.value.filter((job) => job.status === "failed").length);
const jobsTotalPages = computed(() => Math.max(1, Math.ceil(jobs.value.length / jobsPageSize.value)));
const jobsPageItems = computed(() => getPaginationItems(jobsPage.value, jobsTotalPages.value));
const pagedJobs = computed(() => jobs.value.slice((jobsPage.value - 1) * jobsPageSize.value, jobsPage.value * jobsPageSize.value));

watch(route, async () => {
  if (route.value.name === "login") {
    return;
  }
  await loadRouteData();
});

watch(selectedItem, async (item) => {
  isDetailDrawerOpen.value = false;
  fileDetail.value = null;
  previewText.value = "";
  if (previewUrl.value) {
    URL.revokeObjectURL(previewUrl.value);
    previewUrl.value = "";
  }
  if (item?.kind === "file") {
    await loadFileDetailAndPreview(item);
  }
});

watch([page, totalPages], () => {
  if (page.value > totalPages.value) page.value = totalPages.value;
});

watch([search, sortBy, viewMode], () => {
  page.value = 1;
  openMenuId.value = null;
});

watch([recyclePage, recycleTotalPages], () => {
  if (recyclePage.value > recycleTotalPages.value) recyclePage.value = recycleTotalPages.value;
});

watch([backendPage, backendTotalPages], () => {
  if (backendPage.value > backendTotalPages.value) backendPage.value = backendTotalPages.value;
});

watch([jobsPage, jobsTotalPages], () => {
  if (jobsPage.value > jobsTotalPages.value) jobsPage.value = jobsTotalPages.value;
});

onMounted(async () => {
  window.addEventListener("popstate", handlePopState);
  window.addEventListener("keydown", handleGlobalKeydown);
  if (!getAccessToken() && route.value.name !== "login") {
    navigate("/app/login");
    return;
  }
  await loadRouteData();
});

onUnmounted(() => {
  window.removeEventListener("popstate", handlePopState);
  window.removeEventListener("keydown", handleGlobalKeydown);
});

function handlePopState() {
  route.value = parseRoute();
}

function handleGlobalKeydown(event: KeyboardEvent) {
  if (event.key === "Escape") {
    openMenuId.value = null;
  }
}

function parseRoute(): { name: RouteName; directoryId?: string } {
  const path = window.location.pathname;
  if (path.startsWith("/app/login")) return { name: "login" };
  if (path.startsWith("/app/recycle-bin")) return { name: "recycle" };
  if (path.startsWith("/app/storage")) return { name: "storage" };
  if (path.startsWith("/app/policies")) return { name: "policies" };
  if (path.startsWith("/app/jobs")) return { name: "jobs" };
  const match = path.match(/^\/app\/files\/([^/]+)/);
  return { name: "files", directoryId: match?.[1] ?? "dir_root" };
}

function navigate(path: string) {
  window.history.pushState({}, "", path);
  route.value = parseRoute();
}

async function loadRouteData() {
  try {
    if (route.value.name === "files") await loadDirectory();
    if (route.value.name === "recycle") await loadRecycleBin();
    if (route.value.name === "storage") await loadStorage();
    if (route.value.name === "policies") await loadPolicies();
    if (route.value.name === "jobs") await loadJobs();
  } catch (error) {
    showError(error);
  }
}

async function login() {
  isLoginPending.value = true;
  loginError.value = "";
  try {
    const result = await api.login(loginUsername.value, loginPassword.value);
    setAccessToken(result.access_token);
    userInitial.value = result.user.username.slice(0, 1).toUpperCase();
    navigate("/app/files");
  } catch (error) {
    loginError.value = error instanceof Error ? error.message : "Sign in failed.";
  } finally {
    isLoginPending.value = false;
  }
}

function logout() {
  clearAccessToken();
  navigate("/app/login");
}

async function loadDirectory() {
  const result = await api.listDirectoryChildren(directoryId.value);
  directoryItems.value = result.items;
  directoryName.value = directoryId.value === "dir_root" ? "Personal" : result.directory.name;
  updateStack(result.directory.id, result.directory.name);
  selectedItem.value = null;
  page.value = 1;
}

function updateStack(id: string, name: string) {
  const index = parentStack.value.findIndex((item) => item.id === id);
  if (index >= 0) {
    parentStack.value = parentStack.value.slice(0, index + 1);
  } else {
    parentStack.value = [...parentStack.value, { id, name }];
  }
}

function openDirectory(item: DirectoryItem) {
  navigate(item.id === "dir_root" ? "/app/files" : `/app/files/${item.id}`);
}

function goBackDirectory() {
  const nextStack = parentStack.value.slice(0, -1);
  const parent = nextStack[nextStack.length - 1] ?? { id: "dir_root", name: "Personal" };
  parentStack.value = nextStack.length ? nextStack : [{ id: "dir_root", name: "Personal" }];
  navigate(parent.id === "dir_root" ? "/app/files" : `/app/files/${parent.id}`);
}

async function uploadSelectedFile(event: Event) {
  const input = event.target as HTMLInputElement;
  const file = input.files?.[0];
  if (!file) return;
  try {
    await uploadFileToDirectory(file, directoryId.value);
    toast.value = `Uploaded ${file.name}.`;
    await loadDirectory();
  } catch (error) {
    showError(error);
  } finally {
    input.value = "";
  }
}

function openDialog(mode: DialogMode, item?: DirectoryItem) {
  if (item) selectedItem.value = item;
  openMenuId.value = null;
  dialogMode.value = mode;
  fieldValue.value = mode === "rename" ? selectedItem.value?.name ?? "" : "";
  moveTargetId.value = "dir_root";
}

async function confirmDialog() {
  if (!dialogMode.value) return;
  try {
    if (dialogMode.value === "create") {
      await api.createDirectory(directoryId.value, fieldValue.value || "New Folder");
      toast.value = "Folder created.";
    }
    if (dialogMode.value === "rename" && selectedItem.value) {
      if (selectedItem.value.kind === "directory") {
        await api.renameDirectory(selectedItem.value.id, fieldValue.value);
      } else {
        await api.renameFile(selectedItem.value.id, fieldValue.value);
      }
      toast.value = "Renamed.";
    }
    if (dialogMode.value === "move" && selectedItem.value) {
      if (selectedItem.value.kind === "directory") {
        await api.moveDirectory(selectedItem.value.id, moveTargetId.value);
      } else {
        await api.moveFile(selectedItem.value.id, moveTargetId.value);
      }
      toast.value = "Moved.";
    }
    if (dialogMode.value === "delete" && selectedItem.value?.kind === "file") {
      await api.deleteFile(selectedItem.value.id);
      toast.value = "Moved to recycle bin.";
    }
    dialogMode.value = null;
    await loadDirectory();
  } catch (error) {
    showError(error);
  }
}

async function loadFileDetailAndPreview(item: DirectoryItem) {
  fileDetail.value = await api.fileDetail(item.id);
  if (isImage(item.mime_type)) {
    const blob = await api.fileBlob(item.id, "preview");
    previewUrl.value = URL.createObjectURL(blob);
  } else if (item.mime_type === "application/pdf" || item.mime_type?.startsWith("video/")) {
    const blob = await api.fileBlob(item.id, item.mime_type.startsWith("video/") ? "stream" : "preview");
    previewUrl.value = URL.createObjectURL(blob);
  } else if (isText(item.mime_type, item.name)) {
    const blob = await api.fileBlob(item.id, "download");
    previewText.value = truncatePreviewText(await blob.text());
  }
}

async function downloadFile(item: DirectoryItem) {
  if (item.kind !== "file") return;
  openMenuId.value = null;
  const blob = await api.fileBlob(item.id, "download");
  const url = URL.createObjectURL(blob);
  const link = document.createElement("a");
  link.href = url;
  link.download = item.name;
  link.click();
  URL.revokeObjectURL(url);
}

async function reconcileFile(item: DirectoryItem) {
  if (item.kind !== "file") return;
  openMenuId.value = null;
  await api.reconcileFile(item.id);
  toast.value = "Reconcile queued.";
  await loadJobs();
}

function selectItem(item: DirectoryItem) {
  selectedItem.value = item;
}

function toggleItemMenu(item: DirectoryItem) {
  selectedItem.value = item;
  openMenuId.value = item.id;
}

function openItemFromMenu(item: DirectoryItem) {
  openMenuId.value = null;
  if (item.kind === "directory") {
    openDirectory(item);
  } else {
    void downloadFile(item).catch(showError);
  }
}

function openDetailDrawer() {
  openMenuId.value = null;
  isDetailDrawerOpen.value = true;
}

async function loadRecycleBin() {
  const result = await api.listRecycleBin();
  recycleItems.value = result.items.map((item) => ({ ...item, kind: "file" as const, replica_ready_count: null, replica_desired_count: null, replica_missing_count: null }));
  recyclePage.value = 1;
}

async function restoreFile(item: DirectoryItem) {
  await api.restoreFile(item.id);
  toast.value = "File restored.";
  await loadRecycleBin();
}

async function loadStorage() {
  const result = await api.listBackends();
  backends.value = result.items;
  backendPage.value = 1;
}

async function checkBackend(backend: BackendSummary) {
  await api.checkBackend(backend.id);
  toast.value = "Backend check queued.";
  await loadStorage();
}

async function runAllBackendChecks() {
  const result = await api.enqueueHealthChecks();
  const count = "enqueued_jobs" in (result as Record<string, unknown>) ? (result as { enqueued_jobs: number }).enqueued_jobs : backends.value.length;
  toast.value = `Queued ${count} backend health check job${count === 1 ? "" : "s"}.`;
  await loadJobs();
}

function resetBackendCreateForm(type: BackendType = "s3") {
  backendType.value = type;
  backendName.value = type === "local_directory" ? "local-archive" : type === "webdav" ? "openlist-main" : "s3-main";
  backendStabilityClass.value = type === "local_directory" ? "local" : "stable";
  backendReadPriority.value = 80;
  backendWritePriority.value = 80;
  backendConfigText.value = CONFIG_TEMPLATES[type];
  backendFormError.value = "";
}

function handleBackendTypeChange(event: Event) {
  resetBackendCreateForm((event.target as HTMLSelectElement).value as BackendType);
}

async function createBackend() {
  backendFormError.value = "";
  try {
    const config = parseJsonObject(backendConfigText.value);
    await api.createBackend({
      name: backendName.value,
      type: backendType.value,
      stability_class: backendStabilityClass.value,
      read_priority: backendReadPriority.value,
      write_priority: backendWritePriority.value,
      config,
    });
    isCreateBackendOpen.value = false;
    toast.value = "Backend created.";
    await loadStorage();
  } catch (error) {
    backendFormError.value = error instanceof Error ? error.message : "Backend creation failed.";
  }
}

function openCreateBackend() {
  resetBackendCreateForm();
  isCreateBackendOpen.value = true;
}

function openEditBackend(backend: BackendSummary) {
  editingBackend.value = backend;
  editName.value = backend.name;
  editStabilityClass.value = backend.stability_class as StabilityClass;
  editReadPriority.value = backend.read_priority;
  editWritePriority.value = backend.write_priority;
  editConfigText.value = JSON.stringify(backend.config ?? {}, null, 2);
  editSecretText.value = "{}";
  editError.value = "";
}

async function saveBackend() {
  if (!editingBackend.value) return;
  editError.value = "";
  try {
    const publicConfig = parseJsonObject(editConfigText.value);
    const secretConfig = parseJsonObject(editSecretText.value);
    await api.updateBackend(editingBackend.value.id, {
      name: editName.value,
      stability_class: editStabilityClass.value,
      read_priority: editReadPriority.value,
      write_priority: editWritePriority.value,
      config: { ...publicConfig, ...secretConfig },
    });
    editingBackend.value = null;
    toast.value = "Backend updated.";
    await loadStorage();
  } catch (error) {
    editError.value = error instanceof Error ? error.message : "Backend update failed.";
  }
}

async function toggleBackendEnabled(backend: BackendSummary) {
  if (backend.is_enabled) {
    await api.disableBackend(backend.id);
    toast.value = "Backend disabled.";
  } else {
    await api.enableBackend(backend.id);
    toast.value = "Backend enabled.";
  }
  await loadStorage();
}

function parseJsonObject(value: string): Record<string, unknown> {
  const parsed = JSON.parse(value || "{}") as unknown;
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
    throw new Error("Config must be a JSON object.");
  }
  return parsed as Record<string, unknown>;
}

async function loadPolicies() {
  const result = await api.listPlacementPolicies();
  policies.value = result.items;
}

async function savePolicy() {
  if (!currentPolicy.value) return;
  const policy = currentPolicy.value;
  await api.updatePlacementPolicy(policy.file_class, {
    require_local: policy.require_local,
    stable_replica_count: policy.stable_replica_count,
    opportunistic_replica_count: policy.opportunistic_replica_count,
    preferred_backend_ids: policy.preferred_backend_ids,
    excluded_backend_ids: policy.excluded_backend_ids,
    max_non_local_size_bytes: policy.max_non_local_size_bytes,
  });
  toast.value = "Policy saved.";
  await loadPolicies();
}

async function loadJobs() {
  const result = await api.listJobs();
  jobs.value = result.items;
  if (!selectedJob.value || !jobs.value.some((job) => job.id === selectedJob.value?.id)) {
    selectedJob.value = jobs.value[0] ?? null;
  }
}

async function retryJob(job: JobSummary) {
  await api.retryJob(job.id);
  await loadJobs();
}

async function runPendingJobs() {
  await api.runPendingJobs();
  await loadJobs();
}

async function enqueueHealthChecks() {
  await api.enqueueHealthChecks();
  await loadJobs();
}

async function enqueueFullReconcile() {
  await api.enqueueFullReconcile();
  await loadJobs();
}

function showError(error: unknown) {
  toast.value = error instanceof Error ? error.message : "Something went wrong.";
}

function setPageSize(value: Event) {
  pageSize.value = Number((value.target as HTMLSelectElement).value);
  page.value = 1;
}

function setRecyclePageSize(value: Event) {
  recyclePageSize.value = Number((value.target as HTMLSelectElement).value);
  recyclePage.value = 1;
}

function setBackendPageSize(value: Event) {
  backendPageSize.value = Number((value.target as HTMLSelectElement).value);
  backendPage.value = 1;
}

function setJobsPageSize(value: Event) {
  jobsPageSize.value = Number((value.target as HTMLSelectElement).value);
  jobsPage.value = 1;
}

function formatReplicaRatio(item: DirectoryItem) {
  if (item.kind !== "file") return "--";
  return `${item.replica_ready_count ?? 0}/${item.replica_desired_count ?? 0}`;
}

function replicaClass(item: DirectoryItem) {
  return item.replica_missing_count ? "replica-ratio" : "replica-ratio replica-ratio-ready";
}

function iconClass(item: DirectoryItem) {
  if (item.kind === "directory") return "resource-icon resource-icon-folder";
  if (item.mime_type?.includes("image")) return "resource-icon resource-icon-image";
  if (item.mime_type?.includes("pdf")) return "resource-icon resource-icon-pdf";
  if (item.name.endsWith(".md") || item.name.endsWith(".txt")) return "resource-icon resource-icon-document";
  if (item.name.endsWith(".zip")) return "resource-icon resource-icon-archive";
  return "resource-icon";
}

function iconLabel(item: DirectoryItem) {
  if (item.kind === "directory") return "";
  const ext = item.name.split(".").pop();
  return (ext ?? "FILE").slice(0, 3).toUpperCase();
}

function isImage(mime: string | null) {
  return Boolean(mime?.startsWith("image/"));
}

function isText(mime: string | null, name: string) {
  return Boolean(
    mime?.startsWith("text/") ||
      ["application/json", "application/xml", "application/yaml"].includes(mime ?? "") ||
      [".md", ".txt", ".json", ".yaml", ".yml", ".csv", ".log"].some((suffix) => name.toLowerCase().endsWith(suffix)),
  );
}

function truncatePreviewText(value: string) {
  const maxLength = 16_000;
  return value.length > maxLength ? `${value.slice(0, maxLength)}\n\n... preview truncated ...` : value;
}

function formatReplicaVerification(value: string | null) {
  return value ? `Verified ${formatDate(value)}` : "Not verified yet";
}

function formatBackendConfig(config: Record<string, unknown> | null) {
  if (!config) return "Configured";
  if (typeof config.base_path === "string") return config.base_path;
  if (typeof config.bucket === "string") {
    const prefix = typeof config.prefix === "string" && config.prefix ? `/${config.prefix}` : "";
    return `${config.bucket}${prefix}`;
  }
  if (typeof config.endpoint_url === "string") {
    const rootPath = typeof config.root_path === "string" ? config.root_path : "iron";
    return `${config.endpoint_url}/${rootPath}`;
  }
  return "Configured";
}

function getPaginationItems(current: number, total: number): Array<number | "ellipsis"> {
  if (total <= 7) return Array.from({ length: total }, (_, index) => index + 1);
  const values = new Set<number>([1, total, current - 1, current, current + 1]);
  if (current <= 4) [2, 3, 4, 5].forEach((item) => values.add(item));
  if (current >= total - 3) [total - 4, total - 3, total - 2, total - 1].forEach((item) => values.add(item));
  const sorted = Array.from(values).filter((item) => item >= 1 && item <= total).sort((a, b) => a - b);
  return sorted.flatMap((item, index) => {
    const previous = sorted[index - 1];
    if (!previous) return [item];
    if (item - previous === 2) return [previous + 1, item];
    if (item - previous > 2) return ["ellipsis", item];
    return [item];
  });
}
</script>

<template>
  <main v-if="route.name === 'login'" class="login-page">
    <form class="login-form" @submit.prevent="login">
      <div class="section-heading">
        <span class="eyebrow">Iron</span>
        <h2>Sign in</h2>
      </div>
      <label>
        <span>Username</span>
        <input v-model="loginUsername" required />
      </label>
      <label>
        <span>Password</span>
        <input v-model="loginPassword" type="password" required />
      </label>
      <OcButton appearance="filled" color-role="primary" submit="submit" :disabled="isLoginPending">
        {{ isLoginPending ? "Signing in..." : "Sign in" }}
      </OcButton>
      <p v-if="loginError" class="error-text">{{ loginError }}</p>
    </form>
  </main>

  <div v-else class="iron-app">
    <header class="iron-topbar">
      <div class="brand-area">
        <button class="app-switcher-button" type="button" aria-label="Application menu">
          <span v-for="item in 9" :key="item" />
        </button>
        <div class="brand-lockup"><span class="brand-mark">I</span><span>Iron</span></div>
      </div>
      <form class="topbar-search" role="search" @submit.prevent>
        <input v-model="search" aria-label="Search" placeholder="Enter search term" />
      </form>
      <button class="user-chip" type="button" @click="logout">{{ userInitial }}</button>
    </header>

    <div class="iron-workspace">
      <aside class="sidebar">
        <span class="sidebar-kicker">Personal</span>
        <h1 class="sidebar-title">Drive</h1>
        <nav class="main-nav">
          <a :class="route.name === 'files' ? 'nav-link active' : 'nav-link'" @click.prevent="navigate('/app/files')"><span class="nav-symbol nav-symbol-files" />Files</a>
          <a :class="route.name === 'recycle' ? 'nav-link active' : 'nav-link'" @click.prevent="navigate('/app/recycle-bin')"><span class="nav-symbol nav-symbol-recycle" />Deleted files</a>
          <a :class="route.name === 'storage' ? 'nav-link active' : 'nav-link'" @click.prevent="navigate('/app/storage')"><span class="nav-symbol nav-symbol-storage" />Storage</a>
          <a :class="route.name === 'policies' ? 'nav-link active' : 'nav-link'" @click.prevent="navigate('/app/policies')"><span class="nav-symbol nav-symbol-policies" />Policies</a>
          <a :class="route.name === 'jobs' ? 'nav-link active' : 'nav-link'" @click.prevent="navigate('/app/jobs')"><span class="nav-symbol nav-symbol-jobs" />Jobs</a>
        </nav>
        <div class="sidebar-footer">Iron Web UI<br />Personal cloud drive</div>
      </aside>

      <section class="workspace-content">
        <div v-if="route.name === 'files'" class="files-page-layout">
          <section class="files-content-shell">
            <div class="panel-toolbar">
              <div class="files-header">
                <div class="directory-title-row">
                  <button v-if="directoryId !== 'dir_root'" class="directory-back-button" type="button" aria-label="Back to parent folder" @click="goBackDirectory">‹</button>
                  <h2>{{ directoryName }}</h2>
                </div>
                <div class="inline-metrics">
                  <span>{{ folders.length }} folders</span>
                  <span>{{ files.length }} files</span>
                  <span>{{ formatBytes(fileTotalBytes) }}</span>
                </div>
              </div>
              <div class="command-bar files-command-bar">
                <OcButton appearance="filled" color-role="secondary" @click="openDialog('create')"><span class="button-symbol button-symbol-add" />New</OcButton>
                <OcButton appearance="outline" @click="fileInput?.click()"><span class="button-symbol button-symbol-upload" />Upload</OcButton>
                <OcButton appearance="outline" @click="loadDirectory">Refresh</OcButton>
                <OcButton appearance="outline" @click="viewMode = viewMode === 'list' ? 'grid' : 'list'">{{ viewMode === "list" ? "Grid" : "List" }}</OcButton>
                <select v-model="sortBy" class="compact-sort-select" aria-label="Sort current directory">
                  <option value="name">Sort by name</option>
                  <option value="updated">Sort by updated</option>
                  <option value="size">Sort by size</option>
                </select>
              </div>
            </div>
            <input ref="fileInput" hidden type="file" @change="uploadSelectedFile" />
            <p v-if="toast" class="notice-text">{{ toast }}</p>
            <div :class="viewMode === 'grid' ? 'list-surface grid-surface' : 'list-surface'">
              <div class="list-header"><span>Name</span><span>Updated</span><span>Size</span><span>Replicas</span><span>Actions</span></div>
              <div v-if="!pagedItems.length" class="empty-row">Add a folder or upload a file.</div>
              <div :class="viewMode === 'grid' ? 'drive-grid' : 'drive-list'">
                <div
                  v-for="item in pagedItems"
                  :key="item.id"
                  :class="selectedItem?.id === item.id ? 'drive-item active' : 'drive-item'"
                  role="button"
                  tabindex="0"
                  @click="selectItem(item)"
                  @dblclick="item.kind === 'directory' ? openDirectory(item) : undefined"
                >
                  <div class="drive-item-main">
                    <span :class="iconClass(item)">{{ iconLabel(item) }}</span>
                    <span class="resource-name-stack"><strong>{{ item.name }}</strong></span>
                  </div>
                  <span>{{ formatDate(item.updated_at) }}</span>
                  <span>{{ formatBytes(item.size_bytes) }}</span>
                  <span v-if="viewMode === 'list' || item.kind === 'file'" :class="replicaClass(item)">{{ formatReplicaRatio(item) }}</span>
                  <span class="row-actions" @click.stop>
                    <button class="row-action-button" type="button" @click="toggleItemMenu(item)">More</button>
                    <div v-if="openMenuId === item.id" class="menu-popover" role="menu">
                      <button class="menu-item" type="button" @click="openItemFromMenu(item)">{{ item.kind === "directory" ? "Open" : "Download" }}</button>
                      <button v-if="item.kind === 'file'" class="menu-item" type="button" @click="reconcileFile(item)">Reconcile</button>
                      <button class="menu-item" type="button" @click="openDialog('rename', item)">Rename</button>
                      <button class="menu-item" type="button" @click="openDialog('move', item)">Move</button>
                      <button v-if="item.kind === 'file'" class="menu-item menu-item-danger" type="button" @click="openDialog('delete', item)">Delete</button>
                    </div>
                  </span>
                </div>
              </div>
            </div>
            <div class="pagination-bar">
              <span class="pagination-summary">{{ filteredItems.length }} items</span>
              <div class="pagination-controls">
                <button class="pagination-page-button" type="button" :disabled="page === 1" @click="page = Math.max(1, page - 1)">‹</button>
                <span class="pagination-page-list">
                  <template v-for="(item, index) in pageItems" :key="`${item}-${index}`">
                    <span v-if="item === 'ellipsis'" class="pagination-ellipsis">...</span>
                    <button v-else :class="item === page ? 'pagination-page-button active' : 'pagination-page-button'" type="button" @click="page = item">{{ item }}</button>
                  </template>
                </span>
                <button class="pagination-page-button" type="button" :disabled="page === totalPages" @click="page = Math.min(totalPages, page + 1)">›</button>
                <select class="page-size-select" :value="pageSize" @change="setPageSize">
                  <option :value="8">8 / page</option>
                  <option :value="16">16 / page</option>
                  <option :value="32">32 / page</option>
                </select>
              </div>
            </div>
          </section>

          <aside class="detail-panel">
            <div v-if="!selectedItem" class="detail-empty"><span class="eyebrow">Inspector</span><h3>Select a file or folder.</h3></div>
            <template v-else>
              <div class="section-heading"><span class="eyebrow">Inspector</span><h3>{{ selectedItem.name }}</h3></div>
              <img v-if="previewUrl && isImage(selectedItem.mime_type)" class="preview-frame" :src="previewUrl" alt="" />
              <iframe v-else-if="previewUrl && selectedItem.mime_type === 'application/pdf'" class="preview-frame" :src="previewUrl" title="PDF preview" />
              <video v-else-if="previewUrl && selectedItem.mime_type?.startsWith('video/')" class="preview-frame" :src="previewUrl" controls preload="metadata" />
              <pre v-else-if="previewText" class="preview-frame text-preview-frame">{{ previewText }}</pre>
              <div v-else-if="selectedItem.kind === 'file'" class="preview-fallback"><strong>No inline preview</strong><p>Download this file to open it in another app.</p></div>
              <div v-if="selectedItem.kind === 'file'" class="inspector-storage-summary">
                <div><span>Replicas</span><strong>{{ formatReplicaRatio(selectedItem) }}</strong></div>
                <span :class="replicaClass(selectedItem)">{{ selectedItem.replica_missing_count ? "Needs sync" : "Ready" }}</span>
              </div>
              <OcButton v-if="selectedItem.kind === 'file'" class="inspector-info-button" appearance="outline" @click="openDetailDrawer">More Info</OcButton>
            </template>
          </aside>
        </div>

        <div v-if="route.name === 'recycle'" class="page-frame files-content-shell">
          <div class="section-heading"><span>Deleted files</span><h2>Recycle Bin</h2><div class="inline-metrics"><span>{{ recycleItems.length }} items</span><span>{{ formatBytes(recycleTotalBytes) }}</span></div></div>
          <div class="list-surface">
            <div class="list-header recycle-list-header"><span>Name</span><span>Deleted</span><span>Size</span><span>Actions</span></div>
            <div v-if="!recycleItems.length" class="empty-row">Recycle bin is empty.</div>
            <div v-for="item in pagedRecycleItems" :key="item.id" class="drive-item recycle-row" role="button" tabindex="0">
              <div class="drive-item-main"><span :class="iconClass(item)">{{ iconLabel(item) }}</span><strong>{{ item.name }}</strong></div>
              <span>{{ formatDate(item.deleted_at ?? item.updated_at) }}</span>
              <span>{{ formatBytes(item.size_bytes) }}</span>
              <OcButton appearance="outline" size="small" @click="restoreFile(item)">Restore</OcButton>
            </div>
          </div>
          <div class="pagination-bar">
            <span class="pagination-summary">{{ recycleItems.length }} items</span>
            <div class="pagination-controls">
              <button class="pagination-page-button" type="button" :disabled="recyclePage === 1" @click="recyclePage = Math.max(1, recyclePage - 1)">‹</button>
              <span class="pagination-page-list">
                <template v-for="(item, index) in recyclePageItems" :key="`recycle-${item}-${index}`">
                  <span v-if="item === 'ellipsis'" class="pagination-ellipsis">...</span>
                  <button v-else :class="item === recyclePage ? 'pagination-page-button active' : 'pagination-page-button'" type="button" @click="recyclePage = item">{{ item }}</button>
                </template>
              </span>
              <button class="pagination-page-button" type="button" :disabled="recyclePage === recycleTotalPages" @click="recyclePage = Math.min(recycleTotalPages, recyclePage + 1)">›</button>
              <select class="page-size-select" :value="recyclePageSize" @change="setRecyclePageSize">
                <option :value="8">8 / page</option>
                <option :value="16">16 / page</option>
                <option :value="32">32 / page</option>
              </select>
            </div>
          </div>
        </div>

        <div v-if="route.name === 'storage'" class="page-frame files-content-shell">
          <div class="page-toolbar">
            <div class="section-heading"><span>Storage</span><h2>Backends</h2><div class="inline-metrics"><span>{{ backends.length }} backends</span><span>{{ healthyBackendCount }} healthy</span><span>{{ enabledBackendCount }} enabled</span></div></div>
            <div class="command-bar"><OcButton appearance="filled" color-role="secondary" @click="openCreateBackend">Add Backend</OcButton><OcButton appearance="outline" @click="runAllBackendChecks">Run All Checks</OcButton></div>
          </div>
          <div class="list-surface">
            <div class="list-header storage-list-header"><span>Name</span><span>Class</span><span>Status</span><span>Updated</span><span>Actions</span></div>
            <div v-if="!backends.length" class="empty-row">No storage backends configured.</div>
            <div v-for="backend in pagedBackends" :key="backend.id" class="drive-item storage-row">
              <div><strong>{{ backend.name }}</strong><br /><span class="eyebrow">{{ formatBackendType(backend.type) }} · {{ formatBackendConfig(backend.config) }}{{ backend.secret_fields.length ? ` · ${backend.secret_fields.length} secrets` : "" }}</span></div>
              <span>{{ titleCase(backend.stability_class) }}</span>
              <span :class="backend.last_health_status === 'healthy' ? 'status-chip status-chip-success' : 'status-chip'">{{ backend.is_enabled ? titleCase(backend.last_health_status ?? "unknown") : "Disabled" }}</span>
              <span>{{ formatDate(backend.last_health_checked_at ?? backend.updated_at) }}</span>
              <span class="row-actions storage-row-actions">
                <OcButton appearance="outline" size="small" @click="checkBackend(backend)">Check</OcButton>
                <OcButton appearance="outline" size="small" @click="openEditBackend(backend)">Edit</OcButton>
                <OcButton appearance="outline" size="small" @click="toggleBackendEnabled(backend)">{{ backend.is_enabled ? "Disable" : "Enable" }}</OcButton>
              </span>
            </div>
          </div>
          <div class="pagination-bar">
            <span class="pagination-summary">{{ backends.length }} backends</span>
            <div class="pagination-controls">
              <button class="pagination-page-button" type="button" :disabled="backendPage === 1" @click="backendPage = Math.max(1, backendPage - 1)">‹</button>
              <span class="pagination-page-list">
                <template v-for="(item, index) in backendPageItems" :key="`backend-${item}-${index}`">
                  <span v-if="item === 'ellipsis'" class="pagination-ellipsis">...</span>
                  <button v-else :class="item === backendPage ? 'pagination-page-button active' : 'pagination-page-button'" type="button" @click="backendPage = item">{{ item }}</button>
                </template>
              </span>
              <button class="pagination-page-button" type="button" :disabled="backendPage === backendTotalPages" @click="backendPage = Math.min(backendTotalPages, backendPage + 1)">›</button>
              <select class="page-size-select" :value="backendPageSize" @change="setBackendPageSize">
                <option :value="8">8 / page</option>
                <option :value="16">16 / page</option>
                <option :value="32">32 / page</option>
              </select>
            </div>
          </div>
        </div>

        <div v-if="route.name === 'policies'" class="page-frame files-content-shell">
          <div class="section-heading"><span>Placement</span><h2>Replica policies</h2><div class="inline-metrics"><span>{{ policies.length }} policies</span><span>2 default replicas</span></div></div>
          <div class="command-bar" style="justify-content:flex-start;margin:.8rem 0"><OcButton v-for="policy in policies" :key="policy.file_class" :appearance="policy.file_class === selectedPolicyClass ? 'filled' : 'outline'" @click="selectedPolicyClass = policy.file_class">{{ titleCase(policy.file_class) }}</OcButton></div>
          <form v-if="currentPolicy" class="dialog-form policy-form" @submit.prevent="savePolicy"><label><span>Stable replicas</span><input v-model.number="currentPolicy.stable_replica_count" class="dialog-input" type="number" min="0" /></label><label><span>Opportunistic replicas</span><input v-model.number="currentPolicy.opportunistic_replica_count" class="dialog-input" type="number" min="0" /></label><OcButton appearance="filled" color-role="secondary" submit="submit">Save Policy</OcButton></form>
        </div>

        <div v-if="route.name === 'jobs'" class="files-page-layout">
          <section class="files-content-shell">
            <div class="page-toolbar">
              <div class="section-heading"><span>Jobs</span><h2>Background activity</h2><div class="inline-metrics"><span>{{ pendingJobsCount }} pending</span><span>{{ completedJobsCount }} completed</span><span>{{ failedJobsCount }} failed</span></div></div>
              <div class="command-bar"><OcButton appearance="outline" @click="runPendingJobs">Run Pending</OcButton><OcButton appearance="outline" @click="enqueueHealthChecks">Enqueue Checks</OcButton><OcButton appearance="filled" color-role="secondary" @click="enqueueFullReconcile">Full Reconcile</OcButton></div>
            </div>
            <div class="list-surface">
              <div class="list-header jobs-list-header"><span>Job</span><span>Status</span><span>Attempts</span><span>Actions</span></div>
              <div v-if="!jobs.length" class="empty-row">No background jobs recorded yet.</div>
              <div v-for="job in pagedJobs" :key="job.id" :class="selectedJob?.id === job.id ? 'drive-item jobs-row jobs-table-row active' : 'drive-item jobs-row jobs-table-row'" role="button" tabindex="0" @click="selectedJob = job">
                <div><strong>{{ titleCase(job.kind) }}</strong><br /><span class="eyebrow">Run after {{ formatDate(job.run_after) }}</span><br /><span v-if="job.last_error" class="error-text">{{ job.last_error }}</span></div>
                <span class="status-chip">{{ titleCase(job.status) }}</span>
                <span>{{ job.attempt_count }}/{{ job.max_attempts }}</span>
                <OcButton appearance="outline" size="small" @click.stop="retryJob(job)">Retry</OcButton>
              </div>
            </div>
            <div class="pagination-bar">
              <span class="pagination-summary">{{ jobs.length }} jobs</span>
              <div class="pagination-controls">
                <button class="pagination-page-button" type="button" :disabled="jobsPage === 1" @click="jobsPage = Math.max(1, jobsPage - 1)">‹</button>
                <span class="pagination-page-list">
                  <template v-for="(item, index) in jobsPageItems" :key="`jobs-${item}-${index}`">
                    <span v-if="item === 'ellipsis'" class="pagination-ellipsis">...</span>
                    <button v-else :class="item === jobsPage ? 'pagination-page-button active' : 'pagination-page-button'" type="button" @click="jobsPage = item">{{ item }}</button>
                  </template>
                </span>
                <button class="pagination-page-button" type="button" :disabled="jobsPage === jobsTotalPages" @click="jobsPage = Math.min(jobsTotalPages, jobsPage + 1)">›</button>
                <select class="page-size-select" :value="jobsPageSize" @change="setJobsPageSize">
                  <option :value="8">8 / page</option>
                  <option :value="16">16 / page</option>
                  <option :value="32">32 / page</option>
                </select>
              </div>
            </div>
          </section>
          <aside class="detail-panel jobs-detail-panel">
            <div v-if="!selectedJob" class="detail-empty"><span class="eyebrow">Inspector</span><h3>Select a job</h3></div>
            <template v-else>
              <div class="section-heading"><span>Job Detail</span><h3>{{ titleCase(selectedJob.kind) }}</h3></div>
              <div class="inspector-storage-summary"><div><span>Status</span><strong>{{ titleCase(selectedJob.status) }}</strong></div><span class="status-chip">{{ selectedJob.attempt_count }}/{{ selectedJob.max_attempts }}</span></div>
              <div class="meta-list inspector-card jobs-detail-meta">
                <div><span>Run after</span><strong>{{ formatDate(selectedJob.run_after) }}</strong></div>
                <div><span>Created</span><strong>{{ formatDate(selectedJob.created_at) }}</strong></div>
                <div><span>Updated</span><strong>{{ formatDate(selectedJob.updated_at) }}</strong></div>
                <div><span>ID</span><strong class="mono-text">{{ selectedJob.id }}</strong></div>
              </div>
              <div v-if="selectedJob.last_error" class="inspector-card replica-warning-card"><strong>Last error</strong><p>{{ selectedJob.last_error }}</p></div>
              <section class="inspector-section"><span class="eyebrow">Payload</span><pre class="payload-preview">{{ selectedJob.payload_json || '{}' }}</pre></section>
            </template>
          </aside>
        </div>
      </section>
    </div>
  </div>

  <div v-if="toast" class="toast-item">{{ toast }}</div>

  <div v-if="dialogMode" class="dialog-backdrop">
    <form class="dialog-card" role="dialog" aria-modal="true" @submit.prevent="confirmDialog">
      <div class="dialog-header">
        <div><span class="eyebrow">Action</span><h3>{{ dialogMode === "delete" ? "Delete file" : dialogMode === "create" ? "New folder" : dialogMode === "move" ? "Move item" : "Rename" }}</h3></div>
        <OcButton appearance="outline" @click="dialogMode = null">Close</OcButton>
      </div>
      <p v-if="dialogMode === 'delete'">Move <strong>{{ selectedItem?.name }}</strong> to the recycle bin?</p>
      <label v-else-if="dialogMode === 'move'"><span>Target folder</span><select v-model="moveTargetId" class="dialog-select"><option v-for="target in moveTargets" :key="target.id" :value="target.id">{{ target.name }} ({{ target.id }})</option></select></label>
      <label v-else><span>Name</span><input v-model="fieldValue" class="dialog-input" required /></label>
      <div class="dialog-actions">
        <OcButton appearance="outline" @click="dialogMode = null">Cancel</OcButton>
        <button v-if="dialogMode === 'delete'" class="danger-confirm-button" type="submit">Confirm</button>
        <OcButton v-else appearance="filled" color-role="secondary" submit="submit">Confirm</OcButton>
      </div>
    </form>
  </div>

  <div v-if="isCreateBackendOpen" class="dialog-backdrop">
    <form class="dialog-card dialog-card-wide" role="dialog" aria-modal="true" @submit.prevent="createBackend">
      <div class="dialog-header">
        <div><span class="eyebrow">Storage</span><h3>Add storage backend</h3></div>
        <OcButton appearance="outline" @click="isCreateBackendOpen = false">Close</OcButton>
      </div>
      <label><span>Type</span><select class="dialog-select" :value="backendType" @change="handleBackendTypeChange"><option value="local_directory">Local directory</option><option value="s3">S3</option><option value="webdav">OpenList / WebDAV</option></select></label>
      <label><span>Name</span><input v-model="backendName" class="dialog-input" required /></label>
      <label><span>Policy class</span><select v-model="backendStabilityClass" class="dialog-select"><option value="local">Local</option><option value="stable">Stable</option><option value="opportunistic">Opportunistic</option></select></label>
      <div class="split-fields"><label><span>Read priority</span><input v-model.number="backendReadPriority" class="dialog-input" type="number" /></label><label><span>Write priority</span><input v-model.number="backendWritePriority" class="dialog-input" type="number" /></label></div>
      <label><span>Config JSON</span><textarea v-model="backendConfigText" class="dialog-textarea" spellcheck="false" /></label>
      <p v-if="backendFormError" class="error-text">{{ backendFormError }}</p>
      <div class="dialog-actions">
        <OcButton appearance="outline" @click="isCreateBackendOpen = false">Cancel</OcButton>
        <OcButton appearance="filled" color-role="secondary" submit="submit">Create</OcButton>
      </div>
    </form>
  </div>

  <div v-if="editingBackend" class="dialog-backdrop">
    <form class="dialog-card dialog-card-wide" role="dialog" aria-modal="true" @submit.prevent="saveBackend">
      <div class="dialog-header">
        <div><span class="eyebrow">Storage</span><h3>Edit storage backend</h3></div>
        <OcButton appearance="outline" @click="editingBackend = null">Close</OcButton>
      </div>
      <label><span>Type</span><input class="dialog-input" :value="formatBackendType(editingBackend.type)" disabled /></label>
      <label><span>Name</span><input v-model="editName" class="dialog-input" required /></label>
      <label><span>Policy class</span><select v-model="editStabilityClass" class="dialog-select"><option value="local">Local</option><option value="stable">Stable</option><option value="opportunistic">Opportunistic</option></select></label>
      <div class="split-fields"><label><span>Read priority</span><input v-model.number="editReadPriority" class="dialog-input" type="number" /></label><label><span>Write priority</span><input v-model.number="editWritePriority" class="dialog-input" type="number" /></label></div>
      <label><span>Public config JSON</span><textarea v-model="editConfigText" class="dialog-textarea" spellcheck="false" /></label>
      <label><span>Secret override JSON</span><textarea v-model="editSecretText" class="dialog-textarea dialog-textarea-small" spellcheck="false" /></label>
      <div class="inspector-card"><strong>Stored secrets</strong><p>{{ editingBackend.secret_fields.length ? editingBackend.secret_fields.join(", ") : "None" }}</p></div>
      <p v-if="editError" class="error-text">{{ editError }}</p>
      <div class="dialog-actions">
        <OcButton appearance="outline" @click="editingBackend = null">Cancel</OcButton>
        <OcButton appearance="filled" color-role="secondary" submit="submit">Save</OcButton>
      </div>
    </form>
  </div>

  <div v-if="isDetailDrawerOpen && selectedItem?.kind === 'file'" class="detail-drawer-backdrop" @click="isDetailDrawerOpen = false">
    <aside class="detail-drawer" role="dialog" aria-modal="true" @click.stop>
      <div class="detail-drawer-header"><div><span class="eyebrow">Replica Info</span><h3>{{ selectedItem.name }}</h3></div><OcButton appearance="outline" @click="isDetailDrawerOpen = false">Close</OcButton></div>
      <div class="detail-drawer-content">
        <section class="inspector-section">
          <span class="eyebrow">Placement</span>
          <div class="inspector-card storage-summary-card">
            <div><span>Class</span><strong>{{ titleCase(fileDetail?.placement_file_class ?? "unknown") }}</strong></div>
            <div><span>Replicas</span><strong>{{ formatReplicaRatio(selectedItem) }}</strong></div>
            <div><span>Missing</span><strong>{{ fileDetail?.missing_backend_ids.length ?? selectedItem.replica_missing_count ?? 0 }}</strong></div>
          </div>
          <div v-if="fileDetail?.missing_backend_ids.length" class="inspector-card replica-warning-card"><strong>Replica gap detected</strong><p>Missing {{ fileDetail.missing_backend_ids.length }} desired backend{{ fileDetail.missing_backend_ids.length === 1 ? "" : "s" }}.</p></div>
        </section>
        <section class="inspector-section">
          <span class="eyebrow">Desired Backends</span>
          <div class="inspector-card replica-list">
            <div v-for="backend in fileDetail?.desired_backends ?? []" :key="backend.id" class="replica-row">
              <div><strong>{{ backend.name }}</strong><span class="item-muted">{{ formatBackendType(backend.type) }} · {{ titleCase(backend.stability_class) }}</span></div>
              <span :class="fileDetail?.missing_backend_ids.includes(backend.id) ? 'status-chip' : 'status-chip status-chip-success'">{{ fileDetail?.missing_backend_ids.includes(backend.id) ? "Missing" : "Ready" }}</span>
            </div>
            <p v-if="!fileDetail?.desired_backends.length" class="item-muted">No placement targets selected.</p>
          </div>
        </section>
        <section class="inspector-section">
          <span class="eyebrow">Replicas</span>
          <div class="inspector-card replica-list">
            <div v-for="replica in fileDetail?.replicas ?? []" :key="replica.id" class="replica-row">
              <div><strong>{{ replica.backend_name }}</strong><span class="item-muted">{{ formatBytes(replica.size_bytes) }} · {{ formatReplicaVerification(replica.last_verified_at) }}</span></div>
              <span :class="replica.status === 'ready' ? 'status-chip status-chip-success' : 'status-chip'">{{ titleCase(replica.status) }}</span>
            </div>
            <p v-if="!fileDetail?.replicas.length" class="item-muted">No recorded replicas for this file yet.</p>
          </div>
        </section>
        <section class="inspector-section">
          <span class="eyebrow">Preview Artifacts</span>
          <div class="inspector-card">
            <div v-for="artifact in fileDetail?.preview_artifacts ?? []" :key="artifact.id" class="artifact-row"><strong>{{ titleCase(artifact.artifact_type) }}</strong><span>{{ titleCase(artifact.status) }}</span></div>
            <p v-if="!fileDetail?.preview_artifacts.length" class="item-muted">No generated artifacts yet.</p>
          </div>
        </section>
      </div>
    </aside>
  </div>
</template>
