diff --git a/backend/alembic/versions/0005_interface_zone_nullable.py b/backend/alembic/versions/0005_interface_zone_nullable.py new file mode 100644 index 0000000..46299c8 --- /dev/null +++ b/backend/alembic/versions/0005_interface_zone_nullable.py @@ -0,0 +1,21 @@ +"""make interface zone_id nullable + +Revision ID: 0005 +Revises: 0004 +Create Date: 2026-03-01 +""" +from alembic import op +import sqlalchemy as sa + +revision = "0005" +down_revision = "0004" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.alter_column("interfaces", "zone_id", existing_type=sa.Integer(), nullable=True) + + +def downgrade() -> None: + op.alter_column("interfaces", "zone_id", existing_type=sa.Integer(), nullable=False) diff --git a/backend/app/models.py b/backend/app/models.py index ff3a3b9..2e344e6 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -59,11 +59,11 @@ class Interface(Base): id: Mapped[int] = mapped_column(Integer, primary_key=True) config_id: Mapped[int] = mapped_column(Integer, ForeignKey("configs.id"), nullable=False) name: Mapped[str] = mapped_column(String(32), nullable=False) - zone_id: Mapped[int] = mapped_column(Integer, ForeignKey("zones.id"), nullable=False) + zone_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("zones.id"), nullable=True) options: Mapped[str] = mapped_column(Text, default="") config: Mapped["Config"] = relationship("Config", back_populates="interfaces") - zone: Mapped["Zone"] = relationship("Zone", back_populates="interfaces") + zone: Mapped["Zone | None"] = relationship("Zone", back_populates="interfaces") class Policy(Base): diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 2de38b6..fa514c4 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -64,7 +64,7 @@ class ZoneOut(BaseModel): # --- Interface --- class InterfaceCreate(BaseModel): name: str - zone_id: int + zone_id: Optional[int] = None options: str = "" @@ -78,7 +78,7 @@ class InterfaceOut(BaseModel): id: int config_id: int name: str - zone_id: int + zone_id: Optional[int] options: str model_config = {"from_attributes": True} diff --git a/backend/app/shorewall_generator.py b/backend/app/shorewall_generator.py index b51545f..2ec9df8 100644 --- a/backend/app/shorewall_generator.py +++ b/backend/app/shorewall_generator.py @@ -28,7 +28,8 @@ class ShorewallGenerator: def interfaces(self) -> str: lines = [self._header("interfaces"), "#ZONE".ljust(16) + "INTERFACE".ljust(16) + "OPTIONS\n"] for iface in self._config.interfaces: - lines.append(self._col(iface.zone.name, iface.name, iface.options or "-")) + zone = iface.zone.name if iface.zone else "-" + lines.append(self._col(zone, iface.name, iface.options or "-")) return "".join(lines) def policy(self) -> str: diff --git a/frontend/src/routes/ConfigDetail.tsx b/frontend/src/routes/ConfigDetail.tsx index 8337dc1..2275ef5 100644 --- a/frontend/src/routes/ConfigDetail.tsx +++ b/frontend/src/routes/ConfigDetail.tsx @@ -83,13 +83,13 @@ export default function ConfigDetail() { { key: 'zone_id' as const, label: 'Zone', - render: (r: AnyEntity) => zones.find((z) => z.id === r['zone_id'])?.name ?? String(r['zone_id']), + render: (r: AnyEntity) => r['zone_id'] == null ? '-' : (zones.find((z) => z.id === r['zone_id'])?.name ?? String(r['zone_id'])), }, { key: 'options' as const, label: 'Options' }, ] as Column[], fields: [ { name: 'name', label: 'Interface Name', required: true }, - { name: 'zone_id', label: 'Zone', required: true, type: 'select' as const, options: zoneOptions }, + { name: 'zone_id', label: 'Zone', type: 'select' as const, options: [{ value: '', label: '- (no zone)' }, ...zoneOptions] }, { name: 'options', label: 'Options' }, ] as FieldDef[], },