mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-09 05:24:49 +09:00
Every NOT NULL FK from the KNOWN_DANGLING_ON_DELETE audit now declares its parent-side relationship, so the delete-cascade policy door covers it: AI conversations/pending actions, API keys, dashboards, event subscriptions, login links, sent invitations, resource grants (users); env vars + history, previews + settings, WAF policy (applications); surveys, doctor results, proxy stack, tunnels (servers); manifests (projects); DDNS hosts (zones); exposed services (tunnels); drills (backup policies); rollouts (agent versions); site plugin installs (WordPress sites); queue messages (groups). The ratchet set is now EMPTY and stays that way. One new deliberate exception: Workspace.projects — projects carry environments and app groupings, so delete_workspace now refuses (409) while any remain instead of silently destroying them (previously it crashed with IntegrityError). Migration 095 sweeps rows already orphaned on existing installs by pre-fix hard-deletes — including an application's leftover env-var secrets, which a reused SQLite row id could have attached to a future app. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
57 lines
2.3 KiB
Python
57 lines
2.3 KiB
Python
import json
|
|
from datetime import datetime
|
|
|
|
from app import db
|
|
from app.models.json_column_mixin import JsonColumnMixin
|
|
|
|
|
|
class ServerSurvey(JsonColumnMixin, db.Model):
|
|
"""One read-only survey snapshot ("flight") of a paired server (plan 27,
|
|
Decision 3: snapshots, not live state).
|
|
|
|
The agent flies the probe catalog and returns a structured Server Map; the
|
|
panel normalizes it and stores it here as a versioned, immutable snapshot.
|
|
Re-flying is manual (or an optional weekly schedule) and appends a new row,
|
|
so any two flights of the same server can be diffed ("what changed since
|
|
last flight").
|
|
|
|
``catalog_version`` records which probe-catalog version produced the map, so
|
|
a diff can flag that the catalog itself changed between two flights.
|
|
``map_json`` holds the normalized Server Map (services / sites / databases /
|
|
certs / cron / listeners / foreign_panels).
|
|
"""
|
|
|
|
__tablename__ = 'server_surveys'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
server_id = db.Column(db.String(36), db.ForeignKey('servers.id'), nullable=False, index=True)
|
|
# Parent-side backref so the delete-cascade policy covers the NOT NULL FK.
|
|
server = db.relationship('Server',
|
|
backref=db.backref('surveys', lazy='dynamic'))
|
|
catalog_version = db.Column(db.Integer, nullable=False, default=1)
|
|
taken_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, index=True)
|
|
|
|
# Normalized Server Map, stored as a JSON string for portability across the
|
|
# SQLite/Postgres split (mirrors how other snapshot blobs are persisted).
|
|
map_json = db.Column(db.Text, nullable=True)
|
|
|
|
def get_map(self):
|
|
return self._json_read('map_json')
|
|
|
|
def set_map(self, value):
|
|
self.map_json = json.dumps(value) if value else None
|
|
|
|
def to_dict(self, include_map=True):
|
|
data = {
|
|
'id': self.id,
|
|
'server_id': self.server_id,
|
|
'catalog_version': self.catalog_version,
|
|
'taken_at': self.taken_at.isoformat() + 'Z' if self.taken_at else None,
|
|
}
|
|
if include_map:
|
|
data['map'] = self.get_map()
|
|
return data
|
|
|
|
def __repr__(self):
|
|
return f'<ServerSurvey server={self.server_id} v{self.catalog_version} at={self.taken_at}>'
|