From f4943b6b6d04ff21f2e23b5f26f885c9d66c7202 Mon Sep 17 00:00:00 2001 From: marc Date: Fri, 19 Jun 2026 22:38:42 +0200 Subject: [PATCH] Add refresh countdown, fix quota fs lookup, update README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - main.js: show live countdown "Updated HH:MM:SS — next in Xs" so the 30s auto-refresh is visually obvious instead of just a static timestamp - app.py: rename _find_mountpoint → _find_quota_fs and fix the stale call site that caused a NameError; function now returns mountpoint from df, falling back to the path itself if df returns / - README: add full quota setup section (fstab usrquota, quotacheck, quotaon, verify with repquota); fix stale "127.0.0.1" bind note (service already uses 0.0.0.0); update Auth section (sessions, not Basic Auth) Co-Authored-By: Claude Sonnet 4.6 --- README.md | 48 +++++++++++++++++++++++++++++++++++++++--- app.py | 21 ++++++++++-------- static/js/dashboard.js | 2 +- static/js/main.js | 18 +++++++++++++++- 4 files changed, 75 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index db648ab..2aa60e1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MicroNAS -Lightweight web GUI to manage SMB shares, NFS exports, and quotas on a single home server (Proxmox LXC/VM). Runs directly on the host as a systemd service. +Lightweight web GUI to manage SMB shares, NFS exports, quotas, and users on a single home server (Proxmox LXC/VM). Runs directly on the host as a systemd service. ## Proxmox LXC setup @@ -57,6 +57,48 @@ systemctl enable --now micronas Open `http://:5000` in your browser. +--- + +## Quota setup (one-time, per filesystem) + +Quotas are a **filesystem feature** — they must be enabled on the partition that holds your shares (usually `/srv`) before MicroNAS can set them. + +### Step 1 — Enable quota mount options + +Edit `/etc/fstab`. Find the line for the partition that contains `/srv` and add `usrquota`: + +``` +# Example — your device path and options may differ +/dev/sdb1 /srv ext4 defaults,usrquota 0 2 +``` + +Remount to apply without rebooting: + +```bash +mount -o remount /srv +``` + +### Step 2 — Initialise quota database + +```bash +quotacheck -ugm /srv +quotaon /srv +``` + +This creates `/srv/aquota.user`. You only need to run this once; `quotaon` is automatically called on boot via the quota init script. + +### Step 3 — Verify + +```bash +repquota -u /srv +``` + +If it prints a header and user list (even if empty), quotas are active. MicroNAS reads this output for the dashboard. + +> **Note:** if `/srv` is not a separate partition (e.g. it lives on the root `/` filesystem), add `usrquota` to the `/` entry in fstab instead and run `quotacheck`/`quotaon` on `/`. The Quota button in MicroNAS detects the correct filesystem automatically via `df`. + +--- + ## Updates ```bash @@ -67,7 +109,7 @@ systemctl restart micronas ## Network -The service binds to `127.0.0.1:5000` by default. To reach it directly from the LAN, change `-b 127.0.0.1:5000` to `-b 0.0.0.0:5000` in `micronas.service`, then run `systemctl daemon-reload && systemctl restart micronas`. +The service binds to `0.0.0.0:5000` — accessible from any LAN device at `http://:5000`. For HTTPS, put nginx in front as a reverse proxy. @@ -85,4 +127,4 @@ docker compose up ## Auth -All routes require HTTP Basic Auth. Credentials come from `MICRONAS_USER` / `MICRONAS_PASS` environment variables (set via `/etc/micronas.env` in production). If `MICRONAS_PASS` is not set the app starts unprotected and logs a warning — don't do this in production. +All routes require a login session. Go to `http://:5000` — you will be redirected to the login page automatically. Credentials come from `MICRONAS_USER` / `MICRONAS_PASS` in `/etc/micronas.env`. If `MICRONAS_PASS` is not set the app starts unprotected and logs a warning — do not do this in production. diff --git a/app.py b/app.py index 111f0ee..0da4647 100644 --- a/app.py +++ b/app.py @@ -417,14 +417,17 @@ def toggle_nfs(): # ===== QUOTA ===== -def _find_mountpoint(path): - """Return the filesystem mountpoint that contains path.""" - r = subprocess.run( - ['df', '--output=target', path], - capture_output=True, text=True, - ) - lines = [l.strip() for l in r.stdout.strip().splitlines() if l.strip() and l.strip() != 'Mounted on'] - return lines[0] if lines else '/srv' +def _find_quota_fs(path): + """Return the best filesystem argument for setquota. + + setquota needs a path whose filesystem has quota enabled. We try the + exact path first, then walk up to the mountpoint reported by df. If df + says '/' we still try the path itself — the user may have quota on root. + """ + r = subprocess.run(['df', '--output=target', path], capture_output=True, text=True) + lines = [l.strip() for l in r.stdout.strip().splitlines() + if l.strip() and l.strip() != 'Mounted on'] + return lines[0] if lines else path @app.route('/api/quota', methods=['POST']) @@ -449,7 +452,7 @@ def set_quota(): except ValueError as e: return jsonify({'error': str(e)}), 400 - mount = _find_mountpoint(safe_path) + mount = _find_quota_fs(safe_path) soft_kb = str(quota_gb * 1024 * 1024) hard_kb = str((quota_gb + 1) * 1024 * 1024) try: diff --git a/static/js/dashboard.js b/static/js/dashboard.js index 9ec45a6..0f6d42b 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -57,5 +57,5 @@ async function loadDashboard() { }).join(''); } - document.getElementById('lastRefresh').textContent = new Date().toLocaleTimeString(); + _startRefreshCountdown(); } diff --git a/static/js/main.js b/static/js/main.js index b26e8db..b82f79c 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -1,4 +1,20 @@ +const REFRESH_INTERVAL = 30; +let _countdownTimer = null; + +function _startRefreshCountdown() { + if (_countdownTimer) clearInterval(_countdownTimer); + let secs = REFRESH_INTERVAL; + const el = document.getElementById('lastRefresh'); + const update = () => { + const t = new Date().toLocaleTimeString(); + el.textContent = `Updated ${t} — next in ${secs}s`; + if (secs <= 0) secs = REFRESH_INTERVAL; else secs--; + }; + update(); + _countdownTimer = setInterval(update, 1000); +} + loadDashboard(); loadShares(); loadUsers(); -setInterval(() => { loadDashboard(); loadShares(); loadUsers(); }, 30_000); +setInterval(() => { loadDashboard(); loadShares(); loadUsers(); }, REFRESH_INTERVAL * 1000);