modified files
This commit is contained in:
parent
08139ff831
commit
b499759936
7 changed files with 372 additions and 137 deletions
234
docs/OPEN-SOURCE-RELEASE.md
Normal file
234
docs/OPEN-SOURCE-RELEASE.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<User size={14} />
|
||||
<span>Profile</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setUserMenuOpen(false); navigate('/settings?tab=appearance') }}
|
||||
className="flex w-full items-center gap-2.5 px-3 py-2 text-[12px] text-text-secondary hover:text-text-primary hover:bg-page transition-colors cursor-pointer border-none bg-transparent text-left"
|
||||
>
|
||||
<Palette size={14} />
|
||||
<span>Appearance</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setUserMenuOpen(false); navigate('/settings?tab=security') }}
|
||||
className="flex w-full items-center gap-2.5 px-3 py-2 text-[12px] text-text-secondary hover:text-text-primary hover:bg-page transition-colors cursor-pointer border-none bg-transparent text-left"
|
||||
|
|
|
|||
|
|
@ -28,23 +28,27 @@
|
|||
--color-teal: #1ABC9C;
|
||||
--select-bg: rgba(255, 255, 255, 0.04);
|
||||
--select-option-bg: #141518;
|
||||
/* KPI/glass card surface — dark glass over the hero banner in dark mode */
|
||||
--kpi-card-bg: rgba(10, 10, 12, 0.55);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
--color-page: #E4E6EB; /* soft gray, not white */
|
||||
--color-card: #F4F5F7; /* slightly lighter gray cards */
|
||||
--color-sidebar: #DADCE2; /* darker gray sidebar for separation */
|
||||
--color-border: #C4C7CF;
|
||||
--color-gold: #B08D2A; /* slightly darker gold for contrast on light */
|
||||
--color-page: #7E8287; /* warm mid gray page */
|
||||
--color-card: #A3A7AC; /* card surface (KPI cards inherit this) */
|
||||
--color-sidebar: #969A9F; /* secondary panels / sidebar */
|
||||
--color-border: #6E7176; /* darker than card for separation */
|
||||
--color-gold: #D4A437; /* gold accent */
|
||||
--color-success: #1E9E57;
|
||||
--color-warning: #C96A18;
|
||||
--color-danger: #C0392B;
|
||||
--color-text-primary: #1C1E22;
|
||||
--color-text-secondary: #5A5D66;
|
||||
--color-teal: #138D75;
|
||||
--select-bg: rgba(0, 0, 0, 0.04);
|
||||
--select-option-bg: #F4F5F7;
|
||||
--color-text-primary: #1E2328;
|
||||
--color-text-secondary: #3D4248; /* dark muted, readable on mid gray */
|
||||
--color-teal: #0F6E5C;
|
||||
--select-bg: rgba(0, 0, 0, 0.06);
|
||||
--select-option-bg: #A3A7AC;
|
||||
/* KPI cards use the solid card surface in light mode (not dark glass) */
|
||||
--kpi-card-bg: #A3A7AC;
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
ArrowLeftRight,
|
||||
Shuffle,
|
||||
Rocket,
|
||||
Lock,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react'
|
||||
|
||||
|
|
@ -31,6 +32,7 @@ interface GuideEntry {
|
|||
description: string
|
||||
tips?: string[]
|
||||
examples?: { icon?: LucideIcon; label?: string; text: string }[]
|
||||
notInOpenSource?: string[]
|
||||
}
|
||||
|
||||
const guideEntries: GuideEntry[] = [
|
||||
|
|
@ -53,6 +55,11 @@ const guideEntries: GuideEntry[] = [
|
|||
examples: [
|
||||
{ text: 'Connect Proxmox and AWS, then check VM and EC2 health side by side without opening either provider\'s own dashboard.' },
|
||||
],
|
||||
notInOpenSource: [
|
||||
'Node Status groups each integration into a single tile; a dedicated per-integration tab that lists every individual node is a paid add-on.',
|
||||
'The "Network" sub-tab is a placeholder and is intentionally disabled.',
|
||||
'Proxmox LXC container management is a paid add-on (VMs are listed; LXC lifecycle/console is not).',
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: Bookmark,
|
||||
|
|
@ -116,6 +123,9 @@ const guideEntries: GuideEntry[] = [
|
|||
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.' },
|
||||
],
|
||||
notInOpenSource: [
|
||||
'The push-monitoring agent (read-only, outbound-only) is included. The pull agent — a local authenticated API on each VM for monitor-and-manage over one mechanism — is a planned paid add-on.',
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: MonitorSmartphone,
|
||||
|
|
@ -124,6 +134,9 @@ const guideEntries: GuideEntry[] = [
|
|||
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.' },
|
||||
],
|
||||
notInOpenSource: [
|
||||
'For RDP targets, the XFCE desktop is verified working. GNOME and KDE desktops are not yet supported (GNOME is Wayland-only and its RDP needs newer components; KDE is untested) — tracked as a planned add-on. Use XFCE, or VNC/Telnet, for now.',
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: Gauge,
|
||||
|
|
@ -136,10 +149,15 @@ const guideEntries: GuideEntry[] = [
|
|||
{
|
||||
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).',
|
||||
description: 'Your profile, security (password, active sessions, login history), integrations, multi-user management, mesh network gate, 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.' },
|
||||
],
|
||||
notInOpenSource: [
|
||||
'Appearance (themes, light mode, accent color, font size, animations) is a paid add-on — the tab is locked and the app ships with the default dark theme.',
|
||||
'Notifications (email/push/sound toggles) is a non-functional placeholder — there is no delivery mechanism wired up yet.',
|
||||
'Single sign-on (Authentik / OIDC) is a planned paid add-on; local username/password accounts are the built-in auth.',
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: Search,
|
||||
|
|
@ -195,6 +213,33 @@ export default function Help() {
|
|||
</ol>
|
||||
</div>
|
||||
|
||||
<div style={{ ...cardBase, marginBottom: '24px', borderColor: 'rgba(200,164,52,0.25)' }}>
|
||||
<div className="flex items-center gap-3" style={{ marginBottom: '10px' }}>
|
||||
<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,
|
||||
}}
|
||||
>
|
||||
<Lock size={16} style={{ color: '#C8A434' }} />
|
||||
</div>
|
||||
<h3 style={{ fontSize: '14px', color: '#E8E6E0', fontWeight: 600 }}>Open-source edition</h3>
|
||||
</div>
|
||||
<p style={{ fontSize: '12.5px', color: '#A8A6A0', lineHeight: 1.6 }}>
|
||||
This is the free, open-source version of ArchNest. All core functionality — integrations,
|
||||
Terminal, Tunnels, Files, Containers, Host Metrics, Remote Desktop, bookmarks, multi-user
|
||||
accounts, and data backup — is fully included. A few features are reserved for paid add-ons
|
||||
or not yet complete; each card below marks anything that isn't in this edition with a
|
||||
<span style={{ color: '#C8A434', fontWeight: 600 }}> "Not in the open-source version" </span>
|
||||
note. ArchNest is under active development with planned updates roughly every three months.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{guideEntries.map((entry) => {
|
||||
const Icon = entry.icon
|
||||
|
|
@ -251,6 +296,31 @@ export default function Help() {
|
|||
})}
|
||||
</div>
|
||||
)}
|
||||
{entry.notInOpenSource && entry.notInOpenSource.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '12px',
|
||||
padding: '10px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'rgba(122,125,133,0.06)',
|
||||
border: '1px solid rgba(122,125,133,0.18)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-1.5" style={{ marginBottom: '6px' }}>
|
||||
<Lock size={11} style={{ color: '#7A7D85', flexShrink: 0 }} />
|
||||
<span style={{ fontSize: '10px', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.8px', color: '#7A7D85' }}>
|
||||
Not in the open-source version
|
||||
</span>
|
||||
</div>
|
||||
<ul style={{ paddingLeft: '16px', display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||
{entry.notInOpenSource.map((note, i) => (
|
||||
<li key={i} style={{ fontSize: '11px', color: '#7A7D85', lineHeight: 1.5 }}>
|
||||
{note}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -341,7 +341,7 @@ export default function Infrastructure() {
|
|||
return (
|
||||
<div
|
||||
key={card.label}
|
||||
style={{ ...cardBase, backgroundColor: 'rgba(10, 10, 12, 0.5)', padding: '18px', justifyContent: 'center', alignItems: 'center', gap: '8px' }}
|
||||
style={{ ...cardBase, backgroundColor: 'var(--kpi-card-bg)', padding: '18px', justifyContent: 'center', alignItems: 'center', gap: '8px' }}
|
||||
className="hover:!border-gold/20"
|
||||
>
|
||||
<h3 style={{ fontSize: '10px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, textAlign: 'center' }}>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { useEffect, useRef, useState } from 'react'
|
|||
import { useSearchParams } from 'react-router-dom'
|
||||
import { api, ApiError, type Integration, type AuthSession, type LoginEvent, type ManagedUser, type MeshStatus } from '../lib/api'
|
||||
import { useAuth } from '../lib/AuthContext'
|
||||
import { loadAppearance, saveAppearance, applyAppearance, type AppearancePrefs } from '../lib/theme'
|
||||
import {
|
||||
User,
|
||||
Palette,
|
||||
|
|
@ -22,6 +21,7 @@ import {
|
|||
Shield,
|
||||
Monitor,
|
||||
LogOut,
|
||||
Lock,
|
||||
Users,
|
||||
UserPlus,
|
||||
Network,
|
||||
|
|
@ -29,7 +29,7 @@ import {
|
|||
|
||||
const navSections = [
|
||||
{ id: 'profile', label: 'Profile', icon: User },
|
||||
{ id: 'appearance', label: 'Appearance', icon: Palette },
|
||||
{ id: 'appearance', label: 'Appearance', icon: Palette, locked: true },
|
||||
{ id: 'security', label: 'Security', icon: Shield },
|
||||
{ id: 'users', label: 'Users', icon: Users, adminOnly: true },
|
||||
{ id: 'mesh', label: 'Mesh', icon: Network, adminOnly: true },
|
||||
|
|
@ -39,15 +39,6 @@ const navSections = [
|
|||
{ id: 'about', label: 'About', icon: Info },
|
||||
]
|
||||
|
||||
const accentColors = [
|
||||
{ name: 'Gold', color: '#C8A434' },
|
||||
{ name: 'Teal', color: '#2DD4BF' },
|
||||
{ name: 'Purple', color: '#A855F7' },
|
||||
{ name: 'Blue', color: '#3B82F6' },
|
||||
{ name: 'Green', color: '#2ECC71' },
|
||||
{ name: 'Red', color: '#E74C3C' },
|
||||
]
|
||||
|
||||
type FieldDef = { key: string; label: string; secret?: boolean; hint?: string; placeholder?: string; file?: boolean }
|
||||
|
||||
const integrationTypeDefs: { type: string; name: string; multiInstance?: boolean; fields: FieldDef[] }[] = [
|
||||
|
|
@ -290,109 +281,45 @@ function ProfileSection() {
|
|||
}
|
||||
|
||||
function AppearanceSection() {
|
||||
const [prefs, setPrefs] = useState<AppearancePrefs>(loadAppearance)
|
||||
const [accent, setAccent] = useState('Gold')
|
||||
|
||||
// Persist + apply on every change so the UI updates live.
|
||||
function update(patch: Partial<AppearancePrefs>) {
|
||||
setPrefs((prev) => {
|
||||
const next = { ...prev, ...patch }
|
||||
saveAppearance(next)
|
||||
applyAppearance(next)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const theme = prefs.theme
|
||||
const fontSize = prefs.fontSize
|
||||
const radius = prefs.radius
|
||||
const animations = prefs.animations
|
||||
|
||||
// Appearance customization (theme/accent/font/radius/animations) is a paid
|
||||
// feature and is locked in the open-source version. The app still applies its
|
||||
// default dark theme via src/lib/theme.ts at boot; this panel only gates the
|
||||
// user-facing controls. See ROADMAP.md "Appearance — paid add-on".
|
||||
return (
|
||||
<div style={cardBase}>
|
||||
<h3 style={sectionTitle}>Appearance</h3>
|
||||
|
||||
<div className="flex items-center justify-between" style={{ marginBottom: '20px' }}>
|
||||
<span style={{ fontSize: '13px', color: '#E8E6E0' }}>Theme</span>
|
||||
<div className="flex items-center gap-1" style={{ backgroundColor: 'rgba(255,255,255,0.03)', borderRadius: '8px', padding: '3px' }}>
|
||||
{(['dark', 'light'] as const).map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => update({ theme: t })}
|
||||
className="cursor-pointer border-none capitalize"
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
padding: '6px 14px',
|
||||
borderRadius: '6px',
|
||||
color: theme === t ? '#0A0B0D' : '#7A7D85',
|
||||
backgroundColor: theme === t ? '#C8A434' : 'transparent',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{t}
|
||||
</button>
|
||||
))}
|
||||
<div
|
||||
className="flex flex-col items-center justify-center text-center"
|
||||
style={{ padding: '48px 24px', gap: '14px' }}
|
||||
>
|
||||
<div
|
||||
className="flex items-center justify-center rounded-full"
|
||||
style={{ width: '56px', height: '56px', backgroundColor: 'rgba(200,164,52,0.1)' }}
|
||||
>
|
||||
<Lock size={24} color="#C8A434" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<span style={{ fontSize: '13px', color: '#E8E6E0', marginBottom: '10px', display: 'block' }}>Accent Color</span>
|
||||
<div className="flex items-center gap-3">
|
||||
{accentColors.map((a) => (
|
||||
<button
|
||||
key={a.name}
|
||||
onClick={() => setAccent(a.name)}
|
||||
title={a.name}
|
||||
className="cursor-pointer border-none rounded-full flex items-center justify-center"
|
||||
style={{
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
backgroundColor: a.color,
|
||||
outline: accent === a.name ? `2px solid ${a.color}` : 'none',
|
||||
outlineOffset: '3px',
|
||||
}}
|
||||
>
|
||||
{accent === a.name && <Check size={14} color="#0A0B0D" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<div className="flex items-center justify-between" style={{ marginBottom: '8px' }}>
|
||||
<span style={{ fontSize: '13px', color: '#E8E6E0' }}>Font Size</span>
|
||||
<span style={{ fontSize: '11px', color: '#C8A434' }}>{fontSize}px</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={12}
|
||||
max={16}
|
||||
value={fontSize}
|
||||
onChange={(e) => update({ fontSize: Number(e.target.value) })}
|
||||
className="w-full"
|
||||
style={{ accentColor: '#C8A434' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<div className="flex items-center justify-between" style={{ marginBottom: '8px' }}>
|
||||
<span style={{ fontSize: '13px', color: '#E8E6E0' }}>Card Border Radius</span>
|
||||
<span style={{ fontSize: '11px', color: '#C8A434' }}>{radius}px</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={4}
|
||||
max={16}
|
||||
value={radius}
|
||||
onChange={(e) => update({ radius: Number(e.target.value) })}
|
||||
className="w-full"
|
||||
style={{ accentColor: '#C8A434' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span style={{ fontSize: '13px', color: '#E8E6E0' }}>Animations</span>
|
||||
<Toggle on={animations} onClick={() => update({ animations: !animations })} />
|
||||
<span style={{ fontSize: '15px', fontWeight: 600, color: '#E8E6E0' }}>
|
||||
Appearance customization is a premium feature
|
||||
</span>
|
||||
<p style={{ fontSize: '13px', color: '#7A7D85', maxWidth: '420px', lineHeight: 1.6 }}>
|
||||
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.
|
||||
</p>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '1px',
|
||||
color: '#C8A434',
|
||||
border: '1px solid rgba(200,164,52,0.3)',
|
||||
borderRadius: '6px',
|
||||
padding: '5px 12px',
|
||||
}}
|
||||
>
|
||||
Paid add-on
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -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 (
|
||||
<button
|
||||
key={s.id}
|
||||
onClick={() => setActive(s.id)}
|
||||
className="flex items-center gap-2.5 cursor-pointer border-none bg-transparent transition-colors"
|
||||
onClick={() => !isLocked && setActive(s.id)}
|
||||
disabled={isLocked}
|
||||
title={isLocked ? 'Premium feature — not available in the open-source version' : undefined}
|
||||
className="flex items-center gap-2.5 border-none bg-transparent transition-colors"
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
padding: '10px 14px',
|
||||
borderRadius: '8px',
|
||||
color: isActive ? '#C8A434' : '#7A7D85',
|
||||
backgroundColor: isActive ? 'rgba(200,164,52,0.1)' : 'transparent',
|
||||
color: isLocked ? '#4A4D55' : isActive ? '#C8A434' : '#7A7D85',
|
||||
backgroundColor: isActive && !isLocked ? 'rgba(200,164,52,0.1)' : 'transparent',
|
||||
cursor: isLocked ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
<Icon size={15} />
|
||||
{s.label}
|
||||
<span className="flex-1 text-left">{s.label}</span>
|
||||
{isLocked && <Lock size={12} />}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue