diff --git a/public/app.js b/public/app.js
index 1a54b1a..3445d85 100644
--- a/public/app.js
+++ b/public/app.js
@@ -1088,9 +1088,13 @@ function viewSettings() {
if (st.signedIn) { const b = $('#plexout'); b.hidden = false; b.onclick = async () => { await jfetch('/api/plex/logout', { method: 'POST' }); pl.a.pause(); pl.q = []; pl.i = -1; renderMini(); toast('Signed out of Plex'); viewSettings(); }; }
}).catch(() => { const el = $('#plexst'); if (el) el.textContent = 'Unavailable'; });
jfetch('/api/system').then(i => {
- const ok = i.mounted !== false && i.writable, row = (t, d, bad) => `
${t}${d}
`;
+ const anon = i.source && i.source.kind === 'anonymous', ok = i.mounted !== false && !anon && i.writable;
+ const row = (t, d, bad) => `${t}${d}
`;
+ const where = !i.source ? '' : i.source.kind === 'folder' ? ' · host folder ' + esc(i.source.name) : i.source.kind === 'named' ? ' · volume "' + esc(i.source.name) + '"' : '';
$('#stor').innerHTML =
- row(ok ? 'Data is saved on a volume' : 'Data is NOT being kept', i.mounted === false ? 'The data folder is not a mounted volume, so passkeys, calendars and sign-ins are lost when the container is recreated. Mount a volume or a host folder at ' + esc(i.dir) + '.' : !i.writable ? 'The data folder is not writable by the app.' : esc(i.dir) + (i.mounted ? ' (mounted)' : ''), !ok)
+ row(ok ? 'Data is saved on a volume' : 'Data is NOT being kept safely', anon ? 'This is a temporary Docker volume (' + esc(i.source.name) + '…), NOT your host folder. It is deleted when the container is recreated, and your folder stays empty. Check that your stack mounts the host folder at ' + esc(i.dir) + '.'
+ : i.mounted === false ? 'The data folder is not a mounted volume, so passkeys, calendars and sign-ins are lost when the container is recreated. Mount a volume or a host folder at ' + esc(i.dir) + '.'
+ : !i.writable ? 'The data folder is not writable by the app.' : esc(i.dir) + ' (mounted' + where + ')', !ok)
+ row('What is stored', `${i.passkeys} passkey${i.passkeys === 1 ? '' : 's'} · ${i.events} NE Hub event${i.events === 1 ? '' : 's'} · ${i.calendarLinks} calendar link${i.calendarLinks === 1 ? '' : 's'} · ${i.settings || 0} saved setting${i.settings === 1 ? '' : 's'}${i.icloud ? ' · iCloud connected' : ''}`)
+ row('Session secret', i.secretFromEnv ? 'Set in the stack (sign-ins survive restarts)' : 'Generated and kept in the data folder', false)
+ row('Running since', new Date(i.startedAt).toLocaleString(), false);
diff --git a/server.js b/server.js
index 2474fa0..b899b29 100644
--- a/server.js
+++ b/server.js
@@ -267,19 +267,27 @@ async function plexProxy(req, res) {
}
// ---- Storage health: is /data a real mounted volume, and what is in it? (so a wiped volume is obvious)
+/** What is mounted at the data folder? Docker's own anonymous volumes look "mounted" but are lost when the container is recreated. */
+function classifyMount(mountinfoLine) {
+ const root = (mountinfoLine || '').split(' ')[3] || '';
+ const vol = /\/volumes\/([^/]+)\/_data/.exec(root);
+ if (vol) return /^[0-9a-f]{64}$/.test(vol[1]) ? { kind: 'anonymous', name: vol[1].slice(0, 12) } : { kind: 'named', name: vol[1] };
+ return { kind: 'folder', name: root };
+}
function storageInfo() {
const fs = require('fs'), path = require('path');
const dir = path.resolve(process.env.DATA_DIR || path.join(__dirname, 'data'));
- let mounted = null; // null = cannot tell (not Linux)
- try { mounted = fs.readFileSync('/proc/self/mountinfo', 'utf8').split('\n').some(l => l.split(' ')[4] === dir); } catch {}
+ let mounted = null, source = null; // null = cannot tell (not Linux)
+ try { const l = fs.readFileSync('/proc/self/mountinfo', 'utf8').split('\n').find(x => x.split(' ')[4] === dir); mounted = !!l; if (l) source = classifyMount(l); } catch {}
let writable = true; try { const f = path.join(dir, '.write-test'); fs.writeFileSync(f, 'x'); fs.unlinkSync(f); } catch { writable = false; }
const count = (f, pick) => { try { return pick(JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'))); } catch { return 0; } };
- return { dir, mounted, writable, passkeys: count('passkeys.json', j => (j.creds || []).length), calendarLinks: count('calendars.json', j => j.length),
+ return { dir, mounted, source, writable, passkeys: count('passkeys.json', j => (j.creds || []).length), calendarLinks: count('calendars.json', j => j.length),
events: count('events.json', j => j.length), settings: count('settings.json', j => Object.keys(j).length), icloud: fs.existsSync(path.join(dir, 'icloud.json')), secretFromEnv: !!process.env.COOKIE_SECRET,
startedAt: STARTED };
}
const STARTED = Date.now();
-{ const i = storageInfo(); console.log(`[data] ${i.dir}: ${i.passkeys} passkeys, ${i.calendarLinks} calendar links, ${i.events} events; mounted volume: ${i.mounted === null ? 'unknown' : i.mounted ? 'yes' : 'NO (data is lost on redeploy)'}; writable: ${i.writable}; COOKIE_SECRET from env: ${i.secretFromEnv}`);
+{ const i = storageInfo(); console.log(`[data] ${i.dir}: ${i.passkeys} passkeys, ${i.calendarLinks} calendar links, ${i.events} events; mounted: ${i.mounted === null ? 'unknown' : i.mounted ? (i.source.kind + ' ' + i.source.name) : 'NO (data is lost on redeploy)'}; writable: ${i.writable}; COOKIE_SECRET from env: ${i.secretFromEnv}`);
+ if (i.source && i.source.kind === 'anonymous') console.warn('[data] WARNING: /data is an anonymous Docker volume, not your host folder. It is deleted when the container is recreated. Check that your stack mounts the host folder at /data.');
if (i.mounted === false) console.warn('[data] WARNING: the data folder is not a mounted volume. Passkeys, calendars and sessions will be lost whenever the container is recreated. Mount a volume or host folder at /data.'); }
if (!JWT_SECRET) console.warn('WARNING: ONLYOFFICE_JWT_SECRET not set. In-app editing only works if the Document Server has JWT disabled, and save callbacks are unauthenticated. Testing only.');