diff --git a/docs/OPEN-SOURCE-RELEASE.md b/docs/OPEN-SOURCE-RELEASE.md new file mode 100644 index 0000000..9570eca --- /dev/null +++ b/docs/OPEN-SOURCE-RELEASE.md @@ -0,0 +1,234 @@ +# ArchNest — Open-Source Release Readiness (v1) + +This document is the checklist + plan for publishing ArchNest as an open-source +project. It is an internal planning doc — **do not copy this file into the public +repo.** It covers what to copy, what to scrub, licensing, repo structure, +README/screenshots, the release cadence, and resume/LinkedIn framing. + +The public OSS repo should be a **fresh repository with a clean history** (see +"Why a fresh repo" below), not a fork of the working repo. + +--- + +## 1. Security sweep result (done 2026-06-22) + +A full secret/credential sweep of tracked files **and entire git history** +(`git log --all -p`) was run. Result: **clean — no real secrets are committed.** + +- No private keys, no hardcoded secret assignments in tracked source. +- No real `.env`, `.pem`, `.key`, or `.db` was ever committed at any point in + history. The only `BEGIN RSA PRIVATE KEY` / `AKIA…` matches are documentation + prose and AWS's official `AKIAIOSFODNN7EXAMPLE` placeholder. +- `.gitignore` correctly excludes `backend/.env`, `backend/data`, `*.db`. +- Both `.env.example` files contain only placeholders (empty / `change-me-…`). + +### Code-level security review (solid) +- JWT auth (Fastify `@fastify/jwt`). Every route group registers a blanket + `addHook('onRequest', app.authenticate)` before its routes; the three + WebSocket routes (`terminal`, `docker`, `guacamole`) verify + `app.jwt.verify(query.token)` explicitly (WS can't use header hooks). +- Mutating shared-config endpoints (integrations, tunnels, data export/import, + user management) are gated by `adminOnly` / `requireAdmin`. `authenticate` + re-reads `role`/`active` from the DB each request, so demote/deactivate takes + effect immediately even with an older token. +- Integration secrets: `serialize()` returns only secret **key names** + (`secretKeys`), never values. Secrets are encrypted at rest (AES-256-GCM, + `backend/src/db/crypto.ts`). +- Docker agent ingest is a **separately registered** route with a constant-time + bearer-token check; returns 503 when `ARCHNEST_AGENT_TOKEN` is unset (disabled + by default), and is NOT behind the user-auth hook (by design). +- Command-injection surfaces are guarded: tmux session names validated against + `^[A-Za-z0-9_-]{1,64}$` before interpolation; `system.ts` uses `execFile` (no + shell); Docker-over-SSH single-quotes container refs. + +### Things to improve / note for OSS (not leaks) +- **CORS default**: `server.ts` does `origin: process.env.ARCHNEST_CORS_ORIGIN ?? true`. + `true` reflects any origin. Fine for a self-hosted single-origin deploy, but the + OSS README should tell users to set `ARCHNEST_CORS_ORIGIN` in production, and the + default `.env.example` should point at `http://localhost:5173` (it already does + for the backend example). +- Default JWT/secret env values in `backend/.env.example` are `change-me-…` — the + README must stress generating real ones (`openssl rand -hex 32`). The server + already refuses to boot without `ARCHNEST_SECRET_KEY` + `ARCHNEST_JWT_SECRET`. + +--- + +## 2. What to SCRUB / NOT copy to the public repo + +None of these are security leaks, but they are personal/infra-specific or +internal working notes that don't belong in a public project: + +| Item | Why | Action | +|---|---|---| +| `docs/rdp-debug-handoff.md` | Contains lab creds (`sam` / `happy2026`) + private VM IP `192.168.122.55` + personal host names | **Exclude** (or heavily genericize into a "Remote Desktop setup" guide with no creds/IPs) | +| `HANDOFF.md` | Internal session-to-session working notes | **Exclude** | +| `docs/OPEN-SOURCE-RELEASE.md` (this file) | Internal release plan | **Exclude** | +| `archnest.snsnetlabs.com` references in `.env.example`, `docker-compose.yml`, `.github/workflows/deploy.yml` | Personal domain/deploy target | **Genericize** to `example.com` / `localhost`; the deploy workflow should be removed or replaced with a generic CI (build + lint only, no SCP-to-my-server) | +| `.github/workflows/deploy.yml` | SSHes/SCPs to the personal `racknerd1` server | **Remove**; replace with a generic build/test CI workflow | +| `agent/` deploy specifics | Fine to include the agent script, but scrub any host-specific URLs/tokens in its README | **Review + genericize** | +| `assets/` personal background images | Large PNGs; keep the ones the UI needs (hero banner, logo, KPI backgrounds), drop unused experiments (`opt1.bg`, `settings-custom-bg`, `pics/`) | **Trim to what's referenced** | +| Test/scratch files | `backend/data/`, any `*.db`, session logs | Already gitignored — confirm none are force-added | + +Keep `ROADMAP.md` and `TERMIX_MIGRATION.md`? — `ROADMAP.md` yes (genericize: +it's a fine public roadmap once paid-tier framing is softened). `TERMIX_MIGRATION.md` +is build history; optional — can keep as `docs/HISTORY.md` or drop. + +--- + +## 3. Why a fresh repo (recommended) + +The working repo's history contains personal commit author emails, the personal +deploy workflow, the lab-cred debug doc, and the personal domain. The simplest +clean cut for a public project: + +1. Create a new empty public repo (e.g. `archnest` under the personal GitHub). +2. Copy the **working tree** (not `.git`) of the files in the "INCLUDE" list below. +3. Genericize the scrubbed items. +4. `git init`, single initial commit ("Initial public release — ArchNest v1"), + author set to the public identity. +5. Add `LICENSE`, public `README.md`, `CONTRIBUTING.md`, screenshots. + +This avoids dragging history-scrubbing tooling (BFG/`git filter-repo`) and +guarantees nothing personal leaks via an old commit. + +### INCLUDE (the actual app) +``` +src/ # React frontend +backend/src/ # Fastify backend +backend/package.json, backend/tsconfig*.json, backend/Dockerfile +backend/.env.example # (placeholders only — already clean) +package.json, package-lock.json, tsconfig*.json, vite.config.*, index.html +Dockerfile, docker-compose.yml # genericized (no personal domain) +.env.example # genericized +.gitignore, .dockerignore +public/ # fonts + static assets actually referenced +assets/ # ONLY images the UI imports +agent/archnest-docker-agent.sh + a genericized agent/README.md +design-decisions.md, ROADMAP.md # genericized +.kiro/steering/design-rules.md # optional — useful for contributors +LICENSE, README.md, CONTRIBUTING.md, screenshots/ # new, written for OSS +``` + +### EXCLUDE +``` +.git/ # fresh history instead +HANDOFF.md +docs/rdp-debug-handoff.md +docs/OPEN-SOURCE-RELEASE.md (this file) +.github/workflows/deploy.yml (replace with generic CI) +backend/data/, *.db, session logs, *.tsbuildinfo +unused assets/ experiments + pics/ +``` + +--- + +## 4. License + +Recommended: **MIT** or **Apache-2.0**. +- **MIT** — shortest, most permissive, maximum adoption, easiest "I built this" + story. Good default for a portfolio/resume project. +- **Apache-2.0** — same permissiveness plus an explicit patent grant and a + NOTICE mechanism; slightly more "enterprise-friendly." + +Given the goal (resume/LinkedIn showcase, broad adoption, simple), **MIT** is the +recommendation. Add a `LICENSE` file with the chosen license and the author's name ++ year. Note third-party components keep their own licenses (Guacamole = Apache-2.0, +the bundled Nerd Font has its own license already in `public/fonts/NERD-FONTS-LICENSE.txt`). + +--- + +## 5. README.md (public) — outline + +1. **Hero**: one-line pitch + a screenshot/GIF of the Glance dashboard. + > "A self-hosted, web-based control panel for your homelab and cloud — SSH + > terminal, file manager, Docker, tunnels, RDP/VNC, host metrics, and + > integration dashboards, all in one browser tab." +2. **Screenshots** (see §6). +3. **Features** — bullet list grouped by page; mark paid add-ons / not-yet-done + honestly (mirror the in-app Help "Not in the open-source version" notes). +4. **Architecture** — short: React + Vite + TS frontend, Fastify + SQLite backend, + guacd sidecar for RDP/VNC. One diagram is plenty. +5. **Quick start** — `docker compose up` path + the required env vars (with + `openssl rand -hex 32` generation), and the local-dev path (`npm install` / + `npm run dev` in root and `backend/`). +6. **Configuration** — env var table (from `.env.example`), CORS note, first-run + `/api/setup` admin creation, the 10-user cap. +7. **Security notes** — secrets encrypted at rest; set a real CORS origin in prod; + it's designed to sit behind your own mesh/VPN, not be exposed raw to the + internet (mesh prerequisite gate exists, defaults off). +8. **Roadmap** — link `ROADMAP.md` + the "updates ~every 3 months" promise. +9. **Contributing** — link `CONTRIBUTING.md`. +10. **License** — MIT. +11. **Credits / "Built with AI"** — see §7. + +--- + +## 6. Screenshots to capture (for README + LinkedIn) + +Capture in the **default dark theme**, with demo/sanitized data (no real +hostnames, IPs, or tokens — use the placeholder-y names): +- Glance dashboard (hero shot) +- Infrastructure → Node Status with a couple of integrations +- Terminal with a split-pane / multiple tabs +- Files (SFTP browser) + a host-to-host transfer in progress +- Containers list + a container detail tab +- Remote Desktop showing a live XFCE session +- Host Metrics widgets +- Settings → Integrations (shows the breadth) and the locked Appearance "Paid + add-on" card (shows the free/paid split honestly) +- Help page (shows the per-page docs + OSS-edition note) + +A short screen-recording GIF of opening a terminal or RDP session makes the +strongest LinkedIn post. + +--- + +## 7. Resume / LinkedIn framing + +Honest, specific, and ownership-forward. Suggested phrasing: + +> **ArchNest** — a self-hosted, web-based homelab/cloud control panel +> (React + TypeScript + Fastify + SQLite, Dockerized). Single-pane access to SSH +> terminals, SFTP, Docker, SSH tunnels, browser-based RDP/VNC (Apache Guacamole), +> live host metrics, and pluggable infrastructure integrations (Proxmox, AWS, +> Cloudflare, NetBird, Uptime Kuma). Built with AI-assisted development; I owned +> the architecture, product decisions, security review, and integration/debugging +> (e.g. root-caused and fixed a FreeRDP/NLA + Guacamole tunnel-keepalive issue +> end-to-end across browser, proxy, and target VM). Open source under MIT, shipped +> v1, ongoing ~quarterly releases. + +Notes for credibility: +- It's fine and increasingly normal to say "AI-assisted." Pair it with the + *engineering judgment* you provided (architecture, security, debugging) so it + reads as "I directed and verified," not "I prompted and pasted." +- The RDP debugging saga is a genuinely strong, concrete story — it shows + multi-layer debugging (browser ↔ guacd/FreeRDP ↔ xrdp/desktop) and root-cause + rigor. Worth a short LinkedIn write-up on its own. + +--- + +## 8. Pre-publish checklist + +- [ ] Create fresh public repo, copy INCLUDE list, exclude EXCLUDE list. +- [ ] Genericize personal domain → `example.com`/`localhost` in + `.env.example`, `docker-compose.yml`. +- [ ] Replace `.github/workflows/deploy.yml` with a generic build/lint CI (no SCP). +- [ ] Add `LICENSE` (MIT), public `README.md`, `CONTRIBUTING.md`. +- [ ] Capture + add screenshots (sanitized data, dark theme). +- [ ] Re-run a secret scan on the NEW repo before first push + (`git log -p | grep -iE 'AKIA|BEGIN .*PRIVATE KEY|password|secret'` plus a + tool like `gitleaks detect` for good measure). +- [ ] Confirm `npm run build` (root) and `npx tsc --noEmit` (backend) pass on the + copied tree. +- [ ] Confirm first-run works from a clean `docker compose up` with freshly + generated secrets and no prior DB. +- [ ] Tag `v1.0.0`. + +--- + +## 9. Release cadence (commitment) + +Public promise: **updates approximately every 3 months.** Keep a short +`CHANGELOG.md` in the public repo and cut a tagged release each cycle. The Help +page's "Open-source edition" note and the README both reference this cadence — +keep them in sync. diff --git a/src/components/StatusCards.tsx b/src/components/StatusCards.tsx index 18c957a..761e474 100644 --- a/src/components/StatusCards.tsx +++ b/src/components/StatusCards.tsx @@ -5,7 +5,7 @@ import { api, type Integration, type Resource, type Bookmark } from '../lib/api' import { getIntegrationTypeColors } from '../lib/integrationColors' const cardStyle: React.CSSProperties = { - backgroundColor: 'rgba(10, 10, 12, 0.55)', + backgroundColor: 'var(--kpi-card-bg)', backdropFilter: 'blur(10px)', border: '1px solid rgba(200, 164, 52, 0.1)', borderRadius: '12px', diff --git a/src/components/TopBar.tsx b/src/components/TopBar.tsx index 6055f66..cd551cd 100644 --- a/src/components/TopBar.tsx +++ b/src/components/TopBar.tsx @@ -1,6 +1,6 @@ import { useState, useRef, useEffect, useMemo } from 'react' import { useLocation, useNavigate } from 'react-router-dom' -import { Search, Bell, ChevronDown, User, Palette, LogOut, Shield, HelpCircle, Server, Bookmark as BookmarkIcon, LayoutGrid } from 'lucide-react' +import { Search, Bell, ChevronDown, User, LogOut, Shield, HelpCircle, Server, Bookmark as BookmarkIcon, LayoutGrid } from 'lucide-react' import { useAuth } from '../lib/AuthContext' import { api } from '../lib/api' @@ -227,13 +227,6 @@ export default function TopBar() { Profile - - ))} +
+
+
-
- -
- Accent Color -
- {accentColors.map((a) => ( - - ))} -
-
- -
-
- Font Size - {fontSize}px -
- update({ fontSize: Number(e.target.value) })} - className="w-full" - style={{ accentColor: '#C8A434' }} - /> -
- -
-
- Card Border Radius - {radius}px -
- update({ radius: Number(e.target.value) })} - className="w-full" - style={{ accentColor: '#C8A434' }} - /> -
- -
- Animations - update({ animations: !animations })} /> + + Appearance customization is a premium feature + +

+ Themes (light/dark), accent colors, font size, card radius, and animation controls are + part of a paid add-on and are not included in the open-source version of ArchNest. The + app ships with the default ArchNest dark theme. +

+ + Paid add-on +
) @@ -2137,7 +2064,9 @@ export default function Settings() { const visibleSections = navSections.filter((s) => !s.adminOnly || isAdmin) const requestedTab = searchParams.get('tab') const requestedAllowed = - requestedTab && sectionComponents[requestedTab] && visibleSections.some((s) => s.id === requestedTab) + requestedTab && + sectionComponents[requestedTab] && + visibleSections.some((s) => s.id === requestedTab && !('locked' in s && s.locked)) const active = requestedAllowed ? requestedTab! : 'profile' const ActiveSection = sectionComponents[active] @@ -2152,22 +2081,27 @@ export default function Settings() { {visibleSections.map((s) => { const Icon = s.icon const isActive = active === s.id + const isLocked = 'locked' in s && s.locked return ( ) })}