File size: 2,814 Bytes
0f4d9ea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import { useEffect, useState } from 'react'
import { useI18n } from '../i18n'
import { Callout, Spinner } from './ui'
import { federatedStatus, type FederatedStatus } from '../api/ai'

/** Honest, non-authoritative view of the SIMULATED federated-learning demo (PR C3/C4).
 * Reports synthetic-server-simulation status, secure-aggregation DESIGN_ONLY, DP SIMULATED with an
 * exhausted-budget warning, and no payment authority. Never claims "data stays on your device". */
export function FederatedStatusCard({ detailed = false }: { detailed?: boolean }) {
  const { t } = useI18n()
  const [status, setStatus] = useState<FederatedStatus | null>(null)
  const [loading, setLoading] = useState(true)
  const [err, setErr] = useState(false)

  useEffect(() => {
    const c = new AbortController()
    federatedStatus(c.signal)
      .then(setStatus)
      .catch(() => setErr(true))
      .finally(() => setLoading(false))
    return () => c.abort()
  }, [])

  if (loading) return <Spinner label={t('common.loading')} />
  if (err || !status) return <Callout tone="warn">{t('fed.unavailable')}</Callout>

  const dp = status.differential_privacy_detail
  const rows: [string, string][] = [
    ['client_type', status.client_type],
    ['federation', status.federation],
    ['secure_aggregation', status.secure_aggregation],
    ['differential_privacy', status.differential_privacy],
    ['real_device_training', status.real_device_training],
    ['native_local_only_training', status.native_local_only_training],
  ]

  return (
    <div className="federated-status" data-testid="federated-status">
      <ul className="status-list">
        {rows.map(([key, value]) => (
          <li key={key}>
            <span>{t(`federated.label.${key}` as never)}</span>
            <span className="badge tone-info mono" data-testid={`fed-label-${key}`}>{value}</span>
          </li>
        ))}
      </ul>

      {dp?.budget_exhausted && (
        <Callout tone="warn">
          <span data-testid="dp-exhausted">{t('fed.dp.exhausted')}</span>
        </Callout>
      )}

      <p className="muted small" data-testid="fed-no-authority">{t('federated.noAuthority')}</p>

      {detailed && (
        <>
          <p className="muted small" data-testid="fed-synthetic">{t('fed.syntheticClients')}</p>
          {status.aggregators && status.aggregators.length > 0 && (
            <p className="muted small">{t('fed.aggregators')}: <span className="mono">{status.aggregators.join(', ')}</span></p>
          )}
          <p className="muted small">{t('fed.candidate')}: <span className="mono">{status.candidate_status}</span>
            {' · '}<span data-testid="fed-not-promotable">{t('fed.notPromotable')}</span></p>
          <p className="muted small">{status.disclaimer}</p>
        </>
      )}
    </div>
  )
}