// Administrator console I — people and access (WP-U08).
//
// The countersigned grant lifecycle, end to end: grant, countersign, revoke,
// every act visible on the chain.
//
// THREE RULES THIS SCREEN HAS TO GET RIGHT, and each is a critical failure if
// it does not:
//
//   1. A PENDING LEGAL GRANT MUST NEVER LOOK EFFECTIVE. Amber, not green. A
//      screen that shows a countersigned-pending grant as live is the
//      countersign rule undone in pixels — the administrator walks away
//      believing they have given somebody access, and nobody finds out until
//      that person cannot sign in.
//
//   2. REVOKE MUST NOT PROMISE MORE THAN THE SERVICE DELIVERS. WP-U05 promises
//      revocation is honoured at the NEXT REQUEST, not instantly: a request
//      already in flight completes. This screen says exactly that, and the copy
//      is load-bearing rather than decorative.
//
//   3. DORMANCY IS ABOUT ACTS, NOT SIGN-INS. Somebody who signs in every day
//      and does nothing is dormant where it matters. The read model measures it
//      from the audit chain; this screen must not re-describe it as "last seen".

const { useState, useRef } = React;

const ROLES = ['viewer', 'requester', 'procurement', 'auditor', 'legal_reviewer',
               'legal_admin', 'administrator'];
const LEGAL_ROLES = new Set(['legal_reviewer', 'legal_admin']);

// The one place a role name is turned into a sentence. Kept together so the
// screen cannot say "needs approval" in one spot and "pending" in another for
// the same state.
//
// PENDING IS DECIDED BY THE QUEUE, NOT INFERRED FROM THE ROLE. The first version
// of this function reasoned "a Legal role with no effective grant must be
// awaiting a countersign", which is wrong in the most misleading possible
// direction: somebody whose access had been REVOKED rendered as awaiting a
// second name. That reads as "almost there, somebody just needs to approve it"
// when the truth is the opposite — their access was deliberately taken away.
// Found by revoking a reviewer and looking at the screen.
//
// So the caller passes the set of genuinely pending people, straight from
// cw.countersign_pending, and the chip states what the record says rather than
// what the role implies.
function accessChip(p, pendingPeople) {
  if (p.state === 'revoked') return <span className="chip chip-err">revoked</span>;
  if (p.effective_role) return <span className="chip chip-ok">effective</span>;
  // AMBER, and only when a countersign really is outstanding. Never chip-ok,
  // and never merely a lighter green.
  if (pendingPeople.has(p.person))
    return <span className="chip chip-pending">awaiting countersign</span>;
  // No effective role and nothing pending: their grant was revoked, or they
  // never had one. Either way there is nobody to chase.
  return <span className="chip chip-unknown">no access</span>;
}

function activityChip(p) {
  if (p.activity_state === 'active') return null;
  if (p.activity_state === 'dormant')
    return <span className="chip chip-pending" title="No recorded act in 90 days">dormant</span>;
  if (p.activity_state === 'never acted')
    return <span className="chip chip-unknown" title="This account has never done anything">never used</span>;
  return null;
}

// ── The countersign queue ────────────────────────────────────────────────
// ONE component, rendered in TWO places: the administrator's console, where the
// person who proposed the grant can see what they are waiting on, and the Legal
// reviewer's and Legal admin's own review desk, where the people who must clear
// it actually work.
//
// That second placement is the whole reason this is a shared component rather
// than a section of the console. A queue that lives only in the admin console —
// a screen Legal has no reason to open — is a queue that does not get cleared,
// and the countersign rule's entire cost is the wait it adds. ADR-0011 leans on
// this to keep that wait short.
function CountersignQueue({ me, rows, onDone, onError, showAdminNote, routes }) {
  // One act at a time. useActs guards with a ref, so a second click in the
  // same tick never reaches the network — `disabled` alone cannot, because it
  // only takes effect after a render.
  const acts = useActs();

  return (
    <div>
      <PanelHead
        title="Countersign queue"
        sub="Proposed Legal roles. Each confers nothing at all while it sits here."
      />
      <WaitingList
        order="oldest"
        items={rows.map((g) => ({
          key: g.grant_id,
          // BOTH NAMES on the row. Who is being granted what, and who proposed
          // it — a countersign is a second person's judgement about a first
          // person's proposal, and it cannot be given without seeing both.
          title: `${g.display_name || g.person} → ${g.role}`,
          sub: `proposed by ${g.proposed_by}${g.reason ? ` · ${g.reason}` : ''}`,
          at: g.proposed_at,
          chips: (
            <>
              <span className="chip chip-pending">pending</span>
              {me.role === 'legal_admin' && (
                <button
                  className="btn btn-sm"
                  data-testid={`countersign-${g.grant_id}`}
                  disabled={acts.busy !== null}
                  onClick={() => acts.run(`countersign-${g.grant_id}`, async () => {
                    onError(null);
                    const r = await API.countersign({ grant_id: g.grant_id });
                    if (!r.ok) onError(r.reason); else onDone();
                  })}
                >✓ countersign</button>
              )}
              {/* THE ADMINISTRATOR'S HALF OF THE SAME ROW (NT-3). They cannot
                  clear this queue — that is the point of it — and until now
                  that left them watching a grant confer nothing for a week
                  with no act available. Now they can raise it to the people
                  who can clear it. Nothing about the grant changes. */}
              <RaiseNotice
                me={me} routes={routes}
                subject={{ kind: 'account', ref: g.person,
                           about: `${g.display_name || g.person}'s uncountersigned ${g.role} grant` }} />
            </>
          ),
        }))}
        empty={<Empty
          kicker="countersign queue"
          line="Nothing is waiting for a second name."
          sub="Grants of the two Legal roles appear here until a Legal admin accepts them. Until then they confer nothing — not a lesser role, nothing." />}
      />
      {showAdminNote && rows.length > 0 && (
        <div className="caption mt-2">
          You cannot clear this queue yourself — that is the point of it. The same
          list is in every Legal admin's own workspace.
        </div>
      )}
    </div>
  );
}

// ── Grant ────────────────────────────────────────────────────────────────
function GrantForm({ people, onDone, onError }) {
  const [person, setPerson] = useState('');
  const [name, setName] = useState('');
  const [unit, setUnit] = useState('');
  const [role, setRole] = useState('requester');
  const [reason, setReason] = useState('');
  const [busy, setBusy] = useState(false);

  const existing = people.find((p) => p.person === person.trim());

  const submit = async (e) => {
    e.preventDefault();
    if (busy || !person.trim() || !role) return;
    setBusy(true); onError(null);

    // TWO ACTS, NOT ONE, and deliberately not bundled behind a single button
    // press that reports one outcome. Creating an account and granting a role
    // are separate recorded acts; if the second is refused, the first still
    // happened and the screen must be able to say so.
    if (!existing) {
      const made = await API.createAccount({
        person: person.trim(), display_name: name.trim() || person.trim(),
        unit: unit.trim() || null, role,
      });
      if (!made.ok) { setBusy(false); onError(made.reason); return; }
    }
    const granted = await API.grant({
      person: person.trim(), role, reason: reason.trim() || null,
    });
    setBusy(false);
    if (!granted.ok) { onError(granted.reason); return; }
    setPerson(''); setName(''); setUnit(''); setReason('');
    onDone();
  };

  return (
    <form onSubmit={submit} className="panel p-4">
      <PanelHead
        title="Grant access"
        sub="One person, one role. A second role is a revoke and a grant, both recorded."
      />
      <div className="grid grid-cols-2 gap-3">
        <div>
          <label className="section-label">Person</label>
          <input aria-label="Person" className="mt-1.5 w-full font-mono" placeholder="name@clausewerk"
                 value={person} onChange={(e) => setPerson(e.target.value)} />
        </div>
        <div>
          <label className="section-label">Name</label>
          <input aria-label="Name" className="mt-1.5 w-full" placeholder="Their name"
                 value={name} onChange={(e) => setName(e.target.value)}
                 disabled={!!existing} />
        </div>
        <div>
          <label className="section-label">Unit</label>
          <input aria-label="Unit" className="mt-1.5 w-full" placeholder="Procurement"
                 value={unit} onChange={(e) => setUnit(e.target.value)}
                 disabled={!!existing} />
        </div>
        <div>
          <label className="section-label">Role</label>
          <select aria-label="Role" className="mt-1.5 w-full font-mono" value={role}
                  onChange={(e) => setRole(e.target.value)}>
            {ROLES.map((r) => <option key={r} value={r}>{r}</option>)}
          </select>
        </div>
      </div>

      <div className="mt-3">
        <label className="section-label">Why</label>
        <input aria-label="Why" className="mt-1.5 w-full" placeholder="Joining the contracts team"
               value={reason} onChange={(e) => setReason(e.target.value)} />
      </div>

      {/* Said BEFORE the button, not after the fact. Somebody granting a Legal
          role needs to know it will not take effect yet — otherwise they tell
          the new joiner they are set up, and the joiner cannot sign in. */}
      {LEGAL_ROLES.has(role) && (
        <div className="mt-3 panel-2 p-3">
          <div className="tag" style={{ color: 'var(--accent-2)' }}>takes two names</div>
          <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)', lineHeight: 1.6 }}>
            This grant will confer <strong>nothing at all</strong> until a Legal
            admin countersigns it. They will not be able to sign in until then.
            Access to Legal judgement is itself a Legal judgement.
          </div>
        </div>
      )}

      {existing && (
        <div className="caption mt-3">
          {existing.person} already has an account. This grants them a role;
          it does not create a second account.
        </div>
      )}

      <button className="btn btn-primary mt-4" type="submit"
              disabled={busy || !person.trim()}>
        {busy ? 'recording…' : '✓ grant'}
      </button>
    </form>
  );
}

// ── Revoke ───────────────────────────────────────────────────────────────
function RevokeDialog({ person, grantId, onClose, onDone, onError }) {
  const [reason, setReason] = useState('');
  const [busy, setBusy] = useState(false);

  return (
    <div className="panel p-4 mt-4">
      <PanelHead title={`Revoke ${person}`}
                 sub="A revocation is a new row, never an edit. It cannot be undone." />
      <label className="section-label">Why</label>
      <input aria-label="Why" className="mt-1.5 w-full" autoFocus placeholder="Left the company"
             value={reason} onChange={(e) => setReason(e.target.value)} />

      {/* The copy that must not over-promise. WP-U05 delivers revocation at the
          NEXT REQUEST, and this screen says so rather than implying the person
          is thrown out mid-keystroke. The test asserts this sentence is here. */}
      <div className="panel-2 p-3 mt-3">
        <div className="tag">what happens</div>
        <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)', lineHeight: 1.6 }}>
          Their role stops applying <strong>at their next request</strong>. A
          request already in flight will finish. If they have a page open it will
          keep showing what it has already loaded until they touch anything —
          then they are signed out.
          <br /><br />
          Bringing them back later is a new grant, recorded as one.
        </div>
      </div>

      <div className="flex gap-2 mt-4">
        <button className="btn" onClick={onClose}>cancel</button>
        <ActButton
          className="btn btn-primary"
          disabled={busy || !reason.trim()}
          onClick={async () => {
            setBusy(true); onError(null);
            const r = await API.revokeGrant({ grant_id: grantId, reason: reason.trim() });
            setBusy(false);
            if (!r.ok) { onError(r.reason); return; }
            onDone();
          }}
        >
          {busy ? 'recording…' : '✓ revoke'}
        </ActButton>
      </div>
    </div>
  );
}

// ── Closing an account, which is NOT revoking a grant ────────────────────
//
// WHAT WAS MISSING. `POST /accounts/revoke` has been served since `0013` and
// no screen has ever offered it — it sat on the `REACHED_BY_NO_SCREEN` ledger
// in `db/test/a-built-thing-has-a-way-in.test.mjs`. So when somebody left the
// company an Administrator could take away every role they held and could not
// close the account itself.
//
// AND `0013` GIVES THE ADVICE NOBODY COULD FOLLOW. Its own trigger refuses a
// change of identity with the words *"revoke this account and create the
// correct one"* — a repair the application offered no way to perform. That is
// the locked-door-with-no-key shape this repository keeps finding.
//
// ── THE TWO ACTS ARE DIFFERENT AND THE SCREEN MUST SAY SO ─────────────────
//
//   REVOKING A GRANT takes away one role. The person keeps their account, can
//   still sign in, and can be granted something else tomorrow. It is a new row
//   in `cw.role_grant` and it carries a REASON.
//
//   CLOSING AN ACCOUNT ends the person's presence in the system. `0013` makes
//   it terminal: `cw.account_provenance_immutable()` refuses to un-revoke, and
//   `cw.account_no_delete()` refuses to delete, so the row stays forever as
//   part of the access history. Coming back is a NEW account.
//
// Two controls that both said "revoke" would be a trap, so the grant control
// is now labelled `revoke role` and this one `close account`.
//
// ── THE REASON, WHICH THIS FORM REFUSED TO INVENT AND NOW REQUIRES ───────
//
// Until 2026-08-25 this form asked for no reason and said so, because
// `POST /accounts/revoke` took `person` and nothing else and `cw.account` had
// nowhere to put one. That was the honest render of the schema as it stood,
// and the note here said giving it a reason was "a migration and somebody's
// decision, not a screen's to invent".
//
// MIKE MADE THAT DECISION THE SAME DAY: closing an account records a reason.
// `0117` adds the column and `cw.account_closure_says_why()` REFUSES a closure
// performed through the application without one — so this field is required
// here because the database requires it, not merely because the form does.
//
// The reason is also settled once written: the same trigger refuses to let it
// be rewritten afterwards, because an edited reason would read as
// contemporaneous evidence and would not be.
function CloseAccountDialog({ person, displayName, onClose, onDone, onError }) {
  const [confirm, setConfirm] = useState('');
  const [reason, setReason] = useState('');
  const [busy, setBusy] = useState(false);
  // TYPE THE ADDRESS TO ARM IT. The same guard every terminal act in this
  // product wants and this one needs most: it cannot be undone by anybody,
  // including the person who did it by accident, and the rows in this list
  // differ by one word in a small font.
  // BOTH, since 0117. The typed address is the guard against closing the wrong
  // account; the reason is what the record keeps. Neither substitutes for the
  // other, and the database refuses a closure with no reason whatever this
  // form does.
  const armed = confirm.trim().toLowerCase() === person.toLowerCase()
                && reason.trim() !== '';

  return (
    <div className="panel p-4 mt-4" style={{ borderColor: 'var(--danger)' }}>
      <PanelHead title={`Close ${displayName || person}'s account`}
                 sub="This ends their access entirely and cannot be undone by anybody. It is not the same as taking away a role." />

      <div className="panel-2 p-3 mt-3">
        <div className="tag">what happens</div>
        <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)', lineHeight: 1.6 }}>
          They stop being able to act <strong>at their next request</strong>. A
          request already in flight will finish, and a page they have open keeps
          showing what it has already loaded until they touch anything.
          <br /><br />
          The account is <strong>never deleted</strong> — the record of who had
          access is part of the access history, so the row stays and is marked
          closed. <strong>It cannot be re-opened.</strong> Bringing this person
          back means a new account, recorded as one.
          <br /><br />
          The record keeps <strong>who closed it, when, and why</strong>. The
          reason is settled the moment it is written — it cannot be edited
          afterwards, because a reason changed later would look like
          contemporaneous evidence and would not be.
        </div>
      </div>

      <label className="section-label mt-3">Why</label>
      <input aria-label="Why" className="mt-1.5 w-full" data-testid="closure-reason"
             placeholder="Left the company"
             value={reason} onChange={(e) => setReason(e.target.value)} />
      <div className="caption mt-1">
        Required, and permanent. This is the access record's answer to
        &ldquo;why is this person gone&rdquo; for as long as the record exists.
      </div>

      <label className="section-label mt-3">Type <span className="font-mono">{person}</span> to confirm</label>
      <input aria-label="Type the address to confirm" className="mt-1.5 w-full" autoFocus
             placeholder={person}
             value={confirm} onChange={(e) => setConfirm(e.target.value)} />

      <div className="flex gap-2 mt-4">
        <button className="btn" onClick={onClose}>cancel</button>
        <ActButton
          className="btn btn-primary"
          disabled={busy || !armed}
          onClick={async () => {
            setBusy(true); onError(null);
            const r = await API.revokeAccount({ person, reason: reason.trim() });
            setBusy(false);
            if (!r.ok) { onError(r.reason); return; }
            onDone();
          }}
        >
          {busy ? 'closing…' : '✓ close this account'}
        </ActButton>
      </div>
    </div>
  );
}

// WHERE A PERSON'S NOTICES GO (0042, reached 2026-08-25).
//
// The table has existed since OB-09 and `notifications.py` has always read it
// to decide where to send. Nothing in the application could write one, so the
// only addresses in any database were put there by a seed script or a test —
// which is to say a notice had nowhere to go for any real person.
//
// TWO ACTS, NOT ONE, BECAUSE THE SCHEMA SAYS SO. `cw.notification_address` is
// history: its trigger refuses every edit to a live row and refuses to revisit
// a removal, so changing an address is a removal followed by a setting, and the
// record keeps both with the name of whoever chose each. This form does exactly
// that, in that order, rather than offering an "edit" the database would refuse
// — an affordance whose only outcome is a refusal is worse than none.
//
// SELF-SERVICE IS NOT WITHHELD BY THIS SCREEN, it is withheld by the policy:
// 0042's own words are that a person who can redirect their own notifications
// can silence their own countersign nudges. The database refuses every role but
// the Administrator whatever this screen chooses to draw.
function AddressDialog({ person, displayName, current, onClose, onDone, onError }) {
  const [address, setAddress] = useState('');
  const [busy, setBusy] = useState(false);
  const armed = address.trim() !== '' && address.trim() !== (current || '');

  return (
    <div className="panel p-4 mt-4">
      <PanelHead
        title={`Where ${displayName || person}'s notices go`}
        sub="One live address per person. Setting a new one takes the old one off the record first, and the record keeps both." />

      <div className="panel-2 p-3 mt-3">
        <div className="tag">what happens</div>
        <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)', lineHeight: 1.6 }}>
          {current
            ? <>The address on file is <span className="font-mono">{current}</span>.{' '}
                It is <strong>taken off first</strong> and the new one set after
                it, because an address row is never edited — so the record says
                who chose each one, and when.</>
            : <>Nothing can reach this person today. Until an address is set,
                anything waiting on them stays waiting and no notice is sent.</>}
          <br /><br />
          This is <strong>not</strong> how they sign in, and changing it grants
          nothing and takes nothing away. It is only where their notices are
          delivered.
        </div>
      </div>

      <label className="section-label mt-3">Email address</label>
      <input aria-label="Email address" className="mt-1.5 w-full" autoFocus
             data-testid="address-value"
             placeholder="somebody@example.com"
             value={address} onChange={(e) => setAddress(e.target.value)} />

      <div className="flex gap-2 mt-4">
        <button className="btn" onClick={onClose}>cancel</button>
        <ActButton
          className="btn btn-primary"
          disabled={busy || !armed}
          data-testid="address-save"
          onClick={async () => {
            setBusy(true); onError(null);
            // THE REMOVAL FIRST, AND ONLY IF THERE IS ONE. The live-address
            // unique index refuses a second row for the same person and
            // channel, so setting without removing is refused by the database
            // — correctly, and with a sentence about an index that would tell
            // the reader nothing about what to do next.
            if (current) {
              const off = await API.removeNotificationAddress({ person });
              if (!off.ok) { setBusy(false); onError(off.reason); return; }
            }
            const r = await API.setNotificationAddress(
              { person, address: address.trim() });
            setBusy(false);
            if (!r.ok) { onError(r.reason); return; }
            onDone();
          }}
        >
          {busy ? 'saving…' : current ? '✓ replace the address' : '✓ set the address'}
        </ActButton>
      </div>
    </div>
  );
}

// ── The pane ─────────────────────────────────────────────────────────────
// THE CATALOGUE OF NARROWINGS THIS LIST OFFERS (0110), declared once and read
// twice — by the figures that raise them, and by the saved views that put them
// back. A focus is a `test` function and no store holds one, so what a saved
// view keeps is the KEY; this is where a key becomes a test again. Declared
// outside the component so it is the same array on every render.
const PEOPLE_FOCUSES = [
  { key: 'access',  label: 'people with access',
    test: (p) => Boolean(p.effective_role) },
  { key: 'dormant', label: 'dormant or never used',
    test: (p) => p.activity_state === 'dormant' || p.activity_state === 'never acted' },
  { key: 'revoked', label: 'revoked',
    test: (p) => p.activity_state === 'revoked' },
];
const peopleFocus = (k) => PEOPLE_FOCUSES.find((f) => f.key === k);

function PeopleAndAccessConsole({ me }) {
  const activity = usePane(() => API.peopleActivity());
  const summary  = usePane(() => API.accessSummary());
  const queue    = usePane(() => API.countersignQueue());
  const history  = usePane(() => API.accessHistory());
  // The permitted raiser -> recipient pairs (0064), for the raise controls on
  // the countersign queue. Read once here rather than per row.
  const routes   = usePane(() => API.noticeRoutes());
  // WHERE EACH PERSON CAN BE REACHED (0042). Read here rather than per row, for
  // the reason `routes` gives just above it: one request, not one each.
  const addresses = usePane(() => API.notificationAddresses());
  const [error, setError] = useState(null);
  const [revoking, setRevoking] = useState(null);
  const [closing, setClosing] = useState(null);
  const [addressing, setAddressing] = useState(null);
  // THE FIGURES ABOVE NARROW THIS LIST. Hooks, so above the early returns —
  // usePane answers an empty `rows` while it loads and this must still be the
  // same call on every render.
  const filter = useListFilter(activity.rows, {
    view: 'people:access',
    focuses: PEOPLE_FOCUSES,
    fields: ['display_name', 'person', 'unit', 'declared_role', 'effective_role'],
    facet: 'activity_state',
  });
  // The countersign queue is a DIFFERENT list on the same page, so its figure
  // takes somebody there rather than filtering people (see the strip below).
  const queueRef = useRef(null);

  const reloadAll = () => {
    activity.reload(); summary.reload(); queue.reload(); history.reload();
    addresses.reload();
  };

  if (activity.status === 'loading') return <Loading />;
  if (activity.status === 'failed') return <LoadFailed reason={activity.reason} />;

  const people = activity.rows;
  const s = summary.rows[0] ?? {};
  // Straight from cw.countersign_pending — the record of what is genuinely
  // outstanding, rather than an inference from the role somebody was given.
  const pendingPeople = new Set(
    (queue.status === 'loaded' ? queue.rows : []).map((g) => g.person));

  // The grant row a person's access hangs on, needed to revoke it. Taken from
  // the access history rather than held on the account, because the grant is
  // the thing being revoked and the account is not.
  //
  // NEWEST live grant, deliberately, because that is the one cw.effective_role
  // confers (0013: newest wins). The history arrives newest-first, and this
  // used to take the LAST element — the OLDEST live grant. With two live
  // grants (a normal state: granting a second role does not revoke the first)
  // the revoke succeeded, the dialog closed, and the person's effective role
  // was untouched: access the administrator believed removed was still live.
  // THE ADDRESS ON FILE, AND THE ONE THAT WAS TAKEN OFF. The read carries the
  // whole record, newest first, so the live row is the one with no removal and
  // the fallback is the newest row of any kind. Two silences, not one: "nobody
  // ever gave this person an address" and "somebody took theirs off" are
  // different facts, and only the second names somebody to ask.
  const addressFor = (person) => {
    if (addresses.status !== 'loaded') return { live: null, last: null };
    const mine = addresses.rows.filter((a) => a.person === person);
    const live = mine.find((a) => !a.removed_at);
    return { live: live || null, last: live ? null : (mine[0] || null) };
  };

  const liveGrantFor = (person) => {
    if (history.status !== 'loaded') return null;
    const acts = history.rows.filter((g) => g.person === person);
    const granted = acts.filter((g) => g.action === 'granted');
    const revoked = new Set(acts.filter((g) => g.action === 'revoked').map((g) => g.grant_ref));
    const live = granted.filter((g) => !revoked.has(g.grant_id));
    return live.length ? live[0].grant_id : null;
  };

  return (
    <div>
      {/* THE PAGE TITLE LEADS. Converting the people table's own head in place
          left the h1 at y552, under a section head at y272 — a page titled
          half way down itself. The table keeps a head, as a section. */}
      <PaneHead
        title="People and access"
        kicker="Administration"
        sub="Who can act, what they may do, and the countersign that stands between a grant and its use." />

      {/* ── FIVE FIGURES, AND FOUR OF THEM NOW GO SOMEWHERE ───────────────
          Every number here counted a set nobody could see. "1 dormant" over a
          list of six was a search-by-eye; over sixty it is a search by hand
          through a table with no search box, which this pane did not have.

          THE NUMBERS STAY THE DATABASE'S. `cw.access_summary` computes them,
          and the drill selects the same set out of `cw.person_activity` — the
          two were checked against each other on the demonstration database
          before this was written (6 / 0 / 1 / 0 both ways). Recomputing the
          figures in the browser would have made each tile agree with its own
          drill by construction, at the price of quietly replacing a claim the
          DATABASE makes with one this screen makes — and `shared accounts`
          cannot be recomputed here at all, which is the whole point of it.

          `awaiting countersign` IS NOT A FILTER, and this is the one that
          would have been wrong. It counts `cw.countersign_pending` — GRANTS —
          and one person can hold two of them. Narrowing the people list to
          "the 3 awaiting countersign" could honestly show two rows. So it
          takes you to the queue below, which is the list those rows are in. */}
      <TileStrip tiles={[
        { label: 'people with access', n: s.people_with_access,
          to: () => filter.focusOn(peopleFocus('access')),
          on: Boolean(filter.focus && filter.focus.key === 'access'),
          describe: `show the ${s.people_with_access} people who hold a role` },
        { label: 'awaiting countersign', n: s.awaiting_countersign,
          to: showSection(queueRef),
          describe: `go to the countersign queue, which holds ${s.awaiting_countersign} grants` },
        { label: 'dormant or never used', n: s.dormant,
          to: () => filter.focusOn(peopleFocus('dormant')),
          on: Boolean(filter.focus && filter.focus.key === 'dormant'),
          describe: `show the ${s.dormant} people who have recorded nothing lately` },
        { label: 'revoked', n: s.revoked,
          to: () => filter.focusOn(peopleFocus('revoked')),
          on: Boolean(filter.focus && filter.focus.key === 'revoked'),
          describe: `show the ${s.revoked} people whose access was revoked` },
        // ADR-0008's residual, paid off and measured rather than hoped for.
        // INERT ON PURPOSE, and the only one: there is no row behind a nought
        // that is nought by construction. A tile that led to an empty list
        // would suggest the set could be non-empty one day. It cannot.
        { label: 'shared accounts', n: s.shared_accounts },
      ]} />
      <div className="caption mt-2">
        Shared accounts is nought by construction — one row per named person is
        the accounts table's primary key, not a habit. Dormancy counts people
        with no <em>recorded act</em>, not people who have not signed in.
        Awaiting countersign counts <em>grants</em>, not people, so it takes you
        to the queue rather than narrowing the list below.
      </div>

      {error && (
        <div className="panel p-3 mt-4" style={{ borderColor: 'var(--danger)' }}>
          <div className="tag" style={{ color: 'var(--danger)' }}>refused</div>
          {/* The database's own sentence, unchanged. It names the rule. */}
          <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)' }}>{error}</div>
        </div>
      )}

      {queue.status === 'loaded' && (
        <div className="mt-6" ref={queueRef}>
          <CountersignQueue
            me={me} rows={queue.rows}
            onDone={reloadAll} onError={setError}
            routes={routes.rows}
            showAdminNote={me.role === 'administrator'}
          />
        </div>
      )}

      {/* The people table. */}
      <div className="mt-6">
        <PanelHead
          title="Everybody with a grant"
          sub="Who holds what, who gave it to them, and when they last did anything."
        />
        {/* The shared controls. Below eight rows ListFilter draws only a focus
            chip, so a six-person instance gets no furniture it does not need
            and a sixty-person one gets a search box the day it grows one. */}
        <ListFilter filter={filter} testid="people"
                    placeholder="name, address, unit or role"
                    facetLabel="every state" />
        <div className="panel">
          {filter.shown.length === 0 && <NoMatch />}
          {filter.shown.map((p) => (
            <div className="waiting-row people-row" key={p.person}>
              <div className="min-w-0">
                <div className="text-[13px] truncate" style={{ color: 'var(--ink)' }}>
                  {p.display_name}
                  <span className="font-mono ml-2" style={{ fontSize: 11, color: 'var(--mute-2)' }}>
                    {p.person}
                  </span>
                </div>
                {/* WHY THEY ARE GONE, ON THE ROW (0117). A reason that is
                    required, stored and permanent and shown nowhere is a
                    write-only field dressed as a record. Drawn only on a
                    closed account, and only when there IS one: accounts closed
                    before 0117 have none, and they say so rather than
                    rendering an empty quotation. NOT truncated, unlike the
                    line below it — this is the sentence somebody came to
                    read. */}
                {p.state === 'revoked' && (
                  <div className="caption mt-0.5" data-testid="closure-reason-shown">
                    closed{p.revoked_by ? ` by ${p.revoked_by}` : ''}
                    {p.revoked_at ? ` · ${since(p.revoked_at)}` : ''}
                    {' · '}
                    {p.revoked_reason
                      ? <em>{p.revoked_reason}</em>
                      : <span style={{ color: 'var(--mute-2)' }}>
                          no reason recorded — closed before the record kept one
                        </span>}
                  </div>
                )}
                {/* WHERE THEIR NOTICES GO, ON THE ROW. Drawn once the read
                    has landed, and only for a live account: an address on a
                    closed account is a fact about somebody nothing can be
                    waiting on. It says which of the three states it is rather
                    than rendering nothing — silence here would read as
                    "reachable", which is the one thing it must never mean. */}
                {addresses.status === 'loaded' && p.state === 'active' && (() => {
                  const a = addressFor(p.person);
                  return (
                    <div className="caption mt-0.5 truncate"
                         data-testid="notice-address">
                      {a.live
                        ? <>notices go to <span className="font-mono">{a.live.address}</span></>
                        : a.last
                          ? <span style={{ color: 'var(--mute-2)' }}>
                              no address — <span className="font-mono">{a.last.address}</span>{' '}
                              was taken off{a.last.removed_by ? ` by ${a.last.removed_by}` : ''}
                            </span>
                          : <span style={{ color: 'var(--mute-2)' }}>
                              no address — nothing can reach them
                            </span>}
                    </div>
                  );
                })()}
                <div className="caption mt-0.5 truncate">
                  {p.unit ? `${p.unit} · ` : ''}
                  granted by {p.granted_by || p.created_by}
                  {p.countersigned_by && ` · countersigned by ${p.countersigned_by}`}
                  {' · '}
                  {p.acts_recorded === 0
                    ? 'no recorded acts'
                    : `${p.acts_recorded} recorded act${p.acts_recorded === 1 ? '' : 's'}, last ${p.last_act || 'unknown'}`}
                </div>
              </div>
              {/* THE CLUSTER WRAPS RATHER THAN OVERFLOWING, and this is a
                  repair as well as a requirement of the control added beside
                  it. Measured at 375px in the running application: with
                  `shrink-0` these rows had scrollWidth 342 / 376 / 308 / 329
                  against clientWidth 254 — ALREADY OVERFLOWING before this
                  change, with the role chip, the two state chips, the age and
                  one button on a line that cannot shrink. Adding a second
                  button took them to 420 / 453 / 385 / 406, so the defect was
                  inherited and then made worse.

                  `shrink-0` was the cause: it forbids the one thing that would
                  have let the line fit. Wrapping instead puts the chips and
                  the controls on a second line at narrow widths and changes
                  nothing at 1024 and above, where they already fit. Re-measured
                  after: every row 254 of 254, zero overflow, at 375 / 768 /
                  1024 / 1440. */}
              <div className="flex items-center gap-2 flex-wrap justify-end min-w-0">
                <span className="chip chip-std">{p.declared_role}</span>
                {accessChip(p, pendingPeople)}
                {activityChip(p)}
                <span className="waiting-age">
                  {p.last_act_at ? since(p.last_act_at) : '—'}
                </span>
                {/* No revoke button until the access history has actually
                    loaded. Without the history there is no grant to name, and
                    the button posted a null one — a baffling refusal about a
                    missing field, when the truth is the screen could not ask. */}
                {me.role === 'administrator' && p.state === 'active' && p.effective_role
                  && p.person !== me.person && history.status === 'loaded'
                  && liveGrantFor(p.person) && (
                  <button className="btn btn-sm"
                          onClick={() => setRevoking({ person: p.person, grantId: liveGrantFor(p.person) })}>
                    revoke role
                  </button>
                )}
                {/* CLOSING THE ACCOUNT IS OFFERED MORE WIDELY THAN REVOKING A
                    ROLE, deliberately. The control above needs a LIVE GRANT to
                    name, so a person whose roles have already been taken away
                    — or who never held one — offered nothing at all, and that
                    is precisely the account somebody has come here to close.
                    This one needs only the person, which is the only field
                    `POST /accounts/revoke` takes.

                    The three conditions are the same ones the database would
                    enforce anyway, drawn as affordance rather than trusted as
                    permission: `administrator_maintains` refuses any other
                    role, and an account already closed cannot be closed twice
                    because `0013` refuses the un-revoke that would precede it.
                    Self-closure is withheld for the reason the note below the
                    list already gives about revocation — the database would
                    allow it, and an administrator who locks themselves out has
                    to be recovered through the bootstrap path. */}
                {/* MAINTAINING AN ADDRESS IS OFFERED FOR EVERY LIVE ACCOUNT
                    INCLUDING THE ADMINISTRATOR'S OWN, and the two controls
                    around it are not. Those two take something away, and an
                    administrator who takes their own access away has to be
                    recovered through the bootstrap path. This one takes no
                    access away: an administrator with no address of their own
                    is a person the countersign nudges cannot reach, which is
                    the silence this screen exists to end.

                    It waits for `addresses` to load, the way the revoke control
                    waits for the history. Without it the dialog cannot say
                    whether it is setting or replacing, and a setting over a
                    live row is refused by an index. */}
                {me.role === 'administrator' && p.state === 'active'
                  && addresses.status === 'loaded' && (
                  <button className="btn btn-sm"
                          onClick={() => setAddressing({
                            person: p.person, displayName: p.display_name,
                            current: (addressFor(p.person).live || {}).address || null })}>
                    {addressFor(p.person).live ? 'change address' : 'set address'}
                  </button>
                )}
                {me.role === 'administrator' && p.state === 'active'
                  && addresses.status === 'loaded' && addressFor(p.person).live && (
                  <ActButton className="btn btn-sm"
                             onClick={async () => {
                               setError(null);
                               const r = await API.removeNotificationAddress({ person: p.person });
                               if (!r.ok) { setError(r.reason); return; }
                               reloadAll();
                             }}>
                    remove address
                  </ActButton>
                )}
                {me.role === 'administrator' && p.state === 'active'
                  && p.person !== me.person && (
                  <button className="btn btn-sm"
                          style={{ color: 'var(--danger)', borderColor: 'var(--danger)' }}
                          onClick={() => setClosing({ person: p.person, displayName: p.display_name })}>
                    close account
                  </button>
                )}
              </div>
            </div>
          ))}
        </div>
        {/* Nobody revokes themselves through this screen. Not a permission —
            the database would allow it — but an administrator who locks
            themselves out has to be recovered through the bootstrap path, and
            an accidental click should not cost that. */}
      </div>

      {revoking && (
        <RevokeDialog
          person={revoking.person}
          grantId={revoking.grantId}
          onClose={() => setRevoking(null)}
          onError={setError}
          onDone={() => { setRevoking(null); reloadAll(); }}
        />
      )}

      {addressing && (
        <AddressDialog
          person={addressing.person}
          displayName={addressing.displayName}
          current={addressing.current}
          onClose={() => setAddressing(null)}
          onError={setError}
          onDone={() => { setAddressing(null); reloadAll(); }}
        />
      )}

      {closing && (
        <CloseAccountDialog
          person={closing.person}
          displayName={closing.displayName}
          onClose={() => setClosing(null)}
          onError={setError}
          onDone={() => { setClosing(null); reloadAll(); }}
        />
      )}

      {me.role === 'administrator' && (
        <div className="mt-6">
          <GrantForm people={people} onDone={reloadAll} onError={setError} />
        </div>
      )}
    </div>
  );
}
