// Hintergrunddienst: prüft jede Minute auf Neues, zeigt Zähler und Benachrichtigungen, Rechtsklick-Menü, // Tastenkürzel und die Adressleisten-Suche („dh“ + Leertaste). import { apiCall, getSession, getSettings, truncate, checkForExtensionUpdate } from './lib/core.js'; const ALARM = 'deskhub-poll'; const UPDATE_ALARM = 'deskhub-update'; const MENU = 'deskhub-create'; const APP = 'app.html'; async function setBadge(count, color = '#0f766e') { await chrome.action.setBadgeBackgroundColor({ color }); await chrome.action.setBadgeText({ text: count > 0 ? (count > 99 ? '99+' : String(count)) : '' }); } async function ensureAlarm() { // Alle 30 Sekunden (Chrome erlaubt das ab Version 120, ältere runden auf eine Minute); eine ältere Uhr mit anderem Takt wird ersetzt const current = await chrome.alarms.get(ALARM); if (!current || current.periodInMinutes !== 0.5) chrome.alarms.create(ALARM, { periodInMinutes: 0.5, delayInMinutes: 0.1 }); if (!(await chrome.alarms.get(UPDATE_ALARM))) chrome.alarms.create(UPDATE_ALARM, { periodInMinutes: 360, delayInMinutes: 1 }); } /** Neue Version der Erweiterung? Einmal pro Version eine Benachrichtigung, der Hinweis steht außerdem auf der Startseite. */ export async function checkUpdate(force = false) { const info = await checkForExtensionUpdate({ force }); if (!info) return null; const { updateNotified } = await chrome.storage.local.get('updateNotified'); if (updateNotified !== info.version) { await chrome.storage.local.set({ updateNotified: info.version }); chrome.notifications.create('deskhub-update', { type: 'basic', iconUrl: 'icons/icon128.png', title: 'DeskHub-Erweiterung', message: `Version ${info.version} ist verfügbar. Auf der Startseite der Erweiterung steht, wie du sie bekommst.` }); } return info; } async function ensureSetup() { await ensureAlarm(); chrome.contextMenus.removeAll(() => { chrome.contextMenus.create({ id: MENU, title: 'Als DeskHub-Ticket anlegen', contexts: ['selection', 'page'] }); }); } // Auch wenn der Hintergrunddienst nur neu gestartet wurde (kein Installations- oder Start-Ereignis): Die Abfrage-Uhr muss laufen ensureAlarm(); chrome.runtime.onInstalled.addListener(ensureSetup); chrome.runtime.onStartup.addListener(async () => { await ensureSetup(); poll(); checkUpdate(); }); /** * Fragt den Server nach dem echten Stand: Der Zähler zeigt, wie viele Tickets jetzt Aufmerksamkeit brauchen (offen, dir zugewiesen oder noch * niemandem, seit der letzten Kundennachricht nicht geöffnet). Wird ein Ticket geöffnet, beantwortet oder übernommen, sinkt er beim nächsten * Abruf von selbst, auch wenn das in der Weboberfläche oder im Windows-Programm geschah. Benachrichtigt wird nur für Tickets, die neu dazukommen. * Ältere Server ohne diese Angabe liefern nur „seit dem letzten Mal“: Dafür bleibt die frühere, aufsummierende Zählung als Rückfall. */ export async function poll() { const session = await getSession(); if (!session) { await setBadge(0); return { skipped: true }; } const { lastSeen, badgeCount = 0, knownIds = null } = await chrome.storage.local.get(['lastSeen', 'badgeCount', 'knownIds']); try { const { data } = await apiCall('activity.php', { query: lastSeen ? { since: lastSeen } : {} }); const settings = await getSettings(); if (data.attention && Array.isArray(data.attention.unassigned) && Array.isArray(data.attention.mine)) { const unassigned = settings.notifyUnassigned ? data.attention.unassigned : []; const mine = settings.notifyMine ? data.attention.mine : []; const known = new Set(knownIds || []); const freshUnassigned = knownIds === null ? 0 : unassigned.filter((id) => !known.has(id)).length; const freshMine = knownIds === null ? 0 : mine.filter((id) => !known.has(id)).length; const ids = [...unassigned, ...mine]; await chrome.storage.local.set({ lastSeen: data.serverTime, badgeCount: ids.length, knownIds: ids }); await setBadge(ids.length); const fresh = freshUnassigned + freshMine; if (fresh > 0 && settings.notifications) { const parts = []; if (freshUnassigned) parts.push(`${freshUnassigned} neu und nicht zugewiesen`); if (freshMine) parts.push(`${freshMine} Aktualisierung${freshMine === 1 ? '' : 'en'} bei deinen Tickets`); chrome.notifications.create('deskhub-new', { type: 'basic', iconUrl: 'icons/icon128.png', title: 'DeskHub', message: parts.join(', '), priority: 1 }); } return { fresh, total: ids.length }; } const unassigned = settings.notifyUnassigned ? data.newUnassigned || 0 : 0; const replies = settings.notifyMine ? data.newReplies || 0 : 0; const fresh = unassigned + replies; const total = badgeCount + fresh; await chrome.storage.local.set({ lastSeen: data.serverTime, badgeCount: total }); await setBadge(total); if (fresh > 0 && settings.notifications) { const parts = []; if (unassigned) parts.push(`${unassigned} neu und nicht zugewiesen`); if (replies) parts.push(`${replies} Aktualisierung${replies === 1 ? '' : 'en'} bei deinen Tickets`); chrome.notifications.create('deskhub-new', { type: 'basic', iconUrl: 'icons/icon128.png', title: 'DeskHub', message: parts.join(', '), priority: 1 }); } return { fresh, total }; } catch (e) { if (e.status === 401) { // Anmeldung abgelaufen: Zähler zeigt „!“, bis sich jemand neu anmeldet await setBadge(0); await chrome.action.setBadgeText({ text: '!' }); await chrome.action.setBadgeBackgroundColor({ color: '#b91c1c' }); chrome.notifications.create('deskhub-login', { type: 'basic', iconUrl: 'icons/icon128.png', title: 'DeskHub', message: 'Bitte in der Erweiterung neu anmelden.' }); } return { error: e.code || 'ERROR' }; // Netzwerkfehler: still bleiben, beim nächsten Mal erneut } } chrome.alarms.onAlarm.addListener((alarm) => { if (alarm.name === ALARM) poll(); else if (alarm.name === UPDATE_ALARM) checkUpdate(); }); const openWindow = (query = '') => chrome.windows.create({ url: chrome.runtime.getURL(`${APP}?mode=window${query}`), type: 'popup', width: 460, height: 760 }); chrome.contextMenus.onClicked.addListener(async (info, tab) => { if (info.menuItemId !== MENU) return; const subject = truncate((tab?.title || '').trim(), 120); const selection = (info.selectionText || '').trim(); const source = tab?.url && /^https?:/i.test(tab.url) ? `\n\nQuelle: ${tab.url}` : ''; await chrome.storage.local.set({ prefill: { subject, message: (selection + source).trim() } }); await openWindow('&create=1'); }); chrome.commands?.onCommand.addListener(async (command, tab) => { if (command === 'quick-create') await openWindow('&create=1'); else if (command === 'open-panel') { try { await chrome.sidePanel.open({ windowId: tab?.windowId ?? (await chrome.windows.getCurrent()).id }); } catch { await openWindow(); } } }); // Adressleiste: „dh 261031-0299“ öffnet das Ticket, „dh Drucker“ sucht danach. const looksLikeTicket = (s) => /^#?(\d{4,}-\d+|\d{5,}|[A-Za-z]{2,6}-\d+)$/.test(s.trim()); chrome.omnibox?.onInputChanged.addListener((text, suggest) => { const t = text.trim(); if (!t) return suggest([]); suggest(looksLikeTicket(t) ? [{ content: t, description: `Ticket ${t.replace(/^#/, '')} öffnen` }] : [{ content: t, description: `Tickets suchen: ${t.replace(/[<>&]/g, '')}` }]); }); chrome.omnibox?.onInputEntered.addListener(async (text) => { const t = text.trim(); if (!t) return; await openWindow(looksLikeTicket(t) ? `&ticket=${encodeURIComponent(t.replace(/^#/, ''))}` : `&q=${encodeURIComponent(t)}`); }); chrome.notifications.onClicked.addListener(async (id) => { chrome.notifications.clear(id); try { await chrome.action.openPopup(); } catch { await openWindow(); } }); chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { (async () => { // Der Zähler wird nicht mehr beim Öffnen geleert: Er folgt dem Stand auf dem Server. „reset-badge“ (ältere Fenster) fragt deshalb nur neu ab. if (msg?.type === 'reset-badge') sendResponse(await poll()); else if (msg?.type === 'poll-now') sendResponse(await poll()); else if (msg?.type === 'check-update') sendResponse({ update: await checkUpdate(true) }); else if (msg?.type === 'presence') { await apiCall('presence.php', { method: 'POST', body: {} }).catch(() => {}); sendResponse({ ok: true }); } else if (msg?.type === 'session-changed') { await ensureAlarm(); await chrome.storage.local.remove(['lastSeen', 'badgeCount', 'knownIds']); await setBadge(0); sendResponse(await poll()); } else sendResponse({ ok: false }); })().catch((e) => { try { sendResponse({ error: (e && e.code) || 'ERROR' }); } catch { /* Fenster schon zu */ } }); return true; // Antwort kommt asynchron });