Containers/Infrastructure styling fixes + Terminal Nerd Font fallback (#36)

* Add mesh prerequisite gate (NetBird verification before app config)

Implements the design in docs/mesh-prerequisite-gate.md per the user's
DECIDE A-D answers: a permanent admin override, B1 (reachable) verification
with host mesh IP shown informationally, members allowed in with a notice
instead of being blocked, and mesh.required defaulting off so the live
production instance is unaffected.

- system_config kv table + getConfig/setConfig helpers
- /api/system/mesh-status, /mesh/verify, /mesh/override, /mesh/required
- AuthContext gains a 'needs-mesh' status (admins only) and exposes
  meshStatus for a member-facing banner
- MeshGate page reuses the integration create+test flow to connect NetBird

* Make mesh verification universal (CIDR check, not NetBird-specific)

Replace the NetBird-adapter-based "reachable" check with a vendor-agnostic
one: the admin supplies the mesh's IP range (CIDR), and verification just
confirms this host has an address inside it. Works identically for
NetBird, WireGuard, ZeroTier, Tailscale, or any other mesh tech, with no
integration record or vendor API call required.

* Add reachability fallback for routed meshes (VPC peering, etc.)

A host can be on the mesh's "side" of a routed network (e.g. a VPC peered
into a NetBird/WireGuard mesh) without holding a local IP in the mesh's
own CIDR. Local-IP-in-CIDR stays the primary check; if it fails, the admin
can supply a known peer/gateway IP on the mesh and we verify by pinging
it instead. Adds iputils to the backend image for the ping binary.

* Add Mesh section to Settings for configuring/testing the mesh gate

Admins can now toggle mesh.required, run verify/override, and see
current mesh status entirely from the app, without hitting the API
directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019hu9pZvJY4BgmcQeAw2ugk

* Show a host-specific Docker remote-API setup script in Settings

When adding/editing a Docker integration with a tcp:// or http:// remote
URL, display a copyable systemd override + curl verification script
scoped to the entered host:port, so enabling the daemon's API doesn't
require looking up the steps separately.

* Expand Help page with quick-start guide and real-world examples

Adds a quick-start ordering card and per-feature example callouts (with icons) so first-time users see concrete use cases, not just descriptions.

* Update HANDOFF/README for handoff: mesh gate shipped, Docker UX work, no feature queued

Corrects the stale 'mesh gate not built' framing (it shipped across 4 commits, all merged) and documents the Docker setup-script hint + Help page expansion done this session. Leaves a clear next-task list for the picking-up agent: decide on merging claude/youthful-cerf-ibvxfb, then check with the user for the next priority.

* Improve Containers table/tab readability: bold centered headers, taller rows, filing-cabinet tabs

* Make Node Status card scrollable with a 5-column layout and invisible-by-default scrollbar

* Add Nerd Font icon fallback to the Terminal so Starship-style prompts render correctly

Bundles Symbols Nerd Font Mono (MIT, ryanoasis/nerd-fonts) as a glyph-only @font-face and appends it to every Terminal font-family option, so distro icons / git branch glyphs / etc. from prompts like Starship show up instead of broken-glyph boxes. It carries no letterforms, so it never changes how normal text renders.

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Samuel James 2026-06-21 05:01:39 -04:00 committed by GitHub
parent fecaa61c3f
commit ae066a738c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 100 additions and 36 deletions

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Ryan L McIntyre
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Binary file not shown.

View file

@ -1,5 +1,13 @@
@import "tailwindcss";
@font-face {
font-family: 'Symbols Nerd Font Mono';
src: url('/fonts/SymbolsNerdFontMono-Regular.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
}
@theme {
--color-page: #0D0E10;
--color-card: #141518;
@ -39,6 +47,33 @@ html, body {
overflow: hidden;
}
.scrollbar-ghost {
scrollbar-width: thin;
scrollbar-color: transparent transparent;
}
.scrollbar-ghost::-webkit-scrollbar {
width: 6px;
}
.scrollbar-ghost::-webkit-scrollbar-track {
background: transparent;
}
.scrollbar-ghost::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 6px;
}
.scrollbar-ghost:hover::-webkit-scrollbar-thumb,
.scrollbar-ghost:hover {
scrollbar-color: rgba(200, 164, 52, 0.25) transparent;
}
.scrollbar-ghost:hover::-webkit-scrollbar-thumb {
background: rgba(200, 164, 52, 0.25);
}
/* Native <select> dropdown panels are OS/browser-rendered and ignore most
component styling without this, options render with a white background
and near-white text, making them unreadable against this dark theme.

View file

@ -38,7 +38,7 @@ export interface TabState {
const PREFS_KEY = 'archnest-terminal-prefs'
export function defaultPrefs(): TerminalPrefs {
return { themeName: TERM_THEMES[0].name, fontSize: 13, fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace' }
return { themeName: TERM_THEMES[0].name, fontSize: 13, fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace, "Symbols Nerd Font Mono"' }
}
export function loadPrefs(): TerminalPrefs {

View file

@ -305,7 +305,7 @@ export default function Containers() {
</div>
{/* Intra-page tab bar */}
<div className="flex items-center gap-1 border-b border-white/10">
<div className="flex items-end gap-1 border-b border-white/10">
<TabButton label="Containers" active={activeTab === 'list'} onClick={() => setActiveTab('list')} />
{detailTabs.map((t) => (
<TabButton
@ -331,14 +331,14 @@ export default function Containers() {
<div style={cardBase} className="overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr style={{ color: TEXT_SECONDARY, borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
<th className="px-3 py-2 text-left font-medium">Name</th>
<th className="px-3 py-2 text-left font-medium">Image</th>
<th className="px-3 py-2 text-left font-medium">State</th>
<th className="px-3 py-2 text-left font-medium">CPU</th>
<th className="px-3 py-2 text-left font-medium">Memory</th>
<th className="px-3 py-2 text-left font-medium">Ports</th>
<th className="px-3 py-2 text-right font-medium">Actions</th>
<tr style={{ color: TEXT_PRIMARY, borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
<th className="px-3 py-3 text-center text-base font-bold">Name</th>
<th className="px-3 py-3 text-center text-base font-bold">Image</th>
<th className="px-3 py-3 text-center text-base font-bold">State</th>
<th className="px-3 py-3 text-center text-base font-bold">CPU</th>
<th className="px-3 py-3 text-center text-base font-bold">Memory</th>
<th className="px-3 py-3 text-center text-base font-bold">Ports</th>
<th className="px-3 py-3 text-center text-base font-bold">Actions</th>
</tr>
</thead>
<tbody>
@ -354,67 +354,67 @@ export default function Containers() {
const busy = busyId === c.id
return (
<tr key={c.id} style={{ borderBottom: '1px solid rgba(255,255,255,0.04)' }}>
<td className="px-3 py-2">
<td className="px-3 py-4 text-center text-[15px]">
<button
onClick={() => openDetail(c)}
className="text-left hover:underline"
className="hover:underline"
style={{ color: GOLD }}
title="View details"
>
{c.name}
</button>
</td>
<td className="px-3 py-2" style={{ color: TEXT_SECONDARY }}>
<td className="px-3 py-4 text-center text-[15px]" style={{ color: TEXT_SECONDARY }}>
{c.image}
</td>
<td className="px-3 py-2">
<span className="flex items-center gap-1.5">
<td className="px-3 py-4 text-center text-[15px]">
<span className="flex items-center justify-center gap-1.5">
<span className="inline-block h-2 w-2 rounded-full" style={{ background: stateColor(c.state) }} />
<span style={{ color: TEXT_PRIMARY }}>{c.status}</span>
</span>
</td>
<td className="px-3 py-2" style={{ color: TEXT_SECONDARY }}>
<td className="px-3 py-4 text-center text-[15px]" style={{ color: TEXT_SECONDARY }}>
{stats ? `${stats.cpuPercent.toFixed(1)}%` : '—'}
</td>
<td className="px-3 py-2" style={{ color: TEXT_SECONDARY }}>
<td className="px-3 py-4 text-center text-[15px]" style={{ color: TEXT_SECONDARY }}>
{stats ? `${formatBytes(stats.memUsage)} / ${formatBytes(stats.memLimit)}` : '—'}
</td>
<td className="px-3 py-2" style={{ color: TEXT_SECONDARY }}>
<td className="px-3 py-4 text-center text-[15px]" style={{ color: TEXT_SECONDARY }}>
{c.ports || '—'}
</td>
<td className="px-3 py-2">
<div className="flex items-center justify-end gap-1.5">
<td className="px-3 py-4 text-[15px]">
<div className="flex items-center justify-center gap-2">
{canManage ? (
<>
{c.state === 'running' ? (
<>
<button disabled={busy} onClick={() => runAction(c, 'pause')} title="Pause" style={{ color: TEXT_SECONDARY }}>
<Pause size={14} />
<Pause size={16} />
</button>
<button disabled={busy} onClick={() => runAction(c, 'restart')} title="Restart" style={{ color: TEXT_SECONDARY }}>
<RotateCw size={14} />
<RotateCw size={16} />
</button>
<button disabled={busy} onClick={() => runAction(c, 'stop')} title="Stop" style={{ color: TEXT_SECONDARY }}>
<Square size={14} />
<Square size={16} />
</button>
<button disabled={busy} onClick={() => setExecRow(c)} title="Exec terminal" style={{ color: GOLD }}>
<TerminalSquare size={14} />
<TerminalSquare size={16} />
</button>
</>
) : c.state === 'paused' ? (
<button disabled={busy} onClick={() => runAction(c, 'unpause')} title="Unpause" style={{ color: TEXT_SECONDARY }}>
<PlayCircle size={14} />
<PlayCircle size={16} />
</button>
) : (
<button disabled={busy} onClick={() => runAction(c, 'start')} title="Start" style={{ color: TEXT_SECONDARY }}>
<Play size={14} />
<Play size={16} />
</button>
)}
<button disabled={busy} onClick={() => setLogsRow(c)} title="Logs" style={{ color: TEXT_SECONDARY }}>
<ScrollText size={14} />
<ScrollText size={16} />
</button>
<button disabled={busy} onClick={() => removeRow(c)} title="Remove" style={{ color: '#E74C3C' }}>
<Trash2 size={14} />
<Trash2 size={16} />
</button>
</>
) : (
@ -450,17 +450,20 @@ function TabButton({ label, active, onClick, onClose }: { label: string; active:
return (
<div
onClick={onClick}
className="flex cursor-pointer items-center gap-2 rounded-t-md px-3 py-1.5 text-xs"
className="flex cursor-pointer items-center gap-2 rounded-t-md px-4 py-2 text-sm font-semibold"
style={{
color: active ? GOLD : TEXT_SECONDARY,
borderBottom: active ? `2px solid ${GOLD}` : '2px solid transparent',
backgroundColor: active ? 'rgba(200,164,52,0.12)' : 'rgba(255,255,255,0.02)',
border: active ? '1px solid rgba(200,164,52,0.35)' : '1px solid rgba(255,255,255,0.06)',
borderBottom: active ? '1px solid rgba(200,164,52,0.12)' : '1px solid rgba(255,255,255,0.06)',
marginBottom: '-1px',
maxWidth: '200px',
}}
>
<span className="truncate">{label}</span>
{onClose && (
<X
size={12}
size={13}
onClick={(e) => {
e.stopPropagation()
onClose()

View file

@ -246,7 +246,7 @@ export default function Infrastructure() {
<div className="relative z-10 flex flex-1 flex-col">
<h3 style={sectionTitle}>Node Status</h3>
{resources && resources.length > 0 ? (
<div className="grid flex-1 grid-cols-4 content-start gap-3" style={{ overflowY: 'auto' }}>
<div className="scrollbar-ghost grid min-h-0 flex-1 grid-cols-5 content-start gap-2" style={{ overflowY: 'auto' }}>
{resources.map((node, i) => (
<div
key={i}
@ -255,7 +255,7 @@ export default function Infrastructure() {
backgroundColor: 'rgba(10, 10, 12, 0.55)',
border: `1px solid ${nodeStatusColor[node.status]}33`,
borderRadius: '8px',
padding: '10px 12px',
padding: '8px 10px',
display: 'flex',
flexDirection: 'column',
gap: '6px',

View file

@ -8,10 +8,15 @@ const GOLD = '#C8A434'
const TEXT_SECONDARY = '#7A7D85'
const FONT_SIZES = [11, 12, 13, 14, 15, 16]
// "Symbols Nerd Font Mono" is appended as a glyph-only fallback to every option, so
// distro icons / git branch / etc. from prompts like Starship render instead of
// showing as boxes — it has no letterforms of its own, so it never overrides the
// chosen base font for normal text.
const NERD_FALLBACK = '"Symbols Nerd Font Mono"'
const FONT_FAMILIES = [
{ name: 'Monospace', value: 'ui-monospace, SFMono-Regular, Menlo, monospace' },
{ name: 'Fira Code', value: '"Fira Code", ui-monospace, monospace' },
{ name: 'JetBrains Mono', value: '"JetBrains Mono", ui-monospace, monospace' },
{ name: 'Monospace', value: `ui-monospace, SFMono-Regular, Menlo, monospace, ${NERD_FALLBACK}` },
{ name: 'Fira Code', value: `"Fira Code", ui-monospace, monospace, ${NERD_FALLBACK}` },
{ name: 'JetBrains Mono', value: `"JetBrains Mono", ui-monospace, monospace, ${NERD_FALLBACK}` },
]
export default function Terminal() {