mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-05-27 18:54:42 +00:00
Update AppImage 1.2.1.2
This commit is contained in:
Binary file not shown.
@@ -1 +1 @@
|
|||||||
0d74347d2feae2be4b8c6d62d6cd9b1b15b94ef431c088b5580560f6b4751594 ProxMenux-1.2.1.2-beta.AppImage
|
25564b65c641c292c24e45336b49b68a57c1f87de83d2867945a9a1255a6e8a4 ProxMenux-1.2.1.2-beta.AppImage
|
||||||
|
|||||||
@@ -373,6 +373,42 @@ export function StorageOverview() {
|
|||||||
const obsTypeLabel = (t: string) =>
|
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)
|
({ 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 getStorageTypeBadge = (type: string) => {
|
||||||
const typeColors: Record<string, string> = {
|
const typeColors: Record<string, string> = {
|
||||||
pbs: "bg-purple-500/10 text-purple-500 border-purple-500/20",
|
pbs: "bg-purple-500/10 text-purple-500 border-purple-500/20",
|
||||||
@@ -2128,27 +2164,30 @@ export function StorageOverview() {
|
|||||||
{diskObservations.map((obs) => (
|
{diskObservations.map((obs) => (
|
||||||
<div
|
<div
|
||||||
key={obs.id}
|
key={obs.id}
|
||||||
className={`rounded-lg border p-3 text-sm ${
|
className="rounded-lg border p-3 text-sm bg-blue-500/5 border-blue-500/20"
|
||||||
obs.severity === 'critical'
|
|
||||||
? 'bg-red-500/5 border-red-500/20'
|
|
||||||
: '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">
|
<div className="flex items-center gap-2 flex-wrap mb-2">
|
||||||
<Badge className={`text-[10px] px-1.5 py-0 ${
|
<Badge className="text-[10px] px-1.5 py-0 bg-blue-500/10 text-blue-400 border-blue-500/20">
|
||||||
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'
|
|
||||||
}`}>
|
|
||||||
{obsTypeLabel(obs.error_type)}
|
{obsTypeLabel(obs.error_type)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Error message - responsive text wrap */}
|
{/* 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}
|
{obs.raw_message}
|
||||||
</p>
|
</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 */}
|
{/* 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">
|
<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">
|
||||||
|
|||||||
@@ -1606,9 +1606,17 @@ class NotificationManager:
|
|||||||
|
|
||||||
State held in-memory: `self._was_in_quiet_hours[ch_name]`. On
|
State held in-memory: `self._was_in_quiet_hours[ch_name]`. On
|
||||||
first run after restart all channels start as "unknown" — we
|
first run after restart all channels start as "unknown" — we
|
||||||
seed with the current window status WITHOUT firing a summary,
|
seed with the current window status WITHOUT firing a summary
|
||||||
so a Monitor restart in the middle of someone's quiet window
|
when the channel is currently IN its quiet window (a Monitor
|
||||||
doesn't trigger a fake close-of-window flush.
|
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 in→out 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'):
|
if not hasattr(self, '_was_in_quiet_hours'):
|
||||||
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)
|
previously_in = self._was_in_quiet_hours.get(ch_name)
|
||||||
self._was_in_quiet_hours[ch_name] = currently_in
|
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:
|
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
|
continue
|
||||||
# Still in the window → just buffer.
|
# Still in the window → just buffer.
|
||||||
if currently_in:
|
if currently_in:
|
||||||
@@ -1632,6 +1648,28 @@ class NotificationManager:
|
|||||||
print(f"[NotificationManager] quiet flush failed for "
|
print(f"[NotificationManager] quiet flush failed for "
|
||||||
f"{ch_name}: {e}")
|
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:
|
def _flush_quiet_for_channel(self, ch_name: str, channel: Any) -> None:
|
||||||
"""Send a single grouped summary of everything buffered for
|
"""Send a single grouped summary of everything buffered for
|
||||||
`ch_name` during the just-closed quiet window, then drop the
|
`ch_name` during the just-closed quiet window, then drop the
|
||||||
|
|||||||
@@ -317,25 +317,34 @@ def get_error_context(text: str, category: Optional[str] = None, detail_level: s
|
|||||||
if not error:
|
if not error:
|
||||||
return None
|
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 11–100 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":
|
if detail_level == "minimal":
|
||||||
return f"Known issue: {error['cause']}"
|
return f"Known issue: {error['cause']}"
|
||||||
|
|
||||||
elif detail_level == "standard":
|
elif detail_level == "standard":
|
||||||
lines = [
|
lines = [
|
||||||
f"KNOWN PROXMOX ERROR DETECTED:",
|
f"KNOWN PROXMOX ERROR DETECTED:",
|
||||||
f" Cause: {error['cause']}",
|
f" Cause: {error['cause']}",
|
||||||
f" Severity: {error['severity'].upper()}",
|
|
||||||
f" Solution: {error['solution']}"
|
f" Solution: {error['solution']}"
|
||||||
]
|
]
|
||||||
if error.get("url"):
|
if error.get("url"):
|
||||||
lines.append(f" Docs: {error['url']}")
|
lines.append(f" Docs: {error['url']}")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
else: # detailed
|
else: # detailed
|
||||||
lines = [
|
lines = [
|
||||||
f"KNOWN PROXMOX ERROR DETECTED:",
|
f"KNOWN PROXMOX ERROR DETECTED:",
|
||||||
f" Cause: {error.get('cause_detailed', error['cause'])}",
|
f" Cause: {error.get('cause_detailed', error['cause'])}",
|
||||||
f" Severity: {error['severity'].upper()}",
|
|
||||||
f" Solution: {error.get('solution_detailed', error['solution'])}"
|
f" Solution: {error.get('solution_detailed', error['solution'])}"
|
||||||
]
|
]
|
||||||
if error.get("url"):
|
if error.get("url"):
|
||||||
|
|||||||
Reference in New Issue
Block a user