a nav down the left, whatever it selects beside it

two fixed panels could show two of NPM's seven sections and had nowhere to put
the other five. now the left column is the section list — proxy hosts,
redirections, streams, 404 hosts, access lists, certificates, connection — and
the right renders whichever is chosen.

NPM's own vocabulary, in NPM's own order. somebody who has used it should find
the same words, and somebody who has not will end up in NPM's UI eventually
anyway; better names would just mean learning both. "404 hosts" reads oddly and
is theirs, so the hint under it says what it means.

the two panels never speak. the nav writes ?section= and the detail pane reads
it — navigation-audit's rule, and it is what makes a section linkable and
survive a reload. changing section clears ?host=, which belonged to the list
that was open.

three things folded up while doing it. the unconfigured check moved to the
detail pane and happens once, so all seven sections say "no connection yet" the
same way instead of six saying it badly. the settings gear is gone: connection
is a nav row now. and the four list-only sections are one component, differing
only in what a row says — proxy hosts and certificates stay separate because
they have real behaviour, and folding them in would be a props bag that is a
worse version of both.

the counts in the nav are one request each. affordable against a local NPM, and
they answer "is this worth opening" without a click. a count that fails is
absent rather than zero — zero is a fact about NPM, a failure is a fact about
the request, and the detail pane reports that properly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-25 19:00:24 +00:00
co-authored by Claude Opus 5
parent 65c98a49dd
commit 417e091dfe
7 changed files with 433 additions and 42 deletions
+89
View File
@@ -0,0 +1,89 @@
import type { LucideIcon } from 'lucide-react';
import { Globe, Cable, Radio, FileX2, Users, ShieldCheck, Plug } from 'lucide-react';
/**
* The sections, mirroring Nginx Proxy Manager's own tabs.
*
* 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.
*
* `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.
*/
export type SectionKey =
| 'proxy-hosts'
| 'redirections'
| 'streams'
| 'dead-hosts'
| 'access-lists'
| 'certificates'
| 'connection';
export type Section = {
key: SectionKey;
label: string;
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. */
path: string | null;
};
export const SECTIONS: Section[] = [
{
key: 'proxy-hosts',
label: 'Proxy Hosts',
icon: Globe,
hint: 'A domain, forwarded to something running here',
path: '/proxy-manager/hosts',
},
{
key: 'redirections',
label: 'Redirection Hosts',
icon: Cable,
hint: 'A domain that sends visitors somewhere else',
path: '/proxy-manager/redirections',
},
{
key: 'streams',
label: 'Streams',
icon: Radio,
hint: 'A raw TCP or UDP port, forwarded',
path: '/proxy-manager/streams',
},
{
key: 'dead-hosts',
label: '404 Hosts',
icon: FileX2,
hint: 'A domain served deliberately as nothing',
path: '/proxy-manager/dead-hosts',
},
{
key: 'access-lists',
label: 'Access Lists',
icon: Users,
hint: 'Who is allowed to reach a host',
path: '/proxy-manager/access-lists',
},
{
key: 'certificates',
label: 'Certificates',
icon: ShieldCheck,
hint: 'TLS, and how long each one has left',
path: '/proxy-manager/certificates',
},
{
key: 'connection',
label: 'Connection',
icon: Plug,
hint: "Where NPM is, and the credential — owner only",
path: null,
},
];
export const DEFAULT_SECTION: SectionKey = 'proxy-hosts';
export const isSectionKey = (value: string | null): value is SectionKey =>
!!value && SECTIONS.some((s) => s.key === value);