<!-- [DEF:DashboardHub:Page] -->
<script>
  /**
   * @TIER: CRITICAL
   * @PURPOSE: Dashboard Hub - Central hub for managing dashboards with Git status and task actions
   * @LAYER: UI
   * @RELATION: BINDS_TO -> sidebarStore, taskDrawerStore
   * @INVARIANT: Always shows dashboards for the active environment from context store
   *
   * @UX_STATE: Loading -> Shows skeleton loader
   * @UX_STATE: Loaded -> Shows dashboard grid with status badges
   * @UX_STATE: Error -> Shows error banner with retry button
   * @UX_STATE: Selecting -> Checkboxes checked, floating action panel appears
   * @UX_STATE: BulkAction-Modal -> Migration or Backup modal open
   * @UX_FEEDBACK: Floating panel slides up from bottom when items selected
   * @UX_RECOVERY: Refresh button reloads dashboard list
   * @UX_REATIVITY: State: $state, Derived: $derived. Никаких устаревших export let.
   *
   * @TEST_CONTRACT Page_DashboardHub ->
   * {
   *   required_props: {},
   *   optional_props: {},
   *   invariants: [
   *       "Loads specific dashboards for currently selected environment store context",
   *       "Reflects Git sync status explicitly via row badges",
   *       "Submits migration action carrying db_mappings safely"
   *   ]
   * }
   * @TEST_FIXTURE init_state -> {}
   * @TEST_EDGE server_error -> shows error block and retry button
   * @TEST_EDGE bulk_git_actions -> verifies modal functionality when triggered
   * @TEST_INVARIANT rendering_and_selection -> verifies: [init_state]
   */

  import { onMount } from "svelte";
  import { goto } from "$app/navigation";
  import { t } from "$lib/i18n";
  import {
    openDrawerForTask,
    openDrawerForTaskIfPreferred,
  } from "$lib/stores/taskDrawer.js";
  import { api } from "$lib/api.js";
  import { debounce } from "$lib/utils/debounce.js";
  import { addToast } from "$lib/toasts.js";
  import { gitService } from "../../services/gitService.js";
  import MappingTable from "../../components/MappingTable.svelte";
  import {
    environmentContextStore,
    initializeEnvironmentContext,
  } from "$lib/stores/environmentContext.js";

  // State
  let selectedEnv = $state(null);
  let allDashboards = $state([]);
  let dashboards = $state([]);
  let filteredDashboards = $state([]);
  let isLoading = $state(true);
  let error = $state(null);

  // Pagination state
  let currentPage = $state(1);
  let pageSize = $state(10);
  let totalPages = $state(1);
  let total = $state(0);

  // Selection state
  let selectedIds = $state(new Set());
  let isAllSelected = $state(false);
  let isAllVisibleSelected = $state(false);

  // Search state
  let searchQuery = $state("");
  let sortColumn = $state("title");
  let sortDirection = $state("asc");
  let openFilterColumn = $state(null);
  let filterDropdownPosition = $state({ left: 0, top: 0 });
  let columnFilterSearch = $state({
    title: "",
    git_status: "",
    llm_status: "",
    changed_on: "",
    actor: "",
  });
  let columnFilters = $state({
    title: new Set(),
    git_status: new Set(),
    llm_status: new Set(),
    changed_on: new Set(),
    actor: new Set(),
  });

  // Bulk action modal state
  let showMigrateModal = $state(false);
  let showBackupModal = $state(false);
  let targetEnvId = $state(null);
  let backupSchedule = $state("");
  let isSubmittingMigrate = $state(false);
  let isSubmittingBackup = $state(false);
  let dbMappings = $state({});
  let availableDbMappings = $state([]);
  let sourceDatabases = $state([]);
  let targetDatabases = $state([]);
  let isEditingMappings = $state(false);
  let useDbMappings = $state(true);
  let fixCrossFilters = $state(true);

  // Individual action dropdown state
  let openActionDropdown = $state(null); // stores dashboard ID

  // Validation state
  let validatingIds = $state(new Set());
  let gitBusyIds = $state(new Set());
  let gitResolvedIds = $state(new Set());
  let gitLoadingIds = $state(new Set());
  let cachedGitConfigs = $state([]);
  let dashboardsLoadSeq = $state(0);
  let lastLoadedEnvId = $state(null);
  let serverTotal = $state(0);
  let serverTotalPages = $state(1);
  let profileFilterOverrideShowAll = $state(false);
  let effectiveProfileFilter = $state(null);

  // Dry run state
  let isDryRunLoading = $state(false);
  let dryRunData = $state(null);
  let dryRunError = $state(null);

  // Environment options - will be loaded from API
  let environments = $derived($environmentContextStore?.environments || []);

  // Debounced search function
  const debouncedSearch = debounce((query) => {
    searchQuery = query;
    currentPage = 1;
    void loadDashboards();
  }, 300);

  // Load environments and dashboards on mount
  onMount(async () => {
    await initializeEnvironmentContext();
    if (!selectedEnv && $environmentContextStore?.selectedEnvId) {
      selectedEnv = $environmentContextStore.selectedEnvId;
    }
  });

  // Handle document click to close dropdowns
  function handleDocumentClick(event) {
    if (!event.target.closest(".action-dropdown")) {
      closeActionDropdown();
    }
    if (!event.target.closest(".column-filter")) {
      openFilterColumn = null;
    }
  }

  // Add document click listener
  if (typeof document !== "undefined") {
    document.addEventListener("click", handleDocumentClick);
  }

  // Load dashboards from API
  // [DEF:DashboardHub.normalizeTaskStatus:Function]
  /**
   * @PURPOSE: Normalize raw task status to stable lowercase token for UI.
   * @PRE: status can be enum-like string or null.
   * @POST: returns null or normalized token without enum namespace.
   */
  function normalizeTaskStatus(status) {
    if (!status) return null;
    const raw = String(status).trim();
    const plain = raw.includes(".") ? raw.split(".").pop() : raw;
    return plain ? plain.toLowerCase() : null;
  }

  // [/DEF:DashboardHub.normalizeTaskStatus:Function]

  // [DEF:DashboardHub.normalizeValidationStatus:Function]
  /**
   * @PURPOSE: Normalize validation status to pass/fail/warn/unknown.
   * @PRE: status can be any scalar.
   * @POST: returns one of pass|fail|warn|unknown.
   */
  function normalizeValidationStatus(status) {
    if (!status) return "unknown";
    const normalized = String(status).trim().toUpperCase();
    if (normalized === "PASS") return "pass";
    if (normalized === "FAIL") return "fail";
    if (normalized === "WARN") return "warn";
    return "unknown";
  }

  // [/DEF:DashboardHub.normalizeValidationStatus:Function]

  // [DEF:DashboardHub.getValidationBadgeClass:Function]
  /**
   * @PURPOSE: Map validation level to badge class tuple.
   * @PRE: level in pass|fail|warn|unknown.
   * @POST: returns deterministic tailwind class string.
   */
  function getValidationBadgeClass(level) {
    if (level === "pass")
      return "bg-emerald-100 text-emerald-700 border-emerald-200";
    if (level === "fail") return "bg-rose-100 text-rose-700 border-rose-200";
    if (level === "warn") return "bg-amber-100 text-amber-700 border-amber-200";
    return "bg-slate-100 text-slate-700 border-slate-200";
  }

  // [/DEF:DashboardHub.getValidationBadgeClass:Function]

  // [DEF:DashboardHub.getValidationLabel:Function]
  /**
   * @PURPOSE: Map normalized validation level to compact UI label.
   * @PRE: level in pass|fail|warn|unknown.
   * @POST: returns uppercase status label.
   */
  function getValidationLabel(level) {
    if (level === "pass") return "PASS";
    if (level === "fail") return "FAIL";
    if (level === "warn") return "WARN";
    return "UNKNOWN";
  }

  // [/DEF:DashboardHub.getValidationLabel:Function]

  // [DEF:DashboardHub.normalizeOwners:Function]
  /**
   * @PURPOSE: Normalize owners payload to unique non-empty display labels.
   * @PRE: owners can be null, list of strings, or list of user objects.
   * @POST: Returns owner labels preserving source order.
   */
  function normalizeOwners(owners) {
    if (!Array.isArray(owners)) return [];

    const result = [];
    for (const owner of owners) {
      let label = null;
      if (typeof owner === "string") {
        const trimmed = owner.trim();
        if (trimmed) label = trimmed;
      } else if (owner && typeof owner === "object") {
        const firstName =
          typeof owner.first_name === "string" ? owner.first_name.trim() : "";
        const lastName =
          typeof owner.last_name === "string" ? owner.last_name.trim() : "";
        const fullName = [firstName, lastName].filter(Boolean).join(" ");
        label =
          fullName ||
          (typeof owner.full_name === "string" ? owner.full_name.trim() : "") ||
          (typeof owner.username === "string" ? owner.username.trim() : "") ||
          (typeof owner.email === "string" ? owner.email.trim() : "");
      }

      if (label && !result.includes(label)) {
        result.push(label);
      }
    }

    return result;
  }

  // [/DEF:DashboardHub.normalizeOwners:Function]

  // [DEF:DashboardHub.loadDashboards:Function]
  /**
   * @PURPOSE: Load full dashboard dataset for current environment and hydrate grid projection.
   * @PRE: selectedEnv is not null.
   * @POST: allDashboards, dashboards, pagination and selection state are synchronized.
   * @UX_STATE: Loading -> true during request lifecycle.
   * @UX_STATE: Error -> `error` populated when request fails.
   */
  async function loadDashboards() {
    if (!selectedEnv) return;

    const requestSeq = dashboardsLoadSeq + 1;
    dashboardsLoadSeq = requestSeq;
    isLoading = true;
    error = null;
    try {
      const firstResponse = await api.getDashboards(selectedEnv, {
        page: currentPage,
        page_size: pageSize,
        search: searchQuery.trim() || undefined,
        page_context: "dashboards_main",
        apply_profile_default: true,
        override_show_all: profileFilterOverrideShowAll,
        filters: {
          title: Array.from(columnFilters.title),
          git_status: Array.from(columnFilters.git_status),
          llm_status: Array.from(columnFilters.llm_status),
          changed_on: Array.from(columnFilters.changed_on),
          actor: Array.from(columnFilters.actor),
        },
      });
      if (requestSeq !== dashboardsLoadSeq) return;

      const rawDashboards = firstResponse?.dashboards || [];
      serverTotal = Number(firstResponse?.total || 0);
      serverTotalPages = Math.max(1, Number(firstResponse?.total_pages || 1));
      effectiveProfileFilter = firstResponse?.effective_profile_filter || null;
      profileFilterOverrideShowAll = Boolean(
        firstResponse?.effective_profile_filter?.override_show_all,
      );

      gitResolvedIds = new Set();
      gitLoadingIds = new Set();

      allDashboards = rawDashboards.map((d) => {
        const owners = normalizeOwners(d.owners);
        const hasGitStatus = !!d.git_status;
        return {
          id: d.id,
          title: d.title,
          slug: d.slug,
          changedOn: d.last_modified || null,
          changedOnLabel: formatDate(d.last_modified),
          owners,
          actorLabel: owners.length > 0 ? owners.join(", ") : "-",
          git: hasGitStatus
            ? {
                status: d.git_status?.sync_status?.toLowerCase() || "no_repo",
                branch: d.git_status?.branch || null,
                hasRepo: d.git_status?.has_repo === true,
                hasChangesForCommit:
                  d.git_status?.has_changes_for_commit === true,
              }
            : {
                status: "pending",
                branch: null,
                hasRepo: null,
                hasChangesForCommit: false,
              },
          lastTask: d.last_task
            ? {
                status: normalizeTaskStatus(d.last_task.status),
                validationStatus: normalizeValidationStatus(
                  d.last_task.validation_status,
                ),
                id: d.last_task.task_id,
              }
            : null,
          actions: ["migrate", "backup"],
        };
      });

      selectedIds = new Set(
        Array.from(selectedIds).filter((id) =>
          allDashboards.some((dashboard) => dashboard.id === id),
        ),
      );
      applyGridTransforms();
      updateSelectionState();
    } catch (err) {
      if (requestSeq !== dashboardsLoadSeq) return;
      error = err.message || $t.dashboard?.load_failed;
      console.error(
        "[DashboardHub][COHERENCE:FAILED] Load dashboards Error:",
        err,
      );
    } finally {
      if (requestSeq === dashboardsLoadSeq) {
        isLoading = false;
      }
    }
  }

  // Handle search input
  function handleSearch(event) {
    debouncedSearch(event.target.value);
  }

  // Handle page change
  function handlePageChange(page) {
    if (page === currentPage) return;
    currentPage = page;
    void loadDashboards();
  }

  // Handle page size change
  function handlePageSizeChange(event) {
    pageSize = parseInt(event.target.value);
    currentPage = 1;
    void loadDashboards();
  }

  // [DEF:DashboardHub.handleTemporaryShowAll:Function]
  /**
   * @PURPOSE: Temporarily disable profile-default dashboard filter for current page context.
   * @PRE: Dashboards list is loaded in dashboards_main context.
   * @POST: Next request is sent with override_show_all=true.
   */
  function handleTemporaryShowAll() {
    if (profileFilterOverrideShowAll) return;
    profileFilterOverrideShowAll = true;
    currentPage = 1;
    void loadDashboards();
  }
  // [/DEF:DashboardHub.handleTemporaryShowAll:Function]

  // [DEF:DashboardHub.handleRestoreProfileFilter:Function]
  /**
   * @PURPOSE: Re-enable persisted profile-default filtering after temporary override.
   * @PRE: Current page is in override mode.
   * @POST: Next request is sent with override_show_all=false.
   */
  function handleRestoreProfileFilter() {
    if (!profileFilterOverrideShowAll) return;
    profileFilterOverrideShowAll = false;
    currentPage = 1;
    void loadDashboards();
  }
  // [/DEF:DashboardHub.handleRestoreProfileFilter:Function]

  // Update selection state based on current selection
  function updateSelectionState() {
    const visibleCount = dashboards.length;
    const totalCount = filteredDashboards.length;

    isAllSelected = selectedIds.size === totalCount && totalCount > 0;
    isAllVisibleSelected =
      selectedIds.size === visibleCount && visibleCount > 0;
  }

  function replaceSelectedIds(nextSelection) {
    selectedIds = nextSelection;
    updateSelectionState();
  }

  function clearSelectedIds() {
    replaceSelectedIds(new Set());
  }

  // Handle checkbox change for individual dashboard
  function handleCheckboxChange(dashboard, event) {
    const nextSelection = new Set(selectedIds);
    if (event.target.checked) {
      nextSelection.add(dashboard.id);
    } else {
      nextSelection.delete(dashboard.id);
    }
    replaceSelectedIds(nextSelection);
  }

  // Handle select all
  function handleSelectAll() {
    if (isAllSelected) {
      clearSelectedIds();
      return;
    }
    const nextSelection = new Set(selectedIds);
    filteredDashboards.forEach((dashboard) => nextSelection.add(dashboard.id));
    replaceSelectedIds(nextSelection);
  }

  // Handle select visible
  function handleSelectVisible() {
    const nextSelection = new Set(selectedIds);
    if (isAllVisibleSelected) {
      dashboards.forEach((dashboard) => nextSelection.delete(dashboard.id));
    } else {
      dashboards.forEach((dashboard) => nextSelection.add(dashboard.id));
    }
    replaceSelectedIds(nextSelection);
  }

  // [/DEF:DashboardHub.loadDashboards:Function]

  // [DEF:DashboardHub.formatDate:Function]
  /**
   * @PURPOSE: Convert ISO timestamp to locale date string.
   * @PRE: value may be null or invalid date string.
   * @POST: returns formatted date or "-".
   */
  function formatDate(value) {
    if (!value) return "-";
    const date = new Date(value);
    if (Number.isNaN(date.getTime())) return "-";
    return date.toLocaleDateString();
  }

  // [/DEF:DashboardHub.formatDate:Function]

  // [DEF:DashboardHub.getGitSummaryLabel:Function]
  /**
   * @PURPOSE: Compute stable text label for git state column.
   * @PRE: dashboard has git projection fields.
   * @POST: returns localized summary string.
   */
  function getGitSummaryLabel(dashboard) {
    if (dashboard.git?.hasRepo === null) {
      return $t.common?.loading || "Loading";
    }
    if (!dashboard.git?.hasRepo) {
      return $t.dashboard?.status_no_repo || "No Repo";
    }
    return dashboard.git?.hasChangesForCommit
      ? $t.dashboard?.status_changes || "Diff"
      : $t.dashboard?.status_no_changes || "Synced";
  }

  // [/DEF:DashboardHub.getGitSummaryLabel:Function]

  // [DEF:DashboardHub.getLlmSummaryLabel:Function]
  /**
   * @PURPOSE: Compute normalized LLM validation summary label.
   * @PRE: dashboard may have null lastTask.
   * @POST: returns UNKNOWN fallback for missing status.
   */
  function getLlmSummaryLabel(dashboard) {
    return getValidationLabel(
      dashboard.lastTask?.validationStatus || "unknown",
    );
  }

  // [/DEF:DashboardHub.getLlmSummaryLabel:Function]

  // [DEF:DashboardHub.getColumnCellValue:Function]
  /**
   * @PURPOSE: Resolve comparable/filterable display value for any grid column.
   * @PRE: column belongs to filterable column set.
   * @POST: returns non-empty scalar display value.
   */
  function getColumnCellValue(dashboard, column) {
    if (column === "title") return dashboard.title || "-";
    if (column === "git_status")
      return String(dashboard.git?.status || "pending").toLowerCase();
    if (column === "llm_status") return getLlmSummaryLabel(dashboard);
    if (column === "changed_on")
      return dashboard.changedOn ? String(dashboard.changedOn).slice(0, 10) : "-";
    if (column === "actor") return dashboard.actorLabel || "-";
    return "-";
  }

  // [/DEF:DashboardHub.getColumnCellValue:Function]

  // [DEF:DashboardHub.getFilterOptions:Function]
  /**
   * @PURPOSE: Build unique sorted value list for a column filter dropdown.
   * @PRE: allDashboards is hydrated.
   * @POST: returns de-duplicated sorted options.
   */
  function getFilterOptions(column) {
    const options = allDashboards
      .map((dashboard) => getColumnCellValue(dashboard, column))
      .filter((value) => Boolean(value));
    return Array.from(new Set(options)).sort((a, b) =>
      a.localeCompare(b, "ru"),
    );
  }

  // [/DEF:DashboardHub.getFilterOptions:Function]

  // [DEF:DashboardHub.getVisibleFilterOptions:Function]
  /**
   * @PURPOSE: Apply in-dropdown search over full filter options.
   * @PRE: columnFilterSearch contains search token for column.
   * @POST: returns subset for current filter popover list.
   */
  function getVisibleFilterOptions(column) {
    const searchText = (columnFilterSearch[column] || "").toLowerCase();
    return getFilterOptions(column).filter((value) =>
      value.toLowerCase().includes(searchText),
    );
  }

  // [/DEF:DashboardHub.getVisibleFilterOptions:Function]

  // [DEF:DashboardHub.toggleFilterDropdown:Function]
  /**
   * @PURPOSE: Toggle active column filter popover.
   * @PRE: column is valid filter key.
   * @POST: openFilterColumn updated.
   */
  function toggleFilterDropdown(column, event, panelWidth = 256) {
    event?.stopPropagation();
    if (openFilterColumn === column) {
      openFilterColumn = null;
      return;
    }
    const trigger = event?.currentTarget;
    if (trigger?.getBoundingClientRect) {
      const rect = trigger.getBoundingClientRect();
      const viewportWidth =
        typeof window !== "undefined" ? window.innerWidth : 1920;
      const safeLeft = Math.max(
        8,
        Math.min(rect.left, viewportWidth - panelWidth - 8),
      );
      filterDropdownPosition = {
        left: safeLeft,
        top: rect.bottom + 8,
      };
    }
    openFilterColumn = column;
  }

  // [/DEF:DashboardHub.toggleFilterDropdown:Function]

  // [DEF:DashboardHub.toggleFilterValue:Function]
  /**
   * @PURPOSE: Add/remove specific filter value and reapply projection.
   * @PRE: value comes from option list of the same column.
   * @POST: columnFilters updated and grid reprojected from page 1.
   */
  function toggleFilterValue(column, value, checked) {
    const next = new Set(columnFilters[column]);
    if (checked) {
      next.add(value);
    } else {
      next.delete(value);
    }
    columnFilters = { ...columnFilters, [column]: next };
    currentPage = 1;
    void loadDashboards();
  }

  // [/DEF:DashboardHub.toggleFilterValue:Function]

  // [DEF:DashboardHub.clearColumnFilter:Function]
  /**
   * @PURPOSE: Reset selected values for one column.
   * @PRE: column is valid filter key.
   * @POST: filter cleared and projection refreshed.
   */
  function clearColumnFilter(column) {
    columnFilters = { ...columnFilters, [column]: new Set() };
    currentPage = 1;
    void loadDashboards();
  }

  // [/DEF:DashboardHub.clearColumnFilter:Function]

  // [DEF:DashboardHub.selectAllColumnFilterValues:Function]
  /**
   * @PURPOSE: Select all currently visible values in filter popover.
   * @PRE: visible options computed for current search token.
   * @POST: column filter equals current visible option set.
   */
  function selectAllColumnFilterValues(column) {
    columnFilters = {
      ...columnFilters,
      [column]: new Set(getVisibleFilterOptions(column)),
    };
    currentPage = 1;
    void loadDashboards();
  }

  // [/DEF:DashboardHub.selectAllColumnFilterValues:Function]

  // [DEF:DashboardHub.updateColumnFilterSearch:Function]
  /**
   * @PURPOSE: Update local search token for one filter popover.
   * @PRE: value is text from input.
   * @POST: columnFilterSearch updated immutably.
   */
  function updateColumnFilterSearch(column, value) {
    columnFilterSearch = { ...columnFilterSearch, [column]: value };
  }

  // [/DEF:DashboardHub.updateColumnFilterSearch:Function]

  // [DEF:DashboardHub.hasColumnFilter:Function]
  /**
   * @PURPOSE: Determine if column has active selected values.
   * @PRE: column is valid filter key.
   * @POST: returns boolean activation marker.
   */
  function hasColumnFilter(column) {
    return columnFilters[column]?.size > 0;
  }

  // [/DEF:DashboardHub.hasColumnFilter:Function]

  // [DEF:DashboardHub.doesDashboardPassColumnFilters:Function]
  /**
   * @PURPOSE: Evaluate dashboard row against all active column filters.
   * @PRE: dashboard contains projected values for each filterable column.
   * @POST: returns true only when row matches every active filter.
   */
  function doesDashboardPassColumnFilters(dashboard) {
    const columns = Object.keys(columnFilters);
    for (const column of columns) {
      const values = columnFilters[column];
      if (values.size === 0) continue;
      const dashboardValue = getColumnCellValue(dashboard, column);
      if (!values.has(dashboardValue)) return false;
    }
    return true;
  }

  // [/DEF:DashboardHub.doesDashboardPassColumnFilters:Function]

  // [DEF:DashboardHub.getSortValue:Function]
  /**
   * @PURPOSE: Compute stable comparable sort key for chosen column.
   * @PRE: column belongs to sortable set.
   * @POST: returns string/number key suitable for deterministic comparison.
   */
  function getSortValue(dashboard, column) {
    if (column === "title") return (dashboard.title || "").toLowerCase();
    if (column === "git_status")
      return (getGitSummaryLabel(dashboard) || "").toLowerCase();
    if (column === "llm_status")
      return (getLlmSummaryLabel(dashboard) || "").toLowerCase();
    if (column === "changed_on")
      return dashboard.changedOn ? new Date(dashboard.changedOn).getTime() : 0;
    if (column === "actor") return (dashboard.actorLabel || "").toLowerCase();
    return "";
  }

  // [/DEF:DashboardHub.getSortValue:Function]

  // [DEF:DashboardHub.handleSort:Function]
  /**
   * @PURPOSE: Toggle or switch sort order and reapply grid projection.
   * @PRE: column belongs to sortable set.
   * @POST: sortColumn/sortDirection updated and page reset to 1.
   */
  function handleSort(column) {
    if (sortColumn === column) {
      sortDirection = sortDirection === "asc" ? "desc" : "asc";
    } else {
      sortColumn = column;
      sortDirection = "asc";
    }
    currentPage = 1;
    applyGridTransforms();
  }

  // [/DEF:DashboardHub.handleSort:Function]

  // [DEF:DashboardHub.getSortIndicator:Function]
  /**
   * @PURPOSE: Return visual indicator for active/inactive sort header.
   * @PRE: column belongs to sortable set.
   * @POST: returns one of ↕ | ↑ | ↓.
   */
  function getSortIndicator(column) {
    if (sortColumn !== column) return "↕";
    return sortDirection === "asc" ? "↑" : "↓";
  }

  // [/DEF:DashboardHub.getSortIndicator:Function]

  // [DEF:DashboardHub.applyGridTransforms:Function]
  /**
   * @PURPOSE: Apply search + column filters + sort + pagination to grid data.
   * @PRE: allDashboards is current source collection.
   * @POST: filteredDashboards/dashboards/total/totalPages are synchronized.
   * @UX_STATE: Loaded -> visible rows reflect all active controls deterministically.
   */
  function applyGridTransforms() {
    const nextFiltered = allDashboards
      // Search and column filters are server-side.
      .filter(() => true);

    nextFiltered.sort((a, b) => {
      const aValue = getSortValue(a, sortColumn);
      const bValue = getSortValue(b, sortColumn);
      if (aValue < bValue) return sortDirection === "asc" ? -1 : 1;
      if (aValue > bValue) return sortDirection === "asc" ? 1 : -1;
      return 0;
    });

    filteredDashboards = nextFiltered;
    dashboards = filteredDashboards;
    total = serverTotal;
    totalPages = serverTotalPages;
    updateSelectionState();
    void hydrateVisibleGitStatusesBatch();
  }
  // [/DEF:DashboardHub.applyGridTransforms:Function]

  // Toggle action dropdown
  function toggleActionDropdown(dashboardId, event) {
    event.stopPropagation();
    openActionDropdown =
      openActionDropdown === dashboardId ? null : dashboardId;
  }

  // Close action dropdown when clicking outside
  function closeActionDropdown() {
    openActionDropdown = null;
  }

  // Handle action click
  async function handleAction(dashboard, action) {
    console.log(
      `[DashboardHub][REASON] ${action} on dashboard ${dashboard.title}`,
    );
    closeActionDropdown();

    try {
      if (action === "migrate") {
        // Select only this dashboard for migration
        replaceSelectedIds(new Set([dashboard.id]));
        showMigrateModal = true;
        targetEnvId = null;
        dbMappings = {};
        availableDbMappings = []; // Reset mappings until target env is selected
        sourceDatabases = [];
        targetDatabases = [];
        isEditingMappings = false;
      } else if (action === "backup") {
        // Select only this dashboard for backup
        replaceSelectedIds(new Set([dashboard.id]));
        showBackupModal = true;
        backupSchedule = "";
      } else if (action === "commit") {
        // Open commit modal or trigger commit task
        // For now, we'll trigger a commit via git API
        const response = await api.postApi(
          `/git/repositories/${dashboard.id}/commit`,
          {
            message: `Update dashboard ${dashboard.title}`,
          },
        );
        console.log("[DashboardHub][REASON] Commit created:", response);
      }
    } catch (err) {
      console.error("[DashboardHub][COHERENCE:FAILED] Action Error:", err);
      // Error toast is already shown by api.postApi
    }
  }

  // Handle validate - LLM dashboard validation
  async function handleValidate(dashboard) {
    if (validatingIds.has(dashboard.id)) return;

    validatingIds.add(dashboard.id);
    validatingIds = new Set(validatingIds); // Trigger reactivity

    closeActionDropdown();

    try {
      const response = await api.postApi("/tasks", {
        plugin_id: "llm_dashboard_validation",
        params: {
          dashboard_id: dashboard.id.toString(),
          environment_id: selectedEnv,
        },
      });

      console.log("[DashboardHub][REASON] Validation task started:", response);

      // Open task drawer if task was created
      if (response.task_id || response.id) {
        const taskId = response.task_id || response.id;
        openDrawerForTaskIfPreferred(taskId);
      }
    } catch (err) {
      console.error("[DashboardHub][COHERENCE:FAILED] Validation failed:", err);
      alert(
        $t.dashboard?.validation_start_failed +
          ": " +
          (err.message || $t.dashboard?.unknown_error),
      );
    } finally {
      validatingIds.delete(dashboard.id);
      validatingIds = new Set(validatingIds);
    }
  }

  // Handle target environment change
  async function handleTargetEnvChange(event) {
    targetEnvId = event.target.value;
    if (targetEnvId) {
      await Promise.all([loadDbMappings(), loadDatabases()]);
    } else {
      availableDbMappings = [];
      dbMappings = {};
      sourceDatabases = [];
      targetDatabases = [];
    }
  }

  // Load databases for both environments
  async function loadDatabases() {
    if (!selectedEnv || !targetEnvId) return;
    try {
      const [sourceRes, targetRes] = await Promise.all([
        api.getEnvironmentDatabases(selectedEnv),
        api.getEnvironmentDatabases(targetEnvId),
      ]);
      sourceDatabases = sourceRes || [];
      targetDatabases = targetRes || [];
    } catch (err) {
      console.error(
        "[DashboardHub][COHERENCE:FAILED] Failed to load databases:",
        err,
      );
    }
  }

  // Handle mapping update from MappingTable
  function handleMappingUpdate(event) {
    const { sourceUuid, targetUuid } = event.detail;
    dbMappings[sourceUuid] = targetUuid;
    dbMappings = dbMappings; // Trigger reactivity
  }

  // Load database mappings for migration modal
  async function loadDbMappings() {
    if (!selectedEnv || !targetEnvId) return;

    try {
      console.log(
        `[DashboardHub][EXPLORE] Loading DB mappings from ${selectedEnv} to ${targetEnvId}`,
      );
      const response = await api.getDatabaseMappings(selectedEnv, targetEnvId);
      availableDbMappings = response.mappings || [];
      console.log(
        `[DashboardHub][REFLECT] Loaded mappings:`,
        availableDbMappings,
      );

      // Auto-select mappings with high confidence
      dbMappings = {};
      availableDbMappings.forEach((m) => {
        if (m.confidence > 0.8) {
          dbMappings[m.source_db_uuid] = m.target_db_uuid;
        }
      });
    } catch (err) {
      console.error(
        "[DashboardHub][COHERENCE:FAILED] Failed to load DB mappings:",
        err,
      );
      availableDbMappings = [];
    }
  }

  // Calculate Migration Dry Run
  async function calculateDryRun() {
    if (!targetEnvId || selectedIds.size === 0) {
      dryRunData = null;
      return;
    }

    isDryRunLoading = true;
    dryRunError = null;

    try {
      const payload = {
        selected_ids: Array.from(selectedIds).map(Number),
        source_env_id: selectedEnv,
        target_env_id: targetEnvId,
        replace_db_config: useDbMappings,
        fix_cross_filters: fixCrossFilters,
      };

      const response = await api.calculateMigrationDryRun(payload);
      dryRunData = response;
    } catch (err) {
      console.error(
        "[DashboardHub][COHERENCE:FAILED] Dry run calculation failed:",
        err,
      );
      dryRunError =
        err.message || $t.common?.error || "Failed to calculate changes";
      dryRunData = null;
    } finally {
      isDryRunLoading = false;
    }
  }

  // Reactive effect to recalculate dry run when inputs change
  $effect(() => {
    // track dependencies
    const isOpen = showMigrateModal;
    const target = targetEnvId;
    const ids = Array.from(selectedIds).join(",");
    const useDbs = useDbMappings;
    const fixFilters = fixCrossFilters;

    if (isOpen && target && ids) {
      calculateDryRun();
    } else if (!isOpen || !target || !ids) {
      dryRunData = null;
      dryRunError = null;
    }
  });

  // Handle bulk migrate
  async function handleBulkMigrate() {
    if (isSubmittingMigrate) return;
    if (selectedIds.size === 0) return;
    if (!targetEnvId) {
      alert($t.dashboard?.target_env_required);
      return;
    }

    isSubmittingMigrate = true;
    try {
      const response = await api.postApi("/dashboards/migrate", {
        source_env_id: selectedEnv,
        target_env_id: targetEnvId,
        dashboard_ids: Array.from(selectedIds),
        db_mappings: useDbMappings ? dbMappings : {},
        replace_db_config: useDbMappings && Object.keys(dbMappings).length > 0,
        fix_cross_filters: fixCrossFilters,
      });
      console.log(
        "[DashboardHub][REASON] Bulk migration task created:",
        response.task_id,
      );

      // Close modal and open task drawer
      showMigrateModal = false;
      clearSelectedIds();

      if (response.task_id) {
        const taskId =
          typeof response.task_id === "object"
            ? response.task_id.id || response.task_id.task_id
            : response.task_id;
        openDrawerForTaskIfPreferred(taskId);
      }
    } catch (err) {
      console.error(
        "[DashboardHub][COHERENCE:FAILED] Bulk migrate error:",
        err,
      );
      alert($t.dashboard?.migration_task_failed);
    } finally {
      isSubmittingMigrate = false;
    }
  }

  // Handle bulk backup
  async function handleBulkBackup() {
    if (isSubmittingBackup) return;
    if (selectedIds.size === 0) return;

    isSubmittingBackup = true;
    try {
      const response = await api.postApi("/dashboards/backup", {
        env_id: selectedEnv,
        dashboard_ids: Array.from(selectedIds),
        schedule: backupSchedule || undefined,
      });
      console.log(
        "[DashboardHub][REASON] Bulk backup task created:",
        response.task_id,
      );

      // Close modal and open task drawer
      showBackupModal = false;
      clearSelectedIds();

      if (response.task_id) {
        const taskId =
          typeof response.task_id === "object"
            ? response.task_id.id || response.task_id.task_id
            : response.task_id;
        openDrawerForTaskIfPreferred(taskId);
      }
    } catch (err) {
      console.error("[DashboardHub][COHERENCE:FAILED] Bulk backup error:", err);
      alert($t.dashboard?.backup_task_failed);
    } finally {
      isSubmittingBackup = false;
    }
  }

  // Handle task status click - open Task Drawer
  function handleTaskStatusClick(dashboard) {
    if (dashboard.lastTask?.id) {
      console.log(
        `[DashboardHub][EXPLORE] Open task drawer for task ${dashboard.lastTask.id}`,
      );
      openDrawerForTask(dashboard.lastTask.id);
    }
  }

  function navigateToDashboardDetail(dashboard) {
    const dashboardRef = dashboard?.slug || dashboard?.id;
    if (!dashboardRef) return;
    goto(
      `/dashboards/${encodeURIComponent(String(dashboardRef))}?env_id=${encodeURIComponent(selectedEnv)}`,
    );
  }

  // Get status badge class
  function getStatusBadgeClass(status) {
    if (!status) return "bg-gray-100 text-gray-800";
    switch (status.toLowerCase()) {
      case "ok":
        return "bg-green-100 text-green-800";
      case "diff":
        return "bg-yellow-100 text-yellow-800";
      default:
        return "bg-gray-100 text-gray-800";
    }
  }

  function isGitBusy(dashboardId) {
    return gitBusyIds.has(dashboardId);
  }

  function setGitBusy(dashboardId, busy) {
    if (busy) {
      gitBusyIds.add(dashboardId);
    } else {
      gitBusyIds.delete(dashboardId);
    }
    gitBusyIds = new Set(gitBusyIds);
  }

  async function ensureGitConfigs() {
    if (cachedGitConfigs.length > 0) return cachedGitConfigs;
    cachedGitConfigs = await gitService.getConfigs();
    return cachedGitConfigs;
  }

  function updateDashboardGitState(dashboardId, nextGit) {
    const mergeGitState = (collection) =>
      collection.map((dashboard) =>
        dashboard.id === dashboardId
          ? { ...dashboard, git: { ...dashboard.git, ...nextGit } }
          : dashboard,
      );

    allDashboards = mergeGitState(allDashboards);
    filteredDashboards = mergeGitState(filteredDashboards);
    dashboards = mergeGitState(dashboards);
  }

  function normalizeRepositoryStatusPayload(status) {
    const syncState = String(status?.sync_state || "").toUpperCase();
    const syncStatus = String(status?.sync_status || "").toUpperCase();

    const explicitHasRepo = status?.has_repo;
    const hasRepo =
      explicitHasRepo === true
        ? true
        : explicitHasRepo === false
          ? false
          : !(syncState === "NO_REPO" || syncStatus === "NO_REPO");

    if (!hasRepo) {
      return {
        status: "no_repo",
        branch: null,
        hasRepo: false,
        hasChangesForCommit: false,
      };
    }

    const hasChanges =
      syncState === "CHANGES" ||
      syncStatus === "DIFF" ||
      Boolean(status?.is_dirty) ||
      (status?.untracked_files?.length || 0) > 0 ||
      (status?.modified_files?.length || 0) > 0 ||
      (status?.staged_files?.length || 0) > 0;

    return {
      status: hasChanges ? "diff" : "ok",
      branch: status?.current_branch || status?.branch || null,
      hasRepo: true,
      hasChangesForCommit: hasChanges,
    };
  }

  async function fetchDashboardGitStatusesBatch(dashboardIds, force = false) {
    const uniqueIds = Array.from(new Set(dashboardIds || []));
    if (uniqueIds.length === 0) return;

    if (force) {
      uniqueIds.forEach((dashboardId) => gitResolvedIds.delete(dashboardId));
      gitResolvedIds = new Set(gitResolvedIds);
    }

    const pendingIds = uniqueIds.filter(
      (dashboardId) =>
        !gitResolvedIds.has(dashboardId) && !gitLoadingIds.has(dashboardId),
    );
    if (pendingIds.length === 0) return;

    pendingIds.forEach((dashboardId) => gitLoadingIds.add(dashboardId));
    gitLoadingIds = new Set(gitLoadingIds);

    try {
      const batchResult = await gitService.getStatusesBatch(pendingIds);
      const statusesByDashboardId = batchResult?.statuses || {};
      pendingIds.forEach((dashboardId) => {
        const status =
          statusesByDashboardId[dashboardId] ??
          statusesByDashboardId[String(dashboardId)];
        updateDashboardGitState(
          dashboardId,
          status
            ? normalizeRepositoryStatusPayload(status)
            : {
                status: "no_repo",
                branch: null,
                hasRepo: false,
                hasChangesForCommit: false,
              },
        );
      });
    } catch (_err) {
      pendingIds.forEach((dashboardId) => {
        updateDashboardGitState(dashboardId, {
          status: "no_repo",
          branch: null,
          hasRepo: false,
          hasChangesForCommit: false,
        });
      });
    } finally {
      pendingIds.forEach((dashboardId) => {
        gitLoadingIds.delete(dashboardId);
        gitResolvedIds.add(dashboardId);
      });
      gitLoadingIds = new Set(gitLoadingIds);
      gitResolvedIds = new Set(gitResolvedIds);
    }
  }

  async function refreshDashboardGitState(dashboardId, force = false) {
    await fetchDashboardGitStatusesBatch([dashboardId], force);
  }

  async function hydrateVisibleGitStatusesBatch() {
    const pendingIds = dashboards
      .filter(
        (dashboard) =>
          dashboard.git?.hasRepo === null &&
          !gitResolvedIds.has(dashboard.id) &&
          !gitLoadingIds.has(dashboard.id),
      )
      .map((dashboard) => dashboard.id);
    if (pendingIds.length === 0) return;
    await fetchDashboardGitStatusesBatch(pendingIds, false);
  }

  async function handleGitInit(dashboard) {
    setGitBusy(dashboard.id, true);
    try {
      const configs = await ensureGitConfigs();
      if (!configs.length) {
        addToast(
          $t.git?.no_servers_configured || "No Git config found",
          "error",
        );
        return;
      }

      const config = configs[0];
      const defaultRemote = config?.default_repository
        ? `${String(config.url || "").replace(/\/$/, "")}/${config.default_repository}.git`
        : "";
      const remoteUrl = prompt(
        $t.git?.remote_url || "Remote URL",
        defaultRemote,
      );
      if (!remoteUrl) return;

      await gitService.initRepository(
        dashboard.id,
        config.id,
        remoteUrl.trim(),
      );
      addToast($t.git?.init_success || "Repository initialized", "success");
      await refreshDashboardGitState(dashboard.id, true);
    } catch (err) {
      addToast(err?.message || "Git init failed", "error");
    } finally {
      setGitBusy(dashboard.id, false);
    }
  }

  async function handleGitSync(dashboard) {
    setGitBusy(dashboard.id, true);
    try {
      await gitService.sync(dashboard.id, selectedEnv || null);
      addToast($t.git?.sync_success || "Synced", "success");
      await refreshDashboardGitState(dashboard.id, true);
    } catch (err) {
      addToast(err?.message || "Git sync failed", "error");
    } finally {
      setGitBusy(dashboard.id, false);
    }
  }

  async function handleGitCommit(dashboard) {
    if (!dashboard.git?.hasRepo) {
      addToast($t.git?.not_linked || "Repository not linked", "error");
      return;
    }
    if (!dashboard.git?.hasChangesForCommit) {
      addToast($t.git?.nothing_to_commit || "No changes to commit", "error");
      return;
    }

    const message = prompt(
      $t.git?.commit_message || "Commit message",
      `Update dashboard ${dashboard.title}`,
    );
    if (!message?.trim()) return;

    setGitBusy(dashboard.id, true);
    try {
      await gitService.commit(dashboard.id, message.trim());
      addToast($t.git?.commit_success || "Committed", "success");
      await refreshDashboardGitState(dashboard.id, true);
    } catch (err) {
      addToast(err?.message || "Git commit failed", "error");
    } finally {
      setGitBusy(dashboard.id, false);
    }
  }

  async function handleGitPull(dashboard) {
    if (!dashboard.git?.hasRepo) return;
    setGitBusy(dashboard.id, true);
    try {
      await gitService.pull(dashboard.id);
      addToast($t.git?.pull_success || "Pulled", "success");
      await refreshDashboardGitState(dashboard.id, true);
    } catch (err) {
      addToast(err?.message || "Git pull failed", "error");
    } finally {
      setGitBusy(dashboard.id, false);
    }
  }

  async function handleGitPush(dashboard) {
    if (!dashboard.git?.hasRepo) return;
    setGitBusy(dashboard.id, true);
    try {
      await gitService.push(dashboard.id);
      addToast($t.git?.push_success || "Pushed", "success");
      await refreshDashboardGitState(dashboard.id, true);
    } catch (err) {
      addToast(err?.message || "Git push failed", "error");
    } finally {
      setGitBusy(dashboard.id, false);
    }
  }

  // Get task status icon
  function getTaskStatusIcon(status) {
    if (!status) return "";
    switch (status.toLowerCase()) {
      case "running":
        return '<svg class="animate-spin" width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 0 0 1-8 8z"/></svg>';
      case "pending":
        return '<svg class="animate-spin" width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 0 0 1-8 8z"/></svg>';
      case "success":
        return '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z"/></svg>';
      case "failed":
      case "error":
        return '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>';
      case "awaiting_input":
      case "waiting_input":
        return '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>';
      default:
        return "";
    }
  }

  // Generate pagination range with ellipsis
  function getPaginationRange(current, total, maxVisible = 5) {
    if (total <= maxVisible) {
      return Array.from({ length: total }, (_, i) => i + 1);
    }

    const range = [];
    const halfVisible = Math.floor(maxVisible / 2);

    let start = Math.max(1, current - halfVisible);
    let end = Math.min(total, current + halfVisible);

    if (current <= halfVisible + 1) {
      end = maxVisible;
    } else if (current >= total - halfVisible) {
      start = total - maxVisible + 1;
    }

    // Add first page and ellipsis if needed
    if (start > 1) {
      range.push(1);
      if (start > 2) {
        range.push("...");
      }
    }

    // Add visible pages
    for (let i = start; i <= end; i++) {
      range.push(i);
    }

    // Add ellipsis and last page if needed
    if (end < total) {
      if (end < total - 1) {
        range.push("...");
      }
      range.push(total);
    }

    return range;
  }

  $effect(() => {
    const contextEnvId = $environmentContextStore?.selectedEnvId;
    if (contextEnvId && selectedEnv !== contextEnvId) {
      selectedEnv = contextEnvId;
    }
  });

  $effect(() => {
    const envId = selectedEnv;
    if (!envId || envId === lastLoadedEnvId) return;
    lastLoadedEnvId = envId;
    currentPage = 1;
    profileFilterOverrideShowAll = false;
    effectiveProfileFilter = null;
    clearSelectedIds();
    void loadDashboards();
  });
</script>

<div class="mx-auto w-full px-4 lg:px-8 space-y-6">
  <!-- Header -->
  <div class="flex items-center justify-between">
    <h1 class="text-2xl font-bold text-gray-900">
      {$t.nav?.dashboards}
    </h1>
    <div class="flex items-center space-x-4">
      <button
        class="inline-flex items-center justify-center rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-hover"
        onclick={loadDashboards}
      >
        {$t.common?.refresh}
      </button>
    </div>
  </div>

  {#if effectiveProfileFilter?.applied}
    <div class="rounded-lg border border-blue-200 bg-blue-50 px-4 py-2 text-sm text-blue-800">
      <div class="flex flex-wrap items-center justify-between gap-2">
        <span class="font-medium">
          {$t.profile?.filter_badge_active || "Profile filters active"}
        </span>
        <button
          type="button"
          class="rounded border border-blue-300 bg-white px-2.5 py-1 text-xs font-medium text-blue-700 hover:bg-blue-100"
          onclick={handleTemporaryShowAll}
        >
          {$t.profile?.filter_show_all_temporarily || "Show all dashboards temporarily"}
        </button>
      </div>
    </div>
  {:else if effectiveProfileFilter?.override_show_all}
    <div class="rounded-lg border border-amber-200 bg-amber-50 px-4 py-2 text-sm text-amber-800">
      <div class="flex flex-wrap items-center justify-between gap-2">
        <span class="font-medium">
          {$t.profile?.filter_badge_override || "Showing all dashboards temporarily"}
        </span>
        <button
          type="button"
          class="rounded border border-amber-300 bg-white px-2.5 py-1 text-xs font-medium text-amber-700 hover:bg-amber-100"
          onclick={handleRestoreProfileFilter}
        >
          {$t.profile?.filter_restore_default || "Restore default filter"}
        </button>
      </div>
    </div>
  {/if}

  <!-- Error Banner -->
  {#if error}
    <div
      class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4 flex items-center justify-between"
    >
      <span>{error}</span>
      <button
        class="px-4 py-2 bg-destructive text-white rounded hover:bg-destructive-hover transition-colors"
        onclick={loadDashboards}
      >
        {$t.common?.retry}
      </button>
    </div>
  {/if}

  <!-- Loading State -->
  {#if isLoading}
    <div
      class="bg-white border border-slate-200 rounded-xl shadow-sm overflow-hidden"
    >
      <div
        class="grid gap-4 px-6 py-3 bg-slate-50 border-b border-slate-200 font-semibold text-[11px] uppercase tracking-wide text-slate-600"
        style="width: max(100%, 1520px); grid-template-columns: 40px minmax(250px,2.3fr) minmax(118px,0.9fr) minmax(150px,1fr) minmax(280px,2fr) minmax(150px,1fr) 124px;"
      >
        <div class="h-4 rounded bg-gray-200 animate-pulse"></div>
        <div class="h-4 rounded bg-gray-200 animate-pulse"></div>
        <div class="h-4 rounded bg-gray-200 animate-pulse"></div>
        <div class="h-4 rounded bg-gray-200 animate-pulse"></div>
        <div class="h-4 rounded bg-gray-200 animate-pulse"></div>
        <div class="h-4 rounded bg-gray-200 animate-pulse"></div>
        <div class="h-4 rounded bg-gray-200 animate-pulse"></div>
      </div>
      {#each Array(5) as _}
        <div
          class="grid gap-4 px-6 py-4 border-b border-slate-200 last:border-b-0 hover:bg-slate-50 transition-colors"
          style="width: max(100%, 1520px); grid-template-columns: 40px minmax(250px,2.3fr) minmax(118px,0.9fr) minmax(150px,1fr) minmax(280px,2fr) minmax(150px,1fr) 124px;"
        >
          <div class="h-4 rounded bg-gray-200 animate-pulse"></div>
          <div class="h-4 rounded bg-gray-200 animate-pulse"></div>
          <div class="h-4 rounded bg-gray-200 animate-pulse"></div>
          <div class="h-4 rounded bg-gray-200 animate-pulse"></div>
          <div class="h-4 rounded bg-gray-200 animate-pulse"></div>
          <div class="h-4 rounded bg-gray-200 animate-pulse"></div>
          <div class="h-4 rounded bg-gray-200 animate-pulse"></div>
        </div>
      {/each}
    </div>
  {:else if dashboards.length === 0}
    <!-- Empty State -->
    <div class="py-12 text-center text-gray-500">
      <svg
        class="w-16 h-16 mx-auto mb-4 text-gray-400"
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
      >
        <path d="M3 3h18v18H3V3zm16 16V5H5v14h14z" />
      </svg>

      {#if effectiveProfileFilter?.applied}
        <p>{$t.profile?.filter_empty_state || "No dashboards found for active profile filters. Try adjusting your filter settings."}</p>
        <div class="mt-4">
          <button
            type="button"
            class="rounded border border-blue-300 bg-white px-3 py-1.5 text-xs font-medium text-blue-700 hover:bg-blue-50"
            onclick={handleTemporaryShowAll}
          >
            {$t.profile?.filter_show_all_temporarily || "Show all dashboards temporarily"}
          </button>
        </div>
      {:else}
        <p>{$t.dashboard?.empty}</p>
      {/if}
    </div>
  {:else}
    <!-- Toolbar -->
    <div class="flex items-center justify-between mb-4 gap-4">
      <div class="flex items-center gap-2">
        <button
          class="px-3 py-1 text-sm border border-gray-300 rounded hover:bg-gray-100 transition-colors"
          onclick={handleSelectAll}
          disabled={total === 0}
        >
          {isAllSelected
            ? $t.dashboard?.deselect_all
            : $t.dashboard?.select_all}
        </button>
        <button
          class="px-3 py-1 text-sm border border-gray-300 rounded hover:bg-gray-100 transition-colors"
          onclick={handleSelectVisible}
          disabled={dashboards.length === 0}
        >
          {isAllVisibleSelected
            ? $t.dashboard?.deselect_visible
            : $t.dashboard?.select_visible}
        </button>
        {#if selectedIds.size > 0}
          <span class="text-sm text-gray-600">
            {($t.dashboard?.selected_count).replace(
              "{count}",
              String(selectedIds.size),
            )}
          </span>
        {/if}
      </div>
      <div>
        <input
          type="text"
          class="px-4 py-2 border border-gray-300 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-blue-500"
          placeholder={$t.dashboard?.search}
          oninput={handleSearch}
          value={searchQuery}
        />
      </div>
    </div>

    <!-- Dashboard Grid -->
    <div class="bg-white border border-slate-200 rounded-xl shadow-sm">
      <div class="overflow-x-auto overflow-y-visible">
        <!-- Grid Header -->
        <div
          class="grid gap-4 px-6 py-3 bg-slate-50 border-b border-slate-200 font-semibold text-[11px] uppercase tracking-wide text-slate-600"
          style="width: max(100%, 1520px); grid-template-columns: 40px minmax(250px,2.3fr) minmax(118px,0.9fr) minmax(150px,1fr) minmax(280px,2fr) minmax(150px,1fr) 124px;"
        >
          <div></div>
          <div class="flex items-center gap-2">
            <button
              class="hover:text-gray-900 transition-colors"
              onclick={() => handleSort("title")}
            >
              {$t.dashboard?.title}
              {getSortIndicator("title")}
            </button>
            <div class="relative column-filter">
              <button
                class="rounded border px-1.5 py-0.5 text-xs transition-colors {hasColumnFilter(
                  'title',
                )
                  ? 'border-blue-400 text-blue-700 bg-blue-50'
                  : 'border-gray-300 text-gray-500 hover:text-gray-700'}"
                onclick={(event) => toggleFilterDropdown("title", event, 256)}
                title={$t.dashboard?.column_filter}
              >
                ▾
              </button>
              {#if openFilterColumn === "title"}
                <div
                  class="fixed z-50 w-64 rounded-lg border border-gray-200 bg-white shadow-lg p-3"
                  style="left: {filterDropdownPosition.left}px; top: {filterDropdownPosition.top}px;"
                >
                  <input
                    type="text"
                    class="w-full rounded border border-gray-300 px-2 py-1 text-xs"
                    placeholder={$t.dashboard?.search}
                    value={columnFilterSearch.title}
                    oninput={(event) =>
                      updateColumnFilterSearch("title", event.target.value)}
                  />
                  <div class="mt-2 flex items-center justify-between text-xs">
                    <button
                      class="text-blue-600 hover:underline"
                      onclick={() => selectAllColumnFilterValues("title")}
                      >{$t.dashboard?.select_all}</button
                    >
                    <button
                      class="text-gray-500 hover:underline"
                      onclick={() => clearColumnFilter("title")}
                      >{$t.common?.clear || "Clear"}</button
                    >
                  </div>
                  <div class="mt-2 max-h-44 overflow-auto space-y-1 text-xs">
                    {#each getVisibleFilterOptions("title") as value}
                      <label class="flex items-center gap-2">
                        <input
                          type="checkbox"
                          checked={columnFilters.title.has(value)}
                          onchange={(event) =>
                            toggleFilterValue(
                              "title",
                              value,
                              event.target.checked,
                            )}
                        />
                        <span class="truncate">{value}</span>
                      </label>
                    {/each}
                  </div>
                </div>
              {/if}
            </div>
          </div>
          <div class="flex items-center gap-2">
            <button
              class="hover:text-gray-900 transition-colors"
              onclick={() => handleSort("git_status")}
            >
              GIT {getSortIndicator("git_status")}
            </button>
            <div class="relative column-filter">
              <button
                class="rounded border px-1.5 py-0.5 text-xs transition-colors {hasColumnFilter(
                  'git_status',
                )
                  ? 'border-blue-400 text-blue-700 bg-blue-50'
                  : 'border-gray-300 text-gray-500 hover:text-gray-700'}"
                onclick={(event) =>
                  toggleFilterDropdown("git_status", event, 256)}
                title={$t.dashboard?.column_filter}
              >
                ▾
              </button>
              {#if openFilterColumn === "git_status"}
                <div
                  class="fixed z-50 w-64 rounded-lg border border-gray-200 bg-white shadow-lg p-3"
                  style="left: {filterDropdownPosition.left}px; top: {filterDropdownPosition.top}px;"
                >
                  <input
                    type="text"
                    class="w-full rounded border border-gray-300 px-2 py-1 text-xs"
                    placeholder={$t.dashboard?.search}
                    value={columnFilterSearch.git_status}
                    oninput={(event) =>
                      updateColumnFilterSearch(
                        "git_status",
                        event.target.value,
                      )}
                  />
                  <div class="mt-2 flex items-center justify-between text-xs">
                    <button
                      class="text-blue-600 hover:underline"
                      onclick={() => selectAllColumnFilterValues("git_status")}
                      >{$t.dashboard?.select_all}</button
                    >
                    <button
                      class="text-gray-500 hover:underline"
                      onclick={() => clearColumnFilter("git_status")}
                      >{$t.common?.clear || "Clear"}</button
                    >
                  </div>
                  <div class="mt-2 max-h-44 overflow-auto space-y-1 text-xs">
                    {#each getVisibleFilterOptions("git_status") as value}
                      <label class="flex items-center gap-2">
                        <input
                          type="checkbox"
                          checked={columnFilters.git_status.has(value)}
                          onchange={(event) =>
                            toggleFilterValue(
                              "git_status",
                              value,
                              event.target.checked,
                            )}
                        />
                        <span class="truncate">{value}</span>
                      </label>
                    {/each}
                  </div>
                </div>
              {/if}
            </div>
          </div>
          <div class="flex items-center gap-2">
            <button
              class="hover:text-gray-900 transition-colors"
              onclick={() => handleSort("llm_status")}
            >
              {$t.dashboard?.llm_status}
              {getSortIndicator("llm_status")}
            </button>
            <div class="relative column-filter">
              <button
                class="rounded border px-1.5 py-0.5 text-xs transition-colors {hasColumnFilter(
                  'llm_status',
                )
                  ? 'border-blue-400 text-blue-700 bg-blue-50'
                  : 'border-gray-300 text-gray-500 hover:text-gray-700'}"
                onclick={(event) =>
                  toggleFilterDropdown("llm_status", event, 256)}
                title={$t.dashboard?.column_filter}
              >
                ▾
              </button>
              {#if openFilterColumn === "llm_status"}
                <div
                  class="fixed z-50 w-64 rounded-lg border border-gray-200 bg-white shadow-lg p-3"
                  style="left: {filterDropdownPosition.left}px; top: {filterDropdownPosition.top}px;"
                >
                  <input
                    type="text"
                    class="w-full rounded border border-gray-300 px-2 py-1 text-xs"
                    placeholder={$t.dashboard?.search}
                    value={columnFilterSearch.llm_status}
                    oninput={(event) =>
                      updateColumnFilterSearch(
                        "llm_status",
                        event.target.value,
                      )}
                  />
                  <div class="mt-2 flex items-center justify-between text-xs">
                    <button
                      class="text-blue-600 hover:underline"
                      onclick={() => selectAllColumnFilterValues("llm_status")}
                      >{$t.dashboard?.select_all}</button
                    >
                    <button
                      class="text-gray-500 hover:underline"
                      onclick={() => clearColumnFilter("llm_status")}
                      >{$t.common?.clear || "Clear"}</button
                    >
                  </div>
                  <div class="mt-2 max-h-44 overflow-auto space-y-1 text-xs">
                    {#each getVisibleFilterOptions("llm_status") as value}
                      <label class="flex items-center gap-2">
                        <input
                          type="checkbox"
                          checked={columnFilters.llm_status.has(value)}
                          onchange={(event) =>
                            toggleFilterValue(
                              "llm_status",
                              value,
                              event.target.checked,
                            )}
                        />
                        <span class="truncate">{value}</span>
                      </label>
                    {/each}
                  </div>
                </div>
              {/if}
            </div>
          </div>
          <div class="flex items-center gap-2">
            {$t.dashboard?.actions}
          </div>
          <div class="flex items-center gap-2">
            <button
              class="hover:text-gray-900 transition-colors"
              onclick={() => handleSort("changed_on")}
            >
              {$t.dashboard?.changed_on}
              {getSortIndicator("changed_on")}
            </button>
            <div class="relative column-filter">
              <button
                class="rounded border px-1.5 py-0.5 text-xs transition-colors {hasColumnFilter(
                  'changed_on',
                )
                  ? 'border-blue-400 text-blue-700 bg-blue-50'
                  : 'border-gray-300 text-gray-500 hover:text-gray-700'}"
                onclick={(event) =>
                  toggleFilterDropdown("changed_on", event, 256)}
                title={$t.dashboard?.column_filter}
              >
                ▾
              </button>
              {#if openFilterColumn === "changed_on"}
                <div
                  class="fixed z-50 w-64 rounded-lg border border-gray-200 bg-white shadow-lg p-3"
                  style="left: {filterDropdownPosition.left}px; top: {filterDropdownPosition.top}px;"
                >
                  <input
                    type="text"
                    class="w-full rounded border border-gray-300 px-2 py-1 text-xs"
                    placeholder={$t.dashboard?.search}
                    value={columnFilterSearch.changed_on}
                    oninput={(event) =>
                      updateColumnFilterSearch(
                        "changed_on",
                        event.target.value,
                      )}
                  />
                  <div class="mt-2 flex items-center justify-between text-xs">
                    <button
                      class="text-blue-600 hover:underline"
                      onclick={() => selectAllColumnFilterValues("changed_on")}
                      >{$t.dashboard?.select_all}</button
                    >
                    <button
                      class="text-gray-500 hover:underline"
                      onclick={() => clearColumnFilter("changed_on")}
                      >{$t.common?.clear || "Clear"}</button
                    >
                  </div>
                  <div class="mt-2 max-h-44 overflow-auto space-y-1 text-xs">
                    {#each getVisibleFilterOptions("changed_on") as value}
                      <label class="flex items-center gap-2">
                        <input
                          type="checkbox"
                          checked={columnFilters.changed_on.has(value)}
                          onchange={(event) =>
                            toggleFilterValue(
                              "changed_on",
                              value,
                              event.target.checked,
                            )}
                        />
                        <span class="truncate">{value}</span>
                      </label>
                    {/each}
                  </div>
                </div>
              {/if}
            </div>
          </div>
          <div class="flex items-center gap-2">
            <button
              class="hover:text-gray-900 transition-colors"
              onclick={() => handleSort("actor")}
            >
              {$t.dashboard?.owners || "Owners"}
              {getSortIndicator("actor")}
            </button>
            <div class="relative column-filter">
              <button
                class="rounded border px-1.5 py-0.5 text-xs transition-colors {hasColumnFilter(
                  'actor',
                )
                  ? 'border-blue-400 text-blue-700 bg-blue-50'
                  : 'border-gray-300 text-gray-500 hover:text-gray-700'}"
                onclick={(event) => toggleFilterDropdown("actor", event, 288)}
                title={$t.dashboard?.column_filter}
              >
                ▾
              </button>
              {#if openFilterColumn === "actor"}
                <div
                  class="fixed z-50 w-72 rounded-lg border border-gray-200 bg-white shadow-lg p-3"
                  style="left: {filterDropdownPosition.left}px; top: {filterDropdownPosition.top}px;"
                >
                  <input
                    type="text"
                    class="w-full rounded border border-gray-300 px-2 py-1 text-xs"
                    placeholder={$t.dashboard?.search}
                    value={columnFilterSearch.actor}
                    oninput={(event) =>
                      updateColumnFilterSearch("actor", event.target.value)}
                  />
                  <div class="mt-2 flex items-center justify-between text-xs">
                    <button
                      class="text-blue-600 hover:underline"
                      onclick={() => selectAllColumnFilterValues("actor")}
                      >{$t.dashboard?.select_all}</button
                    >
                    <button
                      class="text-gray-500 hover:underline"
                      onclick={() => clearColumnFilter("actor")}
                      >{$t.common?.clear || "Clear"}</button
                    >
                  </div>
                  <div class="mt-2 max-h-44 overflow-auto space-y-1 text-xs">
                    {#each getVisibleFilterOptions("actor") as value}
                      <label class="flex items-center gap-2">
                        <input
                          type="checkbox"
                          checked={columnFilters.actor.has(value)}
                          onchange={(event) =>
                            toggleFilterValue(
                              "actor",
                              value,
                              event.target.checked,
                            )}
                        />
                        <span class="truncate">{value}</span>
                      </label>
                    {/each}
                  </div>
                </div>
              {/if}
            </div>
          </div>
        </div>

        <!-- Grid Rows -->
        {#each dashboards as dashboard}
          <div
            class="grid items-center gap-4 px-6 py-3 border-b border-slate-200 last:border-b-0 hover:bg-slate-50 transition-colors text-[13px]"
            style="width: max(100%, 1520px); grid-template-columns: 40px minmax(250px,2.3fr) minmax(118px,0.9fr) minmax(150px,1fr) minmax(280px,2fr) minmax(150px,1fr) 124px;"
          >
            <!-- Checkbox -->
            <div>
              <input
                type="checkbox"
                checked={selectedIds.has(dashboard.id)}
                onchange={(e) => handleCheckboxChange(dashboard, e)}
              />
            </div>

            <!-- Title -->
            <div class="font-medium text-sm text-slate-900">
              <button
                class="text-left text-blue-700 hover:text-blue-900 hover:underline transition-colors"
                onclick={() => navigateToDashboardDetail(dashboard)}
                title={$t.dashboard?.open_overview}
              >
                {dashboard.title}
              </button>
            </div>

            <!-- Git Status -->
            <div>
              <div class="flex flex-col gap-1">
                <span
                  class="inline-flex w-fit items-center rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide {dashboard.git?.hasRepo === null
                    ? 'bg-slate-100 text-slate-500'
                    : dashboard.git?.hasRepo
                      ? 'bg-blue-100 text-blue-800'
                      : 'bg-slate-100 text-slate-700'}"
                >
                  {dashboard.git?.hasRepo === null
                    ? $t.common?.loading || "Loading"
                    : dashboard.git?.hasRepo
                    ? $t.dashboard?.status_repo || "Repo"
                    : $t.dashboard?.status_no_repo || "No Repo"}
                </span>
                {#if dashboard.git?.hasRepo}
                  <span
                    class="inline-flex w-fit items-center rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide {dashboard
                      .git?.hasChangesForCommit
                      ? 'bg-yellow-100 text-yellow-800'
                      : 'bg-green-100 text-green-800'}"
                  >
                    {dashboard.git?.hasChangesForCommit
                      ? $t.dashboard?.status_changes || "Diff"
                      : $t.dashboard?.status_no_changes || "Synced"}
                  </span>
                {/if}
              </div>
            </div>

            <!-- LLM Validation Status -->
            <div>
              {#if dashboard.lastTask}
                <div
                  class="inline-flex items-center gap-1 rounded-full border px-2.5 py-1 text-xs font-semibold uppercase tracking-wide {getValidationBadgeClass(
                    dashboard.lastTask.validationStatus,
                  )}"
                  onclick={() => handleTaskStatusClick(dashboard)}
                  onkeydown={(e) =>
                    (e.key === "Enter" || e.key === " ") &&
                    handleTaskStatusClick(dashboard)}
                  role="button"
                  tabindex="0"
                  aria-label={$t.dashboard?.view_task}
                >
                  {getValidationLabel(dashboard.lastTask.validationStatus)}
                </div>
              {:else}
                <span class="text-gray-400">-</span>
              {/if}
            </div>

            <!-- Actions -->
            <div class="flex items-center">
              <div class="flex items-center gap-1.5">
                <button
                  class="inline-flex h-8 w-8 items-center justify-center rounded-md border border-slate-200 bg-white text-blue-600 hover:bg-blue-50 hover:border-blue-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
                  onclick={() => handleAction(dashboard, "migrate")}
                  title={$t.dashboard?.action_migrate}
                >
                  <svg
                    width="14"
                    height="14"
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    stroke-width="2"
                  >
                    <path d="M5 12h14M12 5l7 7-7 7" />
                  </svg>
                </button>
                <button
                  class="inline-flex h-8 w-8 items-center justify-center rounded-md border border-slate-200 bg-white text-teal-600 hover:bg-teal-50 hover:border-teal-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
                  onclick={() => handleValidate(dashboard)}
                  disabled={validatingIds.has(dashboard.id)}
                  title={$t.dashboard?.action_validate}
                >
                  {#if validatingIds.has(dashboard.id)}
                    <svg
                      class="animate-spin"
                      width="14"
                      height="14"
                      viewBox="0 0 24 24"
                      fill="none"
                      stroke="currentColor"
                      stroke-width="2"
                    >
                      <circle
                        cx="12"
                        cy="12"
                        r="10"
                        stroke-dasharray="32"
                        stroke-dashoffset="12"
                      />
                    </svg>
                  {:else}
                    <svg
                      width="14"
                      height="14"
                      viewBox="0 0 24 24"
                      fill="none"
                      stroke="currentColor"
                      stroke-width="2"
                    >
                      <path d="M9 12l2 2 4-4" />
                      <circle cx="12" cy="12" r="10" />
                    </svg>
                  {/if}
                </button>
                <button
                  class="inline-flex h-8 w-8 items-center justify-center rounded-md border border-slate-200 bg-white text-amber-600 hover:bg-amber-50 hover:border-amber-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
                  onclick={() => handleAction(dashboard, "backup")}
                  title={$t.dashboard?.action_backup}
                >
                  <svg
                    width="14"
                    height="14"
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    stroke-width="2"
                  >
                    <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
                    <polyline points="7,10 12,15 17,10" />
                    <line x1="12" y1="15" x2="12" y2="3" />
                  </svg>
                </button>
              </div>
            </div>

            <!-- Changed On -->
            <div class="text-xs text-slate-600">
              {dashboard.changedOnLabel}
            </div>

            <!-- Owners -->
            <div class="text-xs">
              {#if dashboard.owners?.length > 0}
                <div class="flex flex-wrap items-center gap-1">
                  {#each dashboard.owners as owner (owner)}
                    <span
                      class="rounded bg-slate-100 px-2 py-0.5 text-xs text-slate-700 truncate max-w-[180px]"
                      title={owner}
                    >
                      {owner}
                    </span>
                  {/each}
                </div>
              {:else}
                <div class="text-gray-400">-</div>
              {/if}
            </div>
          </div>
        {/each}
      </div>

      <!-- Pagination -->
      {#if totalPages > 1}
        <div
          class="flex items-center justify-between px-4 py-3 bg-gray-50 border-t border-gray-200"
        >
          <div class="text-sm text-gray-600">
            {($t.dashboard?.showing)
              .replace("{start}", String((currentPage - 1) * pageSize + 1))
              .replace("{end}", String(Math.min(currentPage * pageSize, total)))
              .replace("{total}", String(total))}
          </div>
          <div class="flex items-center gap-1">
            <button
              class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed min-w-[32px]"
              onclick={() => handlePageChange(1)}
              disabled={currentPage === 1}
            >
              {$t.common?.first}
            </button>
            <button
              class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed min-w-[32px]"
              onclick={() => handlePageChange(currentPage - 1)}
              disabled={currentPage === 1}
            >
              {$t.dashboard?.previous}
            </button>
            {#each getPaginationRange(currentPage, totalPages) as pageNum}
              {#if pageNum === "..."}
                <span class="px-2 py-1 text-gray-500">...</span>
              {:else}
                <button
                  class="page-btn {pageNum === currentPage ? 'active' : ''}"
                  onclick={() => handlePageChange(pageNum)}
                >
                  {pageNum}
                </button>
              {/if}
            {/each}
            <button
              class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed min-w-[32px]"
              onclick={() => handlePageChange(currentPage + 1)}
              disabled={currentPage === totalPages}
            >
              {$t.dashboard?.next}
            </button>
            <button
              class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed min-w-[32px]"
              onclick={() => handlePageChange(totalPages)}
              disabled={currentPage === totalPages}
            >
              {$t.common?.last}
            </button>
          </div>
          <div>
            <select
              class="px-4 py-2 border border-gray-300 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-blue-500"
              value={pageSize}
              onchange={handlePageSizeChange}
            >
              <option value={5}>
                {($t.dashboard?.per_page_option).replace("{count}", "5")}
              </option>
              <option value={10}>
                {($t.dashboard?.per_page_option).replace("{count}", "10")}
              </option>
              <option value={25}>
                {($t.dashboard?.per_page_option).replace("{count}", "25")}
              </option>
              <option value={50}>
                {($t.dashboard?.per_page_option).replace("{count}", "50")}
              </option>
              <option value={100}>
                {($t.dashboard?.per_page_option).replace("{count}", "100")}
              </option>
            </select>
          </div>
        </div>
      {/if}
    </div>

    <!-- Floating Bulk Action Panel -->
    {#if selectedIds.size > 0}
      <div
        class="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 shadow-lg p-4 transition-transform transform translate-y-0"
      >
        <div
          class="flex items-center justify-between w-full px-4 lg:px-8 mx-auto"
        >
          <div class="flex items-center gap-4">
            <span class="font-medium">
              ✓ {($t.dashboard?.selected_count).replace(
                "{count}",
                String(selectedIds.size),
              )}
            </span>
          </div>
          <div class="flex gap-3">
            <button
              class="px-3 py-1 text-sm bg-primary text-white border-primary rounded hover:bg-primary-hover transition-colors"
              onclick={() => {
                showMigrateModal = true;
                targetEnvId = null;
                availableDbMappings = [];
                dbMappings = {};
                sourceDatabases = [];
                targetDatabases = [];
                isEditingMappings = false;
              }}
            >
              {$t.dashboard?.bulk_migrate}
            </button>
            <button
              class="px-3 py-1 text-sm bg-primary text-white border-primary rounded hover:bg-primary-hover transition-colors"
              onclick={() => (showBackupModal = true)}
            >
              {$t.dashboard?.bulk_backup}
            </button>
            <button
              class="px-3 py-1 text-sm border border-gray-300 rounded hover:bg-gray-100 transition-colors"
              onclick={clearSelectedIds}
            >
              {$t.common?.cancel}
            </button>
          </div>
        </div>
      </div>
    {/if}
  {/if}

  <!-- Migration Modal -->
  {#if showMigrateModal}
    <div
      class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
      onclick={() => (showMigrateModal = false)}
      onkeydown={(e) => e.key === "Escape" && (showMigrateModal = false)}
      role="presentation"
    >
      <div
        class="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[80vh] overflow-y-auto"
        onclick={(e) => e.stopPropagation()}
        role="dialog"
        aria-modal="true"
        tabindex="-1"
        aria-labelledby="migrate-modal-title"
        onkeydown={(e) => e.stopPropagation()}
      >
        <div
          class="px-6 py-4 border-b border-gray-200 flex items-center justify-between relative"
        >
          <h2 id="migrate-modal-title" class="text-xl font-bold">
            {($t.dashboard?.migrate_modal_title).replace(
              "{count}",
              String(selectedIds.size),
            )}
          </h2>
          <button
            onclick={() => (showMigrateModal = false)}
            class="absolute top-4 right-4 p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-full transition-all"
            aria-label={$t.common?.close_modal}
          >
            <svg
              xmlns="http://www.w3.org/2000/svg"
              width="20"
              height="20"
              viewBox="0 0 24 24"
              fill="none"
              stroke="currentColor"
              stroke-width="2"
              stroke-linecap="round"
              stroke-linejoin="round"
            >
              <line x1="18" y1="6" x2="6" y2="18"></line>
              <line x1="6" y1="6" x2="18" y2="18"></line>
            </svg>
          </button>
        </div>
        <div class="px-6 py-4">
          <div class="space-y-6">
            <!-- Source Env (Read-only) -->
            <div>
              <label
                for="source-env"
                class="block text-sm font-medium text-gray-500 mb-1"
                >{$t.migration?.source_env}</label
              >
              <div
                id="source-env"
                class="px-4 py-2 bg-gray-50 border border-gray-200 rounded-lg text-gray-700 font-medium"
              >
                {environments.find((e) => e.id === selectedEnv)?.name ||
                  selectedEnv}
                {$t.dashboard?.read_only}
              </div>
            </div>

            <!-- Target Env -->
            <div>
              <label
                for="target-env"
                class="block text-sm font-medium text-gray-700 mb-2"
                >{$t.migration?.target_env}</label
              >
              <select
                id="target-env"
                class="env-dropdown w-full"
                bind:value={targetEnvId}
                onchange={handleTargetEnvChange}
              >
                <option value="">
                  {$t.dashboard?.target_env_placeholder ||
                    "Select target environment..."}
                </option>
                {#each environments.filter((e) => e.id !== selectedEnv) as env}
                  <option value={env.id}>{env.name}</option>
                {/each}
              </select>
            </div>

            <!-- Database Mappings Toggle -->
            <div>
              <div class="flex items-center justify-between mb-2">
                <label
                  for="use-db-mappings"
                  class="block text-sm font-medium text-gray-700"
                  >{$t.migration?.database_mappings}</label
                >
                <label class="relative inline-flex items-center cursor-pointer">
                  <input
                    type="checkbox"
                    id="use-db-mappings"
                    bind:checked={useDbMappings}
                    class="sr-only peer"
                  />
                  <div
                    class="w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-blue-600"
                  ></div>
                  <span class="ml-2 text-xs text-gray-500"
                    >{useDbMappings ? $t.common?.on : $t.common?.off}</span
                  >
                </label>
              </div>

              {#if useDbMappings}
                <div class="flex items-center justify-between mb-2">
                  {#if availableDbMappings.length > 0}
                    <button
                      class="text-sm text-blue-600 hover:underline"
                      onclick={() => (isEditingMappings = !isEditingMappings)}
                    >
                      {isEditingMappings
                        ? $t.dashboard?.view_summary
                        : $t.dashboard?.edit_mappings}
                    </button>
                  {/if}
                </div>

                {#if isEditingMappings}
                  <div
                    class="border border-gray-200 rounded-lg overflow-hidden"
                  >
                    <MappingTable
                      {sourceDatabases}
                      {targetDatabases}
                      mappings={Object.entries(dbMappings).map(([s, t]) => ({
                        source_db_uuid: s,
                        target_db_uuid: t,
                      }))}
                      suggestions={availableDbMappings}
                      onupdate={handleMappingUpdate}
                    />
                  </div>
                {:else}
                  <div
                    class="border border-gray-200 rounded-lg overflow-hidden"
                  >
                    <table class="w-full text-sm text-left">
                      <thead class="bg-gray-50 border-b border-gray-200">
                        <tr>
                          <th class="px-4 py-2 font-semibold text-gray-700"
                            >{$t.dashboard?.source_database}</th
                          >
                          <th class="px-4 py-2 font-semibold text-gray-700"
                            >{$t.dashboard?.target_database}</th
                          >
                          <th class="px-4 py-2 font-semibold text-gray-700"
                            >{$t.dashboard?.match_percent}</th
                          >
                        </tr>
                      </thead>
                      <tbody>
                        {#if availableDbMappings.length > 0}
                          {#each availableDbMappings as mapping}
                            <tr
                              class="border-b border-gray-200 last:border-b-0"
                            >
                              <td class="px-4 py-2">{mapping.source_db}</td>
                              <td class="px-4 py-2">
                                {#if dbMappings[mapping.source_db_uuid]}
                                  {targetDatabases.find(
                                    (d) =>
                                      d.uuid ===
                                      dbMappings[mapping.source_db_uuid],
                                  )?.database_name || mapping.target_db}
                                {:else}
                                  <span class="text-red-500"
                                    >{$t.dashboard?.not_mapped}</span
                                  >
                                {/if}
                              </td>
                              <td class="px-4 py-2">
                                <span
                                  class="px-2 py-0.5 rounded-full text-xs font-medium {mapping.confidence >
                                  0.9
                                    ? 'bg-green-100 text-green-800'
                                    : 'bg-yellow-100 text-yellow-800'}"
                                >
                                  {Math.round(mapping.confidence * 100)}%
                                </span>
                              </td>
                            </tr>
                          {/each}
                        {:else}
                          <tr>
                            <td
                              colspan="3"
                              class="px-4 py-4 text-center text-gray-500 italic"
                            >
                              {targetEnvId
                                ? $t.dashboard?.no_databases_to_map ||
                                  "No databases found to map"
                                : $t.dashboard?.select_target_for_mappings ||
                                  "Select target environment to see mappings"}
                            </td>
                          </tr>
                        {/if}
                      </tbody>
                    </table>
                  </div>
                {/if}
              {:else}
                <p class="text-xs text-gray-400 italic">
                  {$t.dashboard?.mapping_disabled_hint ||
                    "Database mapping is disabled. Dashboards will be imported with original database references."}
                </p>
              {/if}
            </div>

            <!-- Fix Cross-Filters (Spec 022 FR-007) -->
            <div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
              <label class="flex items-start gap-3 cursor-pointer">
                <input
                  type="checkbox"
                  bind:checked={fixCrossFilters}
                  class="mt-0.5 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
                />
                <div>
                  <span class="text-sm font-medium text-gray-900"
                    >{$t.dashboard?.fix_cross_filters_title ||
                      "Fix cross-filter bindings"}</span
                  >
                  <p class="text-xs text-gray-500 mt-0.5">
                    {$t.dashboard?.fix_cross_filters_hint ||
                      "Automatically remap chart and dataset IDs in cross-filters to target environment IDs. Recommended when migrating dashboards with cross-filters."}
                  </p>
                </div>
              </label>
            </div>

            <!-- Selected Dashboards List -->
            <div>
              <p class="block text-sm font-medium text-gray-700 mb-2">
                {$t.dashboard?.selected_dashboards}
              </p>
              <div
                class="max-h-40 overflow-y-auto border border-gray-200 rounded-lg bg-gray-50 p-2"
              >
                {#each Array.from(selectedIds) as id}
                  {#each allDashboards as d}
                    {#if d.id === id}
                      <div class="flex items-center text-sm py-1 px-2">
                        <span class="text-green-600 mr-2">☑</span>
                        <span class="text-gray-700">{d.title}</span>
                      </div>
                    {/if}
                  {/each}
                {/each}
              </div>
            </div>

            <!-- Dry Run Summary -->
            {#if targetEnvId && selectedIds.size > 0}
              <div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
                {#if isDryRunLoading}
                  <div class="flex items-center gap-2 text-blue-700 text-sm">
                    <svg
                      class="animate-spin h-4 w-4"
                      xmlns="http://www.w3.org/2000/svg"
                      fill="none"
                      viewBox="0 0 24 24"
                    >
                      <circle
                        class="opacity-25"
                        cx="12"
                        cy="12"
                        r="10"
                        stroke="currentColor"
                        stroke-width="4"
                      ></circle>
                      <path
                        class="opacity-75"
                        fill="currentColor"
                        d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
                      ></path>
                    </svg>
                    <span
                      >{$t.migration?.calculating_dry_run ||
                        "Calculating changes..."}</span
                    >
                  </div>
                {:else if dryRunError}
                  <div class="text-red-600 text-sm">
                    <span class="font-medium">{$t.common?.error}:</span>
                    {dryRunError}
                  </div>
                {:else if dryRunData?.summary}
                  {@const totalDashboards =
                    dryRunData.summary.dashboards?.create +
                      dryRunData.summary.dashboards?.update +
                      dryRunData.summary.dashboards?.delete || 0}
                  {@const totalCharts =
                    dryRunData.summary.charts?.create +
                      dryRunData.summary.charts?.update +
                      dryRunData.summary.charts?.delete || 0}
                  {@const totalDatasets =
                    dryRunData.summary.datasets?.create +
                      dryRunData.summary.datasets?.update +
                      dryRunData.summary.datasets?.delete || 0}
                  {@const totalAll =
                    totalDashboards + totalCharts + totalDatasets}
                  <div
                    class="text-sm font-medium text-blue-900 flex items-start gap-2"
                  >
                    <svg
                      class="w-5 h-5 text-blue-600 mt-0.5 shrink-0"
                      xmlns="http://www.w3.org/2000/svg"
                      viewBox="0 0 20 20"
                      fill="currentColor"
                    >
                      <path
                        fill-rule="evenodd"
                        d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
                        clip-rule="evenodd"
                      />
                    </svg>
                    <span>
                      {(
                        $t.migration?.dry_run_summary ||
                        "Dry-run report: {total} items will be changed (Dashboards: {dashboards}, Charts: {charts}, Datasets: {datasets})"
                      )
                        .replace("{total}", totalAll)
                        .replace("{dashboards}", totalDashboards)
                        .replace("{charts}", totalCharts)
                        .replace("{datasets}", totalDatasets)}
                    </span>
                  </div>
                {/if}
              </div>
            {/if}
          </div>
        </div>
        <div class="px-6 py-4 border-t border-gray-200 flex justify-end gap-3">
          <button
            class="px-3 py-1 text-sm border border-gray-300 rounded hover:bg-gray-100 transition-colors"
            onclick={() => (showMigrateModal = false)}
            disabled={isSubmittingMigrate}>{$t.common?.cancel}</button
          >
          <button
            class="px-3 py-1 text-sm bg-primary text-white border-primary rounded hover:bg-primary-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
            onclick={handleBulkMigrate}
            disabled={!targetEnvId ||
              selectedIds.size === 0 ||
              isSubmittingMigrate}
          >
            {isSubmittingMigrate ? $t.dashboard?.starting : $t.migration?.start}
          </button>
        </div>
      </div>
    </div>
  {/if}

  <!-- Backup Modal -->
  {#if showBackupModal}
    <div
      class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
      onclick={() => (showBackupModal = false)}
      onkeydown={(e) => e.key === "Escape" && (showBackupModal = false)}
      role="presentation"
    >
      <div
        class="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[80vh] overflow-y-auto"
        onclick={(e) => e.stopPropagation()}
        role="dialog"
        aria-modal="true"
        tabindex="-1"
        aria-labelledby="backup-modal-title"
        onkeydown={(e) => e.stopPropagation()}
      >
        <div
          class="px-6 py-4 border-b border-gray-200 flex items-center justify-between relative"
        >
          <h2 id="backup-modal-title" class="text-xl font-bold">
            {($t.dashboard?.backup_modal_title).replace(
              "{count}",
              String(selectedIds.size),
            )}
          </h2>
          <button
            onclick={() => (showBackupModal = false)}
            class="absolute top-4 right-4 p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-full transition-all"
            aria-label={$t.common?.close_modal}
          >
            <svg
              xmlns="http://www.w3.org/2000/svg"
              width="20"
              height="20"
              viewBox="0 0 24 24"
              fill="none"
              stroke="currentColor"
              stroke-width="2"
              stroke-linecap="round"
              stroke-linejoin="round"
            >
              <line x1="18" y1="6" x2="6" y2="18"></line>
              <line x1="6" y1="6" x2="18" y2="18"></line>
            </svg>
          </button>
        </div>
        <div class="px-6 py-4">
          <div class="space-y-6">
            <!-- Env (Read-only) -->
            <div>
              <label
                for="backup-env"
                class="block text-sm font-medium text-gray-500 mb-1"
                >{$t.dashboard?.environment}</label
              >
              <div
                id="backup-env"
                class="px-4 py-2 bg-gray-50 border border-gray-200 rounded-lg text-gray-700 font-medium"
              >
                {environments.find((e) => e.id === selectedEnv)?.name ||
                  selectedEnv}
                {$t.dashboard?.read_only}
              </div>
            </div>

            <!-- Selected Dashboards List -->
            <div>
              <p class="block text-sm font-medium text-gray-700 mb-2">
                {$t.dashboard?.selected_dashboards}
              </p>
              <div
                class="max-h-40 overflow-y-auto border border-gray-200 rounded-lg bg-gray-50 p-2"
              >
                {#each Array.from(selectedIds) as id}
                  {#each allDashboards as d}
                    {#if d.id === id}
                      <div class="flex items-center text-sm py-1 px-2">
                        <span class="text-green-600 mr-2">☑</span>
                        <span class="text-gray-700">{d.title}</span>
                      </div>
                    {/if}
                  {/each}
                {/each}
              </div>
            </div>

            <!-- Schedule -->
            <div>
              <p
                id="backup-schedule-label"
                class="block text-sm font-medium text-gray-700 mb-2"
              >
                {$t.dashboard?.schedule}
              </p>
              <div class="space-y-3" aria-labelledby="backup-schedule-label">
                <label class="flex items-center">
                  <input
                    type="radio"
                    name="schedule"
                    value=""
                    bind:group={backupSchedule}
                    class="mr-2"
                  />
                  <span class="text-sm">{$t.dashboard?.one_time_backup}</span>
                </label>
                <label class="flex items-center">
                  <input
                    type="radio"
                    name="schedule"
                    value="0 0 * * *"
                    bind:group={backupSchedule}
                    class="mr-2"
                  />
                  <span class="text-sm">{$t.dashboard?.schedule_backup}</span>
                </label>
                {#if backupSchedule !== ""}
                  <div class="ml-6">
                    <label
                      for="cron-expression"
                      class="block text-xs text-gray-500 mb-1"
                      >{$t.dashboard?.cron_expression}</label
                    >
                    <input
                      id="cron-expression"
                      type="text"
                      class="search-input w-full text-sm"
                      placeholder={$t.dashboard?.cron_placeholder ||
                        "0 2 * * * (daily at 2 AM)"}
                      bind:value={backupSchedule}
                    />
                    <button class="text-xs text-blue-600 hover:underline mt-1"
                      >{$t.dashboard?.cron_help}</button
                    >
                  </div>
                {/if}
              </div>
            </div>
          </div>
        </div>
        <div class="px-6 py-4 border-t border-gray-200 flex justify-end gap-3">
          <button
            class="px-3 py-1 text-sm border border-gray-300 rounded hover:bg-gray-100 transition-colors"
            onclick={() => (showBackupModal = false)}
            disabled={isSubmittingBackup}>{$t.common?.cancel}</button
          >
          <button
            class="px-3 py-1 text-sm bg-primary text-white border-primary rounded hover:bg-primary-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
            onclick={handleBulkBackup}
            disabled={selectedIds.size === 0 || isSubmittingBackup}
          >
            {isSubmittingBackup
              ? $t.dashboard?.starting
              : $t.dashboard?.start_backup}
          </button>
        </div>
      </div>
    </div>
  {/if}
</div>

<!-- [/DEF:DashboardHub:Page] -->
