feat: add selected update installs and polished UI

This commit is contained in:
2026-05-03 23:04:30 +02:00
parent 137290e3d7
commit e8d4d2e400
4 changed files with 211 additions and 13 deletions

View File

@@ -17,7 +17,8 @@ const indexHTML = `<!doctype html>
</div>
<div class="actions">
<button id="refreshBtn" type="button" title="Updates pruefen">Refresh</button>
<button id="installBtn" type="button">Installieren</button>
<button id="installSelectedBtn" type="button" disabled>Auswahl installieren</button>
<button id="installBtn" type="button">Alle installieren</button>
</div>
</header>
@@ -57,11 +58,19 @@ const indexHTML = `<!doctype html>
</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>
@@ -161,14 +170,16 @@ const indexHTML = `<!doctype html>
const appCSS = `
:root {
color-scheme: dark;
--bg: #111317;
--panel: #191d23;
--panel-soft: #222832;
--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;
}
@@ -182,7 +193,9 @@ body {
min-width: 320px;
min-height: 100vh;
color: var(--text);
background: var(--bg);
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;
}
@@ -249,10 +262,12 @@ button {
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 {
@@ -273,6 +288,17 @@ 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));
@@ -285,6 +311,7 @@ form button:hover {
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel);
box-shadow: 0 18px 42px rgba(0, 0, 0, 0.18);
}
.stat {
@@ -292,6 +319,7 @@ form button:hover {
gap: 6px;
min-height: 82px;
padding: 14px;
background: linear-gradient(180deg, var(--panel-lift), var(--panel));
}
.stat span,
@@ -345,6 +373,27 @@ select {
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;
@@ -384,7 +433,12 @@ td {
overflow-wrap: anywhere;
}
td:first-child {
.updates-panel td:first-child {
width: 46px;
color: var(--text);
}
.updates-panel td:nth-child(2) {
width: 92px;
color: var(--accent);
font-weight: 720;
@@ -399,6 +453,10 @@ tr:last-child td {
border-bottom: 0;
}
tr.selected td {
background: rgba(110, 168, 254, 0.08);
}
.ghost {
min-height: 34px;
padding: 0 10px;
@@ -477,12 +535,16 @@ input[type="checkbox"] {
grid-template-columns: 1fr 1fr;
}
.selection-bar {
display: grid;
}
#searchInput {
width: 100%;
}
th:nth-child(3),
td:nth-child(3) {
th:nth-child(4),
td:nth-child(4) {
display: none;
}
}
@@ -496,6 +558,9 @@ const diskFree = document.querySelector("#diskFree");
const kernel = document.querySelector("#kernel");
const refreshBtn = document.querySelector("#refreshBtn");
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");
@@ -520,6 +585,7 @@ const saveState = document.querySelector("#saveState");
let currentData = null;
let refreshTimer = null;
let selectedPackages = new Set();
async function request(path, options = {}) {
const response = await fetch(path, {
@@ -568,6 +634,22 @@ function renderTable() {
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 || "-"));
@@ -586,6 +668,7 @@ function renderTable() {
updatesTable.hidden = packages.length === 0;
emptyState.hidden = packages.length !== 0;
updateSelectionUi(packages);
}
function renderIgnored() {
@@ -618,6 +701,8 @@ function cell(value) {
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;
@@ -645,6 +730,14 @@ function render(data) {
scheduleAutoRefresh();
}
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...";
@@ -677,11 +770,22 @@ async function setIgnored(name, ignored) {
refreshBtn.addEventListener("click", loadStatus);
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" });
await request("/api/install", { method: "POST", body: "{}" });
summary.textContent = "Installation im Terminal gestartet.";
} catch (error) {
summary.textContent = error.message;
@@ -690,6 +794,30 @@ installBtn.addEventListener("click", async () => {
}
});
installSelectedBtn.addEventListener("click", async () => {
const packages = [...selectedPackages];
if (packages.length === 0) {
return;
}
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...";