diff --git a/CLAUDE.md b/CLAUDE.md index 41e80d1..c59e08d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,38 +4,49 @@ Project context for Claude Code. See `PLANNING.md` for the full roadmap. ## What this is A lightweight web GUI to manage file shares on a single home server (Proxmox -LXC/VM) — a minimal NAS control panel. It manages **SMB shares**, **NFS -exports**, and **quotas** by editing host config files and reloading the +LXC/VM) — a minimal NAS control panel. Manages **SMB shares**, **NFS exports**, +**quotas**, and **system users** by editing host config files and reloading daemons. Home/LAN use only. -## Files -- `app.py` — Flask backend (API + share/export/quota logic). -- `index.html` — Bootstrap single-page UI. -- `docker-compose.yml` — optional dev/test sandbox only. -- `PLANNING.md` — roadmap and feature list. +## File layout +- `app.py` — Flask backend: all API routes, auth, business logic. +- `templates/index.html` — main SPA shell (Jinja2); no inline JS or CSS. +- `templates/login.html` — login form. +- `static/css/app.css` — custom dark theme (GitHub-palette CSS variables). +- `static/js/utils.js` — shared helpers: `esc()`, `showToast()`, `apiFetch()`. +- `static/js/dashboard.js` — dashboard data loading. +- `static/js/shares.js` — SMB/NFS CRUD (create, edit, delete, toggle). +- `static/js/quotas.js` — quota modal logic. +- `static/js/users.js` — user CRUD + password modal. +- `static/js/main.js` — page init and 30 s auto-refresh. +- `micronas.service` — systemd unit (gunicorn, reads `/etc/micronas.env`). +- `docker-compose.yml` — dev/test sandbox only, not for production. -## How it runs (target) +## How it runs Directly on the LXC/VM as a `systemd` service, Flask behind `gunicorn`. **Not** in Docker for production — the app manipulates host services -(`smbd`, `nfsd`, `/etc/exports`, `exportfs`, `setquota`), so it must run on the -host. Docker stays only as a test sandbox. +(`smbd`, `nfsd`, `/etc/exports`, `exportfs`, `setquota`). + +Auth uses **Flask sessions** with a secret key derived from `MICRONAS_PASS`. +Credentials come from `/etc/micronas.env` (`MICRONAS_USER`, `MICRONAS_PASS`). ## Conventions & constraints -- **Keep it simple.** No database, no auth framework, dependency-light. Config - files (`/etc/samba/smb.conf`, `/etc/exports`) are the source of truth. +- **Keep it simple.** No database, no auth framework, dependency-light. + Config files (`/etc/samba/smb.conf`, `/etc/exports`) are the source of truth. - **Security basics are non-negotiable even for home use:** - Never use `subprocess.run(..., shell=True)` with user input. Use argument lists. - Validate all input: names `^[A-Za-z0-9_-]+$`; paths must resolve under `/srv`. - - All routes require auth (single-user HTTP Basic Auth from an env var). -- **Config edits must be safe:** parse into blocks and rewrite whole files for - edit/delete — do not blindly append. + - All routes require a valid session (`require_auth` decorator). +- **Config edits are safe:** `_parse_smb_conf()` / `_write_smb_conf()` and + `_parse_exports()` / `_write_exports()` parse into blocks and rewrite the + whole file — never blindly append for edit/delete/toggle operations. - LAN-only. Do not add internet-facing features. ## Out of scope -Backups (handled by Proxmox Backup Server), multi-user roles, databases, -internet exposure, multi-host management. +Backups (Proxmox Backup Server), multi-user roles, databases, internet exposure, +multi-host management. ## Working agreement - Work in the phases defined in `PLANNING.md`; pause after each phase for the diff --git a/PLANNING.md b/PLANNING.md index 0828cb0..68c9f2f 100644 --- a/PLANNING.md +++ b/PLANNING.md @@ -30,81 +30,75 @@ on a second client). This app does not implement backup. --- -## Current state (baseline) +## Project structure -- `app.py` — Flask API: list/create SMB & NFS shares, set quotas. -- `index.html` — Bootstrap UI with SMB / NFS / Quotas tabs. -- `docker-compose.yml` — test sandbox. - -### Known issues to fix -- `parse_smb()` is called (`app.py:13`) but never defined → `/api/shares` 500s. -- `subprocess.run(cmd, shell=True)` with f-string input (`app.py:82`) → command - injection as root. -- `add_smb_share` writes raw user input into `smb.conf` → config injection via - newlines. -- No authentication on any route. -- Config is append-only (`open(..., 'a')`) → no working delete/edit; UI Delete - buttons are wired to nothing. -- No input validation on names or paths (path traversal, share of `/`). +``` +MicroNAS/ +├── app.py Flask backend (all API routes + business logic) +├── templates/ +│ ├── index.html Main SPA shell (Jinja2, no inline JS/CSS) +│ └── login.html Login form +├── static/ +│ ├── css/app.css Custom dark theme (GitHub-palette CSS vars) +│ └── js/ +│ ├── utils.js esc(), showToast(), apiFetch() +│ ├── dashboard.js loadDashboard() +│ ├── shares.js SMB/NFS CRUD + edit/delete/toggle +│ ├── quotas.js Quota modal +│ ├── users.js User CRUD + password modal +│ └── main.js Init + auto-refresh +├── micronas.service systemd unit +├── docker-compose.yml Dev/test sandbox only +├── README.md +└── PLANNING.md +``` --- ## Roadmap -### Phase 1 — Correctness & safety (must-have) -- [ ] Implement `parse_smb()` so `/api/shares` works. -- [ ] Remove all `shell=True`; use argument-list `subprocess` calls. -- [ ] Validate input: - - share/user names: `^[A-Za-z0-9_-]+$` - - paths must resolve under `/srv` - - reject anything else with a clear error. -- [ ] Simple login: HTTP Basic Auth, single user + password from env var, - protecting all routes. -- [ ] `systemd` unit + `gunicorn`, plus README run instructions. +### Phase 1 — Correctness & safety ✅ +- [x] Implement `parse_smb()` so `/api/shares` works. +- [x] Remove all `shell=True`; use argument-list `subprocess` calls. +- [x] Validate input: names `^[A-Za-z0-9_-]+$`; paths must resolve under `/srv`. +- [x] Session-based login (custom `/login` page, Flask sessions, secret key + derived from `MICRONAS_PASS`). +- [x] `systemd` unit + `gunicorn` + README install guide. -### Phase 2 — Dashboard & health (the main goal) -A landing page that shows the state of the server at a glance: -- [x] **Daemon status** — is `smbd` running? is `nfsd` running? (green/red). -- [x] **Disk usage** — free / used / total for `/srv`. -- [x] **All shares in one view** — every SMB share and NFS export, their paths, - and who can access them. -- [x] **Per-share data used** — size on disk of each share's path (`du`). -- [x] **Quota usage** — for each user with a quota: used vs limit, with a - progress bar. -- [x] Auto-refresh every 30 seconds + manual refresh button. +### Phase 2 — Dashboard & health ✅ +- [x] **Daemon status** — smbd / nfsd running? (green/red dot). +- [x] **Disk usage** — free / used / total for `/srv` with progress bar. +- [x] **All shares in one view** — SMB + NFS, paths, access, size on disk. +- [x] **Per-share data used** — `du -sb` per share path. +- [x] **Quota usage** — per-user progress bars from `repquota`. +- [x] Auto-refresh every 30 s + manual refresh button. -### Phase 2.5 — UX improvements (next up) -Improvements identified after first real test: +### Phase 2.5 — UX improvements ✅ +- [x] UI redesign — GitHub-dark palette, custom CSS vars, better contrast + and typography. +- [x] Inline quota management — "Quota" button per share row opens a modal + pre-filled with the share's valid users. +- [x] Toast notifications — replaced all `alert()`. +- [x] Frontend split into `templates/` + `static/css/` + `static/js/`. +- [x] User management tab — list users (uid ≥ 1000), create, delete, + change SMB password. +- [x] Login page — replaces browser Basic Auth dialog. -- [x] **UI redesign** — GitHub-dark palette, better contrast, card layout, - breathing room. Custom CSS variables, no extra dependencies. -- [x] **Inline quota management** — removed dedicated Quotas tab. Each SMB - share and NFS export row has a "Quota" button that opens a modal. - For SMB: lists valid_users with individual GB inputs. For NFS: manual - user + GB. Submits one API call per user in parallel. -- [x] **Toast notifications** — replaced all `alert()` with Bootstrap toasts. -- [x] **Auto-refresh every 30 s** (was 60 s). -- [ ] **User management tab** — create, delete, and change the SMB password - of NAS users without touching the command line: - - List all system users (uid ≥ 1000), show whether they have a Samba - account. - - Create user: `useradd -M -s /usr/sbin/nologin` + `smbpasswd -a`. - - Delete user: `smbpasswd -x` + `userdel`. - - Change SMB password: `smbpasswd -s` via modal. - - Backend: `/api/users` GET/POST, `/api/users/` DELETE, - `/api/users//password` POST. - -### Phase 3 — Full management (complete the CRUD) -- [ ] Working **delete** for SMB & NFS (parse config into blocks, rewrite the +### Phase 3 — Full share management ✅ +- [x] Working **delete** for SMB & NFS (parse config into sections, rewrite whole file — not append-only). -- [ ] **Edit** existing shares (path, valid users). -- [ ] Enable/disable a share without deleting it. +- [x] **Edit** existing shares — name, path, valid users (SMB); clients, + ro/rw (NFS). Edit modal pre-filled with current values. +- [x] **Enable/disable** a share without deleting it — SMB uses + `available = no`; NFS comments out the export line. +- [x] Status indicator per share row (green/red dot, reduced opacity when + disabled). ### Phase 4 — Nice-to-have -- [ ] **WebDAV** support. Preferred approach: don't build it in Flask — have the - app drop an Apache/nginx WebDAV config snippet, same pattern as SMB/NFS, - so it reuses the existing architecture. +- [ ] **WebDAV** support. Preferred approach: drop an Apache/nginx config + snippet — don't build it in Flask. - [ ] Simple in-browser file browser for `/srv`. +- [ ] Auto-create share directory on add (with correct owner/permissions). ---