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();
}