Files
LazyUpdateManager/internal/gui/assets.go

1231 lines
34 KiB
Go

package gui
const indexHTML = `<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>LazyUpdateManager</title>
<link rel="stylesheet" href="/app.css">
</head>
<body>
<main class="shell">
<header class="topbar">
<div>
<h1>LazyUpdateManager</h1>
<p id="summary">Pruefe Updates...</p>
</div>
<div class="actions">
<button id="settingsBtn" type="button" data-i18n="settings">Einstellungen</button>
<button id="refreshBtn" type="button" title="Updates pruefen" data-i18n="refresh">Refresh</button>
<button id="installSelectedBtn" type="button" disabled data-i18n="installSelected">Auswahl installieren</button>
<button id="installBtn" type="button" data-i18n="installAll">Alle installieren</button>
</div>
</header>
<section class="stats">
<div class="stat">
<span data-i18n="activeUpdates">Aktive Updates</span>
<strong id="activeCount">0</strong>
</div>
<div class="stat">
<span data-i18n="ignored">Ausgeblendet</span>
<strong id="ignoredCount">0</strong>
</div>
<div class="stat">
<span data-i18n="freeDisk">Freier Speicher</span>
<strong id="diskFree">-</strong>
</div>
<div class="stat">
<span>Kernel</span>
<strong id="kernel">-</strong>
</div>
</section>
<section class="content">
<div class="main-column">
<div class="panel updates-panel">
<div class="panel-head toolbar">
<div>
<h2 data-i18n="updates">Updates</h2>
<span id="lastCheck">Noch nicht geprueft</span>
</div>
<div class="tools">
<input id="searchInput" type="search" placeholder="Pakete suchen" data-i18n-placeholder="searchPackages">
<select id="sourceFilter">
<option value="all" data-i18n="allSources">Alle Quellen</option>
<option value="pacman">Pacman</option>
<option value="aur">AUR</option>
</select>
</div>
</div>
<div class="selection-bar">
<label class="select-all">
<input id="selectAll" type="checkbox">
<span id="selectionText">0 Updates ausgewaehlt</span>
</label>
<span data-i18n="selectiveWarning">Selektive Updates koennen unter Arch riskant sein. Vollupdate bleibt empfohlen.</span>
</div>
<div id="warnings" class="warnings" hidden></div>
<div id="emptyState" class="empty" data-i18n="noMatchingUpdates">Keine passenden Updates gefunden.</div>
<table id="updatesTable" hidden>
<thead>
<tr>
<th></th>
<th data-i18n="source">Quelle</th>
<th data-i18n="package">Paket</th>
<th data-i18n="current">Aktuell</th>
<th data-i18n="available">Verfuegbar</th>
<th></th>
</tr>
</thead>
<tbody id="updatesBody"></tbody>
</table>
</div>
<div id="ignoredPanel" class="panel ignored-panel" hidden>
<div class="panel-head">
<h2 data-i18n="ignoredUpdates">Ausgeblendete Updates</h2>
<span id="ignoredHint"></span>
</div>
<table>
<tbody id="ignoredBody"></tbody>
</table>
</div>
</div>
<aside class="side-column">
<section class="panel system-panel">
<div class="panel-head">
<h2 data-i18n="system">System</h2>
<span id="lockState"></span>
</div>
<dl class="system-list">
<div>
<dt data-i18n="aurHelper">AUR Helper</dt>
<dd id="aurHelper">-</dd>
</div>
<div>
<dt>Terminal</dt>
<dd id="terminalStatus">-</dd>
</div>
<div>
<dt data-i18n="lastSuccessfulCheck">Letzte erfolgreiche Pruefung</dt>
<dd id="lastSuccess">-</dd>
</div>
</dl>
</section>
</aside>
</section>
</main>
<dialog id="settingsDialog">
<form id="settingsForm" method="dialog" class="settings-modal">
<header class="modal-head">
<div>
<h2 data-i18n="settings">Einstellungen</h2>
<p data-i18n="settingsSubtitle">Update-Verhalten, Installation und ausgeblendete Pakete</p>
</div>
<button id="closeSettingsBtn" type="button" class="icon-btn" data-i18n="close">Schliessen</button>
</header>
<nav class="tabs" aria-label="Einstellungen">
<button class="tab active" type="button" data-tab="general" data-i18n="general">Allgemein</button>
<button class="tab" type="button" data-tab="notifications" data-i18n="notifications">Benachrichtigungen</button>
<button class="tab" type="button" data-tab="install" data-i18n="installation">Installation</button>
<button class="tab" type="button" data-tab="ignored" data-i18n="ignored">Ausgeblendet</button>
</nav>
<section class="tab-panel active" data-panel="general">
<label>
<span data-i18n="language">Sprache</span>
<select id="language">
<option value="de">Deutsch</option>
<option value="en">English</option>
</select>
</label>
<label class="toggle">
<input id="checkAur" type="checkbox">
<span data-i18n="checkAurUpdates">AUR Updates pruefen</span>
</label>
<label class="toggle">
<input id="showIgnored" type="checkbox">
<span data-i18n="showIgnoredUpdates">Ausgeblendete Updates in der App anzeigen</span>
</label>
<label>
<span data-i18n="autoRefresh">Auto-Refresh</span>
<input id="autoRefreshMinutes" type="number" min="0" step="1">
<small data-i18n="minutesDisabled">Minuten, 0 deaktiviert</small>
</label>
</section>
<section class="tab-panel" data-panel="notifications">
<label class="toggle">
<input id="notificationsEnabled" type="checkbox">
<span data-i18n="sendWeeklyReminders">Woechentliche Erinnerungen senden</span>
</label>
<label>
<span data-i18n="reminderEvery">Erinnerung alle</span>
<input id="reminderHours" type="number" min="1" step="1">
<small data-i18n="hours">Stunden</small>
</label>
</section>
<section class="tab-panel" data-panel="install">
<label>
<span data-i18n="terminal">Terminal</span>
<select id="terminal">
<option value="auto" data-i18n="automatic">Automatisch</option>
<option value="foot">foot</option>
<option value="kitty">kitty</option>
<option value="alacritty">alacritty</option>
<option value="wezterm">wezterm</option>
<option value="ghostty">ghostty</option>
<option value="konsole">konsole</option>
<option value="xterm">xterm</option>
</select>
</label>
<label class="toggle">
<input id="keepTerminalOpen" type="checkbox">
<span data-i18n="keepTerminalOpen">Terminal nach Installationen offen lassen</span>
</label>
<label class="toggle">
<input id="confirmSelectedInstalls" type="checkbox">
<span data-i18n="confirmSelectedInstalls">Selektive Installationen bestaetigen</span>
</label>
</section>
<section class="tab-panel" data-panel="ignored">
<div id="ignoredSettingsList" class="ignored-settings-list"></div>
</section>
<footer class="modal-actions">
<span id="saveState"></span>
<button type="submit" data-i18n="save">Speichern</button>
</footer>
</form>
</dialog>
<script src="/app.js"></script>
</body>
</html>`
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();
`