Bundled Roundcube webmail in the Mail tab, optional Microsoft 365 OAuth sign-in, collapsible menu
build-container / build (push) Successful in 2m42s

- docker-compose: roundcube service with inline config (Gmail/iCloud/Yahoo presets, framing allowed, persistent key); only reachable through the hub
- hub passes /webmail/ through to Roundcube behind the passkey sign-in; Microsoft's sign-in page opens in the whole window and returns to the Mail tab
- M365_* settings add 'Login with Microsoft 365' (README has the Entra app registration steps)
- Side menu collapses to icons or hides completely (remembered per device, Ctrl/Cmd+B)
- tools/webmail-test.js

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-21 08:56:13 -04:00
1 parent 318006dba3
commit cdf862427e
8 files changed
+230 -11

No files matched your search

+13 -2
View File
@@ -49,6 +49,7 @@ async function loadSettings() {
}
const ICONS = {
menu: '<path d="M4 7h16M4 12h16M4 17h16"/>',
home: '<path d="M3 11l9-8 9 8M5 10v10h14V10"/>',
mail: '<rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 7l9 6 9-6"/>',
files: '<path d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"/>',
@@ -157,7 +158,10 @@ function renderNav(active = activeTab) {
$('#nav').innerHTML = mine.filter(t => PRIMARY.includes(t[0])).map(tab).join('') +
(rest.length ? `<a href="#" id="more" class="${more ? 'on' : ''}" role="button">${icon('more')}<span>More</span></a>` : '');
const mo = $('#more'); if (mo) mo.onclick = e => { e.preventDefault(); openMore(); };
} else $('#nav').innerHTML = brand + mine.map(tab).join('');
} else {
$('#nav').innerHTML = brand + mine.map(tab).join('') + `<div class="spacer"></div><button id="navcol" class="navcol" aria-label="Collapse the menu" title="Collapse or expand the menu">${icon('back')}<span>Collapse</span></button>`;
$('#navcol').onclick = () => setNav({ mini: !navPref().mini });
}
}
phone.addEventListener('change', () => renderNav());
@@ -169,6 +173,12 @@ function openMore() {
const routes = { home: viewHome, mail: viewMail, files: viewFiles, office: viewOffice, audio: viewAudio, verse: viewVerse, podcast: viewPodcast, music: viewMusic, tasks: viewTasks, search: viewSearch, calendar: viewCalendar, settings: viewSettings, edit: viewEdit };
let editor;
// Menu on a tablet or computer: collapse it to icons (wide screens), or hide it completely. Remembered on this device.
const navPref = () => { try { return JSON.parse(localStorage.getItem('nav_pref')) || {}; } catch { return {}; } };
function applyNav() { const p = navPref(); document.body.classList.toggle('nav-mini', !!p.mini); document.body.classList.toggle('nav-hidden', !!p.hidden); }
function setNav(patch) { try { localStorage.setItem('nav_pref', JSON.stringify({ ...navPref(), ...patch })); } catch {} applyNav(); }
applyNav();
addEventListener('keydown', e => { if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 'b' && !/^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName || '')) { e.preventDefault(); setNav({ hidden: !navPref().hidden }); } });
// In-app Back / Forward (an installed app has no browser buttons). Each history entry remembers its position so we know what is possible.
let navIdx = 0, navMax = 0, navStarted = false;
const parentHash = () => { const p = location.hash.split('/'); return p.length > 3 ? p.slice(0, -1).join('/') : ''; }; // e.g. #/files/lib/a/b -> #/files/lib/a
@@ -183,7 +193,8 @@ function drawNav() {
const canBack = navIdx > 0 || !!parentHash(), canFwd = navIdx < navMax;
if (!el.dataset.on) {
el.dataset.on = 1;
el.innerHTML = `<button class="ibtn" id="nvb" aria-label="Back" title="Back">${icon('back')}</button><button class="ibtn" id="nvf" aria-label="Forward" title="Forward">${icon('chev')}</button><span id="nvt"></span>`;
el.innerHTML = `<button class="ibtn navtoggle" id="nvm" aria-label="Show or hide the menu" title="Show or hide the menu (Ctrl/Cmd+B)">${icon('menu')}</button><button class="ibtn" id="nvb" aria-label="Back" title="Back">${icon('back')}</button><button class="ibtn" id="nvf" aria-label="Forward" title="Forward">${icon('chev')}</button><span id="nvt"></span>`;
$('#nvm').onclick = () => setNav({ hidden: !navPref().hidden });
$('#nvb').onclick = () => { if (navIdx > 0) history.back(); else if (parentHash()) location.hash = parentHash(); };
$('#nvf').onclick = () => history.forward();
}
+25
View File
@@ -318,3 +318,28 @@ body:not(.editing) #view { padding-top: 8px; }
.mact .btn { flex: 1; display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 46px; }
.mact .btn svg { width: 20px; height: 20px; }
.mact .btn:not(.ghost) { background: var(--mc); color: #fff; }
/* Collapsible menu (tablet / computer) */
.navtoggle { display: none; }
.navcol { display: none; }
@media (min-width: 768px) {
#nav { overflow-y: auto; scrollbar-width: none; }
.navtoggle { display: grid; }
body.nav-hidden #nav { display: none; }
body.nav-hidden #navbar { padding-left: 12px; }
}
@media (min-width: 1024px) {
.navcol { display: flex; align-items: center; gap: 12px; min-height: 44px; padding: 0 14px; border: 0; background: none; border-radius: 12px; color: var(--muted); font-size: 14px; }
.navcol:hover { background: var(--panel2); color: var(--text); }
.navcol svg { width: 20px; height: 20px; }
body.nav-mini #nav { width: 76px; padding: calc(var(--sat) + 12px) 0 12px var(--sal); }
body.nav-mini #nav a { flex-direction: column; justify-content: center; gap: 2px; padding: 0; font-size: 10px; min-height: 50px; border-radius: 0; flex: none; }
body.nav-mini #nav a svg { width: 22px; height: 22px; }
body.nav-mini #nav a.on { background: none; }
body.nav-mini .brand { display: block; width: 36px; height: 36px; margin: 4px auto 12px; }
body.nav-mini .brand img { width: 36px; height: 36px; border-radius: 10px; }
body.nav-mini .brand span { display: none; }
body.nav-mini .navcol { flex-direction: column; justify-content: center; gap: 2px; padding: 0; min-height: 56px; font-size: 11px; }
body.nav-mini .navcol svg { transform: rotate(180deg); }
body.nav-mini .navcol span { display: none; }
}
+1 -1
View File
@@ -12,7 +12,7 @@ self.addEventListener('activate', e => {
});
self.addEventListener('fetch', e => {
const u = new URL(e.request.url);
if (e.request.method !== 'GET' || u.origin !== location.origin || u.pathname.startsWith('/sf/') || u.pathname.startsWith('/plex/') || u.pathname.startsWith('/api/')) return;
if (e.request.method !== 'GET' || u.origin !== location.origin || u.pathname.startsWith('/sf/') || u.pathname.startsWith('/plex/') || u.pathname.startsWith('/webmail') || u.pathname.startsWith('/api/')) return;
e.respondWith(fetch(e.request).then(r => {
if (r.ok) { const copy = r.clone(); caches.open(CACHE).then(c => c.put(e.request, copy)); }
return r;