import { useEffect, useState, type FormEvent } from 'react';
import { Beaker, CheckCircle2, FileJson2, FileLock2, Pencil, Rocket, Save, SearchCode, X } from 'lucide-react';
import { useApiResource, useServer } from '../context/server-context';
import { useI18n } from '../lib/i18n';
import { hasCapability } from '../lib/permissions';
import type { CommandPolicyDetail, CommandPolicySummary, PolicySimulation, PolicyValidation } from '../types';
import { Badge, Button, Field, Input, PageHeader, Panel, ResourceState, TableShell, Textarea, Toast, formatDate } from '../components/ui';
import { ModalPortal } from '../components/modal-portal';
import { normalizePolicyRule, PolicyRuleEditor } from '../components/policy-rule-editor';
import type { CommandPolicyRule } from '../types';
export function PoliciesPage() {
const { t, locale } = useI18n();
const { api, session } = useServer();
const canPublish = hasCapability(session?.actor, 'policy_publish');
const resource = useApiResource(() => api.policies(), [], ['policy-published']);
const [command, setCommand] = useState('');
const [playerUuid, setPlayerUuid] = useState('');
const [simulation, setSimulation] = useState<PolicySimulation>();
const [simulationError, setSimulationError] = useState<string>();
const [simulating, setSimulating] = useState(false);
const [publish, setPublish] = useState<CommandPolicySummary>();
const [reason, setReason] = useState('');
const [editorPolicy, setEditorPolicy] = useState<CommandPolicyDetail>();
const commandCatalogResource = useApiResource(
() => api.policyCommands(),
[],
['triggers-changed'],
Boolean(editorPolicy),
);
const [rules, setRules] = useState<CommandPolicyRule[]>([]);
const [draftDescription, setDraftDescription] = useState('');
const [editorBusy, setEditorBusy] = useState(false);
const [validation, setValidation] = useState<PolicyValidation>();
const [editorError, setEditorError] = useState<string>();
const [toast, setToast] = useState<{ message: string; tone: 'good' | 'danger' }>();
const simulate = async (event: FormEvent) => {
event.preventDefault();
if (!command.trim()) return;
setSimulating(true);
try {
setSimulation(await api.simulatePolicy(command.trim(), playerUuid.trim() || undefined));
setSimulationError(undefined);
} catch (error) {
setSimulation(undefined);
setSimulationError(error instanceof Error ? error.message : String(error));
} finally {
setSimulating(false);
}
};
const publishPolicy = async (event: FormEvent) => {
event.preventDefault();
if (!publish || !reason.trim()) return;
try {
const operation = await api.publishPolicy(publish.id, publish.version, reason.trim());
setToast({ message: `${t('operation.queued')} · ${operation.id}`, tone: 'good' });
setPublish(undefined);
setReason('');
resource.reload();
} catch (error) {
setToast({ message: error instanceof Error ? error.message : String(error), tone: 'danger' });
}
};
const openEditor = async (policy: CommandPolicySummary) => {
setEditorBusy(true);
setEditorError(undefined);
setValidation(undefined);
try {
const detail = await api.policy(policy.id);
setEditorPolicy(detail);
setRules(detail.rules.map(normalizePolicyRule));
setDraftDescription(detail.description ?? detail.name);
setValidation(detail.validation);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setEditorError(message);
setToast({ message, tone: 'danger' });
} finally {
setEditorBusy(false);
}
};
const persistPolicy = async (publishImmediately: boolean) => {
const active = resource.data?.items.find((item) => item.state === 'ACTIVE');
if (!active || !draftDescription.trim()) return;
setEditorBusy(true);
setEditorError(undefined);
try {
const operation = await api.stagePolicy(
active.version,
draftDescription.trim(),
rules,
);
if (publishImmediately) {
if (!operation.policy) throw new Error('Server did not return the staged policy version.');
const applied = await api.publishPolicy(
operation.policy.id,
operation.policy.version,
draftDescription.trim(),
);
setToast({ message: `${t('policies.appliedImmediately')} · ${applied.id}`, tone: 'good' });
} else {
setToast({ message: `${t('policies.draftSaved')} · ${operation.id}`, tone: 'good' });
}
setEditorPolicy(undefined);
setValidation(undefined);
resource.reload();
} catch (error) {
setEditorError(error instanceof Error ? error.message : String(error));
} finally {
setEditorBusy(false);
}
};
const saveDraft = (event: FormEvent) => {
event.preventDefault();
void persistPolicy(canPublish);
};
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== 's' || !editorPolicy) return;
event.preventDefault();
if (!editorBusy && draftDescription.trim()) void persistPolicy(canPublish);
};
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, [canPublish, draftDescription, editorBusy, editorPolicy, persistPolicy]);
const validateDraft = async (policy: CommandPolicySummary) => {
setEditorBusy(true);
setEditorError(undefined);
try {
const [result, detail] = await Promise.all([api.validatePolicy(policy.id), api.policy(policy.id)]);
setEditorPolicy(detail);
setRules(detail.rules.map(normalizePolicyRule));
setDraftDescription(detail.description ?? detail.name);
setValidation(result);
setToast({ message: t('policies.validationComplete'), tone: result.valid ? 'good' : 'danger' });
} catch (error) {
setEditorError(error instanceof Error ? error.message : String(error));
} finally {
setEditorBusy(false);
}
};
const validationLine = (entry: string | { ruleId?: string; code?: string; message: string }) =>
typeof entry === 'string' ? entry : [entry.ruleId, entry.code, entry.message].filter(Boolean).join(' · ');
return (
<div className="page">
<PageHeader title={t('policies.title')} description={t('policies.description')} />
<div className="policy-layout">
<Panel title={<><FileLock2 size={18} />{t('policies.title')}</>}>
<ResourceState loading={resource.loading} error={resource.error} empty={resource.data?.items.length === 0} onRetry={resource.reload}>
<TableShell>
<thead><tr><th>{t('policies.name')}</th><th>{t('policies.version')}</th><th>{t('policies.rules')}</th><th>{t('policies.state')}</th><th>{t('policies.updated')}</th><th /></tr></thead>
<tbody>{resource.data?.items.map((policy) => (
<tr key={`${policy.id}:${policy.version}`}>
<td><strong>{policy.name}</strong>{policy.description && <small className="table-subline">{policy.description}</small>}</td>
<td><code>v{policy.version}</code></td><td>{policy.groupCount ?? (policy.ruleCount > 0 ? 1 : 0)} {t('policies.groups')} · {policy.ruleCount} {t('policies.rules')}</td>
<td><Badge tone={policy.state === 'ACTIVE' ? 'good' : policy.state === 'DRAFT' ? 'warn' : 'neutral'}>{policy.state}</Badge></td>
<td>{formatDate(policy.updatedAt, locale)}</td>
<td><div className="button-row"><Button variant="ghost" disabled={editorBusy} onClick={() => void openEditor(policy)}><Pencil size={14} />{t('policies.edit')}</Button>{policy.state === 'DRAFT' && <><Button variant="ghost" disabled={editorBusy} onClick={() => void validateDraft(policy)}><CheckCircle2 size={14} />{t('policies.validate')}</Button>{canPublish && <Button variant="secondary" onClick={() => setPublish(policy)}><Rocket size={14} />{t('policies.publish')}</Button>}</>}</div></td>
</tr>
))}</tbody>
</TableShell>
</ResourceState>
</Panel>
<Panel title={<><Beaker size={18} />{t('policies.simulator')}</>} className="simulator-panel">
<form className="form-stack" onSubmit={simulate}>
<Field label={t('policies.command')}><Input value={command} onChange={(event) => setCommand(event.target.value)} placeholder="execute as @e[type=minecraft:zombie] run tp @s ~ ~1 ~" required spellCheck={false} /></Field>
<Field label={t('policies.playerUuid')}><Input value={playerUuid} onChange={(event) => setPlayerUuid(event.target.value)} placeholder="00000000-0000-0000-0000-000000000000" /></Field>
<Button type="submit" disabled={simulating || !command.trim()}><SearchCode size={16} />{t('policies.run')}</Button>
</form>
<div className="simulation-result">
<span className="field__label">{t('policies.result')}</span>
{simulation ? <><Badge tone={simulation.decision === 'DENY' ? 'danger' : simulation.decision === 'GRANT' ? 'good' : 'info'}>{simulation.decision}</Badge><code className="command-normalized">{simulation.normalizedCommand ?? command}</code><ol>{simulation.explanation.map((line, index) => <li key={`${index}:${line}`}>{line}</li>)}</ol></> : simulationError ? <p className="error-text">{simulationError}</p> : <p className="muted">{t('policies.noResult')}</p>}
</div>
</Panel>
</div>
{editorPolicy && <Panel title={<><FileJson2 size={18} />{t('policies.editor')}</>}>
<form className="form-stack" onSubmit={saveDraft}>
<div className="confirm-box"><FileLock2 /><div><p>{t('policies.editorHint')}</p><p>{t('policies.liveHint')}</p></div></div>
<Field label={t('policies.draftDescription')}><Input value={draftDescription} onChange={(event) => setDraftDescription(event.target.value)} maxLength={1000} required /></Field>
<PolicyRuleEditor rules={rules} onChange={(next) => { setRules(next); setValidation(undefined); }}
commandCatalog={commandCatalogResource.data?.commands ?? []}
commandCatalogLoading={commandCatalogResource.loading}
commandCatalogError={commandCatalogResource.error}
onReloadCommandCatalog={commandCatalogResource.reload} />
{validation && <div className={`confirm-box ${validation.valid ? '' : 'confirm-box--danger'}`}><CheckCircle2 /><div><strong>{validation.valid ? t('policies.valid') : t('policies.invalid')}</strong>{[...validation.errors, ...validation.warnings].map((entry, index) => <p key={index}>{validationLine(entry)}</p>)}</div></div>}
{editorError && <p className="error-text" role="alert">{editorError}</p>}
<div className="button-row"><Button type="button" variant="ghost" onClick={() => { setEditorPolicy(undefined); setEditorError(undefined); }}>{t('common.cancel')}</Button>{canPublish && <Button type="button" variant="secondary" disabled={editorBusy || !draftDescription.trim()} onClick={() => void persistPolicy(false)}><Save size={15} />{t('policies.saveDraft')}</Button>}<Button type="submit" disabled={editorBusy || !draftDescription.trim()}>{canPublish ? <Rocket size={15} /> : <Save size={15} />}{canPublish ? t('policies.saveAndApply') : t('policies.saveDraft')}</Button><span className="keyboard-hint"><kbd>Ctrl</kbd>+<kbd>S</kbd></span></div>
</form>
</Panel>}
{publish && <ModalPortal><div className="modal-backdrop"><section className="modal" role="dialog" aria-modal="true"><div className="modal__header"><div><span className="page-header__eyebrow">{publish.id} · v{publish.version}</span><h2>{t('policies.publish')}</h2></div><button onClick={() => setPublish(undefined)} aria-label={t('common.close')}><X /></button></div><form className="form-stack" onSubmit={publishPolicy}><div className="confirm-box"><CheckCircle2 /><p>{t('policies.publishWarning')}</p></div><Field label={t('common.reason')}><Textarea required value={reason} onChange={(event) => setReason(event.target.value)} /></Field><div className="modal__actions"><Button type="button" variant="ghost" onClick={() => setPublish(undefined)}>{t('common.cancel')}</Button><Button type="submit" disabled={!reason.trim()}><Rocket size={15} />{t('policies.publish')}</Button></div></form></section></div></ModalPortal>}
{toast && <Toast {...toast} onClose={() => setToast(undefined)} />}
</div>
);
}
import { useEffect, useState, type FormEvent } from 'react';
import { Beaker, CheckCircle2, FileJson2, FileLock2, Pencil, Rocket, Save, SearchCode, X } from 'lucide-react';
import { useApiResource, useServer } from '../context/server-context';
import { useI18n } from '../lib/i18n';
import { hasCapability } from '../lib/permissions';
import type { CommandPolicyDetail, CommandPolicySummary, PolicySimulation, PolicyValidation } from '../types';
import { Badge, Button, Field, Input, PageHeader, Panel, ResourceState, TableShell, Textarea, Toast, formatDate } from '../components/ui';
import { ModalPortal } from '../components/modal-portal';
import { normalizePolicyRule, PolicyRuleEditor } from '../components/policy-rule-editor';
import type { CommandPolicyRule } from '../types';
export function PoliciesPage() {
const { t, locale } = useI18n();
const { api, session } = useServer();
const canPublish = hasCapability(session?.actor, 'policy_publish');
const resource = useApiResource(() => api.policies(), [], ['policy-published']);
const [command, setCommand] = useState('');
const [playerUuid, setPlayerUuid] = useState('');
const [simulation, setSimulation] = useState<PolicySimulation>();
const [simulationError, setSimulationError] = useState<string>();
const [simulating, setSimulating] = useState(false);
const [publish, setPublish] = useState<CommandPolicySummary>();
const [reason, setReason] = useState('');
const [editorPolicy, setEditorPolicy] = useState<CommandPolicyDetail>();
const commandCatalogResource = useApiResource(
() => api.policyCommands(),
[],
['triggers-changed'],
Boolean(editorPolicy),
);
const [rules, setRules] = useState<CommandPolicyRule[]>([]);
const [draftDescription, setDraftDescription] = useState('');
const [editorBusy, setEditorBusy] = useState(false);
const [validation, setValidation] = useState<PolicyValidation>();
const [editorError, setEditorError] = useState<string>();
const [toast, setToast] = useState<{ message: string; tone: 'good' | 'danger' }>();
const simulate = async (event: FormEvent) => {
event.preventDefault();
if (!command.trim()) return;
setSimulating(true);
try {
setSimulation(await api.simulatePolicy(command.trim(), playerUuid.trim() || undefined));
setSimulationError(undefined);
} catch (error) {
setSimulation(undefined);
setSimulationError(error instanceof Error ? error.message : String(error));
} finally {
setSimulating(false);
}
};
const publishPolicy = async (event: FormEvent) => {
event.preventDefault();
if (!publish || !reason.trim()) return;
try {
const operation = await api.publishPolicy(publish.id, publish.version, reason.trim());
setToast({ message: `${t('operation.queued')} · ${operation.id}`, tone: 'good' });
setPublish(undefined);
setReason('');
resource.reload();
} catch (error) {
setToast({ message: error instanceof Error ? error.message : String(error), tone: 'danger' });
}
};
const openEditor = async (policy: CommandPolicySummary) => {
setEditorBusy(true);
setEditorError(undefined);
setValidation(undefined);
try {
const detail = await api.policy(policy.id);
setEditorPolicy(detail);
setRules(detail.rules.map(normalizePolicyRule));
setDraftDescription(detail.description ?? detail.name);
setValidation(detail.validation);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setEditorError(message);
setToast({ message, tone: 'danger' });
} finally {
setEditorBusy(false);
}
};
const persistPolicy = async (publishImmediately: boolean) => {
const active = resource.data?.items.find((item) => item.state === 'ACTIVE');
if (!active || !draftDescription.trim()) return;
setEditorBusy(true);
setEditorError(undefined);
try {
const operation = await api.stagePolicy(
active.version,
draftDescription.trim(),
rules,
);
if (publishImmediately) {
if (!operation.policy) throw new Error('Server did not return the staged policy version.');
const applied = await api.publishPolicy(
operation.policy.id,
operation.policy.version,
draftDescription.trim(),
);
setToast({ message: `${t('policies.appliedImmediately')} · ${applied.id}`, tone: 'good' });
} else {
setToast({ message: `${t('policies.draftSaved')} · ${operation.id}`, tone: 'good' });
}
setEditorPolicy(undefined);
setValidation(undefined);
resource.reload();
} catch (error) {
setEditorError(error instanceof Error ? error.message : String(error));
} finally {
setEditorBusy(false);
}
};
const saveDraft = (event: FormEvent) => {
event.preventDefault();
void persistPolicy(canPublish);
};
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== 's' || !editorPolicy) return;
event.preventDefault();
if (!editorBusy && draftDescription.trim()) void persistPolicy(canPublish);
};
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, [canPublish, draftDescription, editorBusy, editorPolicy, persistPolicy]);
const validateDraft = async (policy: CommandPolicySummary) => {
setEditorBusy(true);
setEditorError(undefined);
try {
const [result, detail] = await Promise.all([api.validatePolicy(policy.id), api.policy(policy.id)]);
setEditorPolicy(detail);
setRules(detail.rules.map(normalizePolicyRule));
setDraftDescription(detail.description ?? detail.name);
setValidation(result);
setToast({ message: t('policies.validationComplete'), tone: result.valid ? 'good' : 'danger' });
} catch (error) {
setEditorError(error instanceof Error ? error.message : String(error));
} finally {
setEditorBusy(false);
}
};
const validationLine = (entry: string | { ruleId?: string; code?: string; message: string }) =>
typeof entry === 'string' ? entry : [entry.ruleId, entry.code, entry.message].filter(Boolean).join(' · ');
return (
<div className="page">
<PageHeader title={t('policies.title')} description={t('policies.description')} />
<div className="policy-layout">
<Panel title={<><FileLock2 size={18} />{t('policies.title')}</>}>
<ResourceState loading={resource.loading} error={resource.error} empty={resource.data?.items.length === 0} onRetry={resource.reload}>
<TableShell>
<thead><tr><th>{t('policies.name')}</th><th>{t('policies.version')}</th><th>{t('policies.rules')}</th><th>{t('policies.state')}</th><th>{t('policies.updated')}</th><th /></tr></thead>
<tbody>{resource.data?.items.map((policy) => (
<tr key={`${policy.id}:${policy.version}`}>
<td><strong>{policy.name}</strong>{policy.description && <small className="table-subline">{policy.description}</small>}</td>
<td><code>v{policy.version}</code></td><td>{policy.groupCount ?? (policy.ruleCount > 0 ? 1 : 0)} {t('policies.groups')} · {policy.ruleCount} {t('policies.rules')}</td>
<td><Badge tone={policy.state === 'ACTIVE' ? 'good' : policy.state === 'DRAFT' ? 'warn' : 'neutral'}>{policy.state}</Badge></td>
<td>{formatDate(policy.updatedAt, locale)}</td>
<td><div className="button-row"><Button variant="ghost" disabled={editorBusy} onClick={() => void openEditor(policy)}><Pencil size={14} />{t('policies.edit')}</Button>{policy.state === 'DRAFT' && <><Button variant="ghost" disabled={editorBusy} onClick={() => void validateDraft(policy)}><CheckCircle2 size={14} />{t('policies.validate')}</Button>{canPublish && <Button variant="secondary" onClick={() => setPublish(policy)}><Rocket size={14} />{t('policies.publish')}</Button>}</>}</div></td>
</tr>
))}</tbody>
</TableShell>
</ResourceState>
</Panel>
<Panel title={<><Beaker size={18} />{t('policies.simulator')}</>} className="simulator-panel">
<form className="form-stack" onSubmit={simulate}>
<Field label={t('policies.command')}><Input value={command} onChange={(event) => setCommand(event.target.value)} placeholder="execute as @e[type=minecraft:zombie] run tp @s ~ ~1 ~" required spellCheck={false} /></Field>
<Field label={t('policies.playerUuid')}><Input value={playerUuid} onChange={(event) => setPlayerUuid(event.target.value)} placeholder="00000000-0000-0000-0000-000000000000" /></Field>
<Button type="submit" disabled={simulating || !command.trim()}><SearchCode size={16} />{t('policies.run')}</Button>
</form>
<div className="simulation-result">
<span className="field__label">{t('policies.result')}</span>
{simulation ? <><Badge tone={simulation.decision === 'DENY' ? 'danger' : simulation.decision === 'GRANT' ? 'good' : 'info'}>{simulation.decision}</Badge><code className="command-normalized">{simulation.normalizedCommand ?? command}</code><ol>{simulation.explanation.map((line, index) => <li key={`${index}:${line}`}>{line}</li>)}</ol></> : simulationError ? <p className="error-text">{simulationError}</p> : <p className="muted">{t('policies.noResult')}</p>}
</div>
</Panel>
</div>
{editorPolicy && <Panel title={<><FileJson2 size={18} />{t('policies.editor')}</>}>
<form className="form-stack" onSubmit={saveDraft}>
<div className="confirm-box"><FileLock2 /><div><p>{t('policies.editorHint')}</p><p>{t('policies.liveHint')}</p></div></div>
<Field label={t('policies.draftDescription')}><Input value={draftDescription} onChange={(event) => setDraftDescription(event.target.value)} maxLength={1000} required /></Field>
<PolicyRuleEditor rules={rules} onChange={(next) => { setRules(next); setValidation(undefined); }}
commandCatalog={commandCatalogResource.data?.commands ?? []}
commandCatalogLoading={commandCatalogResource.loading}
commandCatalogError={commandCatalogResource.error}
onReloadCommandCatalog={commandCatalogResource.reload} />
{validation && <div className={`confirm-box ${validation.valid ? '' : 'confirm-box--danger'}`}><CheckCircle2 /><div><strong>{validation.valid ? t('policies.valid') : t('policies.invalid')}</strong>{[...validation.errors, ...validation.warnings].map((entry, index) => <p key={index}>{validationLine(entry)}</p>)}</div></div>}
{editorError && <p className="error-text" role="alert">{editorError}</p>}
<div className="button-row"><Button type="button" variant="ghost" onClick={() => { setEditorPolicy(undefined); setEditorError(undefined); }}>{t('common.cancel')}</Button>{canPublish && <Button type="button" variant="secondary" disabled={editorBusy || !draftDescription.trim()} onClick={() => void persistPolicy(false)}><Save size={15} />{t('policies.saveDraft')}</Button>}<Button type="submit" disabled={editorBusy || !draftDescription.trim()}>{canPublish ? <Rocket size={15} /> : <Save size={15} />}{canPublish ? t('policies.saveAndApply') : t('policies.saveDraft')}</Button><span className="keyboard-hint"><kbd>Ctrl</kbd>+<kbd>S</kbd></span></div>
</form>
</Panel>}
{publish && <ModalPortal><div className="modal-backdrop"><section className="modal" role="dialog" aria-modal="true"><div className="modal__header"><div><span className="page-header__eyebrow">{publish.id} · v{publish.version}</span><h2>{t('policies.publish')}</h2></div><button onClick={() => setPublish(undefined)} aria-label={t('common.close')}><X /></button></div><form className="form-stack" onSubmit={publishPolicy}><div className="confirm-box"><CheckCircle2 /><p>{t('policies.publishWarning')}</p></div><Field label={t('common.reason')}><Textarea required value={reason} onChange={(event) => setReason(event.target.value)} /></Field><div className="modal__actions"><Button type="button" variant="ghost" onClick={() => setPublish(undefined)}>{t('common.cancel')}</Button><Button type="submit" disabled={!reason.trim()}><Rocket size={15} />{t('policies.publish')}</Button></div></form></section></div></ModalPortal>}
{toast && <Toast {...toast} onClose={() => setToast(undefined)} />}
</div>
);
}