Update AppImage 1.2.1.2

This commit is contained in:
MacRimi
2026-05-21 17:18:23 +02:00
parent 3723888b0c
commit ef22c88861
5 changed files with 107 additions and 21 deletions
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
0d74347d2feae2be4b8c6d62d6cd9b1b15b94ef431c088b5580560f6b4751594 ProxMenux-1.2.1.2-beta.AppImage
25564b65c641c292c24e45336b49b68a57c1f87de83d2867945a9a1255a6e8a4 ProxMenux-1.2.1.2-beta.AppImage
+51 -12
View File
@@ -373,6 +373,42 @@ export function StorageOverview() {
const obsTypeLabel = (t: string) =>
({ smart_error: 'SMART Error', io_error: 'I/O Error', filesystem_error: 'Filesystem Error', zfs_pool_error: 'ZFS Pool Error', connection_error: 'Connection Error' }[t] || t)
// Translate the short ATA/SCSI error codes that appear inside `{ ... }`
// in a raw kernel observation (e.g. `error: { IDNF }`) into a one-line
// human description. Mirrors `_translate_ata_error` in
// notification_events.py — kept here so the UI does not have to round-trip
// to the backend just to render a friendlier line under the raw message.
// Returns null when no recognised code is present, so the caller can hide
// the extra line for non-ATA observations.
const translateAtaError = (raw: string): string | null => {
if (!raw) return null
const ATA_CODES: Record<string, string> = {
IDNF: 'Sector address not found — possible bad sector or cable issue',
UNC: 'Uncorrectable read error — bad sector',
ABRT: 'Command aborted by drive',
AMNF: 'Address mark not found — surface damage',
TK0NF: 'Track 0 not found — drive hardware failure',
BBK: 'Bad block detected',
ICRC: 'Interface CRC error — cable or connector issue',
MC: 'Media changed',
MCR: 'Media change requested',
WP: 'Write protected',
}
const m = raw.match(/\{\s*([A-Z0-9 ]+)\s*\}/)
if (!m) return null
const codes = m[1].split(/\s+/).filter(Boolean)
const seen = new Set<string>()
const out: string[] = []
for (const c of codes) {
const desc = ATA_CODES[c]
if (desc && !seen.has(c)) {
seen.add(c)
out.push(desc)
}
}
return out.length ? out.join('; ') : null
}
const getStorageTypeBadge = (type: string) => {
const typeColors: Record<string, string> = {
pbs: "bg-purple-500/10 text-purple-500 border-purple-500/20",
@@ -2128,27 +2164,30 @@ export function StorageOverview() {
{diskObservations.map((obs) => (
<div
key={obs.id}
className={`rounded-lg border p-3 text-sm ${
obs.severity === 'critical'
? 'bg-red-500/5 border-red-500/20'
: 'bg-blue-500/5 border-blue-500/20'
}`}
className="rounded-lg border p-3 text-sm bg-blue-500/5 border-blue-500/20"
>
{/* Header with type badge */}
{/* Header with type badge — always blue.
The earlier red/blue split-by-severity was
confusing here because the Observations
panel is a *history* view, not a live
alert; the severity already reaches the
user through the notification channels.
The card just records what happened. */}
<div className="flex items-center gap-2 flex-wrap mb-2">
<Badge className={`text-[10px] px-1.5 py-0 ${
obs.severity === 'critical'
? 'bg-red-500/10 text-red-400 border-red-500/20'
: 'bg-blue-500/10 text-blue-400 border-blue-500/20'
}`}>
<Badge className="text-[10px] px-1.5 py-0 bg-blue-500/10 text-blue-400 border-blue-500/20">
{obsTypeLabel(obs.error_type)}
</Badge>
</div>
{/* Error message - responsive text wrap */}
<p className="text-xs whitespace-pre-wrap break-words opacity-90 font-mono leading-relaxed mb-3">
<p className="text-xs whitespace-pre-wrap break-words opacity-90 font-mono leading-relaxed mb-1">
{obs.raw_message}
</p>
{translateAtaError(obs.raw_message) && (
<p className="text-xs italic opacity-75 mb-3 break-words">
{translateAtaError(obs.raw_message)}
</p>
)}
{/* Dates - stacked on mobile, inline on desktop */}
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-3 text-[10px] text-muted-foreground border-t border-white/5 pt-2">
+42 -4
View File
@@ -1606,9 +1606,17 @@ class NotificationManager:
State held in-memory: `self._was_in_quiet_hours[ch_name]`. On
first run after restart all channels start as "unknown" we
seed with the current window status WITHOUT firing a summary,
so a Monitor restart in the middle of someone's quiet window
doesn't trigger a fake close-of-window flush.
seed with the current window status WITHOUT firing a summary
when the channel is currently IN its quiet window (a Monitor
restart mid-window must not look like a "close" transition).
Recovery seed: if the channel is currently OUT of the quiet
window AND there are leftover rows in `quiet_pending`, those
rows belong to a window that closed during a restart they
would otherwise stay buffered forever because the seed marks
the channel as "out" without ever seeing the inout edge.
Flush them now so the user gets their overnight summary even
when an update lands right as the window closes.
"""
if not hasattr(self, '_was_in_quiet_hours'):
self._was_in_quiet_hours = {}
@@ -1618,8 +1626,16 @@ class NotificationManager:
previously_in = self._was_in_quiet_hours.get(ch_name)
self._was_in_quiet_hours[ch_name] = currently_in
# Seed run (no prior state) — don't fire anything.
# Seed run (no prior state).
if previously_in is None:
# Recovery: leftover buffer from a window that closed
# during a restart must still reach the user.
if not currently_in and self._has_pending_quiet_rows(ch_name):
try:
self._flush_quiet_for_channel(ch_name, channel)
except Exception as e:
print(f"[NotificationManager] quiet recovery flush "
f"failed for {ch_name}: {e}")
continue
# Still in the window → just buffer.
if currently_in:
@@ -1632,6 +1648,28 @@ class NotificationManager:
print(f"[NotificationManager] quiet flush failed for "
f"{ch_name}: {e}")
def _has_pending_quiet_rows(self, ch_name: str) -> bool:
"""Cheap existence check used by the recovery branch of
`_maybe_flush_quiet_hours`. We don't reuse `_flush_*` for this
because a no-op flush call would still open a connection and
do the SELECT a single COUNT keeps the seed pass O(1) per
channel when nothing is pending."""
try:
conn = sqlite3.connect(str(DB_PATH), timeout=10)
conn.execute('PRAGMA journal_mode=WAL')
cursor = conn.cursor()
cursor.execute(
'SELECT 1 FROM quiet_pending WHERE channel = ? LIMIT 1',
(ch_name,),
)
row = cursor.fetchone()
conn.close()
return row is not None
except Exception as e:
print(f"[NotificationManager] quiet pending probe failed for "
f"{ch_name}: {e}")
return False
def _flush_quiet_for_channel(self, ch_name: str, channel: Any) -> None:
"""Send a single grouped summary of everything buffered for
`ch_name` during the just-closed quiet window, then drop the
+13 -4
View File
@@ -317,25 +317,34 @@ def get_error_context(text: str, category: Optional[str] = None, detail_level: s
if not error:
return None
# NOTE: we intentionally do NOT emit a "Severity:" line here.
# The catalogue's severity is the *typical* severity of a class
# of error, not the *actual* severity of the event the user is
# looking at. A SATA cable warning (rate 11100 errors/24h, SMART
# PASSED) used to render "Severity: CRITICAL" in the body because
# the catalogue says SMART_FAILED is critical generically — that
# contradicted the WARNING badge on the notification header and
# frightened operators unnecessarily. The event-level severity
# (computed by `_check_disk_io` with the tiered model) is already
# carried by the notification's own severity field; repeating a
# different value here is noise at best, misinformation at worst.
if detail_level == "minimal":
return f"Known issue: {error['cause']}"
elif detail_level == "standard":
lines = [
f"KNOWN PROXMOX ERROR DETECTED:",
f" Cause: {error['cause']}",
f" Severity: {error['severity'].upper()}",
f" Solution: {error['solution']}"
]
if error.get("url"):
lines.append(f" Docs: {error['url']}")
return "\n".join(lines)
else: # detailed
lines = [
f"KNOWN PROXMOX ERROR DETECTED:",
f" Cause: {error.get('cause_detailed', error['cause'])}",
f" Severity: {error['severity'].upper()}",
f" Solution: {error.get('solution_detailed', error['solution'])}"
]
if error.get("url"):