Ports published on 0.0.0.0 were linked at window.location.hostname. Opened
through a reverse proxy such as Nginx Proxy Manager that is the proxy's
domain, and domain:8080 ended up at the proxy instead of the container.
The backend now reads the host's address from the host's routing table
(${HOST_PROC_PATH}/1/net/route + fib_trie, the /proc mount the compose file
already has): the IPv4 address on the interface with the best default
route. There is no fallback to /proc/net, which in a bridged container is
the container's own namespace with an unreachable 172.x address.
The links use, in order: the new "Host address for port links" setting
(Settings -> General), the page's hostname when it is an IP or localhost,
the detected address, and the page's hostname as a last resort. Ports
bound to one specific address still link there; IPv6 hosts are bracketed.
The setting is validated before anything is written, so a bad value no
longer leaves the interval half-saved in the same request.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
95 lines
2.3 KiB
Python
95 lines
2.3 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
from typing import Optional
|
|
|
|
from sqlmodel import Field, SQLModel
|
|
|
|
|
|
def _now() -> datetime:
|
|
return datetime.now(timezone.utc)
|
|
|
|
|
|
# Notification event names ----------------------------------------------------
|
|
EVENT_UPDATE_AVAILABLE = "update_available"
|
|
EVENT_STACK_START = "stack_start"
|
|
EVENT_STACK_STOP = "stack_stop"
|
|
EVENT_STACK_ERROR = "stack_error"
|
|
EVENT_PULL_FAILED = "pull_failed"
|
|
EVENT_BACKUP_FAILED = "backup_failed"
|
|
EVENT_STACK_AUTO_UPDATED = "stack_auto_updated"
|
|
|
|
ALL_EVENTS = [
|
|
EVENT_UPDATE_AVAILABLE,
|
|
EVENT_STACK_START,
|
|
EVENT_STACK_STOP,
|
|
EVENT_STACK_ERROR,
|
|
EVENT_PULL_FAILED,
|
|
EVENT_BACKUP_FAILED,
|
|
EVENT_STACK_AUTO_UPDATED,
|
|
]
|
|
|
|
WEBHOOK_TYPES = ["ntfy", "discord", "slack", "gotify", "generic"]
|
|
|
|
|
|
class Setting(SQLModel, table=True):
|
|
"""Simple key/value store for runtime-tunable settings (JSON-encoded value)."""
|
|
|
|
key: str = Field(primary_key=True)
|
|
value: str # JSON-encoded
|
|
|
|
|
|
class Webhook(SQLModel, table=True):
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
name: str
|
|
url: str
|
|
type: str = "generic" # one of WEBHOOK_TYPES
|
|
events: str = ",".join(ALL_EVENTS) # comma-separated subscribed events
|
|
enabled: bool = True
|
|
created_at: datetime = Field(default_factory=_now)
|
|
|
|
|
|
# --- API schemas ---
|
|
|
|
|
|
class WebhookCreate(SQLModel):
|
|
name: str
|
|
url: str
|
|
type: str = "generic"
|
|
events: list[str] = ALL_EVENTS
|
|
enabled: bool = True
|
|
|
|
|
|
class WebhookUpdate(SQLModel):
|
|
name: Optional[str] = None
|
|
url: Optional[str] = None
|
|
type: Optional[str] = None
|
|
events: Optional[list[str]] = None
|
|
enabled: Optional[bool] = None
|
|
|
|
|
|
class WebhookRead(SQLModel):
|
|
id: int
|
|
name: str
|
|
url: str
|
|
type: str
|
|
events: list[str]
|
|
enabled: bool
|
|
created_at: datetime
|
|
|
|
|
|
class SettingsRead(SQLModel):
|
|
update_check_interval_minutes: int
|
|
# Where links to published ports point. "" = not set, use detected_host_ip.
|
|
port_link_host: str
|
|
detected_host_ip: Optional[str]
|
|
env_webhook_count: int
|
|
available_events: list[str]
|
|
webhook_types: list[str]
|
|
|
|
|
|
class SettingsUpdate(SQLModel):
|
|
update_check_interval_minutes: Optional[int] = None
|
|
# "" clears it back to the detected address.
|
|
port_link_host: Optional[str] = None
|