import { useState, type FormEvent } from 'react';
import { Command, History, KeyRound, LockKeyhole, Send } from 'lucide-react';
import { useServer } from '../context/server-context';
import { useI18n } from '../lib/i18n';
import type { OperationStatus } from '../types';
import { Badge, Button, EmptyState, Field, Input, PageHeader, Panel, Textarea, Toast, formatDate } from '../components/ui';
export function ConsolePage() {
const { t, locale } = useI18n();
const { api, features, session, connection } = useServer();
const [command, setCommand] = useState('');
const [reason, setReason] = useState('');
const [confirmed, setConfirmed] = useState(false);
const [busy, setBusy] = useState(false);
const [history, setHistory] = useState<OperationStatus[]>([]);
const [toast, setToast] = useState<{ message: string; tone: 'good' | 'danger' }>();
const actor = session?.actor;
const allowed = connection === 'online' && features?.consoleEnabled === true && actor?.roles.includes('owner') && actor.totpVerified && !actor.reauthenticationRequired;
const submit = async (event: FormEvent) => {
event.preventDefault();
if (!allowed || !command.trim() || !reason.trim()) return;
if (!confirmed) {
setConfirmed(true);
return;
}
setBusy(true);
try {
const operation = await api.executeConsole(command.trim().replace(/^\//, ''), reason.trim());
setHistory((current) => [operation, ...current]);
setToast({ message: `${t('operation.queued')} · ${operation.id}`, tone: 'good' });
setCommand('');
setReason('');
setConfirmed(false);
} catch (error) {
setToast({ message: error instanceof Error ? error.message : String(error), tone: 'danger' });
} finally {
setBusy(false);
}
};
return (
<div className="page">
<PageHeader title={t('console.title')} description={t('console.description')} />
{!allowed ? (
<Panel><EmptyState icon={<LockKeyhole />} title={t('console.locked')} detail={t('console.warning')} /></Panel>
) : (
<div className="console-layout">
<Panel title={<><Command size={18} />{t('console.title')}</>}>
<div className="security-strip"><KeyRound size={17} /><span>{t('console.warning')}</span><Badge tone="good">TOTP</Badge></div>
<form className="form-stack console-form" onSubmit={submit}>
<Field label={t('console.command')}><div className="command-input"><span>/</span><Input value={command} onChange={(event) => { setCommand(event.target.value); setConfirmed(false); }} placeholder="save-all flush" required spellCheck={false} autoComplete="off" /></div></Field>
<Field label={t('common.reason')} hint={t('common.reasonRequired')}><Textarea value={reason} onChange={(event) => { setReason(event.target.value); setConfirmed(false); }} rows={3} required /></Field>
{confirmed && <div className="confirm-box confirm-box--danger"><Command /><div><strong>/{command.replace(/^\//, '')}</strong><p>{reason}</p></div></div>}
<Button type="submit" variant={confirmed ? 'danger' : 'primary'} disabled={busy || !command.trim() || !reason.trim()}><Send size={16} />{confirmed ? t('console.execute') : t('common.preview')}</Button>
</form>
</Panel>
<Panel title={<><History size={18} />{t('console.history')}</>}>
{history.length === 0 ? <EmptyState title={t('common.empty')} /> : <div className="operation-list">{history.map((operation) => <article key={operation.id}><div><strong>{operation.kind}</strong><code>{operation.id}</code></div><div><Badge tone={operation.state === 'FAILED' ? 'danger' : operation.state === 'SUCCEEDED' ? 'good' : 'info'}>{operation.state}</Badge><small>{formatDate(operation.createdAt, locale)}</small></div></article>)}</div>}
</Panel>
</div>
)}
{toast && <Toast {...toast} onClose={() => setToast(undefined)} />}
</div>
);
}
import { useState, type FormEvent } from 'react';
import { Command, History, KeyRound, LockKeyhole, Send } from 'lucide-react';
import { useServer } from '../context/server-context';
import { useI18n } from '../lib/i18n';
import type { OperationStatus } from '../types';
import { Badge, Button, EmptyState, Field, Input, PageHeader, Panel, Textarea, Toast, formatDate } from '../components/ui';
export function ConsolePage() {
const { t, locale } = useI18n();
const { api, features, session, connection } = useServer();
const [command, setCommand] = useState('');
const [reason, setReason] = useState('');
const [confirmed, setConfirmed] = useState(false);
const [busy, setBusy] = useState(false);
const [history, setHistory] = useState<OperationStatus[]>([]);
const [toast, setToast] = useState<{ message: string; tone: 'good' | 'danger' }>();
const actor = session?.actor;
const allowed = connection === 'online' && features?.consoleEnabled === true && actor?.roles.includes('owner') && actor.totpVerified && !actor.reauthenticationRequired;
const submit = async (event: FormEvent) => {
event.preventDefault();
if (!allowed || !command.trim() || !reason.trim()) return;
if (!confirmed) {
setConfirmed(true);
return;
}
setBusy(true);
try {
const operation = await api.executeConsole(command.trim().replace(/^\//, ''), reason.trim());
setHistory((current) => [operation, ...current]);
setToast({ message: `${t('operation.queued')} · ${operation.id}`, tone: 'good' });
setCommand('');
setReason('');
setConfirmed(false);
} catch (error) {
setToast({ message: error instanceof Error ? error.message : String(error), tone: 'danger' });
} finally {
setBusy(false);
}
};
return (
<div className="page">
<PageHeader title={t('console.title')} description={t('console.description')} />
{!allowed ? (
<Panel><EmptyState icon={<LockKeyhole />} title={t('console.locked')} detail={t('console.warning')} /></Panel>
) : (
<div className="console-layout">
<Panel title={<><Command size={18} />{t('console.title')}</>}>
<div className="security-strip"><KeyRound size={17} /><span>{t('console.warning')}</span><Badge tone="good">TOTP</Badge></div>
<form className="form-stack console-form" onSubmit={submit}>
<Field label={t('console.command')}><div className="command-input"><span>/</span><Input value={command} onChange={(event) => { setCommand(event.target.value); setConfirmed(false); }} placeholder="save-all flush" required spellCheck={false} autoComplete="off" /></div></Field>
<Field label={t('common.reason')} hint={t('common.reasonRequired')}><Textarea value={reason} onChange={(event) => { setReason(event.target.value); setConfirmed(false); }} rows={3} required /></Field>
{confirmed && <div className="confirm-box confirm-box--danger"><Command /><div><strong>/{command.replace(/^\//, '')}</strong><p>{reason}</p></div></div>}
<Button type="submit" variant={confirmed ? 'danger' : 'primary'} disabled={busy || !command.trim() || !reason.trim()}><Send size={16} />{confirmed ? t('console.execute') : t('common.preview')}</Button>
</form>
</Panel>
<Panel title={<><History size={18} />{t('console.history')}</>}>
{history.length === 0 ? <EmptyState title={t('common.empty')} /> : <div className="operation-list">{history.map((operation) => <article key={operation.id}><div><strong>{operation.kind}</strong><code>{operation.id}</code></div><div><Badge tone={operation.state === 'FAILED' ? 'danger' : operation.state === 'SUCCEEDED' ? 'good' : 'info'}>{operation.state}</Badge><small>{formatDate(operation.createdAt, locale)}</small></div></article>)}</div>}
</Panel>
</div>
)}
{toast && <Toast {...toast} onClose={() => setToast(undefined)} />}
</div>
);
}