import { useEffect, useState, type FormEvent } from 'react'
import { OverviewPanel } from './OverviewPanel'
import {
  beginMyTOTP,
  changeMyPassword,
  confirmMyTOTP,
  createNode,
  createUser,
  createAction,
  deleteAction,
  deleteGroup,
  deleteNode,
  deleteUser,
  DEFAULT_NETWORK_SETTINGS,
  DEFAULT_SECURITY_SETTINGS,
  disableMyTOTP,
  fetchActions,
  fetchAuthLogs,
  fetchGroups,
  fetchMe,
  fetchNetwork,
  logout,
  fetchNodeLogs,
  fetchNodes,
  fetchSecurity,
  fetchSetupStatus,
  fetchStatus,
  fetchUsers,
  isReauthRequiredError,
  patchAction,
  patchUser,
  reauth,
  runNodeHealthCheck,
  type AccessMode,
  type Action,
  type ActionEnvVar,
  type ActionKind,
  type ActionWidgetType,
  type AuthAuditEvent,
  type Group,
  type GroupScopeKind,
  type MeResponse,
  type NetworkSettings,
  type Node,
  type NodeAuditAction,
  type NodeAuditEvent,
  type NodeKind,
  type ReauthCredentials,
  type SecuritySettings,
  type User,
  saveNetwork,
  saveSecurity,
  upsertGroup,
} from './api/client'
import {
  filterAuthEvents,
  filterNodeEvents,
  pageRangeLabel,
  paginateItems,
  resolveDateRangeBounds,
  sortAuthEvents,
  sortNodeEvents,
  type AuthActionQuickFilter,
  type AuthCategoryFilter,
  type AuthLogsSortKey,
  type AuthOutcomeFilter,
  type DateRangePreset,
  type NodeLogsSortKey,
  type SortDirection,
} from './logsFiltering'
import {
  LOGS_PAGE_SIZE_OPTIONS,
  readLogsPageSize,
  writeLogsPageSize,
  type LogsPageSize,
} from './logsPreferences'
import { LoginPage } from './LoginPage'
import { NodeDetailPanel } from './NodeDetailPanel'
import {
  countAuthFailuresToday,
  countNodeHealthErrorsBySection,
  countNodesBySection,
  type NodeSectionCounts,
} from './sidebarCounts'
import { documentTitleFor, SECTION_TITLES } from './documentTitle'
import {
  isResourceSectionId,
  navigateTo,
  parseLocation,
  pathFor,
  subscribeToLocation,
  type ConfigTabId,
  type NodeDetailTabId,
  type ResourceSectionId,
  type ResourceTabId,
  type SectionId,
} from './navigation'
import {
  validateHostname,
  validateListenAddress,
  validatePassword,
} from './passwordPolicy'
import { ReauthModal } from './ReauthModal'
import { SetupWizard } from './SetupWizard'
import {
  applyThemeFromPreference,
  getSystemPrefersDark,
  readThemePreference,
  writeThemePreference,
  type ThemePreference,
} from './theme'
import './App.css'

const NAV_ITEMS: ReadonlyArray<{ id: SectionId; label: string }> = [
  { id: 'overview', label: 'Overview' },
  { id: 'containers', label: 'Containers' },
  { id: 'docker', label: 'Docker' },
  { id: 'vms', label: 'Virtual Machines' },
]

const CONFIG_TABS: ReadonlyArray<{ id: ConfigTabId; label: string }> = [
  { id: 'overview', label: 'Overview' },
  { id: 'users', label: 'Users' },
  { id: 'roles', label: 'Roles' },
  { id: 'groups', label: 'Groups' },
  { id: 'actions', label: 'Actions' },
  { id: 'security', label: 'Security' },
  { id: 'integrations', label: 'Integrations' },
  { id: 'network', label: 'Network' },
  { id: 'advanced', label: 'Advanced' },
]

const RESOURCE_SECTION_KIND: Record<ResourceSectionId, NodeKind> = {
  containers: 'container',
  vms: 'vm',
  docker: 'docker',
}

const RESOURCE_ADD_LABEL: Record<ResourceSectionId, string> = {
  containers: 'Add Containers',
  vms: 'Add Virtual Machines',
  docker: 'Add Docker',
}

const DEFAULT_PERMISSIONS = [
  'nodes.read',
  'nodes.exec',
  'nodes.update',
  'nodes.delete',
  'jobs.read',
  'jobs.run',
  'actions.create',
  'actions.update',
  'actions.delete',
  'actions.execute',
  'users.manage',
  'secrets.manage',
  'roles.manage',
  'logs.read',
] as const

const ADMINISTRATORS_GROUP_NAME = 'Administrators'

function formatUserTimestamp(value: string): string {
  const parsed = Date.parse(value)
  if (Number.isNaN(parsed)) {
    return value || '—'
  }
  return new Date(parsed).toLocaleString()
}

function CogIcon() {
  return (
    <svg
      className="cog-icon"
      viewBox="0 0 24 24"
      width="18"
      height="18"
      aria-hidden="true"
      focusable="false"
    >
      <path
        fill="currentColor"
        d="M19.14 12.94c.04-.31.06-.63.06-.94s-.02-.63-.06-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.49.49 0 0 0-.59-.22l-2.39.96a7.2 7.2 0 0 0-1.62-.94l-.36-2.54A.48.48 0 0 0 13.9 2h-3.8a.48.48 0 0 0-.48.42l-.36 2.54c-.59.24-1.13.55-1.62.94l-2.39-.96a.49.49 0 0 0-.59.22L2.74 8.48a.49.49 0 0 0 .12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94L2.86 14.52a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.3.59.22l2.39-.96c.49.39 1.03.7 1.62.94l.36 2.54c.05.24.24.42.48.42h3.8c.24 0 .43-.18.48-.42l.36-2.54c.59-.24 1.13-.55 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32a.49.49 0 0 0-.12-.61l-2.03-1.58ZM12 15.5A3.5 3.5 0 1 1 12 8.5a3.5 3.5 0 0 1 0 7Z"
      />
    </svg>
  )
}

function UserIcon() {
  return (
    <svg
      className="user-icon"
      viewBox="0 0 24 24"
      width="18"
      height="18"
      aria-hidden="true"
      focusable="false"
    >
      <path
        fill="currentColor"
        d="M12 12c2.7 0 4.8-2.1 4.8-4.8S14.7 2.4 12 2.4 7.2 4.5 7.2 7.2 9.3 12 12 12Zm0 2.4c-3.2 0-9.6 1.6-9.6 4.8v1.2c0 .7.5 1.2 1.2 1.2h16.8c.7 0 1.2-.5 1.2-1.2v-1.2c0-3.2-6.4-4.8-9.6-4.8Z"
      />
    </svg>
  )
}

function LogoutIcon() {
  return (
    <svg
      className="logout-icon"
      viewBox="0 0 24 24"
      width="18"
      height="18"
      aria-hidden="true"
      focusable="false"
    >
      <path
        fill="currentColor"
        d="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5-5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z"
      />
    </svg>
  )
}

function FiltersChevronIcon({ expanded }: { expanded: boolean }) {
  return (
    <svg
      className={
        expanded
          ? 'logs-filters-chevron logs-filters-chevron-expanded'
          : 'logs-filters-chevron'
      }
      viewBox="0 0 24 24"
      width="16"
      height="16"
      aria-hidden="true"
      focusable="false"
    >
      <path
        fill="currentColor"
        d="M7.41 8.59 12 13.17l4.59-4.58L18 10l-6 6-6-6z"
      />
    </svg>
  )
}

function ProfilePanel({
  onSignedOut,
}: {
  onSignedOut: () => void
}) {
  const [me, setMe] = useState<MeResponse | null>(null)
  const [loadError, setLoadError] = useState<string | null>(null)
  const [currentPassword, setCurrentPassword] = useState('')
  const [newPassword, setNewPassword] = useState('')
  const [confirmPassword, setConfirmPassword] = useState('')
  const [passwordTotp, setPasswordTotp] = useState('')
  const [passwordError, setPasswordError] = useState<string | null>(null)
  const [passwordSuccess, setPasswordSuccess] = useState<string | null>(null)

  const [totpPassword, setTotpPassword] = useState('')
  const [totpReauthCode, setTotpReauthCode] = useState('')
  const [pendingSecret, setPendingSecret] = useState<string | null>(null)
  const [pendingUrl, setPendingUrl] = useState<string | null>(null)
  const [qrDataUrl, setQrDataUrl] = useState<string | null>(null)
  const [confirmCode, setConfirmCode] = useState('')
  const [disableCode, setDisableCode] = useState('')
  const [totpError, setTotpError] = useState<string | null>(null)
  const [totpMessage, setTotpMessage] = useState<string | null>(null)

  useEffect(() => {
    let isCancelled = false
    async function load() {
      try {
        const response = await fetchMe()
        if (!isCancelled) {
          if (!response) {
            onSignedOut()
            return
          }
          setMe(response)
        }
      } catch (err) {
        if (!isCancelled) {
          setLoadError(err instanceof Error ? err.message : 'Failed to load profile')
        }
      }
    }
    void load()
    return () => {
      isCancelled = true
    }
  }, [onSignedOut])

  useEffect(() => {
    if (!pendingUrl) {
      setQrDataUrl(null)
      return
    }
    let isCancelled = false
    void import('qrcode').then(async (qrcode) => {
      const dataUrl = await qrcode.toDataURL(pendingUrl, { width: 220, margin: 1 })
      if (!isCancelled) {
        setQrDataUrl(dataUrl)
      }
    })
    return () => {
      isCancelled = true
    }
  }, [pendingUrl])

  async function handleChangePassword(event: FormEvent) {
    event.preventDefault()
    setPasswordError(null)
    setPasswordSuccess(null)
    if (!me) {
      return
    }
    if (newPassword !== confirmPassword) {
      setPasswordError('New passwords do not match')
      return
    }
    const policyError = validatePassword(newPassword, me.username)
    if (policyError) {
      setPasswordError(policyError)
      return
    }
    try {
      await changeMyPassword(currentPassword, newPassword, passwordTotp)
      setPasswordSuccess('Password updated. Sign in again.')
      setCurrentPassword('')
      setNewPassword('')
      setConfirmPassword('')
      setPasswordTotp('')
      onSignedOut()
    } catch (err) {
      setPasswordError(err instanceof Error ? err.message : 'Password change failed')
    }
  }

  async function handleBeginTotp(event: FormEvent) {
    event.preventDefault()
    setTotpError(null)
    setTotpMessage(null)
    try {
      const began = await beginMyTOTP({
        current_password: totpPassword,
        totp_code: totpReauthCode,
      })
      setPendingSecret(began.secret)
      setPendingUrl(began.otpauth_url)
      setTotpMessage('Scan the QR code, then enter a code to confirm.')
    } catch (err) {
      setTotpError(err instanceof Error ? err.message : 'Failed to start TOTP enrollment')
    }
  }

  async function handleConfirmTotp(event: FormEvent) {
    event.preventDefault()
    if (!pendingSecret) {
      return
    }
    setTotpError(null)
    try {
      await confirmMyTOTP(pendingSecret, confirmCode, {
        current_password: totpPassword,
        totp_code: totpReauthCode,
      })
      setPendingSecret(null)
      setPendingUrl(null)
      setConfirmCode('')
      setTotpPassword('')
      setTotpReauthCode('')
      setTotpMessage('Authenticator enabled.')
      const refreshed = await fetchMe()
      if (refreshed) {
        setMe(refreshed)
      }
    } catch (err) {
      setTotpError(err instanceof Error ? err.message : 'Failed to confirm TOTP')
    }
  }

  async function handleDisableTotp(event: FormEvent) {
    event.preventDefault()
    setTotpError(null)
    try {
      await disableMyTOTP({
        current_password: totpPassword,
        totp_code: disableCode,
      })
      setDisableCode('')
      setTotpPassword('')
      setTotpMessage('Authenticator disabled.')
      const refreshed = await fetchMe()
      if (refreshed) {
        setMe(refreshed)
      }
    } catch (err) {
      setTotpError(err instanceof Error ? err.message : 'Failed to disable TOTP')
    }
  }

  if (loadError) {
    return (
      <div className="profile-panel">
        <p className="config-empty" role="alert">
          {loadError}
        </p>
      </div>
    )
  }

  if (!me) {
    return (
      <div className="profile-panel">
        <p className="config-empty">Loading…</p>
      </div>
    )
  }

  return (
    <div className="profile-panel">
      <section className="profile-section">
        <h2 className="profile-section-title">Account</h2>
        <p className="config-hint">Signed in as {me.username}</p>
      </section>

      <section className="profile-section">
        <h2 className="profile-section-title">Change password</h2>
        <form className="profile-form" onSubmit={(event) => void handleChangePassword(event)}>
          <label htmlFor="profile-current-password">Current password</label>
          <input
            id="profile-current-password"
            type="password"
            autoComplete="current-password"
            value={currentPassword}
            onChange={(event) => setCurrentPassword(event.target.value)}
            required
          />
          <label htmlFor="profile-new-password">New password</label>
          <input
            id="profile-new-password"
            type="password"
            autoComplete="new-password"
            value={newPassword}
            onChange={(event) => setNewPassword(event.target.value)}
            required
          />
          <label htmlFor="profile-confirm-password">Confirm new password</label>
          <input
            id="profile-confirm-password"
            type="password"
            autoComplete="new-password"
            value={confirmPassword}
            onChange={(event) => setConfirmPassword(event.target.value)}
            required
          />
          {me.totp_confirmed ? (
            <>
              <label htmlFor="profile-password-totp">Authenticator code</label>
              <input
                id="profile-password-totp"
                type="text"
                inputMode="numeric"
                autoComplete="one-time-code"
                value={passwordTotp}
                onChange={(event) => setPasswordTotp(event.target.value)}
                required
              />
            </>
          ) : null}
          {passwordError ? (
            <p className="groups-error" role="alert">
              {passwordError}
            </p>
          ) : null}
          {passwordSuccess ? (
            <p className="config-hint" role="status">
              {passwordSuccess}
            </p>
          ) : null}
          <button type="submit" className="config-action-save">
            Update password
          </button>
        </form>
      </section>

      <section className="profile-section">
        <h2 className="profile-section-title">Authenticator (TOTP)</h2>
        {!me.totp_enabled ? (
          <p className="config-hint">
            An administrator must enable TOTP on the server before you can enroll.
          </p>
        ) : me.totp_confirmed ? (
          <form className="profile-form" onSubmit={(event) => void handleDisableTotp(event)}>
            <p className="config-hint">Authenticator is enabled on your account.</p>
            <label htmlFor="profile-disable-password">Password</label>
            <input
              id="profile-disable-password"
              type="password"
              autoComplete="current-password"
              value={totpPassword}
              onChange={(event) => setTotpPassword(event.target.value)}
              required
            />
            <label htmlFor="profile-disable-code">Current authenticator code</label>
            <input
              id="profile-disable-code"
              type="text"
              inputMode="numeric"
              autoComplete="one-time-code"
              value={disableCode}
              onChange={(event) => setDisableCode(event.target.value)}
              required
            />
            {totpError ? (
              <p className="groups-error" role="alert">
                {totpError}
              </p>
            ) : null}
            {totpMessage ? (
              <p className="config-hint" role="status">
                {totpMessage}
              </p>
            ) : null}
            <button type="submit" className="groups-delete">
              Disable authenticator
            </button>
          </form>
        ) : pendingSecret ? (
          <form className="profile-form" onSubmit={(event) => void handleConfirmTotp(event)}>
            {qrDataUrl ? (
              <img className="totp-qr" src={qrDataUrl} alt="TOTP QR code" />
            ) : null}
            <p className="config-hint">Secret: {pendingSecret}</p>
            <label htmlFor="profile-confirm-code">Authenticator code</label>
            <input
              id="profile-confirm-code"
              type="text"
              inputMode="numeric"
              autoComplete="one-time-code"
              value={confirmCode}
              onChange={(event) => setConfirmCode(event.target.value)}
              required
            />
            {totpError ? (
              <p className="groups-error" role="alert">
                {totpError}
              </p>
            ) : null}
            <button type="submit" className="config-action-save">
              Confirm authenticator
            </button>
          </form>
        ) : (
          <form className="profile-form" onSubmit={(event) => void handleBeginTotp(event)}>
            <p className="config-hint">
              Re-enter your password to begin authenticator enrollment.
            </p>
            <label htmlFor="profile-totp-password">Password</label>
            <input
              id="profile-totp-password"
              type="password"
              autoComplete="current-password"
              value={totpPassword}
              onChange={(event) => setTotpPassword(event.target.value)}
              required
            />
            {totpError ? (
              <p className="groups-error" role="alert">
                {totpError}
              </p>
            ) : null}
            {totpMessage ? (
              <p className="config-hint" role="status">
                {totpMessage}
              </p>
            ) : null}
            <button type="submit" className="config-action-save">
              Begin enrollment
            </button>
          </form>
        )}
      </section>
    </div>
  )
}

function UsersConfigTab() {
  const [users, setUsers] = useState<ReadonlyArray<User>>([])
  const [groups, setGroups] = useState<ReadonlyArray<Group>>([])
  const [me, setMe] = useState<MeResponse | null>(null)
  const [isLoadingUsers, setIsLoadingUsers] = useState(true)
  const [usersLoadError, setUsersLoadError] = useState<string | null>(null)
  const [actionError, setActionError] = useState<string | null>(null)

  const [createUsername, setCreateUsername] = useState('')
  const [createPassword, setCreatePassword] = useState('')
  const [createGroupNames, setCreateGroupNames] = useState<ReadonlyArray<string>>([])

  const [editingUserId, setEditingUserId] = useState<string | null>(null)
  const [editGroupNames, setEditGroupNames] = useState<ReadonlyArray<string>>([])
  const [setPasswordUserId, setSetPasswordUserId] = useState<string | null>(null)
  const [setPasswordValue, setSetPasswordValue] = useState('')

  type PendingAction =
    | { kind: 'delete'; userId: string }
    | {
        kind: 'grace'
        run: (credentials?: ReauthCredentials) => Promise<void>
      }
  const [pendingAction, setPendingAction] = useState<PendingAction | null>(null)

  const canManageUsers = Boolean(me?.permissions?.includes('users.manage'))
  const requireTotp = Boolean(me?.totp_confirmed && me?.totp_enabled)

  useEffect(() => {
    let isCancelled = false

    async function loadUsers() {
      setIsLoadingUsers(true)
      setUsersLoadError(null)
      try {
        const [usersResponse, groupsResponse, meResponse] = await Promise.all([
          fetchUsers(),
          fetchGroups(),
          fetchMe(),
        ])
        if (!isCancelled) {
          setUsers(usersResponse.users)
          setGroups(groupsResponse.groups)
          setMe(meResponse)
        }
      } catch (err) {
        if (!isCancelled) {
          setUsersLoadError(
            err instanceof Error ? err.message : 'Failed to load users',
          )
        }
      } finally {
        if (!isCancelled) {
          setIsLoadingUsers(false)
        }
      }
    }

    void loadUsers()

    return () => {
      isCancelled = true
    }
  }, [])

  function toggleGroupSelection(
    current: ReadonlyArray<string>,
    groupName: string,
  ): ReadonlyArray<string> {
    if (current.includes(groupName)) {
      return current.filter((name) => name !== groupName)
    }
    return [...current, groupName]
  }

  async function runGraceMutation(mutation: () => Promise<void>) {
    setActionError(null)
    try {
      await mutation()
    } catch (err) {
      if (isReauthRequiredError(err)) {
        setPendingAction({
          kind: 'grace',
          run: async () => {
            // caller will reauth first then call mutation again
            await mutation()
          },
        })
        return
      }
      setActionError(err instanceof Error ? err.message : 'Action failed')
    }
  }

  async function handleCreateUser(event: FormEvent) {
    event.preventDefault()
    setActionError(null)
    const policyError = validatePassword(createPassword, createUsername)
    if (policyError) {
      setActionError(policyError)
      return
    }
    try {
      const response = await createUser({
        username: createUsername,
        password: createPassword,
        group_names: createGroupNames,
      })
      setUsers(response.users)
      setCreateUsername('')
      setCreatePassword('')
      setCreateGroupNames([])
    } catch (err) {
      setActionError(err instanceof Error ? err.message : 'Failed to create user')
    }
  }

  async function handleSaveGroups(userId: string) {
    await runGraceMutation(async () => {
      const response = await patchUser(userId, { group_names: editGroupNames })
      setUsers(response.users)
      setEditingUserId(null)
    })
  }

  async function handleSetPassword(userId: string) {
    const target = users.find((user) => user.id === userId)
    const policyError = validatePassword(setPasswordValue, target?.username ?? '')
    if (policyError) {
      setActionError(policyError)
      return
    }
    await runGraceMutation(async () => {
      const response = await patchUser(userId, { password: setPasswordValue })
      setUsers(response.users)
      setSetPasswordUserId(null)
      setSetPasswordValue('')
    })
  }

  async function handleDisableTotp(userId: string) {
    await runGraceMutation(async () => {
      const response = await patchUser(userId, { disable_totp: true })
      setUsers(response.users)
    })
  }

  async function handleDeleteConfirm(credentials: ReauthCredentials) {
    if (!pendingAction || pendingAction.kind !== 'delete') {
      return
    }
    const response = await deleteUser(pendingAction.userId, credentials)
    setUsers(response.users)
    setPendingAction(null)
  }

  async function handleGraceConfirm(credentials: ReauthCredentials) {
    if (!pendingAction || pendingAction.kind !== 'grace') {
      return
    }
    await reauth(credentials.current_password, credentials.totp_code ?? '')
    await pendingAction.run()
    setPendingAction(null)
  }

  return (
    <div className="groups-panel users-panel">
      {pendingAction?.kind === 'delete' ? (
        <ReauthModal
          title="Confirm delete"
          hint="Re-enter your password to delete this user."
          requireTotp={requireTotp}
          confirmLabel="Delete user"
          onCancel={() => setPendingAction(null)}
          onConfirm={handleDeleteConfirm}
        />
      ) : null}
      {pendingAction?.kind === 'grace' ? (
        <ReauthModal
          title="Confirm identity"
          hint="Re-authentication is required for this change."
          requireTotp={requireTotp}
          confirmLabel="Continue"
          onCancel={() => setPendingAction(null)}
          onConfirm={handleGraceConfirm}
        />
      ) : null}

      {canManageUsers ? (
        <form
          className="users-create-form"
          onSubmit={(event) => void handleCreateUser(event)}
        >
          <p className="config-hint">Create user</p>
          <label htmlFor="create-username">Username</label>
          <input
            id="create-username"
            value={createUsername}
            onChange={(event) => setCreateUsername(event.target.value)}
            required
          />
          <label htmlFor="create-password">Password</label>
          <input
            id="create-password"
            type="password"
            autoComplete="new-password"
            value={createPassword}
            onChange={(event) => setCreatePassword(event.target.value)}
            required
          />
          <fieldset className="users-group-fieldset">
            <legend>Groups (optional)</legend>
            {groups.map((group) => (
              <label key={group.name} className="permission-option">
                <input
                  type="checkbox"
                  checked={createGroupNames.includes(group.name)}
                  onChange={() =>
                    setCreateGroupNames(
                      toggleGroupSelection(createGroupNames, group.name),
                    )
                  }
                />
                <span>{group.name}</span>
              </label>
            ))}
          </fieldset>
          <button type="submit" className="config-action-save">
            Create user
          </button>
        </form>
      ) : null}

      <div className="groups-list">
        <p className="config-hint">Configured accounts</p>
        {actionError ? (
          <p className="config-empty groups-error" role="alert">
            {actionError}
          </p>
        ) : null}
        {isLoadingUsers ? (
          <p className="config-empty">Loading…</p>
        ) : usersLoadError ? (
          <p className="config-empty" role="alert">
            {usersLoadError}
          </p>
        ) : users.length === 0 ? (
          <p className="config-empty">No users configured yet.</p>
        ) : (
          <ul className="groups-ul">
            {users.map((user) => (
              <li key={user.id} className="groups-li">
                <div className="groups-li-content">
                  <div className="groups-li-title">
                    {user.username}
                    {user.is_admin ? (
                      <span className="user-badge">Admin</span>
                    ) : null}
                  </div>
                  <div className="groups-li-meta">
                    Groups:{' '}
                    {user.group_names.length > 0
                      ? user.group_names.join(', ')
                      : '—'}
                  </div>
                  <div className="groups-li-meta">
                    Enabled: {user.enabled ? 'yes' : 'no'} · TOTP:{' '}
                    {user.totp_confirmed ? 'confirmed' : 'not confirmed'}
                  </div>
                  <div className="groups-li-meta">
                    Created: {formatUserTimestamp(user.created_at)} · Password
                    changed: {formatUserTimestamp(user.password_changed_at)}
                  </div>
                  <div className="groups-li-meta">ID: {user.id}</div>

                  {canManageUsers && editingUserId === user.id ? (
                    <div className="users-inline-edit">
                      <fieldset className="users-group-fieldset">
                        <legend>Groups</legend>
                        {groups.map((group) => (
                          <label key={group.name} className="permission-option">
                            <input
                              type="checkbox"
                              checked={editGroupNames.includes(group.name)}
                              onChange={() =>
                                setEditGroupNames(
                                  toggleGroupSelection(editGroupNames, group.name),
                                )
                              }
                            />
                            <span>{group.name}</span>
                          </label>
                        ))}
                      </fieldset>
                      <div className="users-inline-actions">
                        <button
                          type="button"
                          className="config-action-save"
                          onClick={() => void handleSaveGroups(user.id)}
                        >
                          Save groups
                        </button>
                        <button
                          type="button"
                          className="config-action-revert"
                          onClick={() => setEditingUserId(null)}
                        >
                          Cancel
                        </button>
                      </div>
                    </div>
                  ) : null}

                  {canManageUsers && setPasswordUserId === user.id ? (
                    <div className="users-inline-edit">
                      <label htmlFor={`set-password-${user.id}`}>New password</label>
                      <input
                        id={`set-password-${user.id}`}
                        type="password"
                        autoComplete="new-password"
                        value={setPasswordValue}
                        onChange={(event) => setSetPasswordValue(event.target.value)}
                      />
                      <div className="users-inline-actions">
                        <button
                          type="button"
                          className="config-action-save"
                          onClick={() => void handleSetPassword(user.id)}
                        >
                          Set password
                        </button>
                        <button
                          type="button"
                          className="config-action-revert"
                          onClick={() => {
                            setSetPasswordUserId(null)
                            setSetPasswordValue('')
                          }}
                        >
                          Cancel
                        </button>
                      </div>
                    </div>
                  ) : null}
                </div>
                {canManageUsers ? (
                  <div className="users-row-actions">
                    <button
                      type="button"
                      className="config-action-revert"
                      onClick={() => {
                        setEditingUserId(user.id)
                        setEditGroupNames(user.group_names)
                        setSetPasswordUserId(null)
                      }}
                    >
                      Groups
                    </button>
                    <button
                      type="button"
                      className="config-action-revert"
                      onClick={() => {
                        setSetPasswordUserId(user.id)
                        setSetPasswordValue('')
                        setEditingUserId(null)
                      }}
                    >
                      Password
                    </button>
                    {user.totp_confirmed ? (
                      <button
                        type="button"
                        className="config-action-revert"
                        onClick={() => void handleDisableTotp(user.id)}
                      >
                        Disable TOTP
                      </button>
                    ) : null}
                    <button
                      type="button"
                      className="groups-delete"
                      disabled={!user.can_delete}
                      title={
                        user.can_delete
                          ? undefined
                          : 'Cannot delete the last administrator'
                      }
                      onClick={() =>
                        setPendingAction({ kind: 'delete', userId: user.id })
                      }
                    >
                      Delete
                    </button>
                  </div>
                ) : (
                  <button
                    type="button"
                    className="groups-delete"
                    disabled
                    title="Requires users.manage"
                  >
                    Delete
                  </button>
                )}
              </li>
            ))}
          </ul>
        )}
      </div>
    </div>
  )
}

function useThemePreference() {
  const [themePreference, setThemePreference] =
    useState<ThemePreference>(readThemePreference)

  useEffect(() => {
    applyThemeFromPreference(themePreference)

    if (themePreference !== 'auto') {
      return
    }

    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')

    function handleSystemThemeChange() {
      applyThemeFromPreference('auto', getSystemPrefersDark(mediaQuery))
    }

    mediaQuery.addEventListener('change', handleSystemThemeChange)
    return () => {
      mediaQuery.removeEventListener('change', handleSystemThemeChange)
    }
  }, [themePreference])

  function updateThemePreference(nextPreference: ThemePreference) {
    writeThemePreference(nextPreference)
    setThemePreference(nextPreference)
  }

  return { themePreference, updateThemePreference }
}

function OverviewConfigTab({
  draftThemePreference,
  onDraftThemePreferenceChange,
}: {
  draftThemePreference: ThemePreference
  onDraftThemePreferenceChange: (preference: ThemePreference) => void
}) {
  return (
    <div className="config-field">
      <label htmlFor="theme-preference">Theme</label>
      <select
        id="theme-preference"
        className="theme-select"
        value={draftThemePreference}
        onChange={(event) => {
          onDraftThemePreferenceChange(event.target.value as ThemePreference)
        }}
      >
        <option value="auto">Auto</option>
        <option value="light">Light</option>
        <option value="dark">Dark</option>
      </select>
      <p className="config-hint">
        Auto follows the browser preference. Light and Dark always use
        Catppuccin Latte or Mocha.
      </p>
    </div>
  )
}

const ACTION_PLACEHOLDERS: ReadonlyArray<{
  token: string
  description: string
  example: string
}> = [
  {
    token: '{{cc.host}}',
    description: 'ClusterCanvas public hostname',
    example: 'canvas.example.com',
  },
  {
    token: '{{cc.ip}}',
    description: 'ClusterCanvas listen / resolved IP',
    example: '192.168.1.10',
  },
  {
    token: '{{node.host}}',
    description: 'Target node hostname / address as stored',
    example: 'vm-web-01',
  },
  {
    token: '{{node.ip}}',
    description: 'Target node host IP',
    example: '10.0.0.5',
  },
  {
    token: '{{node.username}}',
    description: 'SSH username on the target node',
    example: 'root',
  },
  {
    token: '{{env.NAME}}',
    description: 'Value of an ENV var defined on this action',
    example: '{{env.APP_HOME}} → /opt/app',
  },
  {
    token: '{{secret.NAME}}',
    description:
      'Reserved for the future Secrets page (not resolved yet)',
    example: '{{secret.db_password}}',
  },
]

function emptyEnvRow(): ActionEnvVar {
  return { name: '', value: '' }
}

function ActionsConfigTab() {
  const [actions, setActions] = useState<Action[]>([])
  const [me, setMe] = useState<MeResponse | null>(null)
  const [isLoading, setIsLoading] = useState(true)
  const [loadError, setLoadError] = useState<string | null>(null)
  const [submitError, setSubmitError] = useState<string | null>(null)
  const [showTypePicker, setShowTypePicker] = useState(false)
  const [editorKind, setEditorKind] = useState<ActionKind | null>(null)
  const [editingActionId, setEditingActionId] = useState<string | null>(null)
  const [draftName, setDraftName] = useState('')
  const [draftDescription, setDraftDescription] = useState('')
  const [draftBody, setDraftBody] = useState('')
  const [draftEnv, setDraftEnv] = useState<ActionEnvVar[]>([emptyEnvRow()])
  const [draftRequiresSudo, setDraftRequiresSudo] = useState(false)
  const [draftShowWidget, setDraftShowWidget] = useState(false)
  const [draftWidgetType, setDraftWidgetType] =
    useState<ActionWidgetType>('raw')
  const [isSaving, setIsSaving] = useState(false)
  const [editingIsBuiltin, setEditingIsBuiltin] = useState(false)

  const isEditing = editingActionId !== null
  const showEditor = editorKind !== null
  const canCreateActions = Boolean(me?.permissions?.includes('actions.create'))
  const canUpdateActions = Boolean(me?.permissions?.includes('actions.update'))
  const canDeleteActions = Boolean(me?.permissions?.includes('actions.delete'))
  const fieldsLocked = editingIsBuiltin

  useEffect(() => {
    let isCancelled = false

    async function loadActions() {
      setIsLoading(true)
      setLoadError(null)
      try {
        const [actionsResponse, meResponse] = await Promise.all([
          fetchActions(),
          fetchMe(),
        ])
        if (!isCancelled) {
          setActions([...actionsResponse.actions])
          setMe(meResponse)
        }
      } catch (err) {
        if (!isCancelled) {
          setLoadError(
            err instanceof Error ? err.message : 'Failed to load actions',
          )
        }
      } finally {
        if (!isCancelled) {
          setIsLoading(false)
        }
      }
    }

    void loadActions()

    return () => {
      isCancelled = true
    }
  }, [])

  function clearEditor() {
    setShowTypePicker(false)
    setEditorKind(null)
    setEditingActionId(null)
    setDraftName('')
    setDraftDescription('')
    setDraftBody('')
    setDraftEnv([emptyEnvRow()])
    setDraftRequiresSudo(false)
    setDraftShowWidget(false)
    setDraftWidgetType('raw')
    setEditingIsBuiltin(false)
    setSubmitError(null)
  }

  function openTypePicker() {
    clearEditor()
    setShowTypePicker(true)
  }

  function startCreate(kind: ActionKind) {
    setShowTypePicker(false)
    setEditorKind(kind)
    setEditingActionId(null)
    setEditingIsBuiltin(false)
    setDraftName('')
    setDraftDescription('')
    setDraftBody('')
    setDraftEnv([emptyEnvRow()])
    setDraftRequiresSudo(false)
    setDraftShowWidget(false)
    setDraftWidgetType('raw')
    setSubmitError(null)
  }

  function startEdit(action: Action) {
    if (!canUpdateActions) {
      return
    }
    if (action.builtin && !canUpdateActions) {
      return
    }
    setShowTypePicker(false)
    setEditorKind(action.kind)
    setEditingActionId(action.id)
    setEditingIsBuiltin(Boolean(action.builtin))
    setDraftName(action.name)
    setDraftDescription(action.description)
    setDraftBody(action.body)
    setDraftEnv(
      action.env.length > 0
        ? action.env.map((envVar) => ({ ...envVar }))
        : [emptyEnvRow()],
    )
    setDraftRequiresSudo(Boolean(action.requires_sudo))
    setDraftShowWidget(Boolean(action.show_widget))
    setDraftWidgetType(
      action.widget_type === 'memory' ||
        action.widget_type === 'disk' ||
        action.widget_type === 'raw'
        ? action.widget_type
        : 'raw',
    )
    setSubmitError(null)
  }

  function updateEnvRow(index: number, field: 'name' | 'value', value: string) {
    setDraftEnv((previous) =>
      previous.map((row, rowIndex) =>
        rowIndex === index ? { ...row, [field]: value } : row,
      ),
    )
  }

  function addEnvRow() {
    setDraftEnv((previous) => [...previous, emptyEnvRow()])
  }

  function removeEnvRow(index: number) {
    setDraftEnv((previous) => {
      if (previous.length <= 1) {
        return [emptyEnvRow()]
      }
      return previous.filter((_, rowIndex) => rowIndex !== index)
    })
  }

  function normalizedEnv(): ActionEnvVar[] {
    return draftEnv
      .map((row) => ({
        name: row.name.trim(),
        value: row.value,
      }))
      .filter((row) => row.name !== '' || row.value.trim() !== '')
  }

  async function handleSaveAction() {
    if (!editorKind) {
      return
    }
    setSubmitError(null)
    setIsSaving(true)
    try {
      const env = normalizedEnv()
      const widgetType = draftShowWidget ? draftWidgetType : ('' as const)
      const response = isEditing
        ? await patchAction(
            editingActionId!,
            editingIsBuiltin
              ? {
                  show_widget: draftShowWidget,
                  widget_type: widgetType,
                }
              : {
                  name: draftName,
                  description: draftDescription,
                  body: draftBody,
                  env,
                  requires_sudo: draftRequiresSudo,
                  show_widget: draftShowWidget,
                  widget_type: widgetType,
                },
          )
        : await createAction({
            name: draftName,
            description: draftDescription,
            kind: editorKind,
            body: draftBody,
            env,
            requires_sudo: draftRequiresSudo,
            show_widget: draftShowWidget,
            widget_type: widgetType,
          })
      setActions([...response.actions])
      clearEditor()
    } catch (err) {
      setSubmitError(
        err instanceof Error ? err.message : 'Failed to save action',
      )
    } finally {
      setIsSaving(false)
    }
  }

  async function handleDeleteAction(action: Action) {
    if (action.builtin || !canDeleteActions) {
      return
    }
    if (!window.confirm(`Delete action “${action.name}”?`)) {
      return
    }
    setSubmitError(null)
    try {
      const response = await deleteAction(action.id)
      setActions([...response.actions])
      if (editingActionId === action.id) {
        clearEditor()
      }
    } catch (err) {
      setSubmitError(
        err instanceof Error ? err.message : 'Failed to delete action',
      )
    }
  }

  return (
    <div className="actions-panel">
      <p className="config-hint">
        Pre-defined and custom actions for managed nodes. Placeholders and ENV
        vars are expanded when an action runs.
      </p>

      {submitError && !showEditor ? (
        <p className="config-empty groups-error" role="alert">
          {submitError}
        </p>
      ) : null}

      <div className="actions-layout">
        <div className="actions-list">
          <div className="actions-list-header">
            <p className="config-hint">Configured actions</p>
            <button
              type="button"
              className="groups-save"
              disabled={!canCreateActions}
              title={
                canCreateActions ? undefined : 'Requires actions.create'
              }
              onClick={() => openTypePicker()}
            >
              Add Action…
            </button>
          </div>
          {isLoading ? (
            <p className="config-empty">Loading…</p>
          ) : loadError ? (
            <p className="config-empty" role="alert">
              {loadError}
            </p>
          ) : actions.length === 0 ? (
            <p className="config-empty">No actions configured yet.</p>
          ) : (
            <ul className="groups-ul">
              {actions.map((action) => {
                const isSelected = editingActionId === action.id
                return (
                  <li
                    key={action.id}
                    className={
                      isSelected ? 'groups-li groups-li-selected' : 'groups-li'
                    }
                  >
                    <div className="groups-li-content">
                      <div className="groups-li-title">
                        {action.name}
                        <span className="user-badge">{action.kind}</span>
                        {action.builtin ? (
                          <span className="user-badge">built-in</span>
                        ) : null}
                        {action.requires_sudo ? (
                          <span className="user-badge action-requires-sudo">
                            sudo
                          </span>
                        ) : null}
                        {action.show_widget ? (
                          <span className="user-badge">
                            widget
                            {action.widget_type
                              ? `: ${action.widget_type}`
                              : ''}
                          </span>
                        ) : null}
                      </div>
                      <div className="groups-li-meta">
                        {action.description || 'No description'}
                      </div>
                      <div className="groups-li-meta">
                        <code className="actions-body-preview">
                          {action.body.length > 80
                            ? `${action.body.slice(0, 80)}…`
                            : action.body}
                        </code>
                      </div>
                    </div>
                    <div className="groups-li-actions">
                      <button
                        type="button"
                        className="groups-edit"
                        disabled={!canUpdateActions}
                        title={
                          canUpdateActions
                            ? action.builtin
                              ? 'Edit widget settings'
                              : undefined
                            : 'Requires actions.update'
                        }
                        onClick={() => startEdit(action)}
                      >
                        Edit
                      </button>
                      <button
                        type="button"
                        className="groups-delete"
                        disabled={action.builtin || !canDeleteActions}
                        title={
                          action.builtin
                            ? 'Built-in actions cannot be deleted'
                            : canDeleteActions
                              ? undefined
                              : 'Requires actions.delete'
                        }
                        onClick={() => void handleDeleteAction(action)}
                      >
                        Delete
                      </button>
                    </div>
                  </li>
                )
              })}
            </ul>
          )}
        </div>

        <aside className="actions-placeholders">
          <p className="config-hint">Placeholders</p>
          <p className="config-hint actions-placeholders-intro">
            Use these tokens in the command/script body or ENV values. ENV vars
            are applied before the action runs.
          </p>
          <ul className="actions-placeholder-list">
            {ACTION_PLACEHOLDERS.map((entry) => (
              <li key={entry.token}>
                <code>{entry.token}</code>
                <span>{entry.description}</span>
                <span className="actions-placeholder-example">
                  Example: {entry.example}
                </span>
              </li>
            ))}
          </ul>
        </aside>
      </div>

      {showTypePicker ? (
        <div className="reauth-backdrop" role="presentation">
          <div
            className="reauth-modal actions-type-modal"
            role="dialog"
            aria-modal="true"
            aria-labelledby="action-type-title"
          >
            <h2 id="action-type-title">Add Action</h2>
            <p className="config-hint">Choose the type of action to create.</p>
            <div className="actions-type-choices">
              <button
                type="button"
                className="actions-type-choice"
                onClick={() => startCreate('shell')}
              >
                <strong>Shell Command</strong>
                <span>
                  A single command run over SSH on the target node (e.g.{' '}
                  <code>uptime</code>, <code>df -h</code>).
                </span>
              </button>
              <button
                type="button"
                className="actions-type-choice"
                onClick={() => startCreate('script')}
              >
                <strong>Script</strong>
                <span>
                  A multi-line shell script uploaded and run on the target node.
                </span>
              </button>
            </div>
            <div className="reauth-actions">
              <button
                type="button"
                className="config-action-revert"
                onClick={() => clearEditor()}
              >
                Cancel
              </button>
            </div>
          </div>
        </div>
      ) : null}

      {showEditor && editorKind ? (
        <div className="reauth-backdrop" role="presentation">
          <div
            className="reauth-modal actions-editor-modal"
            role="dialog"
            aria-modal="true"
            aria-labelledby="action-editor-title"
          >
            <h2 id="action-editor-title">
              {isEditing
                ? editingIsBuiltin
                  ? `Widget settings · ${draftName}`
                  : `Edit ${editorKind === 'shell' ? 'shell command' : 'script'}`
                : `New ${editorKind === 'shell' ? 'shell command' : 'script'}`}
            </h2>

            {submitError ? (
              <p className="config-empty groups-error" role="alert">
                {submitError}
              </p>
            ) : null}

            {editingIsBuiltin ? (
              <p className="config-hint">
                Built-in actions keep a fixed command body. You can still enable
                or change the Overview widget.
              </p>
            ) : null}

            <div className="config-field">
              <label htmlFor="action-name">Name</label>
              <input
                id="action-name"
                className="config-input"
                value={draftName}
                disabled={fieldsLocked}
                onChange={(event) => setDraftName(event.target.value)}
              />
            </div>

            <div className="config-field">
              <label htmlFor="action-description">Description</label>
              <input
                id="action-description"
                className="config-input"
                value={draftDescription}
                disabled={fieldsLocked}
                onChange={(event) => setDraftDescription(event.target.value)}
              />
            </div>

            <div className="config-field">
              <label htmlFor="action-body">
                {editorKind === 'shell' ? 'Command' : 'Script'}
              </label>
              {editorKind === 'shell' ? (
                <input
                  id="action-body"
                  className="config-input"
                  value={draftBody}
                  disabled={fieldsLocked}
                  placeholder="echo {{node.ip}}"
                  onChange={(event) => setDraftBody(event.target.value)}
                />
              ) : (
                <textarea
                  id="action-body"
                  className="config-input actions-script-input"
                  value={draftBody}
                  disabled={fieldsLocked}
                  rows={8}
                  placeholder={"#!/bin/sh\necho {{node.host}}"}
                  onChange={(event) => setDraftBody(event.target.value)}
                />
              )}
              {!fieldsLocked ? (
                <p className="config-hint">
                  Do not include a leading <code>sudo</code>; check Requires Sudo
                  instead so ClusterCanvas runs with <code>sudo -n</code>.
                </p>
              ) : null}
            </div>

            <label className="checkbox-row">
              <input
                type="checkbox"
                checked={draftRequiresSudo}
                disabled={fieldsLocked}
                onChange={(event) =>
                  setDraftRequiresSudo(event.target.checked)
                }
              />
              Requires Sudo
            </label>

            <label className="checkbox-row">
              <input
                type="checkbox"
                checked={draftShowWidget}
                onChange={(event) => {
                  const checked = event.target.checked
                  setDraftShowWidget(checked)
                  if (checked && !draftWidgetType) {
                    setDraftWidgetType('raw')
                  }
                }}
              />
              Show Widget
            </label>
            {draftShowWidget ? (
              <div className="config-field">
                <label htmlFor="action-widget-type">Widget type</label>
                <select
                  id="action-widget-type"
                  className="config-input"
                  value={draftWidgetType}
                  onChange={(event) =>
                    setDraftWidgetType(event.target.value as ActionWidgetType)
                  }
                >
                  <option value="memory">Memory (free -m ring)</option>
                  <option value="disk">Disk (df -h ring)</option>
                  <option value="raw">Raw text</option>
                </select>
                <p className="config-hint">
                  Parsed from the last run of this action on each node Overview.
                </p>
              </div>
            ) : null}

            <div className="config-field">
              <label>Environment variables</label>
              <p className="config-hint">
                Applied before the command/script runs. Values may use
                placeholders.
              </p>
              <div className="actions-env-rows">
                {draftEnv.map((row, index) => (
                  <div key={`env-${index}`} className="actions-env-row">
                    <input
                      className="config-input"
                      aria-label={`ENV name ${index + 1}`}
                      placeholder="NAME"
                      value={row.name}
                      disabled={fieldsLocked}
                      onChange={(event) =>
                        updateEnvRow(index, 'name', event.target.value)
                      }
                    />
                    <input
                      className="config-input"
                      aria-label={`ENV value ${index + 1}`}
                      placeholder="value or {{node.ip}}"
                      value={row.value}
                      disabled={fieldsLocked}
                      onChange={(event) =>
                        updateEnvRow(index, 'value', event.target.value)
                      }
                    />
                    <button
                      type="button"
                      className="groups-delete"
                      aria-label={`Remove ENV row ${index + 1}`}
                      disabled={fieldsLocked}
                      onClick={() => removeEnvRow(index)}
                    >
                      Remove
                    </button>
                  </div>
                ))}
              </div>
              <button
                type="button"
                className="groups-new"
                disabled={fieldsLocked}
                onClick={() => addEnvRow()}
              >
                Add ENV variable
              </button>
            </div>

            <div className="reauth-actions">
              <button
                type="button"
                className="config-action-revert"
                onClick={() => clearEditor()}
              >
                Cancel
              </button>
              <button
                type="button"
                className="config-action-save"
                disabled={isSaving}
                onClick={() => void handleSaveAction()}
              >
                {isSaving ? 'Saving…' : isEditing ? 'Save changes' : 'Save action'}
              </button>
            </div>
          </div>
        </div>
      ) : null}
    </div>
  )
}

function GroupsConfigTab() {
  const [groups, setGroups] = useState<ReadonlyArray<Group>>([])
  const [isLoadingGroups, setIsLoadingGroups] = useState(true)
  const [groupsLoadError, setGroupsLoadError] = useState<string | null>(null)

  const [editingGroupName, setEditingGroupName] = useState<string | null>(null)
  const [draftGroupName, setDraftGroupName] = useState('')
  const [draftScopeKind, setDraftScopeKind] = useState<GroupScopeKind>('node')
  const [draftScopeName, setDraftScopeName] = useState('')
  const [draftPermissions, setDraftPermissions] = useState<string[]>([])
  const [submitError, setSubmitError] = useState<string | null>(null)

  const isEditing = editingGroupName !== null

  useEffect(() => {
    let isCancelled = false

    async function loadGroups() {
      setIsLoadingGroups(true)
      setGroupsLoadError(null)
      try {
        const response = await fetchGroups()
        if (!isCancelled) {
          setGroups(response.groups)
        }
      } catch (err) {
        if (!isCancelled) {
          setGroupsLoadError(
            err instanceof Error ? err.message : 'Failed to load groups',
          )
        }
      } finally {
        if (!isCancelled) {
          setIsLoadingGroups(false)
        }
      }
    }

    void loadGroups()

    return () => {
      isCancelled = true
    }
  }, [])

  function clearGroupForm() {
    setEditingGroupName(null)
    setDraftGroupName('')
    setDraftScopeKind('node')
    setDraftScopeName('')
    setDraftPermissions([])
    setSubmitError(null)
  }

  function handleEditGroup(group: Group) {
    setEditingGroupName(group.name)
    setDraftGroupName(group.name)
    setDraftScopeKind(group.scope_kind)
    setDraftScopeName(group.scope_name)
    setDraftPermissions([...group.permissions])
    setSubmitError(null)
  }

  function togglePermission(permission: string, checked: boolean) {
    setDraftPermissions((prev) => {
      if (checked) {
        if (prev.includes(permission)) {
          return prev
        }
        return [...prev, permission]
      }

      return prev.filter((item) => item !== permission)
    })
  }

  async function handleSaveGroup() {
    setSubmitError(null)

    if (!isEditing && draftGroupName.trim() === ADMINISTRATORS_GROUP_NAME) {
      setSubmitError('Administrators group cannot be created here')
      return
    }

    try {
      const response = await upsertGroup({
        name: draftGroupName,
        scope_kind: draftScopeKind,
        scope_name: draftScopeName,
        permissions: draftPermissions,
      })

      setGroups(response.groups)
      clearGroupForm()
    } catch (err) {
      setSubmitError(
        err instanceof Error ? err.message : 'Failed to save group',
      )
    }
  }

  async function handleDeleteGroup(groupName: string) {
    setSubmitError(null)

    try {
      const response = await deleteGroup(groupName)
      setGroups(response.groups)
      if (editingGroupName === groupName) {
        clearGroupForm()
      }
    } catch (err) {
      setSubmitError(
        err instanceof Error ? err.message : 'Failed to delete group',
      )
    }
  }

  return (
    <div className="groups-panel">
      <div className="groups-list">
        <p className="config-hint">Configured groups</p>
        {isLoadingGroups ? (
          <p className="config-empty">Loading…</p>
        ) : groupsLoadError ? (
          <p className="config-empty" role="alert">
            {groupsLoadError}
          </p>
        ) : groups.length === 0 ? (
          <p className="config-empty">No groups configured yet.</p>
        ) : (
          <ul className="groups-ul">
            {groups.map((group) => {
              const isAdministrators = group.name === ADMINISTRATORS_GROUP_NAME
              const isSelected = editingGroupName === group.name
              return (
                <li
                  key={group.name}
                  className={
                    isSelected ? 'groups-li groups-li-selected' : 'groups-li'
                  }
                >
                  <div className="groups-li-content">
                    <div className="groups-li-title">{group.name}</div>
                    <div className="groups-li-meta">
                      Scope: {group.scope_kind}: {group.scope_name}
                    </div>
                    <div className="groups-li-meta">
                      Permissions: {group.permissions.join(', ')}
                    </div>
                  </div>
                  <div className="groups-li-actions">
                    <button
                      type="button"
                      className="groups-edit"
                      onClick={() => handleEditGroup(group)}
                    >
                      Edit
                    </button>
                    <button
                      type="button"
                      className="groups-delete"
                      disabled={isAdministrators}
                      title={
                        isAdministrators
                          ? 'Administrators group cannot be deleted'
                          : undefined
                      }
                      onClick={() => void handleDeleteGroup(group.name)}
                    >
                      Delete
                    </button>
                  </div>
                </li>
              )
            })}
          </ul>
        )}
      </div>

      <div className="groups-form">
        <p className="config-hint">
          {isEditing ? 'Edit group' : 'Create group'}
        </p>

        {submitError ? (
          <p className="config-empty groups-error" role="alert">
            {submitError}
          </p>
        ) : null}

        <div className="config-field">
          <label htmlFor="group-name">Group name</label>
          <input
            id="group-name"
            className="config-input"
            value={draftGroupName}
            readOnly={isEditing}
            onChange={(event) => {
              setDraftGroupName(event.target.value)
            }}
          />
        </div>

        <div className="config-field">
          <label htmlFor="scope-kind">Scope kind</label>
          <select
            id="scope-kind"
            className="theme-select"
            value={draftScopeKind}
            onChange={(event) => {
              setDraftScopeKind(event.target.value as GroupScopeKind)
            }}
          >
            <option value="node">Node</option>
            <option value="group">Group</option>
          </select>
        </div>

        <div className="config-field">
          <label htmlFor="scope-name">Scope name</label>
          <input
            id="scope-name"
            className="config-input"
            value={draftScopeName}
            onChange={(event) => {
              setDraftScopeName(event.target.value)
            }}
          />
        </div>

        <div className="config-field">
          <label>Permissions</label>
          <div className="permission-checkboxes">
            {DEFAULT_PERMISSIONS.map((permission) => {
              const checked = draftPermissions.includes(permission)
              return (
                <label key={permission} className="permission-option">
                  <input
                    type="checkbox"
                    aria-label={permission}
                    checked={checked}
                    onChange={(event) => {
                      togglePermission(permission, event.target.checked)
                    }}
                  />
                  <code>{permission}</code>
                </label>
              )
            })}
          </div>
        </div>

        <div className="groups-form-actions">
          {isEditing ? (
            <button
              type="button"
              className="groups-new"
              onClick={() => clearGroupForm()}
            >
              New group
            </button>
          ) : null}
          <button
            type="button"
            className="groups-save"
            onClick={() => void handleSaveGroup()}
          >
            {isEditing ? 'Save changes' : 'Save group'}
          </button>
        </div>
      </div>
    </div>
  )
}

function NetworkConfigTab() {
  const [draft, setDraft] = useState<NetworkSettings>(DEFAULT_NETWORK_SETTINGS)
  const [rulesText, setRulesText] = useState('')
  const [isLoading, setIsLoading] = useState(true)
  const [loadError, setLoadError] = useState<string | null>(null)
  const [submitError, setSubmitError] = useState<string | null>(null)
  const [saveSucceeded, setSaveSucceeded] = useState(false)

  useEffect(() => {
    let isCancelled = false

    async function loadNetwork() {
      setIsLoading(true)
      setLoadError(null)
      try {
        const response = await fetchNetwork()
        if (!isCancelled) {
          setDraft(response.network)
          setRulesText(response.network.rules.join('\n'))
        }
      } catch (err) {
        if (!isCancelled) {
          setLoadError(
            err instanceof Error
              ? err.message
              : 'Failed to load network settings',
          )
        }
      } finally {
        if (!isCancelled) {
          setIsLoading(false)
        }
      }
    }

    void loadNetwork()

    return () => {
      isCancelled = true
    }
  }, [])

  async function handleSave() {
    setSubmitError(null)
    setSaveSucceeded(false)

    const listenError = validateListenAddress(draft.listen_address)
    if (listenError) {
      setSubmitError(listenError)
      return
    }
    const hostnameError = validateHostname(draft.public_hostname)
    if (hostnameError) {
      setSubmitError(hostnameError)
      return
    }

    const rules = rulesText
      .split('\n')
      .map((line) => line.trim())
      .filter((line) => line.length > 0)

    try {
      const response = await saveNetwork({ ...draft, rules })
      setDraft(response.network)
      setRulesText(response.network.rules.join('\n'))
      setSaveSucceeded(true)
    } catch (err) {
      setSubmitError(
        err instanceof Error ? err.message : 'Failed to save network settings',
      )
    }
  }

  if (isLoading) {
    return <p className="config-empty">Loading…</p>
  }

  if (loadError) {
    return (
      <p className="config-empty" role="alert">
        {loadError}
      </p>
    )
  }

  return (
    <div className="security-panel network-panel">
      <p className="config-hint">
        Listen bind address and IP access control for the admin UI.
      </p>

      {submitError ? (
        <p className="config-empty groups-error" role="alert">
          {submitError}
        </p>
      ) : null}
      {saveSucceeded ? (
        <p className="config-hint" role="status">
          Network settings saved.
        </p>
      ) : null}

      <div className="config-field">
        <label htmlFor="network-listen">Listen address</label>
        <input
          id="network-listen"
          className="config-input"
          value={draft.listen_address}
          onChange={(event) => {
            setDraft((previous) => ({
              ...previous,
              listen_address: event.target.value,
            }))
            setSaveSucceeded(false)
          }}
        />
        <p className="config-hint">
          Default 0.0.0.0 (all interfaces). Applied on next process restart.
        </p>
      </div>

      <div className="config-field">
        <label htmlFor="network-hostname">Public hostname</label>
        <input
          id="network-hostname"
          className="config-input"
          placeholder="admin.mydomain.ext"
          value={draft.public_hostname}
          onChange={(event) => {
            setDraft((previous) => ({
              ...previous,
              public_hostname: event.target.value,
            }))
            setSaveSucceeded(false)
          }}
        />
        <p className="config-hint">
          Leave empty for local access by IP. When set, the UI must be visited
          via that exact hostname.
        </p>
      </div>

      <div className="config-field">
        <label htmlFor="network-access-mode">Access mode</label>
        <select
          id="network-access-mode"
          className="theme-select"
          value={draft.access_mode}
          onChange={(event) => {
            setDraft((previous) => ({
              ...previous,
              access_mode: event.target.value as AccessMode,
            }))
            setSaveSucceeded(false)
          }}
        >
          <option value="open">Open (no IP filter)</option>
          <option value="whitelist">Whitelist (deny non-matching)</option>
          <option value="blacklist">Blacklist (deny matching)</option>
        </select>
      </div>

      {draft.access_mode !== 'open' ? (
        <div className="config-field">
          <label htmlFor="network-rules">IP / CIDR rules (one per line)</label>
          <textarea
            id="network-rules"
            className="config-input wizard-rules"
            rows={5}
            value={rulesText}
            onChange={(event) => {
              setRulesText(event.target.value)
              setSaveSucceeded(false)
            }}
          />
        </div>
      ) : null}

      <button
        type="button"
        className="groups-save"
        onClick={() => void handleSave()}
      >
        Save network settings
      </button>
    </div>
  )
}

function validateSecurityDraft(security: SecuritySettings): string | null {
  if (
    !Number.isInteger(security.idle_timeout_minutes) ||
    security.idle_timeout_minutes < 1 ||
    security.idle_timeout_minutes > 240
  ) {
    return 'Idle timeout must be an integer between 1 and 240 minutes.'
  }
  if (
    !Number.isInteger(security.session_lifetime_hours) ||
    security.session_lifetime_hours < 1
  ) {
    return 'Session lifetime must be an integer of at least 1 hour.'
  }
  if (
    !Number.isInteger(security.reauth_grace_minutes) ||
    security.reauth_grace_minutes < 0
  ) {
    return 'Re-auth grace period must be an integer of at least 0 minutes.'
  }
  return null
}

function parsePositiveIntegerInput(raw: string): number | null {
  if (raw.trim() === '') {
    return null
  }
  const parsed = Number(raw)
  if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) {
    return null
  }
  return parsed
}

function SecurityConfigTab() {
  const [draft, setDraft] = useState<SecuritySettings>(DEFAULT_SECURITY_SETTINGS)
  const [isLoading, setIsLoading] = useState(true)
  const [loadError, setLoadError] = useState<string | null>(null)
  const [submitError, setSubmitError] = useState<string | null>(null)
  const [saveSucceeded, setSaveSucceeded] = useState(false)

  useEffect(() => {
    let isCancelled = false

    async function loadSecurity() {
      setIsLoading(true)
      setLoadError(null)
      try {
        const response = await fetchSecurity()
        if (!isCancelled) {
          setDraft(response.security)
        }
      } catch (err) {
        if (!isCancelled) {
          setLoadError(
            err instanceof Error ? err.message : 'Failed to load security settings',
          )
        }
      } finally {
        if (!isCancelled) {
          setIsLoading(false)
        }
      }
    }

    void loadSecurity()

    return () => {
      isCancelled = true
    }
  }, [])

  async function handleSave() {
    setSubmitError(null)
    setSaveSucceeded(false)

    const validationError = validateSecurityDraft(draft)
    if (validationError) {
      setSubmitError(validationError)
      return
    }

    try {
      const response = await saveSecurity(draft)
      setDraft(response.security)
      setSaveSucceeded(true)
    } catch (err) {
      setSubmitError(
        err instanceof Error ? err.message : 'Failed to save security settings',
      )
    }
  }

  if (isLoading) {
    return <p className="config-empty">Loading…</p>
  }

  if (loadError) {
    return (
      <p className="config-empty" role="alert">
        {loadError}
      </p>
    )
  }

  return (
    <div className="security-panel">
      <p className="config-hint">
        Session and re-authentication policy for administrators.
      </p>

      {submitError ? (
        <p className="config-empty groups-error" role="alert">
          {submitError}
        </p>
      ) : null}
      {saveSucceeded ? (
        <p className="config-hint" role="status">
          Security settings saved.
        </p>
      ) : null}

      <div className="config-field">
        <label htmlFor="idle-timeout">Idle timeout (minutes)</label>
        <input
          id="idle-timeout"
          className="config-input"
          type="number"
          min={1}
          max={240}
          step={1}
          value={draft.idle_timeout_minutes}
          onChange={(event) => {
            const parsed = parsePositiveIntegerInput(event.target.value)
            if (parsed === null) {
              setDraft((prev) => ({ ...prev, idle_timeout_minutes: 0 }))
              return
            }
            setDraft((prev) => ({ ...prev, idle_timeout_minutes: parsed }))
            setSaveSucceeded(false)
          }}
        />
        <p className="config-hint">1–240 minutes (max 4 hours). Default 30.</p>
      </div>

      <div className="config-field">
        <label htmlFor="session-lifetime">Session lifetime (hours)</label>
        <input
          id="session-lifetime"
          className="config-input"
          type="number"
          min={1}
          step={1}
          value={draft.session_lifetime_hours}
          onChange={(event) => {
            const parsed = parsePositiveIntegerInput(event.target.value)
            if (parsed === null) {
              setDraft((prev) => ({ ...prev, session_lifetime_hours: 0 }))
              return
            }
            setDraft((prev) => ({ ...prev, session_lifetime_hours: parsed }))
            setSaveSucceeded(false)
          }}
        />
        <p className="config-hint">At least 1 hour. Default 24.</p>
      </div>

      <div className="config-field">
        <label className="permission-option" htmlFor="totp-enabled">
          <input
            id="totp-enabled"
            type="checkbox"
            checked={draft.totp_enabled}
            onChange={(event) => {
              setDraft((prev) => ({
                ...prev,
                totp_enabled: event.target.checked,
              }))
              setSaveSucceeded(false)
            }}
          />
          Enable TOTP
        </label>
        <p className="config-hint">
          When enabled, users can enroll authenticator apps from their profile.
        </p>
      </div>

      <div className="config-field">
        <label className="permission-option" htmlFor="reauth-sensitive">
          <input
            id="reauth-sensitive"
            type="checkbox"
            checked={draft.reauth_sensitive_actions}
            onChange={(event) => {
              setDraft((prev) => ({
                ...prev,
                reauth_sensitive_actions: event.target.checked,
              }))
              setSaveSucceeded(false)
            }}
          />
          Re-auth for sensitive actions
        </label>
      </div>

      <div className="config-field">
        <label htmlFor="reauth-grace">Re-auth grace period (minutes)</label>
        <input
          id="reauth-grace"
          className="config-input"
          type="number"
          min={0}
          step={1}
          disabled={!draft.reauth_sensitive_actions}
          value={draft.reauth_grace_minutes}
          onChange={(event) => {
            const raw = event.target.value
            if (raw.trim() === '') {
              setDraft((prev) => ({ ...prev, reauth_grace_minutes: 0 }))
              return
            }
            const parsed = Number(raw)
            if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) {
              return
            }
            setDraft((prev) => ({ ...prev, reauth_grace_minutes: parsed }))
            setSaveSucceeded(false)
          }}
        />
        <p className="config-hint">
          Skip re-auth if done within this many minutes. 0 means re-auth for
          every sensitive action. Default 15.
        </p>
      </div>

      <button
        type="button"
        className="groups-save"
        onClick={() => void handleSave()}
      >
        Save security settings
      </button>

      <p className="config-hint">
        Service SSH keys and related credentials will also live here later —
        add keys with a description and an expiry time limit or non-expiry.
      </p>
    </div>
  )
}

function ConfigTabPanelContent({
  activeConfigTab,
  draftThemePreference,
  onDraftThemePreferenceChange,
}: {
  activeConfigTab: ConfigTabId
  draftThemePreference: ThemePreference
  onDraftThemePreferenceChange: (preference: ThemePreference) => void
}) {
  if (activeConfigTab === 'overview') {
    return (
      <OverviewConfigTab
        draftThemePreference={draftThemePreference}
        onDraftThemePreferenceChange={onDraftThemePreferenceChange}
      />
    )
  }

  if (activeConfigTab === 'roles') {
    return (
      <div className="roles-panel">
        <p className="config-hint">
          Built-in permissions that roles can grant.
        </p>
        <ul className="permission-catalog">
          {DEFAULT_PERMISSIONS.map((permission) => (
            <li key={permission}>
              <code>{permission}</code>
            </li>
          ))}
        </ul>
      </div>
    )
  }

  if (activeConfigTab === 'groups') {
    return <GroupsConfigTab />
  }

  if (activeConfigTab === 'actions') {
    return <ActionsConfigTab />
  }

  if (activeConfigTab === 'users') {
    return <UsersConfigTab />
  }

  if (activeConfigTab === 'security') {
    return <SecurityConfigTab />
  }

  if (activeConfigTab === 'network') {
    return <NetworkConfigTab />
  }

  return <p className="config-empty">Coming soon</p>
}

function resourceTabsFor(section: ResourceSectionId): ReadonlyArray<{
  id: ResourceTabId
  label: string
}> {
  return [
    { id: 'overview', label: 'Overview' },
    { id: 'security', label: 'Security' },
    { id: 'add', label: RESOURCE_ADD_LABEL[section] },
  ]
}

function healthDotClass(healthOk: boolean | null | undefined): string {
  if (healthOk === true) {
    return 'node-health-dot node-health-dot-ok'
  }
  if (healthOk === false) {
    return 'node-health-dot node-health-dot-error'
  }
  return 'node-health-dot node-health-dot-unknown'
}

function healthDotLabel(healthOk: boolean | null | undefined): string {
  if (healthOk === true) {
    return 'Healthy'
  }
  if (healthOk === false) {
    return 'Unhealthy'
  }
  return 'Health unknown'
}

function ResourceOverviewTab({
  section,
  canRead,
  canExec,
  canDelete,
  onNodesChanged,
}: {
  section: ResourceSectionId
  canRead: boolean
  canExec: boolean
  canDelete: boolean
  onNodesChanged: () => void
}) {
  const [nodes, setNodes] = useState<ReadonlyArray<Node>>([])
  const [isLoading, setIsLoading] = useState(true)
  const [loadError, setLoadError] = useState<string | null>(null)
  const [checkingNodeId, setCheckingNodeId] = useState<string | null>(null)
  const [me, setMe] = useState<MeResponse | null>(null)
  const [pendingDeleteNodeId, setPendingDeleteNodeId] = useState<string | null>(
    null,
  )
  const requireTotp = Boolean(me?.totp_confirmed && me?.totp_enabled)

  useEffect(() => {
    let isCancelled = false

    async function loadMe() {
      try {
        const response = await fetchMe()
        if (!isCancelled) {
          setMe(response)
        }
      } catch {
        if (!isCancelled) {
          setMe(null)
        }
      }
    }

    void loadMe()

    return () => {
      isCancelled = true
    }
  }, [])

  useEffect(() => {
    if (!canRead) {
      setIsLoading(false)
      setNodes([])
      return
    }

    let isCancelled = false

    async function loadNodes() {
      setIsLoading(true)
      setLoadError(null)
      try {
        const response = await fetchNodes(RESOURCE_SECTION_KIND[section])
        if (!isCancelled) {
          setNodes(response.nodes)
        }
      } catch (err) {
        if (!isCancelled) {
          setLoadError(
            err instanceof Error ? err.message : 'Failed to load nodes',
          )
        }
      } finally {
        if (!isCancelled) {
          setIsLoading(false)
        }
      }
    }

    void loadNodes()

    return () => {
      isCancelled = true
    }
  }, [section, canRead])

  async function handleHealthCheck(nodeId: string) {
    setCheckingNodeId(nodeId)
    try {
      const response = await runNodeHealthCheck(nodeId)
      setNodes((previous) =>
        previous.map((node) => (node.id === nodeId ? response.node : node)),
      )
    } catch (err) {
      setNodes((previous) =>
        previous.map((node) => {
          if (node.id !== nodeId) {
            return node
          }
          return {
            ...node,
            health_ok: false,
            health_message:
              err instanceof Error ? err.message : 'Health check failed',
          }
        }),
      )
    } finally {
      setCheckingNodeId(null)
      onNodesChanged()
    }
  }

  async function handleDeleteConfirm(credentials: ReauthCredentials) {
    if (!pendingDeleteNodeId) {
      return
    }
    const response = await deleteNode(pendingDeleteNodeId, credentials)
    setNodes(
      response.nodes.filter(
        (node) => node.kind === RESOURCE_SECTION_KIND[section],
      ),
    )
    setPendingDeleteNodeId(null)
    onNodesChanged()
  }

  if (!canRead) {
    return (
      <p className="config-hint">
        You need the <code>nodes.read</code> permission to view managed hosts.
      </p>
    )
  }

  if (isLoading) {
    return <p className="config-hint">Loading…</p>
  }

  if (loadError) {
    return <p className="config-error">{loadError}</p>
  }

  if (nodes.length === 0) {
    return (
      <p className="config-hint">
        No hosts registered yet. Use the {RESOURCE_ADD_LABEL[section]} tab to
        add one.
      </p>
    )
  }

  return (
    <>
      {pendingDeleteNodeId ? (
        <ReauthModal
          title="Confirm delete"
          hint="Re-enter your password to delete this host."
          requireTotp={requireTotp}
          confirmLabel="Delete host"
          onCancel={() => {
            setPendingDeleteNodeId(null)
          }}
          onConfirm={handleDeleteConfirm}
        />
      ) : null}
      <ul className="node-list">
        {nodes.map((node) => {
          return (
            <li key={node.id} className="node-list-item">
              <div className="node-list-heading">
                <div className="node-list-heading-main">
                  <span
                    className={healthDotClass(node.health_ok)}
                    title={healthDotLabel(node.health_ok)}
                    aria-label={healthDotLabel(node.health_ok)}
                    role="img"
                  />
                  <strong>{node.name}</strong>
                  <code className="node-id">{node.id}</code>
                </div>
                <button
                  type="button"
                  className="node-edit"
                  onClick={() => {
                    navigateTo(
                      pathFor(
                        section,
                        'overview',
                        'overview',
                        node.id,
                        'healthcheck',
                      ),
                    )
                  }}
                >
                  Edit
                </button>
              </div>
              <p className="config-hint">
                {node.host_ip} · {node.username} · group {node.group_name} ·{' '}
                {node.key_algo}
              </p>
              {node.health_message ? (
                <p
                  className={
                    node.health_ok
                      ? 'node-test-result node-test-result-ok'
                      : node.health_ok === false
                        ? 'node-test-result node-test-result-fail'
                        : 'node-test-result'
                  }
                  role="status"
                >
                  {node.health_message}
                  {node.health_last_checked_at
                    ? ` · ${new Date(node.health_last_checked_at).toLocaleString()}`
                    : ''}
                </p>
              ) : null}
              {canExec || canDelete ? (
                <div className="node-actions">
                  {canExec ? (
                    <button
                      type="button"
                      className="node-test-ssh"
                      disabled={checkingNodeId === node.id}
                      onClick={() => {
                        void handleHealthCheck(node.id)
                      }}
                    >
                      {checkingNodeId === node.id
                        ? 'Checking health…'
                        : 'Health check'}
                    </button>
                  ) : null}
                  {canDelete ? (
                    <button
                      type="button"
                      className="node-delete"
                      onClick={() => {
                        setPendingDeleteNodeId(node.id)
                      }}
                    >
                      Delete
                    </button>
                  ) : null}
                </div>
              ) : null}
            </li>
          )
        })}
      </ul>
    </>
  )
}

function ResourceSecurityTab({ section }: { section: ResourceSectionId }) {
  return (
    <div className="resource-security-panel">
      <p className="config-hint">
        Security controls for {SECTION_TITLES[section].toLowerCase()} hosts
        (key rotation, hardening, and access policy) will live here.
      </p>
    </div>
  )
}

function ResourceAddTab({
  section,
  canExec,
  onCreated,
}: {
  section: ResourceSectionId
  canExec: boolean
  onCreated: () => void
}) {
  const [nodeId, setNodeId] = useState(() => crypto.randomUUID())
  const [name, setName] = useState('')
  const [hostIP, setHostIP] = useState('')
  const [username, setUsername] = useState('clustercanvas')
  const [keyMode, setKeyMode] = useState<'generate' | 'paste'>('generate')
  const [algorithm, setAlgorithm] = useState<'ed25519' | 'rsa'>('ed25519')
  const [rsaBits, setRsaBits] = useState(4096)
  const [kdfRounds, setKdfRounds] = useState(100)
  const [privateKey, setPrivateKey] = useState('')
  const [groupMode, setGroupMode] = useState<'existing' | 'new'>('existing')
  const [groupName, setGroupName] = useState('')
  const [newGroupName, setNewGroupName] = useState('')
  const [groups, setGroups] = useState<ReadonlyArray<Group>>([])
  const [createdPublicKey, setCreatedPublicKey] = useState<string | null>(null)
  const [submitError, setSubmitError] = useState<string | null>(null)
  const [isSubmitting, setIsSubmitting] = useState(false)

  useEffect(() => {
    let isCancelled = false

    async function loadGroups() {
      try {
        const response = await fetchGroups()
        if (!isCancelled) {
          setGroups(response.groups)
          setGroupName((current) =>
            current === '' && response.groups.length > 0
              ? response.groups[0].name
              : current,
          )
        }
      } catch {
        // Group list is best-effort for the form.
      }
    }

    void loadGroups()

    return () => {
      isCancelled = true
    }
  }, [])

  if (!canExec) {
    return (
      <p className="config-hint">
        You need the <code>nodes.exec</code> permission to register a host.
      </p>
    )
  }

  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()
    setSubmitError(null)
    setIsSubmitting(true)
    try {
      const response = await createNode({
        id: nodeId,
        kind: RESOURCE_SECTION_KIND[section],
        name: name.trim(),
        host_ip: hostIP.trim(),
        username: username.trim() || 'clustercanvas',
        ...(groupMode === 'new'
          ? { new_group: { name: newGroupName.trim() } }
          : { group_name: groupName }),
        ...(keyMode === 'generate'
          ? {
              generate: {
                algorithm,
                ...(algorithm === 'rsa' ? { rsa_bits: rsaBits } : {}),
                ...(algorithm === 'ed25519' ? { kdf_rounds: kdfRounds } : {}),
              },
            }
          : { private_key: privateKey }),
      })
      setCreatedPublicKey(response.node.public_key)
      setName('')
      setHostIP('')
      setPrivateKey('')
      setNodeId(crypto.randomUUID())
      onCreated()
    } catch (err) {
      setSubmitError(err instanceof Error ? err.message : 'Failed to create node')
    } finally {
      setIsSubmitting(false)
    }
  }

  return (
    <form className="resource-add-form" onSubmit={handleSubmit}>
      <label className="field">
        <span>Node UUID</span>
        <input
          type="text"
          className="readonly-field"
          value={nodeId}
          readOnly
          aria-readonly="true"
        />
      </label>

      <label className="field">
        <span>Name</span>
        <input
          type="text"
          value={name}
          onChange={(event) => setName(event.target.value)}
          required
          autoComplete="off"
        />
      </label>

      <label className="field">
        <span>Host IP</span>
        <input
          type="text"
          value={hostIP}
          onChange={(event) => setHostIP(event.target.value)}
          required
          autoComplete="off"
          placeholder="192.168.1.10"
        />
      </label>

      <label className="field">
        <span>SSH username</span>
        <input
          type="text"
          value={username}
          onChange={(event) => setUsername(event.target.value)}
          required
          autoComplete="off"
        />
      </label>

      <fieldset className="field-group">
        <legend>SSH key</legend>
        <div className="radio-row">
          <label>
            <input
              type="radio"
              name="key-mode"
              checked={keyMode === 'generate'}
              onChange={() => setKeyMode('generate')}
            />
            Generate
          </label>
          <label>
            <input
              type="radio"
              name="key-mode"
              checked={keyMode === 'paste'}
              onChange={() => setKeyMode('paste')}
            />
            Add existing
          </label>
        </div>

        {keyMode === 'generate' ? (
          <>
            <div className="radio-row">
              <label>
                <input
                  type="radio"
                  name="key-algo"
                  checked={algorithm === 'ed25519'}
                  onChange={() => setAlgorithm('ed25519')}
                />
                Ed25519 (Recommended)
              </label>
              <label>
                <input
                  type="radio"
                  name="key-algo"
                  checked={algorithm === 'rsa'}
                  onChange={() => setAlgorithm('rsa')}
                />
                RSA (Compatibility)
              </label>
            </div>
            {algorithm === 'rsa' ? (
              <label className="field">
                <span>RSA bits</span>
                <select
                  value={rsaBits}
                  onChange={(event) => setRsaBits(Number(event.target.value))}
                >
                  <option value={2048}>2048</option>
                  <option value={3072}>3072</option>
                  <option value={4096}>4096</option>
                </select>
              </label>
            ) : (
              <label className="field">
                <span>KDF rounds</span>
                <input
                  type="number"
                  min={1}
                  value={kdfRounds}
                  onChange={(event) =>
                    setKdfRounds(Number(event.target.value) || 100)
                  }
                />
              </label>
            )}
          </>
        ) : (
          <label className="field">
            <span>Private key (OpenSSH / PEM)</span>
            <textarea
              value={privateKey}
              onChange={(event) => setPrivateKey(event.target.value)}
              rows={8}
              required={keyMode === 'paste'}
              spellCheck={false}
            />
          </label>
        )}
      </fieldset>

      <fieldset className="field-group">
        <legend>Scope / group</legend>
        <div className="radio-row">
          <label>
            <input
              type="radio"
              name="group-mode"
              checked={groupMode === 'existing'}
              onChange={() => setGroupMode('existing')}
            />
            Existing group
          </label>
          <label>
            <input
              type="radio"
              name="group-mode"
              checked={groupMode === 'new'}
              onChange={() => setGroupMode('new')}
            />
            Create a new group
          </label>
        </div>
        {groupMode === 'existing' ? (
          <label className="field">
            <span>Group</span>
            <select
              value={groupName}
              onChange={(event) => setGroupName(event.target.value)}
              required
            >
              {groups.map((group) => (
                <option key={group.name} value={group.name}>
                  {group.name}
                </option>
              ))}
            </select>
          </label>
        ) : (
          <label className="field">
            <span>New group name</span>
            <input
              type="text"
              value={newGroupName}
              onChange={(event) => setNewGroupName(event.target.value)}
              required
              autoComplete="off"
            />
            <span className="config-hint">
              Scoped to this node UUID. Set permissions later under
              Configuration → Groups.
            </span>
          </label>
        )}
      </fieldset>

      {submitError ? <p className="config-error">{submitError}</p> : null}
      {createdPublicKey ? (
        <div className="created-public-key">
          <p className="config-hint">
            Host registered. Install this public key on the remote account:
          </p>
          <textarea readOnly value={createdPublicKey} rows={3} />
        </div>
      ) : null}

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Saving…' : 'Register host'}
      </button>
    </form>
  )
}

function ResourceSectionPanel({
  section,
  activeResourceTab,
  onResourceTabChange,
  nodeId,
  nodeDetailTab,
  onNodesChanged,
}: {
  section: ResourceSectionId
  activeResourceTab: ResourceTabId
  onResourceTabChange: (tab: ResourceTabId) => void
  nodeId: string | null
  nodeDetailTab: NodeDetailTabId
  onNodesChanged: () => void
}) {
  const [me, setMe] = useState<MeResponse | null>(null)
  const [overviewKey, setOverviewKey] = useState(0)

  useEffect(() => {
    let isCancelled = false

    async function loadMe() {
      try {
        const response = await fetchMe()
        if (!isCancelled) {
          setMe(response)
        }
      } catch {
        if (!isCancelled) {
          setMe(null)
        }
      }
    }

    void loadMe()

    return () => {
      isCancelled = true
    }
  }, [])

  const canRead = Boolean(me?.permissions?.includes('nodes.read'))
  const canExec = Boolean(me?.permissions?.includes('nodes.exec'))
  const canDelete = Boolean(me?.permissions?.includes('nodes.delete'))
  const tabs = resourceTabsFor(section)

  if (nodeId) {
    return (
      <NodeDetailPanel
        section={section}
        nodeId={nodeId}
        activeTab={nodeDetailTab}
        onNodesChanged={onNodesChanged}
      />
    )
  }

  return (
    <div className="config-panel">
      <div
        className="config-tabs"
        role="tablist"
        aria-label={SECTION_TITLES[section]}
      >
        {tabs.map((tab) => {
          const isSelected = activeResourceTab === tab.id
          return (
            <button
              key={tab.id}
              type="button"
              role="tab"
              id={`resource-tab-${section}-${tab.id}`}
              className={
                isSelected ? 'config-tab config-tab-active' : 'config-tab'
              }
              aria-selected={isSelected}
              aria-controls={`resource-tab-panel-${section}`}
              tabIndex={isSelected ? 0 : -1}
              onClick={() => onResourceTabChange(tab.id)}
            >
              {tab.label}
            </button>
          )
        })}
      </div>

      <div
        className="config-tab-panel"
        role="tabpanel"
        id={`resource-tab-panel-${section}`}
        aria-labelledby={`resource-tab-${section}-${activeResourceTab}`}
      >
        {activeResourceTab === 'overview' ? (
          <ResourceOverviewTab
            key={overviewKey}
            section={section}
            canRead={canRead}
            canExec={canExec}
            canDelete={canDelete}
            onNodesChanged={onNodesChanged}
          />
        ) : null}
        {activeResourceTab === 'security' ? (
          <ResourceSecurityTab section={section} />
        ) : null}
        {activeResourceTab === 'add' ? (
          <ResourceAddTab
            section={section}
            canExec={canExec}
            onCreated={() => setOverviewKey((value) => value + 1)}
          />
        ) : null}
      </div>
    </div>
  )
}

function LogsPanel() {
  type LogsSection = 'nodes' | 'auth'
  type KindFilter = 'all' | NodeKind
  type NodeActionFilter = 'all' | NodeAuditAction

  const [logsSection, setLogsSection] = useState<LogsSection>('nodes')
  const [kindFilter, setKindFilter] = useState<KindFilter>('all')
  const [nodeActionFilter, setNodeActionFilter] =
    useState<NodeActionFilter>('all')
  const [authOutcomeFilter, setAuthOutcomeFilter] =
    useState<AuthOutcomeFilter>('all')
  const [authCategoryFilter, setAuthCategoryFilter] =
    useState<AuthCategoryFilter>('all')
  const [authActionFilter, setAuthActionFilter] =
    useState<AuthActionQuickFilter>('all')
  const [searchQuery, setSearchQuery] = useState('')
  const [datePreset, setDatePreset] = useState<DateRangePreset>('all')
  const [customDateFrom, setCustomDateFrom] = useState('')
  const [customDateTo, setCustomDateTo] = useState('')
  const [pageSize, setPageSize] = useState<LogsPageSize>(() =>
    readLogsPageSize(),
  )
  const [filtersExpanded, setFiltersExpanded] = useState(false)
  const [page, setPage] = useState(1)
  const [nodeSortKey, setNodeSortKey] = useState<NodeLogsSortKey>('at')
  const [authSortKey, setAuthSortKey] = useState<AuthLogsSortKey>('at')
  const [sortDirection, setSortDirection] = useState<SortDirection>('desc')
  const [nodeEvents, setNodeEvents] = useState<ReadonlyArray<NodeAuditEvent>>(
    [],
  )
  const [authEvents, setAuthEvents] = useState<ReadonlyArray<AuthAuditEvent>>(
    [],
  )
  const [isLoading, setIsLoading] = useState(true)
  const [loadError, setLoadError] = useState<string | null>(null)
  const [canReadLogs, setCanReadLogs] = useState(false)
  const [permissionsReady, setPermissionsReady] = useState(false)

  useEffect(() => {
    let isCancelled = false

    async function loadPermissions() {
      try {
        const me = await fetchMe()
        if (isCancelled) {
          return
        }
        setCanReadLogs(Boolean(me?.permissions?.includes('logs.read')))
      } catch {
        if (!isCancelled) {
          setCanReadLogs(false)
        }
      } finally {
        if (!isCancelled) {
          setPermissionsReady(true)
        }
      }
    }

    void loadPermissions()

    return () => {
      isCancelled = true
    }
  }, [])

  useEffect(() => {
    if (!permissionsReady || !canReadLogs) {
      return
    }

    let isCancelled = false

    async function load() {
      setIsLoading(true)
      setLoadError(null)
      try {
        if (logsSection === 'nodes') {
          const response = await fetchNodeLogs()
          if (!isCancelled) {
            setNodeEvents(response.events)
          }
          return
        }

        const response = await fetchAuthLogs()
        if (!isCancelled) {
          setAuthEvents(response.events)
        }
      } catch (err) {
        if (!isCancelled) {
          setLoadError(
            err instanceof Error ? err.message : 'Failed to load logs',
          )
        }
      } finally {
        if (!isCancelled) {
          setIsLoading(false)
        }
      }
    }

    void load()

    return () => {
      isCancelled = true
    }
  }, [logsSection, canReadLogs, permissionsReady])

  useEffect(() => {
    setPage(1)
  }, [
    logsSection,
    kindFilter,
    nodeActionFilter,
    authOutcomeFilter,
    authCategoryFilter,
    authActionFilter,
    searchQuery,
    datePreset,
    customDateFrom,
    customDateTo,
    pageSize,
  ])

  const kindFilters: ReadonlyArray<{ id: KindFilter; label: string }> = [
    { id: 'all', label: 'All' },
    { id: 'container', label: 'Containers' },
    { id: 'docker', label: 'Docker' },
    { id: 'vm', label: 'Virtual Machines' },
  ]

  const nodeActionFilters: ReadonlyArray<{
    id: NodeActionFilter
    label: string
  }> = [
    { id: 'all', label: 'All actions' },
    { id: 'create', label: 'create' },
    { id: 'update', label: 'update' },
    { id: 'delete', label: 'delete' },
    { id: 'test_ssh', label: 'test_ssh' },
    { id: 'health_check', label: 'health_check' },
  ]

  const authOutcomeFilters: ReadonlyArray<{
    id: AuthOutcomeFilter
    label: string
  }> = [
    { id: 'all', label: 'All outcomes' },
    { id: 'failure', label: 'Failures' },
    { id: 'success', label: 'Success' },
  ]

  const authCategoryFilters: ReadonlyArray<{
    id: AuthCategoryFilter
    label: string
  }> = [
    { id: 'all', label: 'All categories' },
    { id: 'auth', label: 'auth' },
    { id: 'user', label: 'user' },
    { id: 'group', label: 'group' },
    { id: 'self', label: 'self' },
  ]

  const authActionFilters: ReadonlyArray<{
    id: AuthActionQuickFilter
    label: string
  }> = [
    { id: 'all', label: 'All events' },
    { id: 'login_failures', label: 'Login failures' },
    { id: 'login', label: 'login' },
    { id: 'logout', label: 'logout' },
    { id: 'user_create', label: 'user_create' },
    { id: 'user_update', label: 'user_update' },
    { id: 'user_delete', label: 'user_delete' },
    { id: 'group_create', label: 'group_create' },
    { id: 'group_update', label: 'group_update' },
    { id: 'group_delete', label: 'group_delete' },
    { id: 'password_change', label: 'password_change' },
    { id: 'totp_enable', label: 'totp_enable' },
    { id: 'totp_disable', label: 'totp_disable' },
  ]

  const datePresets: ReadonlyArray<{ id: DateRangePreset; label: string }> = [
    { id: 'all', label: 'All time' },
    { id: 'today', label: 'Today' },
    { id: '7d', label: 'Last 7 days' },
    { id: '30d', label: 'Last 30 days' },
    { id: 'custom', label: 'Custom' },
  ]

  const { from: dateFrom, to: dateTo } = resolveDateRangeBounds(
    datePreset,
    customDateFrom,
    customDateTo,
  )

  const filteredNodeEvents = sortNodeEvents(
    filterNodeEvents(nodeEvents, {
      search: searchQuery,
      kind: kindFilter,
      action: nodeActionFilter,
      dateFrom,
      dateTo,
    }),
    nodeSortKey,
    sortDirection,
  )

  const filteredAuthEvents = sortAuthEvents(
    filterAuthEvents(authEvents, {
      search: searchQuery,
      outcome: authOutcomeFilter,
      category: authCategoryFilter,
      actionFilter: authActionFilter,
      dateFrom,
      dateTo,
    }),
    authSortKey,
    sortDirection,
  )

  const activeEvents =
    logsSection === 'nodes' ? filteredNodeEvents : filteredAuthEvents
  const nodePagination = paginateItems(filteredNodeEvents, page, pageSize)
  const authPagination = paginateItems(filteredAuthEvents, page, pageSize)
  const pagination =
    logsSection === 'nodes' ? nodePagination : authPagination
  const displayNodeEvents =
    logsSection === 'nodes' ? nodePagination.pageItems : []
  const displayAuthEvents =
    logsSection === 'auth' ? authPagination.pageItems : []

  function handlePageSizeChange(nextSize: LogsPageSize) {
    setPageSize(nextSize)
    writeLogsPageSize(nextSize)
  }

  function toggleNodeSort(key: NodeLogsSortKey) {
    if (nodeSortKey === key) {
      setSortDirection((current) => (current === 'asc' ? 'desc' : 'asc'))
      return
    }
    setNodeSortKey(key)
    setSortDirection(key === 'at' ? 'desc' : 'asc')
  }

  function toggleAuthSort(key: AuthLogsSortKey) {
    if (authSortKey === key) {
      setSortDirection((current) => (current === 'asc' ? 'desc' : 'asc'))
      return
    }
    setAuthSortKey(key)
    setSortDirection(key === 'at' ? 'desc' : 'asc')
  }

  function sortIndicator(active: boolean): string {
    if (!active) {
      return ''
    }
    return sortDirection === 'asc' ? ' ▲' : ' ▼'
  }

  function authRowClassName(event: AuthAuditEvent): string {
    if (event.outcome === 'failure') {
      return 'logs-row-failure'
    }
    if (event.category === 'self') {
      return 'logs-row-self'
    }
    if (event.outcome === 'success') {
      return 'logs-row-success'
    }
    return ''
  }

  function renderFilterChip<T extends string>(
    filters: ReadonlyArray<{ id: T; label: string }>,
    selected: T,
    onSelect: (id: T) => void,
    ariaLabel: string,
  ) {
    return (
      <div className="logs-filters" role="tablist" aria-label={ariaLabel}>
        {filters.map((filter) => {
          const isSelected = selected === filter.id
          return (
            <button
              key={filter.id}
              type="button"
              role="tab"
              className={
                isSelected ? 'config-tab config-tab-active' : 'config-tab'
              }
              aria-selected={isSelected}
              onClick={() => onSelect(filter.id)}
            >
              {filter.label}
            </button>
          )
        })}
      </div>
    )
  }

  if (!permissionsReady) {
    return (
      <div className="logs-panel">
        <p className="config-hint">Loading…</p>
      </div>
    )
  }

  if (!canReadLogs) {
    return (
      <p className="config-hint">
        You need the <code>logs.read</code> permission to view activity logs.
      </p>
    )
  }

  return (
    <div className="logs-panel">
      <div className="logs-filters" role="tablist" aria-label="Log section">
        {(
          [
            { id: 'nodes' as const, label: 'Nodes' },
            { id: 'auth' as const, label: 'Users & Auth' },
          ] as const
        ).map((section) => {
          const isSelected = logsSection === section.id
          return (
            <button
              key={section.id}
              type="button"
              role="tab"
              className={
                isSelected ? 'config-tab config-tab-active' : 'config-tab'
              }
              aria-selected={isSelected}
              onClick={() => {
                setLogsSection(section.id)
                setSortDirection('desc')
                if (section.id === 'nodes') {
                  setNodeSortKey('at')
                } else {
                  setAuthSortKey('at')
                }
              }}
            >
              {section.label}
            </button>
          )
        })}
      </div>

      <div className="logs-toolbar">
        <label className="logs-search-label" htmlFor="logs-search">
          Search
          <input
            id="logs-search"
            type="search"
            className="logs-search-input"
            placeholder="Node, user, group, event, detail…"
            value={searchQuery}
            onChange={(event) => setSearchQuery(event.target.value)}
          />
        </label>

        <label className="logs-page-size-label" htmlFor="logs-page-size">
          Rows
          <select
            id="logs-page-size"
            value={pageSize}
            onChange={(event) =>
              handlePageSizeChange(Number(event.target.value) as LogsPageSize)
            }
          >
            {LOGS_PAGE_SIZE_OPTIONS.map((size) => (
              <option key={size} value={size}>
                {size}
              </option>
            ))}
          </select>
        </label>

        <button
          type="button"
          className={
            filtersExpanded
              ? 'logs-filters-toggle logs-filters-toggle-open'
              : 'logs-filters-toggle'
          }
          aria-expanded={filtersExpanded}
          aria-controls="logs-filters-panel"
          onClick={() => setFiltersExpanded((current) => !current)}
        >
          <FiltersChevronIcon expanded={filtersExpanded} />
          {filtersExpanded ? 'Hide filters' : 'Filters…'}
        </button>
      </div>

      {filtersExpanded ? (
        <div
          id="logs-filters-panel"
          className="logs-filters-panel"
          role="region"
          aria-label="Log filters"
        >
          {renderFilterChip(
            datePresets,
            datePreset,
            setDatePreset,
            'Date range',
          )}

          {datePreset === 'custom' ? (
            <div className="logs-date-custom">
              <label htmlFor="logs-date-from">
                From
                <input
                  id="logs-date-from"
                  type="datetime-local"
                  value={customDateFrom}
                  onChange={(event) => setCustomDateFrom(event.target.value)}
                />
              </label>
              <label htmlFor="logs-date-to">
                To
                <input
                  id="logs-date-to"
                  type="datetime-local"
                  value={customDateTo}
                  onChange={(event) => setCustomDateTo(event.target.value)}
                />
              </label>
            </div>
          ) : null}

          {logsSection === 'nodes'
            ? renderFilterChip(
                kindFilters,
                kindFilter,
                setKindFilter,
                'Log kind filter',
              )
            : null}
          {logsSection === 'nodes'
            ? renderFilterChip(
                nodeActionFilters,
                nodeActionFilter,
                setNodeActionFilter,
                'Node action filter',
              )
            : null}

          {logsSection === 'auth'
            ? renderFilterChip(
                authOutcomeFilters,
                authOutcomeFilter,
                setAuthOutcomeFilter,
                'Auth outcome filter',
              )
            : null}
          {logsSection === 'auth'
            ? renderFilterChip(
                authCategoryFilters,
                authCategoryFilter,
                setAuthCategoryFilter,
                'Auth category filter',
              )
            : null}
          {logsSection === 'auth'
            ? renderFilterChip(
                authActionFilters,
                authActionFilter,
                setAuthActionFilter,
                'Auth event filter',
              )
            : null}
        </div>
      ) : null}

      {isLoading ? <p className="config-hint">Loading…</p> : null}
      {loadError ? <p className="config-error">{loadError}</p> : null}

      {!isLoading && !loadError && activeEvents.length === 0 ? (
        <p className="config-hint">
          {logsSection === 'nodes'
            ? 'No node activity matches the current filters.'
            : 'No user or auth activity matches the current filters.'}
        </p>
      ) : null}

      {!isLoading && displayNodeEvents.length > 0 ? (
        <div className="logs-table-scroll">
          <table className="logs-table">
            <thead>
              <tr>
                {(
                  [
                    { key: 'at' as const, label: 'When' },
                    { key: 'action' as const, label: 'Action' },
                    { key: 'actor' as const, label: 'Actor' },
                    { key: 'node' as const, label: 'Node' },
                    { key: 'kind' as const, label: 'Kind' },
                    { key: 'detail' as const, label: 'Detail' },
                  ] as const
                ).map((column) => (
                  <th key={column.key} scope="col">
                    <button
                      type="button"
                      className="logs-sort-button"
                      onClick={() => toggleNodeSort(column.key)}
                    >
                      {column.label}
                      {sortIndicator(nodeSortKey === column.key)}
                    </button>
                  </th>
                ))}
              </tr>
            </thead>
            <tbody>
              {displayNodeEvents.map((event) => (
                <tr key={event.id}>
                  <td>{formatUserTimestamp(event.at)}</td>
                  <td>
                    <code>{event.action}</code>
                  </td>
                  <td>{event.actor}</td>
                  <td>
                    <strong>{event.node_name}</strong>
                    <div>
                      <code className="node-id">{event.node_id}</code>
                    </div>
                  </td>
                  <td>{event.node_kind}</td>
                  <td>{event.detail || '—'}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      ) : null}

      {!isLoading && displayAuthEvents.length > 0 ? (
        <div className="logs-table-scroll">
          <table className="logs-table">
            <thead>
              <tr>
                {(
                  [
                    { key: 'at' as const, label: 'When' },
                    { key: 'action' as const, label: 'Action' },
                    { key: 'outcome' as const, label: 'Outcome' },
                    { key: 'category' as const, label: 'Category' },
                    { key: 'actor' as const, label: 'Actor' },
                    { key: 'target' as const, label: 'Target' },
                    { key: 'detail' as const, label: 'Detail' },
                  ] as const
                ).map((column) => (
                  <th key={column.key} scope="col">
                    <button
                      type="button"
                      className="logs-sort-button"
                      onClick={() => toggleAuthSort(column.key)}
                    >
                      {column.label}
                      {sortIndicator(authSortKey === column.key)}
                    </button>
                  </th>
                ))}
              </tr>
            </thead>
            <tbody>
              {displayAuthEvents.map((event) => (
                <tr key={event.id} className={authRowClassName(event)}>
                  <td>{formatUserTimestamp(event.at)}</td>
                  <td>
                    <code>{event.action}</code>
                  </td>
                  <td>{event.outcome}</td>
                  <td>{event.category}</td>
                  <td>{event.actor}</td>
                  <td>{event.target || '—'}</td>
                  <td>{event.detail || '—'}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      ) : null}

      {!isLoading && activeEvents.length > 0 ? (
        <div className="logs-pagination">
          <span className="logs-pagination-label">
            {pageRangeLabel(
              activeEvents.length,
              pagination.safePage,
              pageSize,
            )}
          </span>
          <div className="logs-pagination-controls">
            <button
              type="button"
              className="config-tab"
              disabled={pagination.safePage <= 1}
              onClick={() => setPage((current) => Math.max(1, current - 1))}
            >
              Previous
            </button>
            <span className="logs-pagination-page">
              Page {pagination.safePage} of {pagination.totalPages}
            </span>
            <button
              type="button"
              className="config-tab"
              disabled={pagination.safePage >= pagination.totalPages}
              onClick={() =>
                setPage((current) =>
                  Math.min(pagination.totalPages, current + 1),
                )
              }
            >
              Next
            </button>
          </div>
        </div>
      ) : null}
    </div>
  )
}

function ConfigurationPanel({
  activeConfigTab,
  onConfigTabChange,
  themePreference,
  onThemePreferenceChange,
}: {
  activeConfigTab: ConfigTabId
  onConfigTabChange: (tab: ConfigTabId) => void
  themePreference: ThemePreference
  onThemePreferenceChange: (preference: ThemePreference) => void
}) {
  const [draftThemePreference, setDraftThemePreference] =
    useState<ThemePreference>(themePreference)

  useEffect(() => {
    setDraftThemePreference(themePreference)
  }, [themePreference])

  const isDirty = draftThemePreference !== themePreference

  return (
    <div className="config-panel">
      <div className="config-tabs" role="tablist" aria-label="Configuration">
        {CONFIG_TABS.map((tab) => {
          const isSelected = activeConfigTab === tab.id
          return (
            <button
              key={tab.id}
              type="button"
              role="tab"
              id={`config-tab-${tab.id}`}
              className={
                isSelected ? 'config-tab config-tab-active' : 'config-tab'
              }
              aria-selected={isSelected}
              aria-controls="config-tab-panel"
              tabIndex={isSelected ? 0 : -1}
              onClick={() => onConfigTabChange(tab.id)}
            >
              {tab.label}
            </button>
          )
        })}
      </div>

      <div
        className="config-tab-panel"
        role="tabpanel"
        id="config-tab-panel"
        aria-labelledby={`config-tab-${activeConfigTab}`}
      >
        <ConfigTabPanelContent
          activeConfigTab={activeConfigTab}
          draftThemePreference={draftThemePreference}
          onDraftThemePreferenceChange={setDraftThemePreference}
        />
      </div>

      <div className="config-actions">
        <button
          type="button"
          className="config-action-revert"
          disabled={!isDirty}
          onClick={() => setDraftThemePreference(themePreference)}
        >
          Revert
        </button>
        <button
          type="button"
          className="config-action-save"
          disabled={!isDirty}
          onClick={() => onThemePreferenceChange(draftThemePreference)}
        >
          Save
        </button>
      </div>
    </div>
  )
}

type BootState =
  | { kind: 'loading' }
  | { kind: 'setup'; clientIp: string }
  | { kind: 'login' }
  | { kind: 'ready'; username: string }

function App() {
  const initialLocation = parseLocation(window.location.pathname)
  const [activeSection, setActiveSection] = useState<SectionId>(
    initialLocation.section,
  )
  const [activeConfigTab, setActiveConfigTab] = useState<ConfigTabId>(
    initialLocation.configTab,
  )
  const [activeResourceTab, setActiveResourceTab] = useState<ResourceTabId>(
    initialLocation.resourceTab,
  )
  const [activeNodeId, setActiveNodeId] = useState<string | null>(
    initialLocation.nodeId,
  )
  const [activeNodeDetailTab, setActiveNodeDetailTab] =
    useState<NodeDetailTabId>(initialLocation.nodeDetailTab)
  const [profileDisplayName, setProfileDisplayName] = useState<string | null>(
    null,
  )
  const [backendVersion, setBackendVersion] = useState('…')
  const [bootState, setBootState] = useState<BootState>({ kind: 'loading' })
  const [canReadLogs, setCanReadLogs] = useState(false)
  const [nodeSectionCounts, setNodeSectionCounts] =
    useState<NodeSectionCounts | null>(null)
  const [nodeHealthErrorCounts, setNodeHealthErrorCounts] =
    useState<NodeSectionCounts | null>(null)
  const [sidebarCountsRefreshToken, setSidebarCountsRefreshToken] = useState(0)
  const [authFailuresToday, setAuthFailuresToday] = useState<number | null>(
    null,
  )
  const { themePreference, updateThemePreference } = useThemePreference()

  const profileLabel = profileDisplayName ?? 'Profile'

  useEffect(() => {
    return subscribeToLocation((pathname) => {
      const location = parseLocation(pathname)
      setActiveSection(location.section)
      setActiveConfigTab(location.configTab)
      setActiveResourceTab(location.resourceTab)
      setActiveNodeId(location.nodeId)
      setActiveNodeDetailTab(location.nodeDetailTab)
    })
  }, [])

  useEffect(() => {
    let isCancelled = false

    async function boot() {
      try {
        const setupStatus = await fetchSetupStatus()
        if (isCancelled) {
          return
        }
        if (!setupStatus.completed) {
          setBootState({ kind: 'setup', clientIp: setupStatus.client_ip })
          navigateTo('/setup')
          return
        }

        const me = await fetchMe()
        if (isCancelled) {
          return
        }
        if (!me) {
          setBootState({ kind: 'login' })
          navigateTo('/login')
          return
        }

        setProfileDisplayName(me.username)
        setCanReadLogs(Boolean(me.permissions?.includes('logs.read')))
        setBootState({ kind: 'ready', username: me.username })
        if (
          window.location.pathname === '/setup' ||
          window.location.pathname === '/login'
        ) {
          navigateTo('/')
        }
      } catch {
        if (!isCancelled) {
          setBootState({ kind: 'login' })
          navigateTo('/login')
        }
      }
    }

    void boot()

    return () => {
      isCancelled = true
    }
  }, [])

  useEffect(() => {
    if (bootState.kind !== 'ready') {
      return
    }

    let isCancelled = false

    async function loadSidebarCounts(idleExempt: boolean) {
      try {
        const nodesResponse = await fetchNodes(undefined, fetch, {
          idleExempt,
        })
        if (!isCancelled) {
          setNodeSectionCounts(countNodesBySection(nodesResponse.nodes))
          setNodeHealthErrorCounts(
            countNodeHealthErrorsBySection(nodesResponse.nodes),
          )
        }
      } catch {
        if (!isCancelled) {
          setNodeSectionCounts(null)
          setNodeHealthErrorCounts(null)
        }
      }

      if (!canReadLogs) {
        if (!isCancelled) {
          setAuthFailuresToday(null)
        }
        return
      }

      try {
        const authResponse = await fetchAuthLogs(fetch, { idleExempt })
        if (!isCancelled) {
          setAuthFailuresToday(countAuthFailuresToday(authResponse.events))
        }
      } catch {
        if (!isCancelled) {
          setAuthFailuresToday(null)
        }
      }
    }

    void loadSidebarCounts(false)

    const pollIntervalId = window.setInterval(() => {
      void loadSidebarCounts(true)
    }, 30_000)

    return () => {
      isCancelled = true
      window.clearInterval(pollIntervalId)
    }
  }, [bootState.kind, canReadLogs, activeSection, sidebarCountsRefreshToken])

  useEffect(() => {
    if (bootState.kind !== 'ready') {
      return
    }

    let isCancelled = false

    async function loadBackendVersion() {
      try {
        const status = await fetchStatus()
        if (!isCancelled) {
          setBackendVersion(status.version)
        }
      } catch {
        if (!isCancelled) {
          setBackendVersion('unavailable')
        }
      }
    }

    void loadBackendVersion()

    return () => {
      isCancelled = true
    }
  }, [bootState.kind])

  useEffect(() => {
    if (bootState.kind === 'loading') {
      document.title = documentTitleFor('loading')
      return
    }
    if (bootState.kind === 'setup') {
      document.title = documentTitleFor('setup')
      return
    }
    if (bootState.kind === 'login') {
      document.title = documentTitleFor('login')
      return
    }
    document.title = documentTitleFor(activeSection)
  }, [bootState.kind, activeSection])

  function goToSection(section: SectionId) {
    if (section === 'setup' || section === 'login') {
      return
    }
    setActiveSection(section)
    setActiveNodeId(null)
    setActiveNodeDetailTab('overview')
    if (section === 'configuration') {
      navigateTo(pathFor(section, activeConfigTab))
      return
    }
    if (isResourceSectionId(section)) {
      setActiveResourceTab('overview')
      navigateTo(pathFor(section, 'overview', 'overview'))
      return
    }
    setActiveResourceTab('overview')
    navigateTo(pathFor(section))
  }

  function goToConfigTab(tab: ConfigTabId) {
    setActiveSection('configuration')
    setActiveConfigTab(tab)
    setActiveNodeId(null)
    navigateTo(pathFor('configuration', tab))
  }

  function goToResourceTab(tab: ResourceTabId) {
    if (!isResourceSectionId(activeSection)) {
      return
    }
    setActiveResourceTab(tab)
    setActiveNodeId(null)
    setActiveNodeDetailTab('overview')
    navigateTo(pathFor(activeSection, 'overview', tab))
  }

  function clearSignedInState() {
    setProfileDisplayName(null)
    setCanReadLogs(false)
  }

  async function handleLogout() {
    try {
      await logout()
    } catch {
      // Still leave the signed-in UI even if the session is already gone.
    }
    clearSignedInState()
    window.history.replaceState(null, '', '/login?reason=logout')
    setBootState({ kind: 'login' })
  }

  if (bootState.kind === 'loading') {
    return (
      <div className="wizard-shell">
        <p className="config-hint">Loading…</p>
      </div>
    )
  }

  if (bootState.kind === 'setup') {
    return (
      <SetupWizard
        clientIp={bootState.clientIp}
        onCompleted={() => {
          window.history.replaceState(null, '', '/login?from=setup')
          setBootState({ kind: 'login' })
        }}
      />
    )
  }

  if (bootState.kind === 'login') {
    return (
      <LoginPage
        onLoggedIn={(username) => {
          setProfileDisplayName(username)
          void (async () => {
            try {
              const me = await fetchMe()
              setCanReadLogs(Boolean(me?.permissions?.includes('logs.read')))
            } catch {
              setCanReadLogs(false)
            }
            setBootState({ kind: 'ready', username })
            navigateTo('/')
            setActiveSection('overview')
          })()
        }}
      />
    )
  }

  return (
    <div className="app-shell">
      <aside className="sidebar" aria-label="Main menu">
        <div className="sidebar-header">
          <h1 className="brand">Cluster Canvas</h1>
          <p className="version-line">Frontend Version {__FRONTEND_VERSION__}</p>
          <p className="version-line">Backend Version {backendVersion}</p>
        </div>

        <nav className="sidebar-nav" aria-label="Sections">
          {NAV_ITEMS.slice(0, 1).map((item) => (
            <button
              key={item.id}
              type="button"
              className={
                activeSection === item.id
                  ? 'nav-item nav-item-active'
                  : 'nav-item'
              }
              aria-current={activeSection === item.id ? 'page' : undefined}
              onClick={() => goToSection(item.id)}
            >
              {item.label}
            </button>
          ))}
          <div className="nav-section-divider" aria-hidden="true" />
          {NAV_ITEMS.slice(1).map((item) => {
            const sectionCount =
              isResourceSectionId(item.id) && nodeSectionCounts !== null
                ? nodeSectionCounts[item.id]
                : null
            const healthErrorCount =
              isResourceSectionId(item.id) && nodeHealthErrorCounts !== null
                ? nodeHealthErrorCounts[item.id]
                : null
            return (
              <button
                key={item.id}
                type="button"
                className={
                  activeSection === item.id
                    ? 'nav-item nav-item-active'
                    : 'nav-item'
                }
                aria-current={activeSection === item.id ? 'page' : undefined}
                onClick={() => goToSection(item.id)}
              >
                <span className="nav-item-label">{item.label}</span>
                {healthErrorCount !== null && healthErrorCount > 0 ? (
                  <span
                    className="nav-count-badge nav-count-badge-alert"
                    aria-label={`${healthErrorCount} unhealthy`}
                  >
                    {healthErrorCount}
                  </span>
                ) : null}
                {sectionCount !== null ? (
                  <span className="nav-count-badge" aria-hidden="true">
                    {sectionCount}
                  </span>
                ) : null}
              </button>
            )
          })}
        </nav>

        {canReadLogs ? (
          <div className="sidebar-logs">
            <p className="sidebar-category-label">Logs</p>
            <button
              type="button"
              className={
                activeSection === 'logs'
                  ? 'nav-item nav-item-active'
                  : 'nav-item'
              }
              aria-current={activeSection === 'logs' ? 'page' : undefined}
              onClick={() => goToSection('logs')}
            >
              <span className="nav-item-label">Activity</span>
              {authFailuresToday !== null && authFailuresToday > 0 ? (
                <span
                  className="nav-count-badge nav-count-badge-alert"
                  aria-hidden="true"
                >
                  {authFailuresToday}
                </span>
              ) : null}
            </button>
          </div>
        ) : null}

        <div className="sidebar-footer">
          <div className="sidebar-footer-profile-row">
            <button
              type="button"
              className={
                activeSection === 'profile'
                  ? 'nav-item nav-item-active profile-item'
                  : 'nav-item profile-item'
              }
              aria-current={activeSection === 'profile' ? 'page' : undefined}
              onClick={() => goToSection('profile')}
            >
              <UserIcon />
              {profileLabel}
            </button>
            <button
              type="button"
              className="nav-item logout-item"
              aria-label="Log out"
              onClick={() => {
                void handleLogout()
              }}
            >
              <LogoutIcon />
            </button>
          </div>
          <button
            type="button"
            className={
              activeSection === 'configuration'
                ? 'nav-item nav-item-active config-item'
                : 'nav-item config-item'
            }
            aria-current={
              activeSection === 'configuration' ? 'page' : undefined
            }
            onClick={() => goToSection('configuration')}
          >
            <CogIcon />
            Configuration
          </button>
        </div>
      </aside>

      <main className="content" aria-label={SECTION_TITLES[activeSection]}>
        <h2>
          {activeSection === 'profile' ? profileLabel : SECTION_TITLES[activeSection]}
        </h2>
        {activeSection === 'configuration' ? (
          <ConfigurationPanel
            activeConfigTab={activeConfigTab}
            onConfigTabChange={goToConfigTab}
            themePreference={themePreference}
            onThemePreferenceChange={updateThemePreference}
          />
        ) : activeSection === 'profile' ? (
          <ProfilePanel
            onSignedOut={() => {
              clearSignedInState()
              setBootState({ kind: 'login' })
              navigateTo('/login')
            }}
          />
        ) : activeSection === 'logs' ? (
          <LogsPanel />
        ) : isResourceSectionId(activeSection) ? (
          <ResourceSectionPanel
            section={activeSection}
            activeResourceTab={activeResourceTab}
            onResourceTabChange={goToResourceTab}
            nodeId={activeNodeId}
            nodeDetailTab={activeNodeDetailTab}
            onNodesChanged={() => {
              setSidebarCountsRefreshToken((token) => token + 1)
            }}
          />
        ) : activeSection === 'overview' ? (
          <OverviewPanel />
        ) : (
          <p>Coming soon</p>
        )}
      </main>
    </div>
  )
}

export default App
