"""What a backup carries. Every table Steward defines is named here as kept or left out, and tests/core/test_backup_contents.py fails on one that is neither, so a new table is a decision someone makes in this file. The operator's ruling (System 180 "Backup & Restore", 2026-10-15): a backup is for system recovery. It carries settings and keys, what would otherwise have to be set up again, and no logs or history: not the audit log, not alert history, not metrics. Left-out tables come back with their structure and no rows, and fill again as Steward runs. A table that is in neither list (an external plugin's) is backed up whole: a table nobody thought about is kept, never lost. """ from __future__ import annotations # What would otherwise have to be set up again. KEPT: frozenset[str] = frozenset({ "app_settings", "users", "api_tokens", "hosts", "host_agent_registrations", # A group's maintenance cadence (#5768): configuration. "host_maintenance_plans", "monitors", "alert_rules", "maintenance_windows", "ansible_targets", "ansible_groups", "ansible_target_groups", "ansible_schedules", "dashboards", "dashboard_widgets", "dashboard_share_tokens", "snmp_devices", "prometheus_targets", "prometheus_widget_templates", # Names given to network clients by hand (UniFi's alias column). "unifi_known_clients", # Syslog senders: which were added, under what name, and which ignored. "syslog_sources", "syslog_patterns", }) # Rows that are measurements, logs, history or in-flight work: left out. EXCLUDED: frozenset[str] = frozenset({ # Logs and history (the 2026-10-15 ruling): a recovery doesn't need them. "audit_events", "alert_events", # Live state: each rule starts inactive, and alert_reconcile gives a # restored rule its state row within a minute (ensure_rule_states). "alert_states", # Metric anomalies and their reviews (#5740): history of the metrics. "metric_anomalies", "metric_anomaly_spans", # A pending invite is in flight, not setup; it can be sent again. "user_invites", "plugin_metrics", "plugin_metric_latest", "plugin_metrics_hourly", # Learned from the metrics above, so relearned once they fill again. "metric_norms", "monitor_results", # A host's downtime as its agent reported it: history, like the metrics. "host_downtime", # Maintenance jobs (#5767): run history, like ansible_runs. "host_maintenance_jobs", "host_maintenance_runs", "email_outbox", "email_outbox_events", "password_resets", # Run history; its logs live on disk, not in the database. "ansible_runs", "docker_containers", "docker_disk_usage", "docker_events", "docker_host_status", "docker_images", "docker_logs", "docker_swarm_nodes", "docker_swarm_services", "traefik_access_summaries", "traefik_certs", "traefik_global_stats", "traefik_metrics", "traefik_status_codes", "unifi_alarms", "unifi_client_snapshots", "unifi_devices", "unifi_dpi_snapshots", "unifi_events", "unifi_fw_rules", "unifi_networks", "unifi_port_forwards", "unifi_speedtest_results", "unifi_wan_stats", "unifi_wlan_stats", "syslog_lines", }) # ── What a check compares (#5823) ──────────────────────────────────────────── # Checking a backup compares its kept rows with the ones in place. Some columns # of kept tables are Steward's own record of what it last saw, not something # anyone set up: they change by the minute, so comparing them would call every # backup out of date. They are left out of the comparison (a restore still # carries them). `updated_at` is bookkeeping wherever it appears. ACTIVITY: dict[str, frozenset[str]] = { "api_tokens": frozenset({"last_used_at"}), # What the agent reports about its host; the enrolment (host_id, # token_hash, token_created_at) is what is compared. "host_agent_registrations": frozenset({ "agent_version", "kernel", "distro", "arch", "host_ip", "last_seen_at", "clock_offset_s", "clock_checked_at", "updates_manager", "updates_pending", "updates_security", "updates_checked_at", "updates_error", "reboot_required", "reboot_required_since", "reboot_packages", "maint_helper", "drive_health", "drive_health_at"}), "host_maintenance_plans": frozenset({"last_slot_at"}), "monitors": frozenset({"last_checked_at"}), "ansible_schedules": frozenset({"last_run_at", "last_run_id", "last_error"}), "snmp_devices": frozenset({"last_polled_at", "last_ok_at", "last_error", "missing"}), "prometheus_targets": frozenset({"last_scraped_at", "last_ok_at", "last_error", "last_kept", "last_dropped", "last_capped"}), # What the sender last sent; its address, name, state and host are setup. "syslog_sources": frozenset({"hostname_seen", "first_seen_at", "last_seen_at", "lines_total", "last_severity", "last_line"}), # Scraped from the controller; the name and note are what someone set. "unifi_known_clients": frozenset({"hostname", "ip", "mac_vendor", "is_blocked", "first_seen", "last_seen", "scraped_at"}), } ACTIVITY_EVERYWHERE: frozenset[str] = frozenset({"updated_at"}) # Settings Steward writes as it runs: a record of its own work, not setup. STATE_SETTINGS: frozenset[str] = frozenset({ "backup.history", "backup.last_attempt_at", "backup.last_success_at", "reports.last_sent_at", "cleanup.last_run", }) # A network client only counts once someone has named it or noted it; the # rest are the controller's, and come and go as devices do. HAND_SET_ONLY: dict[str, tuple[str, ...]] = {"unifi_known_clients": ("name", "note")} # How a check groups what it found, in the order it shows them. A table # not named here is shown under "Other". AREAS: tuple[tuple[str, tuple[str, ...]], ...] = ( ("Settings", ("app_settings",)), ("Users and API tokens", ("users", "api_tokens")), ("Hosts and agents", ("hosts", "host_agent_registrations", "host_maintenance_plans")), ("Monitors", ("monitors",)), ("Alert rules and maintenance windows", ("alert_rules", "maintenance_windows")), ("Ansible", ("ansible_targets", "ansible_groups", "ansible_target_groups", "ansible_schedules")), ("Dashboards", ("dashboards", "dashboard_widgets", "dashboard_share_tokens")), ("Plugins", ("snmp_devices", "prometheus_targets", "prometheus_widget_templates", "unifi_known_clients", "syslog_sources", "syslog_patterns")), )