Add Outlook unread-mail widget with device-code login

- outlook-auth.js: one-time device-code flow against Microsoft login;
  stores the refresh token in the outlook-tokens compose volume
- update-outlook.js: per-minute cron fetching the unread count and the
  latest unread messages via Microsoft Graph; caches access tokens and
  persists rotated refresh tokens
- Widget is hidden until the mailbox is actually authorized, so it
  stays out of sight while (admin) consent is pending
- New external card for Outlook (Office 365) with the official icon
- compose: pass OUTLOOK_CLIENT_ID/OUTLOOK_TENANT/GITEA_TOKEN from .env
- README: Entra ID app registration steps and the tenant-ID hint for
  directories not resolvable via the organizations endpoint
This commit is contained in:
2026-09-04 17:38:17 +02:00
parent c15cfc51a9
commit dfe246b7ca
12 changed files with 564 additions and 6 deletions
+65
View File
@@ -38,6 +38,7 @@ const SERVICES = [
{ section: "internal", name: "Netbird", url: "https://netbird.gg36.at", cat: "network", icon: '<img class="card-logo" src="icons/netbird.svg" alt="" />', liveStatus: "netbird" },
// ----- External -----
{ section: "external", name: "Outlook (Office 365)", url: "https://outlook.office.com/mail/", cat: "external", icon: '<img class="card-logo" src="icons/outlook.ico" alt="" />' },
{ section: "external", name: "gitlab-ixsol", url: "https://gitlab.ixsol.wien", cat: "external", icon: '<img class="card-logo" src="icons/gitlab.svg" alt="" />' },
{ section: "external", name: "gem360", url: "https://gem360.ixslo.wien", cat: "external", icon: '<img class="card-logo" src="icons/odoo.svg" alt="" />' },
];
@@ -603,6 +604,68 @@ async function loadHealth() {
}
}
// ---------------------------------------------------------------------------
// Outlook unread mail (via /outlook.json, refreshed every minute server-side)
// ---------------------------------------------------------------------------
function relTime(iso) {
const t = new Date(iso).getTime();
if (!t) return "";
const mins = Math.floor((Date.now() - t) / 60000);
if (mins < 1) return "now";
if (mins < 60) return `${mins}m`;
const hours = Math.floor(mins / 60);
if (hours < 24) return `${hours}h`;
return `${Math.floor(hours / 24)}d`;
}
function renderOutlookWidget(o) {
const widget = document.getElementById("widget-outlook");
const el = document.getElementById("outlook-mail");
if (!widget || !el) return;
// Show the widget only once the mailbox is actually authorized; hide it
// while not configured, waiting for (admin) consent or on auth errors.
const authorized = Boolean(o && o.enabled && !o.error);
widget.hidden = !authorized;
if (!authorized) return;
const account = o.account ? `<div class="mail-account">${escapeHtml(o.account)}</div>` : "";
const badge = `<span class="git-badge mail">${o.unreadCount ?? 0} unread</span>`;
const msgs = o.messages || [];
if (msgs.length === 0) {
el.innerHTML = `${account}${badge}<p class="widget-empty">No unread mail.</p>`;
return;
}
el.innerHTML =
`${account}${badge}` +
msgs
.map(
(m) => `
<div class="mail-row">
<div class="mail-head">
<span class="mail-from" title="${escapeHtml(m.from || "")}">${escapeHtml(m.from || "unknown")}</span>
<span class="mail-time">${escapeHtml(relTime(m.received))}</span>
</div>
<div class="mail-subject" title="${escapeHtml(m.subject || "")}">${escapeHtml(m.subject || "(no subject)")}</div>
</div>`
)
.join("");
}
async function loadOutlook() {
try {
const res = await fetch("outlook.json", { cache: "no-store" });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
renderOutlookWidget(await res.json());
} catch {
// keep the current content; the next poll will retry
}
}
// ---------------------------------------------------------------------------
// Search / filter
// ---------------------------------------------------------------------------
@@ -664,3 +727,5 @@ loadOllamaStats();
setInterval(loadOllamaStats, 5000); // live Ollama loaded models every 5 s
loadHealth();
setInterval(loadHealth, 30000); // service health badges every 30 s
loadOutlook();
setInterval(loadOutlook, 60000); // unread mail every 60 s