Files
Juan DenisandClaude Fable 5 ffec0ca3ad fix(restore-points): json one-door model reads; modernize last-domain restore test
RestorePoint hand-rolled json.loads for payload/coverage — now reads
through JsonColumnMixin._json_read like every other model, keeping the
models-json ratchet green.

test_restoring_the_last_domain_re_enables_the_site mocked create_site
and counted enable_site calls, but since write_vhost became the one
enabling door (enable=True re-links sites-enabled and create_site goes
through it), write_app_vhost no longer calls enable_site separately —
test_site_domain_writer_does_not_enable_twice pins that. The restore
test now asserts through write_vhost's enable flag instead, still
failing if a restore path ever opts out of enabling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 03:13:06 -04:00

63 lines
2.2 KiB
Python

"""Immutable checkpoints for panel-managed configuration surfaces."""
from app import db
from app.models.json_column_mixin import JsonColumnMixin
from app.models.mixins import SerializableMixin, TimestampMixin, uuid_pk
class RestorePoint(JsonColumnMixin, TimestampMixin, SerializableMixin, db.Model):
"""One point on a scope's linear configuration timeline."""
__tablename__ = 'restore_points'
id = uuid_pk()
server_id = db.Column(
db.String(36), db.ForeignKey('servers.id'), nullable=True, index=True,
)
scope_type = db.Column(db.String(32), nullable=False)
scope_id = db.Column(db.String(255), nullable=False)
trigger = db.Column(db.String(32), nullable=False)
label = db.Column(db.String(255), nullable=True)
payload_hash = db.Column(db.String(64), nullable=False)
payload_json = db.Column(db.Text, nullable=False)
coverage_json = db.Column(db.Text, nullable=False, default='[]')
actor_user_id = db.Column(
db.Integer, db.ForeignKey('users.id'), nullable=True, index=True,
)
expires_at = db.Column(db.DateTime, nullable=True, index=True)
keep = db.Column(db.Boolean, nullable=False, default=False)
server = db.relationship('Server')
actor = db.relationship('User', foreign_keys=[actor_user_id])
__table_args__ = (
db.Index('ix_restore_points_scope', 'scope_type', 'scope_id'),
db.Index(
'ix_restore_points_scope_timeline',
'server_id', 'scope_type', 'scope_id', 'created_at',
),
)
__serialize_exclude__ = ('payload_json', 'coverage_json')
def get_payload(self):
return self._json_read('payload_json', expect=dict)
def get_coverage(self):
return self._json_read('coverage_json', default=[], expect=list)
def serialize_extra(self):
return {
'payload': self.get_payload(),
'coverage': self.get_coverage(),
}
@classmethod
def latest_for_scope(cls, scope_type, scope_id, server_id=None):
query = cls.query.filter_by(
scope_type=str(scope_type),
scope_id=str(scope_id),
server_id=server_id,
)
return query.order_by(cls.created_at.desc(), cls.id.desc()).first()