727 Commits

Author SHA1 Message Date
courtmanr@gmail.com a402d23503 fix(backups): synthesize per-guest task status from vzdump job logs
Scheduled multi-guest vzdump jobs run under a single UPID whose VMID slot
is empty, so pollBackupTasks stored them with VMID 0 and the guest-centric
backups coverage view dropped them entirely: only individually backed-up
guests ever showed task status. (Regressed with the v6.0.0 guest-centric
redesign, which removed the flat task table that used to render job runs.)

pollBackupTasks now fetches the job task's log and parses the per-guest
markers ("Starting Backup of VM", "Finished Backup of VM (duration)",
"Backup of VM failed - reason") into synthetic per-guest BackupTask
entries. Their IDs embed the parent UPID, keeping them stable across polls
and distinct from individually-run backups; per-guest times are
reconstructed from the job start plus the printed durations. Finished
jobs' logs are immutable, so results are cached per instance|UPID and each
finished run is fetched at most once, with a per-cycle fetch cap so a
historical backlog trickles in without stalling the backup poll budget.

The task listing now uses source=all + typefilter=vzdump, so running jobs
are visible too: guests covered by an in-progress job get a "running"
synthetic task, which also feeds resolveBackupIntentContext and
suppresses offline/backup alerts for guests the job is actively backing
up. The frontend needs no changes - synthetic tasks carry real VMIDs and
flow through the existing coverage model, recovery mapper, and alert
intent paths.

Contract: monitoring.md completion obligation 13 records the per-guest
synthesis boundary; proofs land in monitor_backup_job_tasks_test.go,
monitor_alert_intent_test.go, and cluster_client_api_test.go.

Reported by Johannes Strasser (support thread "PBS Bug").

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 20:03:46 +01:00
courtmanr@gmail.com c08da19ae7 fix(ceph): parse Quincy+/Squid status schema for MON and MGR counts
Ceph Quincy and later (including Squid on PVE 9) dropped the monmap
mons array and the mgrmap active_name/standbys arrays from ceph status
output, replacing them with monmap.num_mons and mgrmap.num_standbys,
with quorum membership reported at the top level of the payload. Both
the host agent parser and the Proxmox API path only understood the
legacy arrays, so modern clusters showed 0 monitors and undercounted
managers.

- hostagent: read num_mons/num_standbys and top-level quorum data,
  taking the largest available signal, and base the mon/mgr service
  rows on the same counts
- pkg/proxmox: decode mgrmap num_standbys and top-level
  quorum_names/quorum on CephStatus
- monitoring: fall back to the new fields when counting MON/MGR
  daemons, and log Ceph 401/403 failures at warn level with a hint to
  grant Sys.Audit on / instead of hiding them at debug
- models: prefer the larger non-zero MON/MGR counts when merging Ceph
  cluster records from multiple sources

Fixes #1626, Refs discussion #1290

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 19:31:15 +01:00
courtmanr@gmail.com 83581a4cfb Restore Monitor struct alignment broken by the cadence overrides
Adding the long-named override fields to the Monitor struct widened
gofmt's alignment for the whole field block, which rewrote ~60
unrelated lines and broke four canonical-guardrail tests that pin
struct fields verbatim (TestAvailabilityProviderStaysOnCanonical
MonitoringPath and friends). Fold the overrides into a nested
runtimePollingOverrides struct with short field names so the block's
alignment — set by guestMetadataRefreshJitter — is untouched, leaving
monitor.go a six-line functional diff against main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 12:42:23 +01:00
courtmanr@gmail.com df72955144 Cover pbsPollingInterval in the live polling-cadence setters
The live-setter fix for #1619 pushed backup and PMG cadences into
running monitors but left pbsPollingInterval out entirely: it was
neither applied to the handler's base config nor pushed to live
monitors, so a saved PBS interval still waited for an unrelated
restart. Add the matching override, setter and scheduler wiring, apply
the value to the base config, and fan it out from the settings handler
like the other cadence settings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 12:42:22 +01:00
courtmanr@gmail.com 61df0814ea Push polling cadence settings into live monitors on save
Saving backupPollingInterval, pmgPollingInterval or backupPollingEnabled
only mutated the API handler's base config. Monitors poll against a
detached DeepCopy of that config, and only pvePollingInterval set the
reload flag, so the saved values never reached shouldRunBackupPoll or
the PMG scheduler until an unrelated monitor reload happened.

Add mutex-guarded runtime overrides on Monitor with live setters, fan
them out to every tenant monitor from the settings handler (mirroring
forEachNotificationManager), and clear the per-instance last-poll
timestamps when lowering the backup interval or re-enabling backup
polling so the next cycle runs an immediate catch-up poll.

Fixes #1619

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 12:42:22 +01:00
rcourtman 6060936cb3 Cover pure helpers and persistence round-trips across the backend
Seventeen files closing the zero-coverage functions the current source drop
left behind. Every named target was measured off 0.0 percent by a per-function
coverage delta, re-measured against current main.

- config: the five new durable Proxmox cluster-node identity helpers
  (deterministic id, endpoint equality, alias lookup, id existence, lookup by
  id) to 100 percent; VMware and agent-profile persistence round-trips under
  t.TempDir including that AppendProfileChangeLog appends rather than
  replaces; AI chat session save, load, delete, per-user scoping and age
  cleanup, with explicit timestamps rather than time.Now-relative fuzz;
  PVEInstance.DeepCopy asserted for nested independence.
- truenas: incidentFromPoolStatus over every pool health string,
  RecordsFromSnapshot over nil, empty and populated snapshots, both
  TransportStatus accessors, and the RPC handshake and auth typed errors
  through errors.Is and errors.As.
- unifiedresources: the maintenance-window operator-state lifecycle on
  MemoryStore including the not-found and already-cleared arms, plus the
  four remaining View accessors asserted on their exact formatted output.
- api: restoreAgentExecMetadata, buildAlertConnectionSnapshotsWithRuntimeSources
  and both mock series generators, asserted on shape, ordering and
  determinism rather than non-emptiness.
- cmd/pulse-control-plane: the four remaining MSP and mobile proof report
  printers, asserted on the concrete strings in captured stdout.
- ai: cost.EmptySummary, approval.emptyExecutionState, demo.IsDemoRuntimeIntended
  and tools.findCanonicalAppContainerResourceByReferences across no-match,
  first-match, later-match and ambiguous references.
- monitoring, models, alerts: trueNASAppRunning,
  supplementalProviderOwnedSourcesForOrg, IOCounterPresence.Effective,
  ValidAlertIntentSignal and intentTimePointer.

No source file is modified. Adversarial review returned no rejects across all
seventeen files and flagged four padding cases plus one dead table field; all
were removed and the per-function coverage re-measured as identical, proving
they carried nothing.

PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT=test-only branch coverage, no source or contract change
2026-07-25 00:21:35 +01:00
rcourtman 2f4e9957a5 Stop recording metrics for grace-period guests
ac0fb263c changed preserved guests to carry their real runtime status
("running") instead of the stringified aggregate status ("online"). The
three guest metric recording loops gate on Status == "running", so guests
carried forward while their node sits in the grace period started passing
that gate. The carried-forward projection has no counters, so every cycle
wrote CPU, disk and network zeroes into the history and the persistent
store. Sparklines and stored metrics showed a collapse to zero for a guest
Pulse could not see, rather than a gap. Memory escaped only by accident,
because the projection leaves usage unknown and historyMemoryUsage returns
the -1 sentinel.

Freshly built guests are stamped with the cycle's sample time while
preserved guests keep the LastSeen of the cycle that observed them, so that
field already distinguishes the two. All three loops now gate on
guestObservedInCycle: the efficient path in recordGuestMetrics, and the
traditional VM and container paths, which had the same defect inline.

The guard fails open when LastSeen is absent. Dropping a real sample is the
worse error and the harder one to notice, so no evidence means record.

Regression coverage asserts both directions, and both were verified by
breaking them: removing the guard reports the fabricated zero sample, and
inverting it reports the observed guest losing its sample entirely.
2026-07-24 22:37:29 +01:00
rcourtman 74b822eef9 Stop discovery suppression failing open on configured hosts
Discovery fingerprints hosts on the scanned subnet, and its PBS fallback
probes api2/json/status and api2/json/config/datastore with no credentials,
which PBS records as authentication failures. Configured connections are meant
to be skipped for exactly that reason, so every gap in the skip writes noise
into an operator's own server log on every scan (#1616, split from #1606).

Two paths failed open. Hostname resolution kept only the first IPv4 and broke
out of the loop, leaving a multi-homed host partly suppressed while discovery
reaches whichever address it reaches. Resolution failure returned nothing at
all with no record that suppression had been skipped, so the host stayed
scannable and looked identical to one that was never configured.

Resolution now covers every IPv4 a configured host answers with, and both a
lookup failure and an IPv4-less answer are logged as the suppression gaps they
are. An unresolvable host still yields no entry, which is unavoidable, but it
no longer does so silently and cannot affect the other configured hosts.

This does not by itself explain the reporter's case in #1616. Their sanitized
export rewrites the PBS host to a generated placeholder, so it cannot show
whether the connection was configured by address or by name.

Verified: internal/monitoring, gofmt, canonical completion guard, status,
control-plane, registry and contract audits, active-target automated and
hybrid readiness proofs, and all thirteen release-control unit test modules.
2026-07-24 22:34:54 +01:00
rcourtman d82d3bd627 Record the wearout evidence boundary in contracts and proofs
Adds the subsystem contract deltas and verification artifacts for the
preceding wearout and physical-disk-node fixes, which landed without them.

The contracts now state the rule rather than leaving it implicit in each
caller. Wearout has two absent states and one real zero, -1 is the unreported
sentinel, and 0 is evidence only from a device that reports endurance at all.
storagehealth.WearoutReported is the single authority, and alerting, the
unified risk projection, and the frontend all gate on it. Storage-recovery
records that disk-to-node association is evidence-directional, so a disk with
no Proxmox scope still matches on node name while a scoped disk stays inside
its instance.

Proofs cover the predicate itself, a spent SSD alerting where it previously
stayed silent, absent evidence staying quiet on both the unreported sentinel
and a rotational zero, the view returning the sentinel rather than the struct
zero value, and the presentation helpers agreeing with the server.

Verified: full frontend suite 1061 files / 19405 tests, canonical completion
guard, gofmt, go build ./..., and internal/alerts, internal/monitoring,
internal/storagehealth, internal/unifiedresources.
2026-07-24 21:08:32 +01:00
rcourtman b254d0f19d Restore physical disk node matching and unify the wearout sentinel
Physical disks reported by a host agent carry no Proxmox scope, so tightening
matchesPhysicalDiskNode to require an instance on both sides dropped every
agent-reported disk off the Proxmox node it lives on. The Physical Disks node
filter rendered no options at all, grouping lost the node, and metric target
resolution could not find it. Match on node name when the disk side has no
instance evidence, and keep rejecting a Proxmox-scoped disk against a node
outside that instance, so cluster isolation still holds in the direction that
motivated the change.

Wearout had two competing readings of 0. The presentation layer treated it as
no endurance remaining while the alert path treated it as absent and stayed
silent, so a disk could read critical in the UI and never alert. Two sources
also published 0 for a disk that reported nothing: PhysicalDiskView.Wearout
returned the struct zero value on a nil view, as did the nil-view branch of
physicalDiskFromReadStateView. Both now return the documented -1 sentinel.

storagehealth.WearoutReported is the single authority for whether a reading is
evidence at all: -1 is absent, 0 is real only from a device that reports
endurance, and rotational disks never do. Risk assessment, alerting, and the
frontend all gate on it rather than each reinventing the boundary.

Verified: full frontend suite 1061 files / 19402 tests, eslint, theme and
canonical-platform audits, tsc, go build ./..., and internal/alerts,
internal/monitoring, internal/storagehealth, internal/unifiedresources.
2026-07-24 20:58:09 +01:00
rcourtman afc419ddab Add durable Proxmox node display names 2026-07-24 15:46:56 +01:00
rcourtman 478a9e933d Complete canonical web interface links
Preserve tenant-scoped metadata through partial URL updates and project stable URLs across runtime identities. Use a safe adjacent launch control across overview tables with desktop and mobile regression coverage.
2026-07-24 12:42:16 +01:00
rcourtman 599c8e6349 feat: add evidence-based pool health alerts 2026-07-24 12:40:05 +01:00
rcourtman 20b7d6788d Add legacy issue regression coverage 2026-07-24 11:59:50 +01:00
rcourtman 3a913752de Fix Proxmox cluster membership reconciliation 2026-07-24 11:21:27 +01:00
rcourtman 49217d284d fix: correct Proxmox guest rate sampling 2026-07-24 10:26:50 +01:00
rcourtman ac0fb263c2 Fix Proxmox workload refresh coherence 2026-07-24 10:11:56 +01:00
rcourtman 4815a5c80d Fix PBS connection health authority 2026-07-24 09:45:05 +01:00
rcourtman bf67ba9201 Fix cache-aware Linux memory fallbacks 2026-07-24 00:19:40 +01:00
rcourtman b81ba7dd06 Migrate TrueNAS to supported JSON-RPC transport 2026-07-24 00:10:17 +01:00
rcourtman 8d23529c02 Fix availability check identity projection 2026-07-23 23:46:34 +01:00
rcourtman 3c6d933e9d Fix Proxmox cluster connection authority
Keep the configured cluster URL as primary and recover discovered member
endpoints asynchronously while it is healthy. Reset member reachability
evidence when the effective address changes.

Refs #1437 and #1493
2026-07-23 22:43:34 +01:00
rcourtman 15dbabfe9a Preserve resource URLs across runtime identity changes
Contract-Neutral: Docker URL migration touches shared monitor ingest files without changing the host deletion or re-enrollment contract.
2026-07-23 22:33:07 +01:00
rcourtman 0a95b25f41 Harden host agent re-enrollment lifecycle 2026-07-23 22:29:39 +01:00
rcourtman 87fe506c0a Bound state refresh and recovery ingest ownership 2026-07-23 22:24:52 +01:00
rcourtman d618fb8b74 Harden hypervisor guest CPU authority 2026-07-23 22:19:35 +01:00
rcourtman b8ea840f11 fix(monitoring): make Unraid task state freshness-safe 2026-07-23 22:17:25 +01:00
rcourtman 8c341753ae Normalize physical disk endurance evidence 2026-07-23 22:11:22 +01:00
rcourtman 13c4b57885 Fix VMware connection test degradation handling 2026-07-23 22:06:57 +01:00
rcourtman 615fa4418b Restore Docker agents to Hosts inventory 2026-07-23 22:06:56 +01:00
rcourtman 6a527ba9f2 Fix Proxmox physical disk inventory continuity 2026-07-23 22:05:09 +01:00
rcourtman 5772589363 Prove TrueNAS alert threshold identity persistence 2026-07-23 21:54:57 +01:00
rcourtman d7793a255d Fix TrueNAS boot and replication health classification 2026-07-23 21:37:26 +01:00
rcourtman 20f9da541a Fix Proxmox backup posture identity 2026-07-23 14:55:44 +01:00
rcourtman a45b568d79 Fix backend release gate failures 2026-07-23 14:03:44 +01:00
rcourtman d2c268c0e5 Fix agent update triage and durable action recovery 2026-07-23 11:19:50 +01:00
rcourtman 610f9e0cb3 Cover telemetry classifiers, lifecycle records and install snapshot counts
Five new branch-coverage tests over the packages the telemetry lifecycle drop
touched, taking twenty-two functions to full coverage without touching any
source or existing test.

internal/telemetry: the deployment method precedence chain, the duration bucket
asserted one nanosecond either side of every boundary so the strict comparison
is pinned, every activation stage in its precedence order with the rank
ordering proved strictly increasing, the estate size bucket at each inclusive
boundary, the monitored resource count proved to draw on each counted field
individually, and the update failure classifier through every category arm
including the status short-circuit that wins over error text.

The lifecycle and install-id records are exercised as write-then-read round
trips under a temp directory and a frozen clock: a first-ever ping with nothing
on disk, a later ping against an existing record, an unreadable record, a
directory that cannot be written, and a reset proved to change the identifier
and persist the new one.

internal/monitoring: the alert outcome accumulation across the cutoff boundary
and each classification, and the install outcome accumulation over the arms
reachable without a live monitor.

internal/api: the read state snapshot mapping asserted field by field with
collection independence, both diagnostics collection normalizers, the in-memory
zip entry extraction across a missing entry, a non-zip payload and a zero-length
entry, and the trusted proxy check over IPv4, IPv6 and malformed input.

Contract-Neutral: test-only branch coverage, no contract surface touched
2026-07-23 07:15:13 +01:00
rcourtman cba394e56b Cover control plane status helpers, patrol assertions and setup classifiers
Five new branch-coverage tests taking previously unreached functions from zero
to covered, with no source or existing test touched.

cmd/pulse-control-plane: the MSP status failure lookup across exact, absent,
case-differing and substring inputs, and the status state ordering proved
deterministic across repeated runs over the same map so a random map iteration
order cannot pass by luck.

internal/ai/eval: the signal coverage assertion driven through both the lazy
quality evaluation path and the supplied-quality path, at the minimum rate
boundary and either side of it, asserting the formatted message rather than
only the pass or fail outcome, plus the approval write command builder.

internal/api: the setup script server name derivation including the fallback
arm, the patrol autonomy level validator, the approval risk assessment across
every level it can return, and the two typed error messages.

internal/hostagent: the token already exists classifier over each phrase it
recognises, a wrapped error carrying the phrase, and near-miss messages that
must not match.

internal/monitoring: the limited temperature buffer including its truncation
boundary and its aliasing behaviour, and the mock series generators asserted on
determinism under a fixed seed and on their value bounds.

Reported for follow-up, not fixed here: the node escalation arm in the approval
risk assessment is unreachable for the current command set, generatePlateauSeries
panics on a negative point count and leaves its tail unclamped, and the limited
temperature buffer returns a slice that aliases its internal storage.

Contract-Neutral: test-only branch coverage, no contract surface touched
2026-07-23 06:34:03 +01:00
rcourtman dbca44937b Add privacy-safe telemetry lifecycle and outcome signals 2026-07-23 01:10:38 +01:00
rcourtman 8f32bdd396 Fix ping timeout ceiling expectation 2026-07-22 16:15:31 +01:00
rcourtman 671b159711 Retain last-known platform on removed agents for scoped cleanup handoff
Removed-agent records kept no platform identity, so the fleet doctor
emitted an empty platform for removed rows and the Agent Doctor
uninstall handoff could never tighten to a single platform command.
Host and Docker removal now capture the agent's last-known reported
platform onto the removed record, and the fleet diagnostic resolves the
retained value through the same strict normalization as live subjects,
so /api/agents/diagnostics reports a platform for removed agents and
the handoff shipped in 4fc86ec18 scopes to one command automatically.
A retained value that does not normalize to a known platform still
yields an empty diagnostic platform rather than a guess, and removed
Kubernetes clusters retain nothing because the cluster report never
carries a platform. The field is additive and optional, so removed
lists serialized before it existed load unchanged. Monitoring contract
updated; proofs staged in agent_fleet_doctor_test.go and
monitor_host_agents_test.go.
2026-07-22 11:06:27 +01:00
rcourtman fa54516f8c Explain the missing CAP_NET_RAW grant when ICMP probes are blocked
In-place updates never rewrite the systemd unit, so installs upgraded
past v6.1.0-rc.1 keep a unit without AmbientCapabilities=CAP_NET_RAW
and every ICMP probe fails with ping's raw stderr. Detect the
capability failure and point at the unit and its fix instead (#1554).
2026-07-22 09:09:25 +01:00
rcourtman 567303778a Cover the connection and monitoring pure helpers left untested by the fresh drop
The connections aggregator, alerts and grouping helpers plus the monitoring
group-key, availability-key and read-state mapper helpers were all at zero
coverage. Each of these is a pure value-in to value-out function on a path
that decides how a connection is grouped, whether an alert snapshot is
emitted, and how host thermal state crosses the read-state boundary, so a
silent regression there is invisible until it reaches a user.

Adds branch coverage for buildPMGConnection, connectionTypeForAlerts,
snapshotConnectionsForAlerts, uniqueMatch, connectedInfrastructureGroupKey,
availabilityConnectionKey, ConnectionHealthKey, pingArgs,
hostThermalStateFromReadStateView, latestMetricPoint and MemorySourceTrust,
including nil and whitespace arms, candidate precedence order, and clone
independence in both directions for the mapper. Every named target moves
from zero to full statement coverage apart from the pingArgs arms that are
unreachable on this platform.

Test-only change.

PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT=test-only branch coverage, no contract surface touched
2026-07-22 06:42:33 +01:00
rcourtman 7354d8d19f Keep integration-monitored machines out of Agent Doctor and surface workload-only agents
The connections ledger derives agent rows from the unified fabric, which
includes machines whose telemetry comes from platform integrations (vSphere
ESXi hosts, TrueNAS). Agent Doctor rendered every one as a permanent
'Unknown / no structured reason' row, while agents the ledger does not carry
(Docker-only, Kubernetes-only) were silently dropped from the fleet view.

- Expose HostView.IntegrationSource() (source-set based: only SourceAgent
  ingest counts, since integration providers fabricate an Agent payload) and
  plumb it through models.Host to the connections ledger as the optional
  integrationSource field.
- Agent Doctor skips integration-backed connections and appends
  diagnostics-only agents, honoring scope, so the doctor covers exactly the
  real Pulse Agent fleet.
- Update readiness agent checks no longer count integration-backed machines
  as registered agents.
- Humanize doctor copy: plain-language stale message with '10m 2s'-style
  durations, offline wording without enum leakage, no 'Supported target:
  Unknown' cell when no target is published, host-local command banner only
  when a command is actually offered, and a compact non-zero summary strip.

Contracts updated for unified-resources, monitoring, api-contracts,
agent-lifecycle, and dependent storage-recovery; verification via
views_test.go, monitor_host_agents_test.go, state_host_test.go,
contract_test.go, and the frontend connections API test.
2026-07-21 21:24:15 +01:00
rcourtman 0e6fa57c08 Normalize Proxmox-reported disk health to the canonical vocabulary
PVE's disks/list endpoint labels healthy SCSI/SAS drives OK while ATA
drives say PASSED, and failing ATA drives come back as FAILED! with the
bang. Pulse ingested those raw strings, so a healthy SAS drive rendered
as health Unknown even though the Proxmox UI showed S.M.A.R.T. OK. The
host agent and TrueNAS paths already normalize their health text; the
PVE ingestion was the only entry that did not.

Map OK/PASS to PASSED and FAIL-containing values to FAILED at ingestion,
and accept OK as healthy in the disk presentation layer as defense for
state produced by older servers.

Refs #1595

Contract-Neutral: behavioral fix: normalize PVE disk health strings at ingestion, no public contract delta (#1595)
2026-07-21 20:30:54 +01:00
rcourtman 59e6f25a65 Detect SAS transport and parse SCSI attributes in SMART collection
smartctl reports SAS drives with device protocol SCSI, so detectDiskType
fell through to its blanket sata default, and that non-empty type also
masked the text-output transport evidence the fallback parser had
already extracted. The wrong sata label then blocked the merge layer
from promoting the smartctl serial over the SAS transport address
Proxmox reports.

Classify SCSI-protocol devices via the scsi_transport_protocol
descriptor, let the text and sysfs refinements upgrade a generic scsi
label, and apply the legacy sata default only after all evidence is
exhausted. Parse the SCSI log-page fields (power-on hours, grown defect
count, endurance used) that SCSI drives report instead of an ATA
attribute table, and let agent-reported sas replace coarse hdd/ssd/sata
types during the disk merge.

Refs #1595

Contract-Neutral: behavioral fix: SAS transport detection and SCSI attribute parsing in host agent SMART collection; no public contract delta (#1595)
2026-07-21 17:36:14 +01:00
rcourtman 4f43878ebb Add focused branch coverage and repair infrastructure source contract
Contract-Neutral: test-only coverage and source-contract assertions; no runtime or contract behavior changed
2026-07-20 22:41:19 +01:00
rcourtman 057cf74629 Add alert intent policies and delivery receipts 2026-07-20 20:27:39 +01:00
rcourtman e23f19459d Add Agent Doctor fleet diagnostics workflow 2026-07-20 19:53:34 +01:00
rcourtman b6a74576bc Integrate trust-gate reliability fixes 2026-07-20 16:03:29 +01:00