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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user