oxios 1.11.0

Oxios Agent OS — Agent Operating System powered by oxi-sdk
import { useQuery } from '@tanstack/react-query'
import { createFileRoute } from '@tanstack/react-router'
import { FileWarning, KeyRound, Shield } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { EmptyState } from '@/components/shared/empty-state'
import { ErrorState } from '@/components/shared/error-state'
import { LoadingCards } from '@/components/shared/loading'
import { RefreshButton } from '@/components/shared/refresh-button'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { api } from '@/lib/api-client'

export const Route = createFileRoute('/security')({ component: SecurityPage })

function SecurityPage() {
  const { t } = useTranslation()
  const {
    data: audits,
    isLoading: auditLoading,
    isError: auditError,
    refetch,
    isFetching,
  } = useQuery({
    queryKey: ['audit'],
    queryFn: async () => {
      const res = await api.get<{
        items: {
          timestamp: string
          agent_name: string
          action: string
          resource: string
          allowed: boolean
          reason: string | null
        }[]
      }>('/api/audit')
      return res
    },
    refetchInterval: 15000,
  })

  const {
    data: permissions,
    isError: permissionsError,
    refetch: refetchPermissions,
  } = useQuery({
    queryKey: ['permissions'],
    queryFn: () =>
      api.get<{
        roles: string[]
        policies: { name: string; effect: string; resources: string[] }[]
      }>('/api/security/permissions'),
    refetchInterval: 15000,
  })

  const [auditPage, setAuditPage] = useState(1)
  const AUDIT_PAGE_SIZE = 20

  if (auditLoading) return <LoadingCards count={4} />
  if (auditError) return <ErrorState onRetry={() => refetch()} />

  const entries = (Array.isArray(audits?.items) ? audits.items : []).map((e) => ({
    ...e,
    id: `${e.timestamp}-${e.agent_name}`,
    agent_id: e.agent_name,
  }))

  const totalPages = Math.ceil(entries.length / AUDIT_PAGE_SIZE)
  const pagedEntries = entries.slice((auditPage - 1) * AUDIT_PAGE_SIZE, auditPage * AUDIT_PAGE_SIZE)

  return (
    <div className="space-y-6">
      <div className="flex items-center justify-between">
        <div>
          <h1 className="text-2xl font-bold">{t('security.title')}</h1>
          <p className="text-muted-foreground">{t('security.subtitle')}</p>
        </div>
        <RefreshButton onClick={() => refetch()} isFetching={isFetching} />
      </div>

      {/* Permissions */}
      {permissionsError ? (
        <ErrorState onRetry={() => refetchPermissions()} />
      ) : permissions ? (
        <div className="space-y-3">
          {permissions.policies
            .slice()
            .sort((a, b) => b.resources.length - a.resources.length)
            .map((policy) => {
              const roleName = policy.name.replace('-default', '')
              return (
                <Card key={policy.name}>
                  <CardContent className="pt-4">
                    <div className="flex items-center justify-between mb-2">
                      <div className="flex items-center gap-2">
                        <KeyRound className="h-4 w-4 text-muted-foreground" />
                        <span className="font-medium">{roleName}</span>
                      </div>
                      <span className="text-xs text-muted-foreground">
                        {policy.resources.length}{' '}
                        {policy.resources.length !== 1
                          ? t('security.permissions')
                          : t('security.permission')}
                      </span>
                    </div>
                    {policy.resources.length > 0 ? (
                      <div className="flex gap-1.5 flex-wrap">
                        {policy.resources.map((resource) => (
                          <Badge key={resource} variant="secondary" className="text-xs">
                            {resource}
                          </Badge>
                        ))}
                      </div>
                    ) : (
                      <p className="text-xs text-muted-foreground">{t('security.noPermissions')}</p>
                    )}
                  </CardContent>
                </Card>
              )
            })}
        </div>
      ) : null}

      {/* Audit Trail */}
      <Card>
        <CardHeader>
          <CardTitle className="flex items-center gap-2">
            <FileWarning className="h-4 w-4" /> {t('security.auditTrail')}
          </CardTitle>
        </CardHeader>
        <CardContent>
          {pagedEntries.length === 0 ? (
            <EmptyState
              icon={<Shield className="h-8 w-8" />}
              title={t('security.noAuditEntries')}
              description={t('security.noAuditEntriesDescription')}
              className="py-6"
            />
          ) : (
            <div className="space-y-2">
              {pagedEntries.map((entry) => (
                <div
                  key={entry.id}
                  className="flex items-center justify-between rounded-lg border p-3"
                >
                  <div className="flex items-center gap-3">
                    <Badge variant={entry.allowed ? 'success' : 'destructive'} className="shrink-0">
                      {entry.allowed ? t('security.allow') : t('security.deny')}
                    </Badge>
                    <div>
                      <p className="font-medium text-sm">{entry.action}</p>
                      {entry.resource && (
                        <p className="text-xs text-muted-foreground">{entry.resource}</p>
                      )}
                      {entry.agent_id && (
                        <p className="text-xs text-muted-foreground">
                          {t('security.agent')}: {entry.agent_id.slice(0, 8)}...
                        </p>
                      )}
                      {entry.reason && <p className="text-xs text-warning">{entry.reason}</p>}
                    </div>
                  </div>
                  <div className="text-right">
                    <p className="text-xs text-muted-foreground">
                      {new Date(entry.timestamp).toLocaleString()}
                    </p>
                  </div>
                </div>
              ))}
            </div>
          )}
          {/* Pagination */}
          {totalPages > 1 && (
            <div className="flex items-center justify-between pt-3 border-t mt-3">
              <p className="text-xs text-muted-foreground">
                {t('security.showingEntries', {
                  start: (auditPage - 1) * AUDIT_PAGE_SIZE + 1,
                  end: Math.min(auditPage * AUDIT_PAGE_SIZE, entries.length),
                  total: entries.length,
                })}
              </p>
              <div className="flex gap-1">
                <Button
                  variant="outline"
                  size="sm"
                  disabled={auditPage <= 1}
                  onClick={() => setAuditPage((p) => Math.max(1, p - 1))}
                >
                  {t('common.previous')}
                </Button>
                <Button
                  variant="outline"
                  size="sm"
                  disabled={auditPage >= totalPages}
                  onClick={() => setAuditPage((p) => Math.min(totalPages, p + 1))}
                >
                  {t('common.next')}
                </Button>
              </div>
            </div>
          )}
        </CardContent>
      </Card>
    </div>
  )
}