1041 lines
26 KiB
Go
1041 lines
26 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">Einstellungen</button>
|
|
<button id="refreshBtn" type="button" title="Updates pruefen">Refresh</button>
|
|
<button id="installSelectedBtn" type="button" disabled>Auswahl installieren</button>
|
|
<button id="installBtn" type="button">Alle installieren</button>
|
|
</div>
|
|
</header>
|
|
|
|
<section class="stats">
|
|
<div class="stat">
|
|
<span>Aktive Updates</span>
|
|
<strong id="activeCount">0</strong>
|
|
</div>
|
|
<div class="stat">
|
|
<span>Ausgeblendet</span>
|
|
<strong id="ignoredCount">0</strong>
|
|
</div>
|
|
<div class="stat">
|
|
<span>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>Updates</h2>
|
|
<span id="lastCheck">Noch nicht geprueft</span>
|
|
</div>
|
|
<div class="tools">
|
|
<input id="searchInput" type="search" placeholder="Pakete suchen">
|
|
<select id="sourceFilter">
|
|
<option value="all">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>Selektive Updates koennen unter Arch riskant sein. Vollupdate bleibt empfohlen.</span>
|
|
</div>
|
|
<div id="warnings" class="warnings" hidden></div>
|
|
<div id="emptyState" class="empty">Keine passenden Updates gefunden.</div>
|
|
<table id="updatesTable" hidden>
|
|
<thead>
|
|
<tr>
|
|
<th></th>
|
|
<th>Quelle</th>
|
|
<th>Paket</th>
|
|
<th>Aktuell</th>
|
|
<th>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>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>System</h2>
|
|
<span id="lockState"></span>
|
|
</div>
|
|
<dl class="system-list">
|
|
<div>
|
|
<dt>AUR Helper</dt>
|
|
<dd id="aurHelper">-</dd>
|
|
</div>
|
|
<div>
|
|
<dt>Terminal</dt>
|
|
<dd id="terminalStatus">-</dd>
|
|
</div>
|
|
<div>
|
|
<dt>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>Einstellungen</h2>
|
|
<p>Update-Verhalten, Installation und ausgeblendete Pakete</p>
|
|
</div>
|
|
<button id="closeSettingsBtn" type="button" class="icon-btn">Schliessen</button>
|
|
</header>
|
|
|
|
<nav class="tabs" aria-label="Einstellungen">
|
|
<button class="tab active" type="button" data-tab="general">Allgemein</button>
|
|
<button class="tab" type="button" data-tab="notifications">Benachrichtigungen</button>
|
|
<button class="tab" type="button" data-tab="install">Installation</button>
|
|
<button class="tab" type="button" data-tab="ignored">Ausgeblendet</button>
|
|
</nav>
|
|
|
|
<section class="tab-panel active" data-panel="general">
|
|
<label class="toggle">
|
|
<input id="checkAur" type="checkbox">
|
|
<span>AUR Updates pruefen</span>
|
|
</label>
|
|
<label class="toggle">
|
|
<input id="showIgnored" type="checkbox">
|
|
<span>Ausgeblendete Updates in der App anzeigen</span>
|
|
</label>
|
|
<label>
|
|
<span>Auto-Refresh</span>
|
|
<input id="autoRefreshMinutes" type="number" min="0" step="1">
|
|
<small>Minuten, 0 deaktiviert</small>
|
|
</label>
|
|
</section>
|
|
|
|
<section class="tab-panel" data-panel="notifications">
|
|
<label class="toggle">
|
|
<input id="notificationsEnabled" type="checkbox">
|
|
<span>Woechentliche Erinnerungen senden</span>
|
|
</label>
|
|
<label>
|
|
<span>Erinnerung alle</span>
|
|
<input id="reminderHours" type="number" min="1" step="1">
|
|
<small>Stunden</small>
|
|
</label>
|
|
</section>
|
|
|
|
<section class="tab-panel" data-panel="install">
|
|
<label>
|
|
<span>Terminal</span>
|
|
<select id="terminal">
|
|
<option value="auto">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>Terminal nach Installationen offen lassen</span>
|
|
</label>
|
|
<label class="toggle">
|
|
<input id="confirmSelectedInstalls" type="checkbox">
|
|
<span>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">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 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();
|
|
|
|
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 "-";
|
|
}
|
|
return new Intl.DateTimeFormat("de-DE", {
|
|
dateStyle: "short",
|
|
timeStyle: "short",
|
|
}).format(new Date(value));
|
|
}
|
|
|
|
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 = "Ausblenden";
|
|
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 ? "1 Paket" : ignored.length + " Pakete";
|
|
|
|
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 = "Einblenden";
|
|
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;
|
|
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 = data.summary;
|
|
activeCount.textContent = data.total;
|
|
ignoredCount.textContent = data.ignored_total;
|
|
installBtn.disabled = data.total === 0 || data.system.pacman_locked;
|
|
diskFree.textContent = data.system.disk_free + " frei";
|
|
kernel.textContent = data.system.kernel;
|
|
aurHelper.textContent = data.system.aur_helper || "nicht gefunden";
|
|
terminalStatus.textContent = data.system.terminal || "nicht gefunden";
|
|
lastCheck.textContent = "Letzte Pruefung: " + formatDate(data.state.last_check);
|
|
lastSuccess.textContent = formatDate(data.state.last_success);
|
|
lockState.textContent = data.system.pacman_locked ? "Pacman gesperrt" : "Bereit";
|
|
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;
|
|
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 = "Keine ausgeblendeten Pakete.";
|
|
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 = "Einblenden";
|
|
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 ? "1 Update ausgewaehlt" : selectedCount + " Updates ausgewaehlt";
|
|
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 = "Pruefe Updates...";
|
|
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);
|
|
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 = "Installation im Terminal gestartet.";
|
|
} 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("Ausgewaehlte Pakete gezielt installieren? Unter Arch ist ein vollstaendiges Systemupdate meistens sicherer.");
|
|
if (!ok) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
installSelectedBtn.disabled = true;
|
|
try {
|
|
await request("/api/install", {
|
|
method: "POST",
|
|
body: JSON.stringify({ packages }),
|
|
});
|
|
summary.textContent = "Ausgewaehlte Installation im Terminal gestartet.";
|
|
} catch (error) {
|
|
summary.textContent = error.message;
|
|
} finally {
|
|
installSelectedBtn.disabled = false;
|
|
}
|
|
});
|
|
|
|
form.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
saveState.textContent = "Speichere...";
|
|
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,
|
|
reminder_interval_hours: Number(reminderHours.value),
|
|
auto_refresh_minutes: Number(autoRefreshMinutes.value),
|
|
terminal: terminal.value,
|
|
ignored_packages: currentData?.settings.ignored_packages || [],
|
|
}),
|
|
});
|
|
saveState.textContent = "Gespeichert";
|
|
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();
|
|
`
|