import { useEffect, useRef, useState } from 'react'; import { AlertTriangle, FileLock2, Loader2, Pencil, RotateCcw, Save, ShieldCheck } from 'lucide-react'; import { timeAgo } from './format'; import { useHeadscalePolicy, policySaveFailure, type PolicySaveFailure } from './useHeadscalePolicy'; import { PolicyAssistant } from './PolicyAssistant'; import { Card, SectionHeader, Button, ErrorNote } from './Cards'; import { ViewShell } from './ViewShell'; // The tailnet's ACL document. A plain textarea on purpose — this is HuJSON, where the comments and the // hand-kept alignment are half the document's value to whoever maintains it, and a rich editor that // reformats or a client-side parser that disagrees with Headscale would both destroy more than they add. // // It opens READ-ONLY behind an Edit button. This is the document that decides which machine can reach // which, it is usually being looked at rather than changed, and a textarea focused by a stray click is a // way to alter it without meaning to. Edit mode also brings up the assistant, because "I do not know what // this file should look like" is the actual reason this screen was hard to use. // // Validation is entirely Headscale's. It has the only parser that counts: it resolves groups, tags and // host aliases, and it is what will actually enforce the result. Officer sends the text up untouched and // shows the verdict verbatim — including the line and column, which is the whole reason to show it at all. // // Two failures, deliberately styled differently. A REJECTED document is a normal part of editing and stays // inline next to the save button. A READ-ONLY server means this screen cannot do its job at all and says so // at the top, permanently, because the owner needs to go and edit a file on the server instead. /** Ctrl/Cmd-S while the textarea has focus. An ACL is long enough that reaching for the button breaks flow. */ function useSaveShortcut(onSave: () => void, enabled: boolean) { const handler = useRef(onSave); handler.current = onSave; useEffect(() => { if (!enabled) return; const onKeyDown = (ev: KeyboardEvent) => { if ((ev.metaKey || ev.ctrlKey) && ev.key.toLowerCase() === 's') { ev.preventDefault(); handler.current(); } }; window.addEventListener('keydown', onKeyDown); return () => window.removeEventListener('keydown', onKeyDown); }, [enabled]); } const ReadOnlyBanner = ({ message }: { message: string }) => (
Headscale said: {message}
It is reading its policy from a file on disk rather than from its database, so the API refuses writes — a save here would be overwritten on the next restart anyway. Edit the file on the server (the Console section is one way in) and reload it there. Everything below is still the live document, and still readable.
{message}
Nothing was saved — the tailnet is still running the previous policy.
This document decides which node may reach which. A policy that saves cleanly can still cut a machine off — Headscale checks that the document is valid, not that it is what you meant. {editing ? ' Ctrl/Cmd-S saves.' : ' Press Edit to change it.'}