diff --git a/README.md b/README.md index a0637ff..e63ec67 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,7 @@ Staff-only dashboard views expose the same operational state through Django temp Host pages include a safe snapshot discovery action that records existing snapshots into SQL. Host pages also include a read-only SQL retention plan view before any destructive pruning action. Schedules can be created or updated from host pages using the same SQL-backed scheduler model. +Host config can be edited from host pages while keeping host identity stable. The remaining internal engine code still contains reusable backup primitives: diff --git a/src/pobsync_backend/forms.py b/src/pobsync_backend/forms.py index 20ca033..f2f33ad 100644 --- a/src/pobsync_backend/forms.py +++ b/src/pobsync_backend/forms.py @@ -2,10 +2,68 @@ from __future__ import annotations from django import forms -from .models import ScheduleConfig +from .models import HostConfig, ScheduleConfig from .scheduler import parse_cron_expr +class NewlineListField(forms.CharField): + widget = forms.Textarea + + def __init__(self, *args, **kwargs) -> None: + kwargs.setdefault("required", False) + super().__init__(*args, **kwargs) + + def prepare_value(self, value): + if isinstance(value, list): + return "\n".join(str(item) for item in value) + return value + + def to_python(self, value) -> list[str]: + if not value: + return [] + if isinstance(value, list): + return [str(item).strip() for item in value if str(item).strip()] + return [line.strip() for line in str(value).splitlines() if line.strip()] + + +class NullableNewlineListField(NewlineListField): + def to_python(self, value) -> list[str] | None: + parsed = super().to_python(value) + return parsed or None + + +class HostConfigForm(forms.ModelForm): + includes = NewlineListField(help_text="One include path per line. Leave empty to include defaults.") + excludes_add = NewlineListField(help_text="One additional exclude pattern per line.") + excludes_replace = NullableNewlineListField( + help_text="Optional. When set, replaces global excludes; one pattern per line." + ) + rsync_extra_args = NewlineListField(help_text="One extra rsync argument per line.") + + class Meta: + model = HostConfig + fields = ( + "address", + "enabled", + "ssh_user", + "ssh_port", + "source_root", + "includes", + "excludes_add", + "excludes_replace", + "rsync_extra_args", + "retention_daily", + "retention_weekly", + "retention_monthly", + "retention_yearly", + ) + help_texts = { + "ssh_user": "Leave empty to use the global SSH user.", + "ssh_port": "Leave empty to use the global SSH port.", + "source_root": "Leave empty to use the global default source root.", + } + + class ScheduleConfigForm(forms.ModelForm): cron_expr = forms.CharField( label="Cron expression", diff --git a/src/pobsync_backend/templates/pobsync_backend/base.html b/src/pobsync_backend/templates/pobsync_backend/base.html index 5667901..ad73b00 100644 --- a/src/pobsync_backend/templates/pobsync_backend/base.html +++ b/src/pobsync_backend/templates/pobsync_backend/base.html @@ -104,13 +104,14 @@ .form-grid { display: grid; gap: 14px; max-width: 680px; } .field { display: grid; gap: 5px; } .field label { font-weight: 650; } - .field input[type="text"], .field input[type="number"] { + .field input[type="text"], .field input[type="number"], .field textarea { border: 1px solid var(--border); border-radius: 6px; font: inherit; padding: 8px 10px; width: 100%; } + .field textarea { min-height: 92px; resize: vertical; } .field .helptext { color: var(--muted); font-size: 12px; } .errorlist { color: var(--failed); diff --git a/src/pobsync_backend/templates/pobsync_backend/host_detail.html b/src/pobsync_backend/templates/pobsync_backend/host_detail.html index c929c7e..14a9c1c 100644 --- a/src/pobsync_backend/templates/pobsync_backend/host_detail.html +++ b/src/pobsync_backend/templates/pobsync_backend/host_detail.html @@ -6,6 +6,7 @@