diff --git a/web/DetailPanel.tsx b/web/DetailPanel.tsx index a2c8957..7031135 100644 --- a/web/DetailPanel.tsx +++ b/web/DetailPanel.tsx @@ -1,11 +1,12 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useSearchParams } from 'react-router'; import { useClient } from 'hooks/useClient'; -import { DEFAULT_SECTION, isSectionKey, type SectionKey } from './sections'; +import { DEFAULT_SECTION, SECTIONS, isSectionKey, type SectionKey } from './sections'; import { HostsPanel } from './HostsPanel'; import { CertificatesPanel } from './CertificatesPanel'; import { ConnectionPanel } from './ConnectionPanel'; import { ListSection, REDIRECTIONS, STREAMS, DEAD_HOSTS, ACCESS_LISTS } from './ListSection'; +import { PlannedSection } from './PlannedSection'; import { useNpmChanged } from './channel'; /** @@ -56,6 +57,11 @@ export const DetailPanel = () => { return
Loading…
; } + // A planned section says so before anything else — including before the connection check, because "you + // have not connected NPM" is not why Domains is empty and would be a misleading thing to be told. + const planned = SECTIONS.find((s) => s.key === section && s.planned); + if (planned) return ; + if (!configured || section === 'connection') return ; switch (section) { @@ -71,5 +77,8 @@ export const DetailPanel = () => { return ; case 'access-lists': return ; + default: + // `domains` and `dns-providers` are handled above; this keeps the switch total without a cast. + return null; } }; diff --git a/web/NavPanel.tsx b/web/NavPanel.tsx index 4e50a10..9f01852 100644 --- a/web/NavPanel.tsx +++ b/web/NavPanel.tsx @@ -90,8 +90,12 @@ export const NavPanel = () => { - {section.label} + + {section.label} + {count !== undefined && {count}} + {/* Dimmed and labelled rather than hidden — see `sections.ts`. */} + {section.planned && soon} {section.hint} diff --git a/web/PlannedSection.tsx b/web/PlannedSection.tsx new file mode 100644 index 0000000..6bcb930 --- /dev/null +++ b/web/PlannedSection.tsx @@ -0,0 +1,43 @@ +import { Hammer } from 'lucide-react'; +import type { Section } from './sections'; + +/** + * A section that is agreed and not built. + * + * It says so plainly, and then says what it will be. Both halves matter: "not built" alone reads as an + * error, and a description alone reads as a feature that is broken. Together they read as a plan, which is + * what they are. + * + * The alternative was to hide these until they exist, and that loses the thing the nav is good at — a + * person opening this plugin can see what it is FOR, not only what it currently does. `README.md` carries + * the full design; this is the two sentences that fit on a screen. + */ + +const COPY: Partial> = { + domains: { + what: 'The domains you manage on this machine, and who each one is registered with.', + why: 'Right now that list lives in a document somebody edits by hand, which is right on the day it is written. Officer knowing it for real is what lets a certificate be issued without being told the same facts again.', + }, + 'dns-providers': { + what: 'An API key per registrar — Cloudflare, GoDaddy, whoever — kept in the secret store and never handed back out.', + why: 'This is what makes HTTPS possible on a hostname that only exists on your private network. Let’s Encrypt cannot visit a private address, so the only proof it accepts is a DNS record, and placing one needs the registrar’s API.', + }, +}; + +export const PlannedSection = ({ section }: { section: Section }) => { + const copy = COPY[section.key]; + + return ( +
+ +

{section.label} is not built yet.

+ {copy && ( + <> +

{copy.what}

+

{copy.why}

+ + )} +

The design is in this plugin’s README.

+
+ ); +}; diff --git a/web/sections.ts b/web/sections.ts index 4c4f8c9..2a1c558 100644 --- a/web/sections.ts +++ b/web/sections.ts @@ -1,24 +1,38 @@ import type { LucideIcon } from 'lucide-react'; -import { Globe, Cable, Radio, FileX2, Users, ShieldCheck, Plug } from 'lucide-react'; +import { Globe, Cable, Radio, FileX2, Users, ShieldCheck, Plug, Network, KeyRound } from 'lucide-react'; /** - * The sections, mirroring Nginx Proxy Manager's own tabs. + * The sections. * - * Deliberately NPM's vocabulary rather than ours. Somebody who has used NPM should find the same words in - * the same order, and somebody who has not is going to end up in NPM's UI eventually anyway — inventing - * better names would only mean they have to learn both. + * ── NPM's words, not NPM's order ── * - * `404 Hosts` is the one that reads oddly, and it is NPM's own name: a domain served deliberately as - * nothing. The description says so, because the label cannot. + * The VOCABULARY is deliberately NPM's. Somebody who has used it should find the same words, and somebody + * who has not will end up in NPM's UI eventually anyway — inventing better names only means learning both. + * `404 Hosts` reads oddly and is theirs; the hint says what it means, because the label cannot. + * + * The ORDER is ours, and this file said the opposite until 2026-08-25. NPM's nav order is an accident of + * how NPM grew; ours is a claim about what people come here for. Proxy hosts, then certificates, then + * streams — that is most of every visit. The rest are real and rarely the reason you opened this. + * + * **Being able to make that claim is most of why this is a plugin** rather than a link to NPM. A wrapper + * that reproduces the original's ordering has given up the one thing it was for. + * + * ── Two groups ── + * + * Everything above `domains` is a thing NPM owns and this reads. From `domains` down is OFFICER's own — + * what it knows about your domains, the DNS credentials it holds, and where NPM is. Those are not NPM + * concepts and should not pretend to be. */ export type SectionKey = | 'proxy-hosts' - | 'redirections' + | 'certificates' | 'streams' + | 'redirections' | 'dead-hosts' | 'access-lists' - | 'certificates' + | 'domains' + | 'dns-providers' | 'connection'; export type Section = { @@ -27,8 +41,17 @@ export type Section = { icon: LucideIcon; /** One line under the label in the nav. What this is, for somebody who does not already know. */ hint: string; - /** The plugin route its count comes from, or null for the settings pane. */ + /** The plugin route its count comes from, or null when there is nothing to count. */ path: string | null; + /** + * Agreed and not built. + * + * Listed rather than hidden on purpose: these two are the shape the plugin is going towards, and a nav + * that shows them says what this is FOR to somebody who opens it — including the owner, six months from + * now, wondering what was decided. The pane says plainly that it does not exist, which is better than a + * feature that looks half-present. + */ + planned?: boolean; }; export const SECTIONS: Section[] = [ @@ -40,11 +63,11 @@ export const SECTIONS: Section[] = [ path: '/proxy-manager/hosts', }, { - key: 'redirections', - label: 'Redirection Hosts', - icon: Cable, - hint: 'A domain that sends visitors somewhere else', - path: '/proxy-manager/redirections', + key: 'certificates', + label: 'Certificates', + icon: ShieldCheck, + hint: 'TLS, and how long each one has left', + path: '/proxy-manager/certificates', }, { key: 'streams', @@ -53,6 +76,13 @@ export const SECTIONS: Section[] = [ hint: 'A raw TCP or UDP port, forwarded', path: '/proxy-manager/streams', }, + { + key: 'redirections', + label: 'Redirection Hosts', + icon: Cable, + hint: 'A domain that sends visitors somewhere else', + path: '/proxy-manager/redirections', + }, { key: 'dead-hosts', label: '404 Hosts', @@ -67,18 +97,29 @@ export const SECTIONS: Section[] = [ hint: 'Who is allowed to reach a host', path: '/proxy-manager/access-lists', }, + + // ── Officer's own, below here ── { - key: 'certificates', - label: 'Certificates', - icon: ShieldCheck, - hint: 'TLS, and how long each one has left', - path: '/proxy-manager/certificates', + key: 'domains', + label: 'Domains', + icon: Network, + hint: 'The domains you manage, and who registers each', + path: null, + planned: true, + }, + { + key: 'dns-providers', + label: 'DNS Providers', + icon: KeyRound, + hint: 'API keys, so a private hostname can still get TLS', + path: null, + planned: true, }, { key: 'connection', label: 'Connection', icon: Plug, - hint: "Where NPM is, and the credential — owner only", + hint: 'Where NPM is, and the credential — owner only', path: null, }, ];