oxios 1.5.2

Oxios Agent OS — Agent Operating System powered by oxi-sdk
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import {
  AlertCircle,
  ArrowDownToLine,
  CheckCircle2,
  ChevronDown,
  ChevronUp,
  ExternalLink,
  Loader2,
} from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
import { api } from '@/lib/api-client'
import type { ChangelogResponse, UpdateCheckResponse } from '@/types'

function formatDate(iso: string): string {
  if (!iso) return '-'
  try {
    return new Date(iso).toLocaleDateString(undefined, {
      year: 'numeric',
      month: 'short',
      day: 'numeric',
    })
  } catch {
    return iso
  }
}

export function SystemUpdateCard() {
  const { t } = useTranslation()
  const queryClient = useQueryClient()
  const [showNotes, setShowNotes] = useState(false)

  const {
    data: check,
    isLoading: checkLoading,
    isError: checkError,
    refetch: refetchCheck,
  } = useQuery({
    queryKey: ['update-check'],
    queryFn: () => api.get<UpdateCheckResponse>('/api/update/check'),
    staleTime: 5 * 60 * 1000,
  })

  const { data: changelog, isLoading: changelogLoading } = useQuery({
    queryKey: ['update-changelog'],
    queryFn: () => api.get<ChangelogResponse>('/api/update/changelog'),
    staleTime: 5 * 60 * 1000,
    enabled: showNotes,
  })

  const updateMutation = useMutation({
    mutationFn: (): Promise<Record<string, unknown>> =>
      api.post('/api/update/run', { binary: true, web: true }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['update-check'] })
      queryClient.invalidateQueries({ queryKey: ['status'] })
    },
  })

  // Loading state
  if (checkLoading) {
    return (
      <Card>
        <CardHeader>
          <CardTitle className="flex items-center gap-2">
            <ArrowDownToLine className="h-4 w-4" />
            {t('update.title')}
          </CardTitle>
        </CardHeader>
        <CardContent>
          <div className="flex items-center gap-2 text-sm text-muted-foreground">
            <Loader2 className="h-4 w-4 animate-spin" />
            {t('update.checking')}
          </div>
        </CardContent>
      </Card>
    )
  }

  // Error state
  if (checkError || !check) {
    return (
      <Card>
        <CardHeader>
          <CardTitle className="flex items-center gap-2">
            <ArrowDownToLine className="h-4 w-4" />
            {t('update.title')}
          </CardTitle>
        </CardHeader>
        <CardContent>
          <div className="flex items-center justify-between">
            <div className="flex items-center gap-2 text-sm text-destructive">
              <AlertCircle className="h-4 w-4" />
              {t('update.checkFailed')}
            </div>
            <Button variant="outline" size="sm" onClick={() => refetchCheck()}>
              {t('common.retry')}
            </Button>
          </div>
        </CardContent>
      </Card>
    )
  }

  const isUpToDate = !check.update_available

  return (
    <Card>
      <CardHeader>
        <CardTitle className="flex items-center gap-2">
          <ArrowDownToLine className="h-4 w-4" />
          {t('update.title')}
        </CardTitle>
      </CardHeader>
      <CardContent className="space-y-4">
        {/* Version comparison */}
        <div className="grid grid-cols-2 gap-4">
          <div>
            <p className="text-xs text-muted-foreground">{t('update.currentVersion')}</p>
            <p className="text-lg font-mono font-semibold">{check.current_version}</p>
          </div>
          <div>
            <p className="text-xs text-muted-foreground">{t('update.latestVersion')}</p>
            <div className="flex items-center gap-2">
              <p className="text-lg font-mono font-semibold">{check.latest_version}</p>
              {isUpToDate ? (
                <Badge variant="success" className="text-xs">
                  {t('update.upToDate')}
                </Badge>
              ) : (
                <Badge variant="destructive" className="text-xs">
                  {t('update.updateAvailable')}
                </Badge>
              )}
            </div>
          </div>
        </div>

        <Separator />

        {/* Update available */}
        {!isUpToDate && (
          <>
            <div className="text-xs text-muted-foreground">{t('update.methodDescription')}</div>

            {/* Update button */}
            <div className="flex items-center gap-3">
              <Button
                onClick={() => updateMutation.mutate()}
                disabled={updateMutation.isPending}
                className="gap-2"
              >
                {updateMutation.isPending ? (
                  <>
                    <Loader2 className="h-4 w-4 animate-spin" />
                    {t('update.updating')}
                  </>
                ) : (
                  <>
                    <ArrowDownToLine className="h-4 w-4" />
                    {t('update.updateTo', { version: check.latest_version })}
                  </>
                )}
              </Button>
              <a
                href={check.html_url}
                target="_blank"
                rel="noopener noreferrer"
                className="text-xs text-muted-foreground hover:text-foreground flex items-center gap-1"
              >
                {t('update.viewOnGithub')}
                <ExternalLink className="h-3 w-3" />
              </a>
            </div>

            {/* Update result */}
            {updateMutation.isSuccess && updateMutation.data && (
              <div className="rounded-lg border border-success-subtle p-3 text-sm text-success">
                <div className="flex items-center gap-2 mb-1">
                  <CheckCircle2 className="h-4 w-4" />
                  <span className="font-medium">{t('update.updateSuccess')}</span>
                </div>
                <p className="text-xs">
                  {(updateMutation.data as Record<string, unknown>)?.message as string}
                </p>
                {((updateMutation.data as Record<string, unknown>)?.binary_updated as boolean) && (
                  <p className="text-xs mt-1 text-warning">{t('update.restartRequired')}</p>
                )}
              </div>
            )}

            {updateMutation.isError && (
              <div className="rounded-lg border border-error-subtle p-3 text-sm text-error">
                <div className="flex items-center gap-2">
                  <AlertCircle className="h-4 w-4" />
                  <span>{t('update.updateFailed')}</span>
                </div>
                <p className="text-xs mt-1">
                  {(updateMutation.error as Error)?.message || t('update.unknownError')}
                </p>
              </div>
            )}
          </>
        )}

        {/* Already up to date */}
        {isUpToDate && (
          <div className="flex items-center gap-2 text-sm text-success">
            <CheckCircle2 className="h-4 w-4" />
            <span>{t('update.alreadyUpToDate')}</span>
          </div>
        )}

        <Separator />

        {/* Release notes toggle */}
        <div>
          <button
            type="button"
            onClick={() => setShowNotes(!showNotes)}
            className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
          >
            {showNotes ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
            {t('update.releaseNotes')}
          </button>
          {showNotes && (
            <div className="mt-2 text-xs text-muted-foreground">
              {changelogLoading ? (
                <div className="flex items-center gap-2">
                  <Loader2 className="h-3 w-3 animate-spin" />
                  {t('update.loadingNotes')}
                </div>
              ) : changelog ? (
                <div className="space-y-2">
                  <div className="flex items-center gap-2 text-foreground font-medium">
                    <span>{changelog.tag_name}</span>
                    <span className="text-muted-foreground">
                      {formatDate(changelog.published_at)}
                    </span>
                  </div>
                  <pre className="whitespace-pre-wrap font-sans bg-muted/50 rounded-lg p-3 max-h-64 overflow-y-auto">
                    {changelog.body}
                  </pre>
                </div>
              ) : null}
            </div>
          )}
        </div>

        {/* Published date */}
        {check.published_at && (
          <p className="text-xs text-muted-foreground">
            {t('update.publishedAt', { date: formatDate(check.published_at) })}
          </p>
        )}
      </CardContent>
    </Card>
  )
}