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:
parent
fecaa61c3f
commit
ae066a738c
7 changed files with 100 additions and 36 deletions
21
public/fonts/NERD-FONTS-LICENSE.txt
Normal file
21
public/fonts/NERD-FONTS-LICENSE.txt
Normal 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.
|
||||||
BIN
public/fonts/SymbolsNerdFontMono-Regular.woff2
Normal file
BIN
public/fonts/SymbolsNerdFontMono-Regular.woff2
Normal file
Binary file not shown.
|
|
@ -1,5 +1,13 @@
|
||||||
@import "tailwindcss";
|
@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 {
|
@theme {
|
||||||
--color-page: #0D0E10;
|
--color-page: #0D0E10;
|
||||||
--color-card: #141518;
|
--color-card: #141518;
|
||||||
|
|
@ -39,6 +47,33 @@ html, body {
|
||||||
overflow: hidden;
|
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
|
/* Native <select> dropdown panels are OS/browser-rendered and ignore most
|
||||||
component styling — without this, options render with a white background
|
component styling — without this, options render with a white background
|
||||||
and near-white text, making them unreadable against this dark theme.
|
and near-white text, making them unreadable against this dark theme.
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ export interface TabState {
|
||||||
const PREFS_KEY = 'archnest-terminal-prefs'
|
const PREFS_KEY = 'archnest-terminal-prefs'
|
||||||
|
|
||||||
export function defaultPrefs(): TerminalPrefs {
|
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 {
|
export function loadPrefs(): TerminalPrefs {
|
||||||
|
|
|
||||||
|
|
@ -305,7 +305,7 @@ export default function Containers() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Intra-page tab bar */}
|
{/* 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')} />
|
<TabButton label="Containers" active={activeTab === 'list'} onClick={() => setActiveTab('list')} />
|
||||||
{detailTabs.map((t) => (
|
{detailTabs.map((t) => (
|
||||||
<TabButton
|
<TabButton
|
||||||
|
|
@ -331,14 +331,14 @@ export default function Containers() {
|
||||||
<div style={cardBase} className="overflow-hidden">
|
<div style={cardBase} className="overflow-hidden">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr style={{ color: TEXT_SECONDARY, borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
|
<tr style={{ color: TEXT_PRIMARY, 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-3 text-center text-base font-bold">Name</th>
|
||||||
<th className="px-3 py-2 text-left font-medium">Image</th>
|
<th className="px-3 py-3 text-center text-base font-bold">Image</th>
|
||||||
<th className="px-3 py-2 text-left font-medium">State</th>
|
<th className="px-3 py-3 text-center text-base font-bold">State</th>
|
||||||
<th className="px-3 py-2 text-left font-medium">CPU</th>
|
<th className="px-3 py-3 text-center text-base font-bold">CPU</th>
|
||||||
<th className="px-3 py-2 text-left font-medium">Memory</th>
|
<th className="px-3 py-3 text-center text-base font-bold">Memory</th>
|
||||||
<th className="px-3 py-2 text-left font-medium">Ports</th>
|
<th className="px-3 py-3 text-center text-base font-bold">Ports</th>
|
||||||
<th className="px-3 py-2 text-right font-medium">Actions</th>
|
<th className="px-3 py-3 text-center text-base font-bold">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
@ -354,67 +354,67 @@ export default function Containers() {
|
||||||
const busy = busyId === c.id
|
const busy = busyId === c.id
|
||||||
return (
|
return (
|
||||||
<tr key={c.id} style={{ borderBottom: '1px solid rgba(255,255,255,0.04)' }}>
|
<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
|
<button
|
||||||
onClick={() => openDetail(c)}
|
onClick={() => openDetail(c)}
|
||||||
className="text-left hover:underline"
|
className="hover:underline"
|
||||||
style={{ color: GOLD }}
|
style={{ color: GOLD }}
|
||||||
title="View details"
|
title="View details"
|
||||||
>
|
>
|
||||||
{c.name}
|
{c.name}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</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}
|
{c.image}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2">
|
<td className="px-3 py-4 text-center text-[15px]">
|
||||||
<span className="flex items-center gap-1.5">
|
<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 className="inline-block h-2 w-2 rounded-full" style={{ background: stateColor(c.state) }} />
|
||||||
<span style={{ color: TEXT_PRIMARY }}>{c.status}</span>
|
<span style={{ color: TEXT_PRIMARY }}>{c.status}</span>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</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)}%` : '—'}
|
{stats ? `${stats.cpuPercent.toFixed(1)}%` : '—'}
|
||||||
</td>
|
</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)}` : '—'}
|
{stats ? `${formatBytes(stats.memUsage)} / ${formatBytes(stats.memLimit)}` : '—'}
|
||||||
</td>
|
</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 || '—'}
|
{c.ports || '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2">
|
<td className="px-3 py-4 text-[15px]">
|
||||||
<div className="flex items-center justify-end gap-1.5">
|
<div className="flex items-center justify-center gap-2">
|
||||||
{canManage ? (
|
{canManage ? (
|
||||||
<>
|
<>
|
||||||
{c.state === 'running' ? (
|
{c.state === 'running' ? (
|
||||||
<>
|
<>
|
||||||
<button disabled={busy} onClick={() => runAction(c, 'pause')} title="Pause" style={{ color: TEXT_SECONDARY }}>
|
<button disabled={busy} onClick={() => runAction(c, 'pause')} title="Pause" style={{ color: TEXT_SECONDARY }}>
|
||||||
<Pause size={14} />
|
<Pause size={16} />
|
||||||
</button>
|
</button>
|
||||||
<button disabled={busy} onClick={() => runAction(c, 'restart')} title="Restart" style={{ color: TEXT_SECONDARY }}>
|
<button disabled={busy} onClick={() => runAction(c, 'restart')} title="Restart" style={{ color: TEXT_SECONDARY }}>
|
||||||
<RotateCw size={14} />
|
<RotateCw size={16} />
|
||||||
</button>
|
</button>
|
||||||
<button disabled={busy} onClick={() => runAction(c, 'stop')} title="Stop" style={{ color: TEXT_SECONDARY }}>
|
<button disabled={busy} onClick={() => runAction(c, 'stop')} title="Stop" style={{ color: TEXT_SECONDARY }}>
|
||||||
<Square size={14} />
|
<Square size={16} />
|
||||||
</button>
|
</button>
|
||||||
<button disabled={busy} onClick={() => setExecRow(c)} title="Exec terminal" style={{ color: GOLD }}>
|
<button disabled={busy} onClick={() => setExecRow(c)} title="Exec terminal" style={{ color: GOLD }}>
|
||||||
<TerminalSquare size={14} />
|
<TerminalSquare size={16} />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : c.state === 'paused' ? (
|
) : c.state === 'paused' ? (
|
||||||
<button disabled={busy} onClick={() => runAction(c, 'unpause')} title="Unpause" style={{ color: TEXT_SECONDARY }}>
|
<button disabled={busy} onClick={() => runAction(c, 'unpause')} title="Unpause" style={{ color: TEXT_SECONDARY }}>
|
||||||
<PlayCircle size={14} />
|
<PlayCircle size={16} />
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button disabled={busy} onClick={() => runAction(c, 'start')} title="Start" style={{ color: TEXT_SECONDARY }}>
|
<button disabled={busy} onClick={() => runAction(c, 'start')} title="Start" style={{ color: TEXT_SECONDARY }}>
|
||||||
<Play size={14} />
|
<Play size={16} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button disabled={busy} onClick={() => setLogsRow(c)} title="Logs" style={{ color: TEXT_SECONDARY }}>
|
<button disabled={busy} onClick={() => setLogsRow(c)} title="Logs" style={{ color: TEXT_SECONDARY }}>
|
||||||
<ScrollText size={14} />
|
<ScrollText size={16} />
|
||||||
</button>
|
</button>
|
||||||
<button disabled={busy} onClick={() => removeRow(c)} title="Remove" style={{ color: '#E74C3C' }}>
|
<button disabled={busy} onClick={() => removeRow(c)} title="Remove" style={{ color: '#E74C3C' }}>
|
||||||
<Trash2 size={14} />
|
<Trash2 size={16} />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -450,17 +450,20 @@ function TabButton({ label, active, onClick, onClose }: { label: string; active:
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={onClick}
|
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={{
|
style={{
|
||||||
color: active ? GOLD : TEXT_SECONDARY,
|
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',
|
maxWidth: '200px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="truncate">{label}</span>
|
<span className="truncate">{label}</span>
|
||||||
{onClose && (
|
{onClose && (
|
||||||
<X
|
<X
|
||||||
size={12}
|
size={13}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onClose()
|
onClose()
|
||||||
|
|
|
||||||
|
|
@ -246,7 +246,7 @@ export default function Infrastructure() {
|
||||||
<div className="relative z-10 flex flex-1 flex-col">
|
<div className="relative z-10 flex flex-1 flex-col">
|
||||||
<h3 style={sectionTitle}>Node Status</h3>
|
<h3 style={sectionTitle}>Node Status</h3>
|
||||||
{resources && resources.length > 0 ? (
|
{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) => (
|
{resources.map((node, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
|
|
@ -255,7 +255,7 @@ export default function Infrastructure() {
|
||||||
backgroundColor: 'rgba(10, 10, 12, 0.55)',
|
backgroundColor: 'rgba(10, 10, 12, 0.55)',
|
||||||
border: `1px solid ${nodeStatusColor[node.status]}33`,
|
border: `1px solid ${nodeStatusColor[node.status]}33`,
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
padding: '10px 12px',
|
padding: '8px 10px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: '6px',
|
gap: '6px',
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,15 @@ const GOLD = '#C8A434'
|
||||||
const TEXT_SECONDARY = '#7A7D85'
|
const TEXT_SECONDARY = '#7A7D85'
|
||||||
|
|
||||||
const FONT_SIZES = [11, 12, 13, 14, 15, 16]
|
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 = [
|
const FONT_FAMILIES = [
|
||||||
{ name: 'Monospace', value: 'ui-monospace, SFMono-Regular, Menlo, monospace' },
|
{ name: 'Monospace', value: `ui-monospace, SFMono-Regular, Menlo, monospace, ${NERD_FALLBACK}` },
|
||||||
{ name: 'Fira Code', value: '"Fira Code", ui-monospace, monospace' },
|
{ name: 'Fira Code', value: `"Fira Code", ui-monospace, monospace, ${NERD_FALLBACK}` },
|
||||||
{ name: 'JetBrains Mono', value: '"JetBrains Mono", ui-monospace, monospace' },
|
{ name: 'JetBrains Mono', value: `"JetBrains Mono", ui-monospace, monospace, ${NERD_FALLBACK}` },
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function Terminal() {
|
export default function Terminal() {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue