package gui const indexHTML = ` LazyUpdateManager

LazyUpdateManager

Pruefe Updates...

Aktive Updates 0
Ausgeblendet 0
Freier Speicher -
Kernel -

Updates

Noch nicht geprueft
Selektive Updates koennen unter Arch riskant sein. Vollupdate bleibt empfohlen.
Keine passenden Updates gefunden.
` const appCSS = ` :root { color-scheme: dark; --bg: #0f1215; --panel: #191e25; --panel-soft: #232a34; --panel-lift: #202732; --text: #f4f7fb; --muted: #9ba7b6; --line: #303844; --accent: #44d19d; --accent-strong: #2eb984; --blue: #6ea8fe; --warn: #f4c95d; --danger: #ff6f61; } * { box-sizing: border-box; } body { margin: 0; min-width: 320px; min-height: 100vh; color: var(--text); background: radial-gradient(circle at top left, rgba(68, 209, 157, 0.14), transparent 34rem), linear-gradient(180deg, #13171d 0%, var(--bg) 22rem); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } button, input, select { font: inherit; } .shell { width: min(1260px, calc(100vw - 32px)); margin: 0 auto; padding: 24px 0; } .topbar { display: flex; align-items: center; justify-content: space-between; gap: 18px; margin-bottom: 16px; } h1, h2, p, dl, dd { margin: 0; } h1 { font-size: 28px; font-weight: 740; } h2 { font-size: 15px; font-weight: 700; } #summary, #lastCheck, #ignoredHint, #saveState { color: var(--muted); } #summary { margin-top: 5px; } .actions, .tools { display: flex; gap: 10px; } button { border: 1px solid var(--line); border-radius: 8px; min-height: 40px; padding: 0 14px; color: var(--text); background: var(--panel-soft); cursor: pointer; transition: border-color 140ms ease, background 140ms ease, transform 140ms ease; } button:hover { border-color: var(--accent); transform: translateY(-1px); } button:disabled { cursor: progress; opacity: 0.62; } #installBtn, form button { border-color: transparent; color: #06130e; background: var(--accent); font-weight: 760; } #installBtn:hover, form button:hover { background: var(--accent-strong); } #installSelectedBtn { border-color: rgba(110, 168, 254, 0.42); color: #dcebff; background: rgba(110, 168, 254, 0.12); font-weight: 720; } #installSelectedBtn:hover { border-color: var(--blue); } .stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; } .stat, .panel { border: 1px solid var(--line); border-radius: 8px; background: var(--panel); box-shadow: 0 18px 42px rgba(0, 0, 0, 0.18); } .stat { display: grid; gap: 6px; min-height: 82px; padding: 14px; background: linear-gradient(180deg, var(--panel-lift), var(--panel)); } .stat span, dt, small { color: var(--muted); } .stat strong { font-size: 24px; } .content { display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: 16px; align-items: start; } .main-column, .side-column { display: grid; gap: 16px; } .panel-head { display: flex; align-items: center; justify-content: space-between; gap: 14px; min-height: 58px; padding: 0 16px; border-bottom: 1px solid var(--line); } .toolbar { align-items: center; } input, select { min-height: 40px; border: 1px solid var(--line); border-radius: 8px; padding: 0 10px; color: var(--text); background: var(--panel-soft); } #searchInput { width: min(32vw, 320px); } .selection-bar { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 46px; padding: 0 16px; border-bottom: 1px solid var(--line); color: var(--warn); background: rgba(244, 201, 93, 0.07); } .select-all { display: flex; grid-template-columns: none; align-items: center; gap: 10px; color: var(--text); white-space: nowrap; } .warnings { margin: 14px 16px 0; padding: 12px; border: 1px solid rgba(244, 201, 93, 0.45); border-radius: 8px; color: var(--warn); background: rgba(244, 201, 93, 0.08); white-space: pre-wrap; } .empty { padding: 44px 16px; color: var(--muted); text-align: center; } table { width: 100%; border-collapse: collapse; } th, td { padding: 12px 16px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: middle; } th { color: var(--muted); font-size: 12px; text-transform: uppercase; } td { overflow-wrap: anywhere; } .updates-panel td:first-child { width: 46px; color: var(--text); } .updates-panel td:nth-child(2) { width: 92px; color: var(--accent); font-weight: 720; } td:last-child { width: 132px; text-align: right; } tr:last-child td { border-bottom: 0; } tr.selected td { background: rgba(110, 168, 254, 0.08); } .ghost { min-height: 34px; padding: 0 10px; color: var(--muted); background: transparent; } .danger { color: var(--danger); } .system-list { display: grid; gap: 14px; padding: 16px; } .system-list div { display: flex; justify-content: space-between; gap: 16px; } .system-list dd { text-align: right; } #lockState { color: var(--accent); } #lockState.locked { color: var(--danger); } form { display: grid; gap: 16px; padding: 16px; } dialog { width: min(760px, calc(100vw - 28px)); max-height: min(760px, calc(100vh - 28px)); border: 1px solid var(--line); border-radius: 8px; padding: 0; color: var(--text); background: var(--panel); box-shadow: 0 28px 80px rgba(0, 0, 0, 0.45); } dialog::backdrop { background: rgba(0, 0, 0, 0.62); } .settings-modal { padding: 0; } .modal-head, .modal-actions { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 16px; } .modal-head { border-bottom: 1px solid var(--line); } .modal-head p { margin-top: 4px; color: var(--muted); } .icon-btn { min-height: 36px; } .tabs { display: flex; gap: 8px; padding: 12px 16px 0; border-bottom: 1px solid var(--line); } .tab { min-height: 38px; border-bottom-left-radius: 0; border-bottom-right-radius: 0; color: var(--muted); background: transparent; } .tab.active { border-color: var(--line); border-bottom-color: var(--panel); color: var(--text); background: var(--panel-soft); } .tab-panel { display: none; gap: 16px; padding: 16px; } .tab-panel.active { display: grid; } .modal-actions { border-top: 1px solid var(--line); } .ignored-settings-list { display: grid; gap: 10px; } .ignored-setting-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 42px; padding: 0 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-soft); } .empty-inline { margin: 0; color: var(--muted); } label { display: grid; gap: 8px; color: var(--muted); } .toggle { grid-template-columns: 18px 1fr; align-items: center; color: var(--text); } input[type="checkbox"] { width: 18px; min-height: 18px; accent-color: var(--accent); } @media (max-width: 920px) { .shell { width: min(100vw - 20px, 1260px); padding: 16px 0; } .topbar, .content, .stats { grid-template-columns: 1fr; display: grid; } .actions, .tools { display: grid; grid-template-columns: 1fr 1fr; } .selection-bar { display: grid; } .tabs { flex-wrap: wrap; } #searchInput { width: 100%; } th:nth-child(4), td:nth-child(4) { display: none; } } ` const appJS = ` const summary = document.querySelector("#summary"); const activeCount = document.querySelector("#activeCount"); const ignoredCount = document.querySelector("#ignoredCount"); const diskFree = document.querySelector("#diskFree"); const kernel = document.querySelector("#kernel"); const refreshBtn = document.querySelector("#refreshBtn"); const settingsBtn = document.querySelector("#settingsBtn"); const settingsDialog = document.querySelector("#settingsDialog"); const closeSettingsBtn = document.querySelector("#closeSettingsBtn"); const installBtn = document.querySelector("#installBtn"); const installSelectedBtn = document.querySelector("#installSelectedBtn"); const selectAll = document.querySelector("#selectAll"); const selectionText = document.querySelector("#selectionText"); const updatesTable = document.querySelector("#updatesTable"); const updatesBody = document.querySelector("#updatesBody"); const ignoredPanel = document.querySelector("#ignoredPanel"); const ignoredBody = document.querySelector("#ignoredBody"); const ignoredHint = document.querySelector("#ignoredHint"); const emptyState = document.querySelector("#emptyState"); const warnings = document.querySelector("#warnings"); const searchInput = document.querySelector("#searchInput"); const sourceFilter = document.querySelector("#sourceFilter"); const lastCheck = document.querySelector("#lastCheck"); const lastSuccess = document.querySelector("#lastSuccess"); const aurHelper = document.querySelector("#aurHelper"); const terminalStatus = document.querySelector("#terminalStatus"); const lockState = document.querySelector("#lockState"); const form = document.querySelector("#settingsForm"); const checkAur = document.querySelector("#checkAur"); const showIgnored = document.querySelector("#showIgnored"); const notificationsEnabled = document.querySelector("#notificationsEnabled"); const keepTerminalOpen = document.querySelector("#keepTerminalOpen"); const confirmSelectedInstalls = document.querySelector("#confirmSelectedInstalls"); const language = document.querySelector("#language"); const reminderHours = document.querySelector("#reminderHours"); const autoRefreshMinutes = document.querySelector("#autoRefreshMinutes"); const terminal = document.querySelector("#terminal"); const saveState = document.querySelector("#saveState"); const ignoredSettingsList = document.querySelector("#ignoredSettingsList"); const tabs = document.querySelectorAll(".tab"); const tabPanels = document.querySelectorAll(".tab-panel"); let currentData = null; let refreshTimer = null; let selectedPackages = new Set(); const translations = { de: { checkingUpdates: "Pruefe Updates...", noUpdates: "Keine Updates verfuegbar.", oneUpdate: "1 Update verfuegbar.", manyUpdates: "{count} Updates verfuegbar.", settings: "Einstellungen", refresh: "Refresh", installSelected: "Auswahl installieren", installAll: "Alle installieren", activeUpdates: "Aktive Updates", ignored: "Ausgeblendet", freeDisk: "Freier Speicher", updates: "Updates", searchPackages: "Pakete suchen", allSources: "Alle Quellen", selectiveWarning: "Selektive Updates koennen unter Arch riskant sein. Vollupdate bleibt empfohlen.", noMatchingUpdates: "Keine passenden Updates gefunden.", source: "Quelle", package: "Paket", current: "Aktuell", available: "Verfuegbar", ignoredUpdates: "Ausgeblendete Updates", system: "System", aurHelper: "AUR Helper", lastSuccessfulCheck: "Letzte erfolgreiche Pruefung", settingsSubtitle: "Update-Verhalten, Installation und ausgeblendete Pakete", close: "Schliessen", general: "Allgemein", notifications: "Benachrichtigungen", installation: "Installation", language: "Sprache", checkAurUpdates: "AUR Updates pruefen", showIgnoredUpdates: "Ausgeblendete Updates in der App anzeigen", autoRefresh: "Auto-Refresh", minutesDisabled: "Minuten, 0 deaktiviert", sendWeeklyReminders: "Woechentliche Erinnerungen senden", reminderEvery: "Erinnerung alle", hours: "Stunden", terminal: "Terminal", automatic: "Automatisch", keepTerminalOpen: "Terminal nach Installationen offen lassen", confirmSelectedInstalls: "Selektive Installationen bestaetigen", save: "Speichern", hide: "Ausblenden", show: "Einblenden", notFound: "nicht gefunden", ready: "Bereit", pacmanLocked: "Pacman gesperrt", lastCheck: "Letzte Pruefung: {date}", free: "{value} frei", noIgnoredPackages: "Keine ausgeblendeten Pakete.", onePackage: "1 Paket", manyPackages: "{count} Pakete", oneSelected: "1 Update ausgewaehlt", manySelected: "{count} Updates ausgewaehlt", installStarted: "Installation im Terminal gestartet.", selectedInstallStarted: "Ausgewaehlte Installation im Terminal gestartet.", confirmSelected: "Ausgewaehlte Pakete gezielt installieren? Unter Arch ist ein vollstaendiges Systemupdate meistens sicherer.", saving: "Speichere...", saved: "Gespeichert", }, en: { checkingUpdates: "Checking updates...", noUpdates: "No updates available.", oneUpdate: "1 update available.", manyUpdates: "{count} updates available.", settings: "Settings", refresh: "Refresh", installSelected: "Install selected", installAll: "Install all", activeUpdates: "Active updates", ignored: "Ignored", freeDisk: "Free disk", updates: "Updates", searchPackages: "Search packages", allSources: "All sources", selectiveWarning: "Selective updates can be risky on Arch. A full update is recommended.", noMatchingUpdates: "No matching updates found.", source: "Source", package: "Package", current: "Current", available: "Available", ignoredUpdates: "Ignored updates", system: "System", aurHelper: "AUR helper", lastSuccessfulCheck: "Last successful check", settingsSubtitle: "Update behavior, installation, and ignored packages", close: "Close", general: "General", notifications: "Notifications", installation: "Installation", language: "Language", checkAurUpdates: "Check AUR updates", showIgnoredUpdates: "Show ignored updates in the app", autoRefresh: "Auto-refresh", minutesDisabled: "Minutes, 0 disables it", sendWeeklyReminders: "Send weekly reminders", reminderEvery: "Remind every", hours: "Hours", terminal: "Terminal", automatic: "Automatic", keepTerminalOpen: "Keep terminal open after installs", confirmSelectedInstalls: "Confirm selective installs", save: "Save", hide: "Hide", show: "Show", notFound: "not found", ready: "Ready", pacmanLocked: "Pacman locked", lastCheck: "Last check: {date}", free: "{value} free", noIgnoredPackages: "No ignored packages.", onePackage: "1 package", manyPackages: "{count} packages", oneSelected: "1 update selected", manySelected: "{count} updates selected", installStarted: "Installation started in terminal.", selectedInstallStarted: "Selected installation started in terminal.", confirmSelected: "Install selected packages only? On Arch, a full system update is usually safer.", saving: "Saving...", saved: "Saved", }, }; function currentLanguage() { return currentData?.settings.language || language.value || "de"; } function t(key, params = {}) { const dict = translations[currentLanguage()] || translations.de; let value = dict[key] || translations.de[key] || key; for (const [name, replacement] of Object.entries(params)) { value = value.replaceAll("{" + name + "}", replacement); } return value; } function applyTranslations() { document.documentElement.lang = currentLanguage(); for (const element of document.querySelectorAll("[data-i18n]")) { element.textContent = t(element.dataset.i18n); } for (const element of document.querySelectorAll("[data-i18n-placeholder]")) { element.placeholder = t(element.dataset.i18nPlaceholder); } refreshBtn.title = t("updates"); } async function request(path, options = {}) { const response = await fetch(path, { headers: { "Content-Type": "application/json" }, ...options, }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || "Request failed"); } return data; } function formatDate(value) { if (!value || value.startsWith("0001-")) { return "-"; } const locale = currentLanguage() === "en" ? "en-US" : "de-DE"; return new Intl.DateTimeFormat(locale, { dateStyle: "short", timeStyle: "short", }).format(new Date(value)); } function summaryText(total) { if (total === 0) { return t("noUpdates"); } if (total === 1) { return t("oneUpdate"); } return t("manyUpdates", { count: total }); } function visiblePackages() { if (!currentData) { return []; } const query = searchInput.value.trim().toLowerCase(); const source = sourceFilter.value; return currentData.packages.filter((pkg) => { if (pkg.Ignored && !currentData.settings.show_ignored) { return false; } if (source !== "all" && pkg.Source !== source) { return false; } if (query && !pkg.Name.toLowerCase().includes(query)) { return false; } return !pkg.Ignored; }); } function renderTable() { const packages = visiblePackages(); updatesBody.replaceChildren(); for (const pkg of packages) { const row = document.createElement("tr"); row.classList.toggle("selected", selectedPackages.has(pkg.Name)); const selectCell = document.createElement("td"); const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.checked = selectedPackages.has(pkg.Name); checkbox.addEventListener("change", () => { if (checkbox.checked) { selectedPackages.add(pkg.Name); } else { selectedPackages.delete(pkg.Name); } renderTable(); }); selectCell.append(checkbox); row.append(selectCell); row.append(cell(pkg.Source)); row.append(cell(pkg.Name)); row.append(cell(pkg.Current || "-")); row.append(cell(pkg.Available || "-")); const actionCell = document.createElement("td"); const button = document.createElement("button"); button.className = "ghost"; button.type = "button"; button.textContent = t("hide"); button.addEventListener("click", () => setIgnored(pkg.Name, true)); actionCell.append(button); row.append(actionCell); updatesBody.append(row); } updatesTable.hidden = packages.length === 0; emptyState.hidden = packages.length !== 0; updateSelectionUi(packages); } function renderIgnored() { const ignored = currentData.packages.filter((pkg) => pkg.Ignored); ignoredBody.replaceChildren(); ignoredPanel.hidden = ignored.length === 0 || !currentData.settings.show_ignored; ignoredHint.textContent = ignored.length === 1 ? t("onePackage") : t("manyPackages", { count: ignored.length }); for (const pkg of ignored) { const row = document.createElement("tr"); row.append(cell(pkg.Name)); row.append(cell(pkg.Available || "-")); const actionCell = document.createElement("td"); const button = document.createElement("button"); button.className = "ghost"; button.type = "button"; button.textContent = t("show"); button.addEventListener("click", () => setIgnored(pkg.Name, false)); actionCell.append(button); row.append(actionCell); ignoredBody.append(row); } } function cell(value) { const element = document.createElement("td"); element.textContent = value; return element; } function render(data) { currentData = data; language.value = data.settings.language || "de"; applyTranslations(); const availableNames = new Set(data.packages.filter((pkg) => !pkg.Ignored).map((pkg) => pkg.Name)); selectedPackages = new Set([...selectedPackages].filter((name) => availableNames.has(name))); summary.textContent = summaryText(data.total); activeCount.textContent = data.total; ignoredCount.textContent = data.ignored_total; installBtn.disabled = data.total === 0 || data.system.pacman_locked; diskFree.textContent = t("free", { value: data.system.disk_free }); kernel.textContent = data.system.kernel; aurHelper.textContent = data.system.aur_helper || t("notFound"); terminalStatus.textContent = data.system.terminal || t("notFound"); lastCheck.textContent = t("lastCheck", { date: formatDate(data.state.last_check) }); lastSuccess.textContent = formatDate(data.state.last_success); lockState.textContent = data.system.pacman_locked ? t("pacmanLocked") : t("ready"); lockState.classList.toggle("locked", data.system.pacman_locked); checkAur.checked = data.settings.check_aur; showIgnored.checked = data.settings.show_ignored; notificationsEnabled.checked = data.settings.notifications_enabled; keepTerminalOpen.checked = data.settings.keep_terminal_open; confirmSelectedInstalls.checked = data.settings.confirm_selected_installs; language.value = data.settings.language || "de"; reminderHours.value = data.settings.reminder_interval_hours; autoRefreshMinutes.value = data.settings.auto_refresh_minutes; terminal.value = data.settings.terminal || "auto"; warnings.hidden = data.warnings.length === 0; warnings.textContent = data.warnings.join("\\n"); renderTable(); renderIgnored(); renderIgnoredSettings(); scheduleAutoRefresh(); } function renderIgnoredSettings() { ignoredSettingsList.replaceChildren(); const names = currentData?.settings.ignored_packages || []; if (names.length === 0) { const empty = document.createElement("p"); empty.className = "empty-inline"; empty.textContent = t("noIgnoredPackages"); ignoredSettingsList.append(empty); return; } for (const name of names) { const row = document.createElement("div"); row.className = "ignored-setting-row"; const label = document.createElement("span"); label.textContent = name; const button = document.createElement("button"); button.type = "button"; button.className = "ghost"; button.textContent = t("show"); button.addEventListener("click", () => setIgnored(name, false)); row.append(label, button); ignoredSettingsList.append(row); } } function updateSelectionUi(packages = visiblePackages()) { const selectedCount = selectedPackages.size; selectionText.textContent = selectedCount === 1 ? t("oneSelected") : t("manySelected", { count: selectedCount }); installSelectedBtn.disabled = selectedCount === 0 || currentData?.system.pacman_locked; selectAll.checked = packages.length > 0 && packages.every((pkg) => selectedPackages.has(pkg.Name)); selectAll.indeterminate = packages.some((pkg) => selectedPackages.has(pkg.Name)) && !selectAll.checked; } async function loadStatus() { refreshBtn.disabled = true; summary.textContent = t("checkingUpdates"); try { render(await request("/api/status")); } catch (error) { summary.textContent = error.message; } finally { refreshBtn.disabled = false; } } function scheduleAutoRefresh() { clearInterval(refreshTimer); const minutes = Number(autoRefreshMinutes.value); if (!minutes || minutes < 1) { return; } refreshTimer = setInterval(loadStatus, minutes * 60 * 1000); } async function setIgnored(name, ignored) { await request("/api/ignore", { method: "POST", body: JSON.stringify({ name, ignored }), }); await loadStatus(); } refreshBtn.addEventListener("click", loadStatus); settingsBtn.addEventListener("click", () => settingsDialog.showModal()); closeSettingsBtn.addEventListener("click", () => settingsDialog.close()); searchInput.addEventListener("input", renderTable); sourceFilter.addEventListener("change", renderTable); language.addEventListener("change", () => { if (currentData) { currentData.settings.language = language.value; } applyTranslations(); if (currentData) { summary.textContent = summaryText(currentData.total); renderTable(); renderIgnored(); renderIgnoredSettings(); updateSelectionUi(); diskFree.textContent = t("free", { value: currentData.system.disk_free }); aurHelper.textContent = currentData.system.aur_helper || t("notFound"); terminalStatus.textContent = currentData.system.terminal || t("notFound"); lastCheck.textContent = t("lastCheck", { date: formatDate(currentData.state.last_check) }); lockState.textContent = currentData.system.pacman_locked ? t("pacmanLocked") : t("ready"); } }); selectAll.addEventListener("change", () => { const packages = visiblePackages(); for (const pkg of packages) { if (selectAll.checked) { selectedPackages.add(pkg.Name); } else { selectedPackages.delete(pkg.Name); } } renderTable(); }); installBtn.addEventListener("click", async () => { installBtn.disabled = true; try { await request("/api/install", { method: "POST", body: "{}" }); summary.textContent = t("installStarted"); } catch (error) { summary.textContent = error.message; } finally { installBtn.disabled = false; } }); installSelectedBtn.addEventListener("click", async () => { const packages = [...selectedPackages]; if (packages.length === 0) { return; } if (currentData?.settings.confirm_selected_installs) { const ok = window.confirm(t("confirmSelected")); if (!ok) { return; } } installSelectedBtn.disabled = true; try { await request("/api/install", { method: "POST", body: JSON.stringify({ packages }), }); summary.textContent = t("selectedInstallStarted"); } catch (error) { summary.textContent = error.message; } finally { installSelectedBtn.disabled = false; } }); form.addEventListener("submit", async (event) => { event.preventDefault(); saveState.textContent = t("saving"); try { await request("/api/settings", { method: "PUT", body: JSON.stringify({ check_aur: checkAur.checked, show_ignored: showIgnored.checked, notifications_enabled: notificationsEnabled.checked, keep_terminal_open: keepTerminalOpen.checked, confirm_selected_installs: confirmSelectedInstalls.checked, language: language.value, reminder_interval_hours: Number(reminderHours.value), auto_refresh_minutes: Number(autoRefreshMinutes.value), terminal: terminal.value, ignored_packages: currentData?.settings.ignored_packages || [], }), }); saveState.textContent = t("saved"); await loadStatus(); } catch (error) { saveState.textContent = error.message; } }); for (const tab of tabs) { tab.addEventListener("click", () => { const target = tab.dataset.tab; for (const item of tabs) { item.classList.toggle("active", item === tab); } for (const panel of tabPanels) { panel.classList.toggle("active", panel.dataset.panel === target); } }); } loadStatus(); `