-
Updates
-
0
+
+
+
+
+
Keine passenden Updates gefunden.
+
+
+
+ | Quelle |
+ Paket |
+ Aktuell |
+ Verfuegbar |
+ |
+
+
+
+
+
+
+
+
+
Ausgeblendete Updates
+
+
+
-
-
Keine Updates gefunden.
-
-
-
- | Quelle |
- Paket |
- Aktuell |
- Verfuegbar |
-
-
-
-
-
@@ -85,16 +161,16 @@ const indexHTML = `
const appCSS = `
:root {
color-scheme: dark;
- --bg: #101114;
- --panel: #191b20;
- --panel-soft: #20242b;
- --text: #f1f5f9;
- --muted: #9aa4b2;
- --line: #303640;
- --accent: #42d392;
- --accent-strong: #2ab67c;
- --warn: #f5c451;
- --danger: #ff6b6b;
+ --bg: #111317;
+ --panel: #191d23;
+ --panel-soft: #222832;
+ --text: #f4f7fb;
+ --muted: #9ba7b6;
+ --line: #303844;
+ --accent: #44d19d;
+ --accent-strong: #2eb984;
+ --warn: #f4c95d;
+ --danger: #ff6f61;
}
* {
@@ -117,9 +193,9 @@ select {
}
.shell {
- width: min(1180px, calc(100vw - 32px));
+ width: min(1260px, calc(100vw - 32px));
margin: 0 auto;
- padding: 28px 0;
+ padding: 24px 0;
}
.topbar {
@@ -127,31 +203,40 @@ select {
align-items: center;
justify-content: space-between;
gap: 18px;
- margin-bottom: 18px;
+ margin-bottom: 16px;
}
h1,
h2,
-p {
+p,
+dl,
+dd {
margin: 0;
}
h1 {
font-size: 28px;
- font-weight: 720;
+ font-weight: 740;
}
h2 {
- font-size: 16px;
- font-weight: 680;
+ font-size: 15px;
+ font-weight: 700;
+}
+
+#summary,
+#lastCheck,
+#ignoredHint,
+#saveState {
+ color: var(--muted);
}
#summary {
margin-top: 5px;
- color: var(--muted);
}
-.actions {
+.actions,
+.tools {
display: flex;
gap: 10px;
}
@@ -172,15 +257,15 @@ button:hover {
button:disabled {
cursor: progress;
- opacity: 0.65;
+ opacity: 0.62;
}
#installBtn,
form button {
border-color: transparent;
+ color: #06130e;
background: var(--accent);
- color: #07110d;
- font-weight: 700;
+ font-weight: 760;
}
#installBtn:hover,
@@ -188,47 +273,86 @@ form button:hover {
background: var(--accent-strong);
}
-.content {
+.stats {
display: grid;
- grid-template-columns: minmax(0, 1fr) 320px;
- gap: 18px;
- align-items: start;
+ 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);
}
+.stat {
+ display: grid;
+ gap: 6px;
+ min-height: 82px;
+ padding: 14px;
+}
+
+.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;
- min-height: 54px;
+ gap: 14px;
+ min-height: 58px;
padding: 0 16px;
border-bottom: 1px solid var(--line);
}
-#countBadge {
- display: inline-flex;
+.toolbar {
align-items: center;
- justify-content: center;
- min-width: 32px;
- height: 26px;
- border-radius: 999px;
- color: #07110d;
- background: var(--accent);
- font-weight: 760;
+}
+
+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);
}
.warnings {
margin: 14px 16px 0;
padding: 12px;
- border: 1px solid rgba(245, 196, 81, 0.45);
+ border: 1px solid rgba(244, 201, 93, 0.45);
border-radius: 8px;
color: var(--warn);
- background: rgba(245, 196, 81, 0.08);
+ background: rgba(244, 201, 93, 0.08);
+ white-space: pre-wrap;
}
.empty {
@@ -263,13 +387,53 @@ td {
td:first-child {
width: 92px;
color: var(--accent);
- font-weight: 700;
+ font-weight: 720;
+}
+
+td:last-child {
+ width: 132px;
+ text-align: right;
}
tr:last-child td {
border-bottom: 0;
}
+.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;
@@ -288,43 +452,33 @@ label {
color: var(--text);
}
-input,
-select {
- width: 100%;
- min-height: 40px;
- border: 1px solid var(--line);
- border-radius: 8px;
- padding: 0 10px;
- color: var(--text);
- background: var(--panel-soft);
-}
-
input[type="checkbox"] {
width: 18px;
min-height: 18px;
accent-color: var(--accent);
}
-small,
-#saveState {
- color: var(--muted);
-}
-
-@media (max-width: 820px) {
+@media (max-width: 920px) {
.shell {
- width: min(100vw - 20px, 1180px);
+ width: min(100vw - 20px, 1260px);
padding: 16px 0;
}
.topbar,
- .content {
- display: grid;
+ .content,
+ .stats {
grid-template-columns: 1fr;
+ display: grid;
}
- .actions {
- grid-template-columns: 48px 1fr;
+ .actions,
+ .tools {
display: grid;
+ grid-template-columns: 1fr 1fr;
+ }
+
+ #searchInput {
+ width: 100%;
}
th:nth-child(3),
@@ -336,19 +490,37 @@ small,
const appJS = `
const summary = document.querySelector("#summary");
-const countBadge = document.querySelector("#countBadge");
+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 installBtn = document.querySelector("#installBtn");
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 reminderHours = document.querySelector("#reminderHours");
+const autoRefreshMinutes = document.querySelector("#autoRefreshMinutes");
const terminal = document.querySelector("#terminal");
const saveState = document.querySelector("#saveState");
+let currentData = null;
+let refreshTimer = null;
+
async function request(path, options = {}) {
const response = await fetch(path, {
headers: { "Content-Type": "application/json" },
@@ -361,31 +533,116 @@ async function request(path, options = {}) {
return data;
}
-function render(data) {
- summary.textContent = data.summary;
- countBadge.textContent = data.total;
- installBtn.disabled = data.total === 0;
+function formatDate(value) {
+ if (!value || value.startsWith("0001-")) {
+ return "-";
+ }
+ return new Intl.DateTimeFormat("de-DE", {
+ dateStyle: "short",
+ timeStyle: "short",
+ }).format(new Date(value));
+}
- checkAur.checked = data.settings.check_aur;
- reminderHours.value = data.settings.reminder_interval_hours;
- terminal.value = data.settings.terminal || "auto";
-
- updatesBody.replaceChildren();
- for (const pkg of data.packages) {
- const row = document.createElement("tr");
- for (const value of [pkg.Source, pkg.Name, pkg.Current || "-", pkg.Available || "-"]) {
- const cell = document.createElement("td");
- cell.textContent = value;
- row.appendChild(cell);
+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;
}
- updatesBody.appendChild(row);
+ 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.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 = data.packages.length === 0;
- emptyState.hidden = data.packages.length !== 0;
+ updatesTable.hidden = packages.length === 0;
+ emptyState.hidden = packages.length !== 0;
+}
+
+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;
+ 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;
+ 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();
+ scheduleAutoRefresh();
}
async function loadStatus() {
@@ -400,7 +657,26 @@ async function loadStatus() {
}
}
+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);
+searchInput.addEventListener("input", renderTable);
+sourceFilter.addEventListener("change", renderTable);
installBtn.addEventListener("click", async () => {
installBtn.disabled = true;
@@ -422,8 +698,11 @@ form.addEventListener("submit", async (event) => {
method: "PUT",
body: JSON.stringify({
check_aur: checkAur.checked,
+ show_ignored: showIgnored.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";
diff --git a/internal/gui/server.go b/internal/gui/server.go
index 4f0ec6a..bf96ef3 100644
--- a/internal/gui/server.go
+++ b/internal/gui/server.go
@@ -8,40 +8,62 @@ import (
"log"
"net"
"net/http"
+ "os"
"os/exec"
"runtime"
"strings"
+ "syscall"
"time"
"lazy-update-manager/internal/config"
+ "lazy-update-manager/internal/state"
"lazy-update-manager/internal/updater"
)
type Server struct {
configPath string
+ statePath string
}
type StatusResponse struct {
- Summary string `json:"summary"`
- Total int `json:"total"`
- Packages []updater.Package `json:"packages"`
- Warnings []string `json:"warnings"`
- Settings config.Config `json:"settings"`
+ Summary string `json:"summary"`
+ Total int `json:"total"`
+ IgnoredTotal int `json:"ignored_total"`
+ Packages []updater.Package `json:"packages"`
+ Warnings []string `json:"warnings"`
+ Settings config.Config `json:"settings"`
+ State state.Store `json:"state"`
+ System SystemStatus `json:"system"`
}
-func Run(configPath string, openBrowser bool) error {
+type SystemStatus struct {
+ AURHelper string `json:"aur_helper"`
+ Terminal string `json:"terminal"`
+ PacmanLocked bool `json:"pacman_locked"`
+ DiskFree string `json:"disk_free"`
+ DiskFreePercent int `json:"disk_free_percent"`
+ Kernel string `json:"kernel"`
+}
+
+type ignoreRequest struct {
+ Name string `json:"name"`
+ Ignored bool `json:"ignored"`
+}
+
+func Run(configPath, statePath string, openBrowser bool) error {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return err
}
- server := &Server{configPath: configPath}
+ server := &Server{configPath: configPath, statePath: statePath}
mux := http.NewServeMux()
mux.HandleFunc("/", server.index)
mux.HandleFunc("/app.css", server.css)
mux.HandleFunc("/app.js", server.js)
mux.HandleFunc("/api/status", server.status)
mux.HandleFunc("/api/settings", server.settings)
+ mux.HandleFunc("/api/ignore", server.ignore)
mux.HandleFunc("/api/install", server.install)
url := "http://" + listener.Addr().String()
@@ -74,11 +96,28 @@ func (s *Server) status(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
defer cancel()
- result, err := updater.CheckWithOptions(ctx, updater.Options{CheckAUR: cfg.CheckAUR})
+ result, err := updater.CheckWithOptions(ctx, updater.Options{
+ CheckAUR: cfg.CheckAUR,
+ IgnoredPackages: cfg.IgnoredPackages,
+ })
if err != nil {
writeError(w, err, http.StatusInternalServerError)
return
}
+ now := time.Now()
+ store, err := state.Load(s.statePath)
+ if err != nil {
+ writeError(w, err, http.StatusInternalServerError)
+ return
+ }
+ store.LastCheck = now
+ store.LastSuccess = now
+ store.LastUpdateCount = result.Total()
+ store.LastSummary = result.Summary()
+ if err := state.Save(s.statePath, store); err != nil {
+ writeError(w, err, http.StatusInternalServerError)
+ return
+ }
packages := result.Packages
if packages == nil {
@@ -90,11 +129,14 @@ func (s *Server) status(w http.ResponseWriter, r *http.Request) {
}
writeJSON(w, StatusResponse{
- Summary: result.Summary(),
- Total: result.Total(),
- Packages: packages,
- Warnings: warnings,
- Settings: cfg,
+ Summary: result.Summary(),
+ Total: result.Total(),
+ IgnoredTotal: result.IgnoredTotal(),
+ Packages: packages,
+ Warnings: warnings,
+ Settings: cfg,
+ State: store,
+ System: systemStatus(cfg),
})
}
@@ -123,6 +165,38 @@ func (s *Server) settings(w http.ResponseWriter, r *http.Request) {
}
}
+func (s *Server) ignore(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ var req ignoreRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ writeError(w, err, http.StatusBadRequest)
+ return
+ }
+ req.Name = strings.TrimSpace(req.Name)
+ if req.Name == "" {
+ writeError(w, errors.New("package name is required"), http.StatusBadRequest)
+ return
+ }
+
+ cfg, err := config.Load(s.configPath)
+ if err != nil {
+ writeError(w, err, http.StatusInternalServerError)
+ return
+ }
+
+ cfg.IgnoredPackages = setIgnored(cfg.IgnoredPackages, req.Name, req.Ignored)
+ if err := config.Save(s.configPath, cfg); err != nil {
+ writeError(w, err, http.StatusInternalServerError)
+ return
+ }
+
+ writeJSON(w, cfg)
+}
+
func (s *Server) install(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
@@ -150,6 +224,80 @@ func (s *Server) install(w http.ResponseWriter, r *http.Request) {
writeJSON(w, map[string]string{"status": "started"})
}
+func setIgnored(names []string, name string, ignored bool) []string {
+ seen := map[string]bool{}
+ next := []string{}
+ for _, item := range names {
+ item = strings.TrimSpace(item)
+ if item == "" || item == name || seen[item] {
+ continue
+ }
+ seen[item] = true
+ next = append(next, item)
+ }
+ if ignored {
+ next = append(next, name)
+ }
+ return next
+}
+
+func systemStatus(cfg config.Config) SystemStatus {
+ terminal, _, _ := terminalCommand(cfg)
+ return SystemStatus{
+ AURHelper: updater.AURHelper(),
+ Terminal: terminal,
+ PacmanLocked: fileExists("/var/lib/pacman/db.lck"),
+ DiskFree: diskFree("/"),
+ DiskFreePercent: diskFreePercent("/"),
+ Kernel: kernelVersion(),
+ }
+}
+
+func fileExists(path string) bool {
+ _, err := os.Stat(path)
+ return err == nil
+}
+
+func diskFree(path string) string {
+ var stat syscall.Statfs_t
+ if err := syscall.Statfs(path, &stat); err != nil {
+ return "unknown"
+ }
+ bytes := stat.Bavail * uint64(stat.Bsize)
+ return humanBytes(bytes)
+}
+
+func diskFreePercent(path string) int {
+ var stat syscall.Statfs_t
+ if err := syscall.Statfs(path, &stat); err != nil || stat.Blocks == 0 {
+ return 0
+ }
+ return int((stat.Bavail * 100) / stat.Blocks)
+}
+
+func humanBytes(bytes uint64) string {
+ const unit = 1024
+ if bytes < unit {
+ return fmt.Sprintf("%d B", bytes)
+ }
+ value := float64(bytes)
+ for _, suffix := range []string{"KiB", "MiB", "GiB", "TiB"} {
+ value = value / unit
+ if value < unit {
+ return fmt.Sprintf("%.1f %s", value, suffix)
+ }
+ }
+ return fmt.Sprintf("%.1f PiB", value/unit)
+}
+
+func kernelVersion() string {
+ out, err := exec.Command("uname", "-r").Output()
+ if err != nil {
+ return "unknown"
+ }
+ return strings.TrimSpace(string(out))
+}
+
func terminalCommand(cfg config.Config) (string, []string, error) {
updateCommand := "lazy-update-manager update; printf '\\nDone. Press enter to close... '; read _"
if helper := updater.AURHelper(); helper != "" && cfg.CheckAUR {
diff --git a/internal/state/state.go b/internal/state/state.go
index 44ca480..16b0a6b 100644
--- a/internal/state/state.go
+++ b/internal/state/state.go
@@ -11,6 +11,9 @@ import (
type Store struct {
LastReminder time.Time `json:"last_reminder"`
LastUpdateCount int `json:"last_update_count"`
+ LastCheck time.Time `json:"last_check"`
+ LastSuccess time.Time `json:"last_success"`
+ LastSummary string `json:"last_summary"`
}
func Load(path string) (Store, error) {
diff --git a/internal/updater/updater.go b/internal/updater/updater.go
index 5206478..060f55a 100644
--- a/internal/updater/updater.go
+++ b/internal/updater/updater.go
@@ -12,6 +12,7 @@ type Package struct {
Name string
Current string
Available string
+ Ignored bool
}
type Result struct {
@@ -20,11 +21,28 @@ type Result struct {
}
type Options struct {
- CheckAUR bool
+ CheckAUR bool
+ IgnoredPackages []string
}
func (r Result) Total() int {
- return len(r.Packages)
+ total := 0
+ for _, pkg := range r.Packages {
+ if !pkg.Ignored {
+ total++
+ }
+ }
+ return total
+}
+
+func (r Result) IgnoredTotal() int {
+ total := 0
+ for _, pkg := range r.Packages {
+ if pkg.Ignored {
+ total++
+ }
+ }
+ return total
}
func (r Result) Summary() string {
@@ -60,9 +78,26 @@ func CheckWithOptions(ctx context.Context, opts Options) (Result, error) {
result.Packages = append(result.Packages, aur...)
}
+ markIgnored(&result, opts.IgnoredPackages)
return result, nil
}
+func markIgnored(result *Result, names []string) {
+ ignored := map[string]bool{}
+ for _, name := range names {
+ name = strings.TrimSpace(name)
+ if name != "" {
+ ignored[name] = true
+ }
+ }
+ if len(ignored) == 0 {
+ return
+ }
+ for i := range result.Packages {
+ result.Packages[i].Ignored = ignored[result.Packages[i].Name]
+ }
+}
+
func checkPacman(ctx context.Context) ([]Package, error) {
if _, err := exec.LookPath("checkupdates"); err == nil {
out, err := exec.CommandContext(ctx, "checkupdates").Output()