Docker setup-script hint + expanded Help page (#35)

* 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.

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Samuel James 2026-06-21 04:34:59 -04:00 committed by GitHub
parent fcac50cc02
commit 07116a0475
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 218 additions and 15 deletions

View file

@ -1,6 +1,6 @@
# ArchNest — Handoff Notes
Status snapshot as of **2026-06-20**. Written so a fresh AI session (or human) can pick this up with zero prior context. Branch names rotate every session — always run `git branch --show-current` and work on a fresh feature branch off `main` (recent branches have used a `kiro/<feature>` naming pattern).
Status snapshot as of **2026-06-21**. Written so a fresh AI session (or human) can pick this up with zero prior context. Branch names rotate every session — always run `git branch --show-current` and work on a fresh feature branch off `main` (recent branches have used a `kiro/<feature>` or `claude/<feature>` naming pattern).
## TL;DR
@ -12,8 +12,17 @@ Since then, **Docker container visibility/management was expanded** (shipped, de
- **Persistent SSH terminal sessions** (PR #30) — terminals stay connected across in-app page navigation.
- **Docker-over-SSH management** + **Docker push-agent monitoring** (PR #31) — see the "Docker: three ways" section below.
### → NEXT TASK for the picking-up agent: the **Mesh Prerequisite Gate**
This is **designed but NOT built**. Full design + the 4 open decisions are in **`docs/mesh-prerequisite-gate.md`** — read it first. It requires a NetBird mesh to be configured/tested/verified before the rest of the app can be configured. **The hard part is lockout-safety** (a failed mesh test must never lock the admin out). **Do not start coding until the user answers DECIDE AD in that doc** (escape-hatch behavior, what "verified" means, member behavior, and crucially whether to default the gate OFF so it doesn't immediately gate the live production instance). Use `AskUserQuestion`.
**The Mesh Prerequisite Gate is now built and shipped** (no longer the open task): NetBird-mesh-required-before-config, with universal CIDR-based verification (not NetBird-specific), a routed-mesh/VPC-peering reachability fallback, and a dedicated "Mesh" section in Settings to configure/test it. Defaults OFF, so it does not lock the live instance. Commits: `46d95fc` (gate), `0409159` (universal CIDR check), `800072f` (routed-mesh fallback), `4a4a5a0` (Settings UI) — all merged to `main`.
Most recently (this session, real user dogfooding rather than a planned feature): walked the user through replacing a broken/insecure Docker-TCP-API integration attempt with a working **SSH Host** integration to a real VM ("Portainer VM," running Portainer + a test container), confirmed Docker-over-SSH container management works end to end, and added supporting UX:
- **Docker setup-script hint in Settings** (commit `628187b`, branch `claude/youthful-cerf-ibvxfb`, **pushed but NOT YET merged to `main`** — user explicitly deferred merging once already; revisit with the user before merging) — when editing a Docker (`type: 'docker'`) integration's `baseUrl`, Settings now renders a copyable systemd-override + `curl` verification script scoped to that exact host/port, so users don't have to hand-derive the remote-API-enablement steps themselves.
- **Help page expansion** (commit `36a79ab`, same branch, pushed) — every page entry in `src/pages/Help.tsx` now has at least one real-world example callout (icon + optional label + scenario text), plus a "New here? Start in this order" quick-start card above the grid, aimed at first-time users who don't yet know which page does what.
### → NEXT TASK for the picking-up agent
No new feature is queued. Pick up from here:
1. **Decide with the user whether to merge `claude/youthful-cerf-ibvxfb` into `main`.** It contains the Docker setup-script hint (`628187b`) and the Help page expansion (`36a79ab`), both already build-clean (`npm run build` passes). Nothing else is blocking it.
2. **Ask the user if removing the unused Docker API integration (the one superseded by the SSH Host setup) is done** — this was a live-instance UI action on their end, not something done via this repo's code.
3. Otherwise, check with the user for the next priority — there is no pending design doc or half-built feature waiting right now (mesh gate and Docker UX work above are both fully shipped or ready-to-merge).
## Standing rules (read before doing anything)
@ -66,6 +75,9 @@ See `TERMIX_MIGRATION.md` for the phase-by-phase record of the original feature
11. **Settings UX fixes** — secret fields show a "· saved" indicator instead of appearing blank/deleted after reload (`secretKeys: string[]` on the integration serializer); SSH host cards default-collapsed if already configured; SSH private-key/cert fields support file upload to avoid paste corruption.
12. **Persistent terminal sessions** (PR #30) — SSH terminal tabs/panes stay connected when you navigate to other pages and back. See `src/lib/TerminalSessionContext.tsx`.
13. **Docker-over-SSH + agent monitoring** (PR #31) — two new ways to see/manage Docker without exposing the Engine TCP socket. See "Docker: three ways" below.
14. **Mesh Prerequisite Gate** (`46d95fc`, `0409159`, `800072f`, `4a4a5a0`) — requires a verified mesh network (universal CIDR check, not NetBird-specific, with a routed-mesh/VPC-peering fallback) before the app can be configured; defaults OFF; configurable/testable from a dedicated Settings → Mesh section.
15. **Docker integration setup-script hint** (`628187b`, on `claude/youthful-cerf-ibvxfb`, not yet merged) — Settings shows a host-specific systemd-override + curl script when configuring a Docker (`type: 'docker'`) integration's `baseUrl`, so enabling the remote Engine API doesn't require looking up the steps elsewhere.
16. **Help page expansion** (`36a79ab`, same branch) — quick-start ordering card + real-world example callouts per page, for first-time users.
## Docker: three ways (PR #31)
@ -122,6 +134,6 @@ Moved to **`ROADMAP.md`** ("Known non-blocking stubs"). Summary: the Infrastruct
1. Read this file, then `ROADMAP.md` (deferred/tiered work), then `docs/` (subsystem design docs — `docker-agent-monitoring.md`, `mesh-prerequisite-gate.md`), then `TERMIX_MIGRATION.md` for feature-level history, then skim `git log --oneline -30`.
2. Frontend: prefer `npm run build` (`tsc -b && vite build`) over a plain `tsc --noEmit` (stricter, catches more). Backend: `npx tsc --noEmit -p .` from `backend/`. Both must pass before any commit.
3. **The next planned feature is the Mesh Prerequisite Gate** — designed in `docs/mesh-prerequisite-gate.md`, NOT built. It has open decisions (AD) that **must be answered by the user before coding** (especially DECIDE D: defaulting the gate OFF so it doesn't lock the live production instance). Auth Phases 1-3 are done; Phase 4 SSO is a deferred paid AWS add-on (`ROADMAP.md`).
3. **The Mesh Prerequisite Gate is built and shipped** (Settings → Mesh; defaults OFF). **There is no other planned feature queued right now** — check the "→ NEXT TASK" section above first (merge decision on `claude/youthful-cerf-ibvxfb`), then ask the user for the next priority. Auth Phases 1-3 are done; Phase 4 SSO is a deferred paid AWS add-on (`ROADMAP.md`).
4. If asked to add a feature, follow existing patterns: integration adapters in `backend/src/integrations/`, SSH-backed engines in `backend/src/ssh/`, one route file per feature in `backend/src/routes/`, one `api.ts` entry + page component per frontend feature. Subsystem-level work gets a `docs/` design doc first.
5. For anything ambiguous in scope, use `AskUserQuestion` rather than guessing — that's how the auth phases, the Docker agent tiering, and the mesh-gate decisions were all scoped.

View file

@ -30,15 +30,16 @@ backend routes are built and working — there is no pending/on-hold page.
Auth is feature-complete for self-hosted (Phases 1-3: user menu wiring,
password/sessions/login-log, multi-user roles with a 10-seat cap); Phase 4
(Authentik SSO) is **deferred to a paid AWS add-on** — see `ROADMAP.md`.
Recently shipped: persistent terminal sessions across navigation, and Docker
Recently shipped: persistent terminal sessions across navigation, Docker
container visibility/management three ways (Engine TCP API, `docker` CLI over
SSH, and a read-only push agent — see `docs/docker-agent-monitoring.md`).
SSH, and a read-only push agent — see `docs/docker-agent-monitoring.md`), and
the **Mesh Prerequisite Gate** — a universal CIDR-based mesh-verification
requirement (with a routed-mesh/VPC-peering fallback, not NetBird-specific),
configurable from Settings → Mesh and defaulting OFF so it can't lock the live
instance.
The **next planned feature is the Mesh Prerequisite Gate** — requiring a
verified NetBird mesh before the app can be configured. It is **designed but
not built** (`docs/mesh-prerequisite-gate.md`) and has open decisions that need
the user's sign-off before coding (notably defaulting it OFF so it can't lock
the live instance). See `HANDOFF.md` for where to resume.
There is no feature currently in progress. See `HANDOFF.md` for the latest
status and next steps.
If you're a fresh AI session: read this file, then `HANDOFF.md` (current
task state + standing workflow rules), then `design-decisions.md` (visual

View file

@ -10,6 +10,11 @@ import {
Gauge,
Settings,
Search,
Lightbulb,
ArrowRightLeft,
ArrowLeftRight,
Shuffle,
Rocket,
type LucideIcon,
} from 'lucide-react'
@ -25,6 +30,7 @@ interface GuideEntry {
title: string
description: string
tips?: string[]
examples?: { icon?: LucideIcon; label?: string; text: string }[]
}
const guideEntries: GuideEntry[] = [
@ -34,6 +40,9 @@ const guideEntries: GuideEntry[] = [
description:
'The home dashboard. Shows overall system health, a rollup of connected integrations, recent activity, and shortcuts into the rest of the app.',
tips: ['Click "Connected Integrations" entries to jump straight to Infrastructure.'],
examples: [
{ text: 'First thing in the morning: open Glance to see if anything went offline overnight before digging into any one page.' },
],
},
{
icon: Server,
@ -41,6 +50,9 @@ const guideEntries: GuideEntry[] = [
description:
'Lists every connected integration (Proxmox, AWS, Docker, NetBird, Cloudflare, Uptime Kuma, Weather, SSH hosts) and the live resources/health each one reports.',
tips: ['Add new integrations from Settings → Integrations — they show up here automatically.'],
examples: [
{ text: 'Connect Proxmox and AWS, then check VM and EC2 health side by side without opening either provider\'s own dashboard.' },
],
},
{
icon: Bookmark,
@ -50,61 +62,139 @@ const guideEntries: GuideEntry[] = [
'Icons are auto-detected from the title or URL (e.g. typing "Proxmox" picks up the real Proxmox logo) — pick "Choose manually" if it guesses wrong.',
'Star a bookmark to pin it to the Favorites panel.',
],
examples: [
{ text: 'Organize your router admin page, NAS UI, and internal wiki into "Network", "Storage", and "Docs" categories so new teammates can find them without asking.' },
],
},
{
icon: Terminal,
title: 'Terminal',
description: 'A full SSH terminal to any host you\'ve added as an integration — supports tabs, split panes, jump hosts, and certificate auth.',
tips: ['Session output can be logged; theme and font preferences are remembered between visits.'],
examples: [
{ text: 'SSH into a public-facing jump host, then open a second tab routed through it to reach a private VM that has no public IP of its own.' },
],
},
{
icon: Waypoints,
title: 'Tunnels',
description: 'Local, remote, and dynamic (SOCKS5) SSH tunnels. Tunnels can be set to auto-start whenever the backend boots.',
description:
'Local, remote, and dynamic (SOCKS5) SSH tunnels, each riding on top of an existing SSH Host integration. Tunnels can be set to auto-start whenever the backend boots, and the app keeps retrying if a connection drops.',
tips: ['Pick an SSH host from Settings → Integrations → SSH Hosts first — tunnels are created on top of one.'],
examples: [
{
icon: ArrowRightLeft,
label: 'Local Forward',
text: 'A database on a remote server only listens on its own localhost. Forward remote 5432 to your local 5432, and connect a DB client as if it were running on your machine.',
},
{
icon: ArrowLeftRight,
label: 'Remote Forward',
text: 'You\'re running something on your laptop (e.g. a dev server on :3000) and need a remote server to reach it temporarily — remote forward exposes your local port on the remote side.',
},
{
icon: Shuffle,
label: 'Dynamic (SOCKS5)',
text: 'Point your browser\'s proxy settings at the SOCKS5 port and browse as if you were sitting inside the remote network — useful for reaching a whole subnet of internal services through one SSH host, instead of forwarding each port individually.',
},
],
},
{
icon: FolderOpen,
title: 'Files',
description: 'Browse, edit, upload, and download files over SFTP on any connected SSH host — and transfer files directly between two hosts without round-tripping through your machine.',
tips: ['Use the "Send to another host" action on a file row to start a host-to-host transfer; progress shows live in the panel at the bottom.'],
examples: [
{ text: 'Move a large backup folder from one VPS straight to another, without downloading gigabytes to your laptop first just to re-upload them.' },
],
},
{
icon: Box,
title: 'Containers',
description: 'Manage Docker containers on remote hosts — start, stop, view logs, and exec into a running container.',
description: 'Manage Docker containers on remote hosts — start, stop, view logs, and exec into a running container. Containers can come from a direct Docker API connection, an SSH host (runs the `docker` CLI for you), or a lightweight monitoring agent.',
tips: ['No Docker API port to expose? Add the host as an SSH Host integration instead — containers show up the same way, just routed through SSH.'],
examples: [
{ text: 'A container crashed on a VM you don\'t want to manually SSH into — pick the host on the Containers page, hit restart, and watch the new logs without leaving the browser.' },
],
},
{
icon: MonitorSmartphone,
title: 'Remote Desktop',
description: 'RDP, VNC, and Telnet sessions to remote machines, streamed through the built-in Guacamole proxy — no separate client needed.',
examples: [
{ text: 'Open a VNC session into a headless Ubuntu box to fix a stuck GUI app, entirely inside the browser tab — no VNC viewer install required.' },
],
},
{
icon: Gauge,
title: 'Host Metrics',
description: 'Live CPU, memory, disk, network, listening-port, firewall, process, and login-activity widgets for any SSH-managed host.',
examples: [
{ text: 'Catch a host slowly running out of disk space days before it takes a service down with it.' },
],
},
{
icon: Settings,
title: 'Settings',
description: 'Your profile, integrations, appearance, notifications, and full data export/import (back up or migrate every integration, bookmark, and tunnel as a single JSON file).',
examples: [
{ text: 'Export everything before reinstalling the OS on the box ArchNest itself runs on, then import it on the fresh install to get back to where you left off.' },
],
},
{
icon: Search,
title: 'Search (top bar)',
description: 'The search box at the top of every page looks across pages, integrations, and bookmarks at once — press Enter to jump to the top match.',
examples: [
{ text: 'Type "proxmox" and hit Enter to jump straight to your Proxmox integration, instead of clicking through to Infrastructure first.' },
],
},
]
const quickStartSteps = [
'Add at least one integration in Settings → Integrations — an SSH Host is the easiest first connection, since Terminal, Files, Tunnels, Host Metrics, and Containers can all use it.',
'Open Terminal or Files to confirm the connection actually works end to end.',
'If you need to reach something deeper in a private network (a DB, an internal site, a whole subnet), set up a Tunnel for it instead of opening more ports.',
'Bookmark the dashboards and tools you check often in BookNest, so the next visit is one click instead of a search.',
]
export default function Help() {
return (
<div className="p-8" style={{ maxWidth: '1100px' }}>
<div style={{ marginBottom: '28px' }}>
<div style={{ marginBottom: '24px' }}>
<h1 style={{ fontSize: '22px', color: '#E8E6E0', fontWeight: 700, marginBottom: '6px' }}>How ArchNest works</h1>
<p style={{ fontSize: '13px', color: '#7A7D85' }}>
A quick tour of every page and what it's for. Use the sidebar to navigate, or the search bar at the top to jump straight to something.
A quick tour of every page and what it's for, with real examples for each. Use the sidebar to navigate, or the
search bar at the top to jump straight to something.
</p>
</div>
<div style={{ ...cardBase, marginBottom: '24px', borderColor: 'rgba(200,164,52,0.25)' }}>
<div className="flex items-center gap-3" style={{ marginBottom: '12px' }}>
<div
className="flex items-center justify-center"
style={{
width: '36px',
height: '36px',
borderRadius: '8px',
backgroundColor: 'rgba(200,164,52,0.14)',
border: '1px solid rgba(200,164,52,0.3)',
flexShrink: 0,
}}
>
<Rocket size={18} style={{ color: '#C8A434' }} />
</div>
<h3 style={{ fontSize: '14px', color: '#E8E6E0', fontWeight: 600 }}>New here? Start in this order</h3>
</div>
<ol style={{ paddingLeft: '20px', display: 'flex', flexDirection: 'column', gap: '6px' }}>
{quickStartSteps.map((step, i) => (
<li key={i} style={{ fontSize: '12.5px', color: '#A8A6A0', lineHeight: 1.6 }}>
{step}
</li>
))}
</ol>
</div>
<div className="grid grid-cols-2 gap-4">
{guideEntries.map((entry) => {
const Icon = entry.icon
@ -136,6 +226,31 @@ export default function Help() {
))}
</ul>
)}
{entry.examples && entry.examples.length > 0 && (
<div style={{ marginTop: '12px', display: 'flex', flexDirection: 'column', gap: '8px' }}>
{entry.examples.map((ex, i) => {
const ExIcon = ex.icon ?? Lightbulb
return (
<div
key={i}
className="flex items-start gap-2"
style={{
padding: '8px 10px',
borderRadius: '8px',
backgroundColor: 'rgba(200,164,52,0.05)',
border: '1px solid rgba(200,164,52,0.1)',
}}
>
<ExIcon size={13} style={{ color: '#C8A434', flexShrink: 0, marginTop: '1px' }} />
<p style={{ fontSize: '11.5px', color: '#A8A6A0', lineHeight: 1.5 }}>
{ex.label && <span style={{ color: '#C8A434', fontWeight: 600 }}>{ex.label}: </span>}
{ex.text}
</p>
</div>
)
})}
</div>
)}
</div>
)
})}

View file

@ -784,6 +784,77 @@ function SshHostsSection() {
type NewIntegrationDraft = { id: number; type: string; values: Record<string, string> }
function dockerHostInfo(baseUrl: string): { host: string; port: string } | null {
if (!baseUrl || baseUrl.startsWith('unix://')) return null
try {
const u = new URL(baseUrl.replace(/^tcp:\/\//, 'http://'))
if (u.protocol !== 'http:' && u.protocol !== 'https:') return null
if (!u.hostname) return null
return { host: u.hostname, port: u.port || '2375' }
} catch {
return null
}
}
function DockerSetupHint({ baseUrl }: { baseUrl: string }) {
const [copied, setCopied] = useState(false)
const info = dockerHostInfo(baseUrl)
if (!info) return null
const script = `sudo mkdir -p /etc/systemd/system/docker.service.d
sudo tee /etc/systemd/system/docker.service.d/override.conf > /dev/null <<EOF
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H fd:// -H tcp://${info.host}:${info.port}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
curl http://${info.host}:${info.port}/version`
return (
<div
style={{
marginTop: '12px',
padding: '12px 14px',
borderRadius: '8px',
border: '1px solid rgba(200,164,52,0.12)',
backgroundColor: 'rgba(255,255,255,0.02)',
}}
>
<div className="flex items-center justify-between" style={{ marginBottom: '8px' }}>
<span style={{ fontSize: '11px', color: '#7A7D85' }}>
Run this on <strong style={{ color: '#E8E6E0' }}>{info.host}</strong> to expose its Docker API on port {info.port}:
</span>
<button
onClick={() => {
navigator.clipboard.writeText(script)
setCopied(true)
setTimeout(() => setCopied(false), 1500)
}}
className="flex items-center gap-1 cursor-pointer border-none bg-transparent"
style={{ fontSize: '10.5px', fontWeight: 600, color: copied ? '#2ECC71' : '#C8A434', flexShrink: 0 }}
>
{copied ? <Check size={11} /> : null}
{copied ? 'Copied' : 'Copy script'}
</button>
</div>
<pre
style={{
fontSize: '10.5px',
color: '#9DA0A8',
fontFamily: 'monospace',
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
margin: 0,
lineHeight: 1.5,
}}
>
{script}
</pre>
</div>
)
}
function IntegrationsSection() {
const { user } = useAuth()
const isAdmin = user?.role === 'admin'
@ -1051,6 +1122,9 @@ function IntegrationsSection() {
'••••••••••••',
existing,
)}
{def.type === 'docker' && (
<DockerSetupHint baseUrl={draft.baseUrl ?? existing.config.baseUrl ?? ''} />
)}
</div>
)
})}
@ -1100,6 +1174,7 @@ function IntegrationsSection() {
(f, value) => setNewDraftField(draft.id, f.key, value),
'',
)}
{def.type === 'docker' && <DockerSetupHint baseUrl={draft.values.baseUrl ?? ''} />}
</div>
)
})}