v0.7.00-beta - apps koppelen, eenvoudiger instellen, geavanceerde opties verborgen
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m16s
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m16s
Apps onderling verbinden:
- Elke stack draaide als eigen compose-project met een eigen netwerk, waardoor
stacks elkaar niet op naam konden bereiken. Bij installeren staat nu
"Verbinden met andere apps" aan: de stack komt op een gedeeld bridge-netwerk
(SHARED_NETWORK, standaard "serverup") dat op aanvraag wordt aangemaakt.
- compose_transform.add_shared_network() hangt elke service eraan en laat
bestaande netwerken staan, dus het werkt samen met een macvlan-adres.
- Nieuw /api/apps/connectable geeft de geinstalleerde apps met hun
containernamen, zodat de installatiemodal kan laten kiezen.
Kiezen in plaats van typen:
- Een veld met "connect": {"app": "mosquitto", "scheme": "mqtt", "port": 1883}
toont een keuzelijst met wat er al draait, met handmatig invullen als
terugval. Toegepast op zigbee2mqtt -> mosquitto en open-webui -> ollama.
- Is de doel-app nog niet geinstalleerd, dan zegt het veld dat.
Uitleg per instelling:
- Info-icoon per veld opent een blok met de volledige uitleg plus de
standaardwaarde. Uitgeschreven voor de instellingen die de meeste vragen
oproepen (downloadmap vs mediamap, shm_size bij frigate, poort 80 bij caddy,
PUID bij de arr-stack, GPU bij ollama, publiek adres bij wg-easy).
Geavanceerde opties:
- advanced: true op een veld of groep zet het achter een uitklapblok met
teller. Toegepast op wachtwoorden, PUID/PGID, USB-paden en bewaartermijnen
van 28 apps.
- Test bewaakt dat elke app zichtbare velden overhoudt en dat koppelvelden naar
een bestaande app verwijzen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7oLCRYzY5ixJ5Sv8Y8EFb
This commit is contained in:
parent
146d7ae79d
commit
4450b3c171
43 changed files with 663 additions and 74 deletions
43
CHANGELOG.md
43
CHANGELOG.md
|
|
@ -1,3 +1,46 @@
|
|||
# v0.7.00-beta — Apps koppelen, eenvoudiger instellen, geavanceerde opties verborgen
|
||||
|
||||
## 🔗 Apps kunnen elkaar nu bereiken
|
||||
|
||||
Elke stack draaide als eigen compose-project en kreeg daarmee zijn eigen
|
||||
netwerk. Twee stacks konden elkaar dus niet op naam vinden: Zigbee2MQTT zag de
|
||||
Mosquitto-broker uit een andere stack simpelweg niet.
|
||||
|
||||
Bij het installeren staat nu **"Verbinden met andere apps"** aan. De stack komt
|
||||
dan op een gedeeld bridge-netwerk (`serverup`), waar containers elkaar op naam
|
||||
oplossen. Het netwerk wordt aangemaakt zodra de eerste stack het nodig heeft.
|
||||
|
||||
## 🎯 Kiezen in plaats van typen
|
||||
|
||||
Een veld dat naar een andere app verwijst, toont voortaan een **keuzelijst met
|
||||
wat je al hebt draaien** in plaats van een leeg tekstveld. Installeer je
|
||||
Zigbee2MQTT en heb je Mosquitto al staan, dan staat `mqtt://mosquitto:1883` er
|
||||
meteen ingevuld. Hetzelfde voor Open WebUI dat je Ollama vindt.
|
||||
|
||||
Staat de app er nog niet, dan zegt het veld dat — met de mogelijkheid om alsnog
|
||||
handmatig een adres in te vullen.
|
||||
|
||||
## 💬 Uitleg per instelling
|
||||
|
||||
Achter elk veld met toelichting staat een **info-icoon**. Klikken opent een blok
|
||||
met de volledige uitleg en de standaardwaarde. Voor de instellingen die de
|
||||
meeste vragen oproepen is die uitleg uitgeschreven — waarom je downloadmap op
|
||||
hetzelfde bestandssysteem moet staan als je mediamap, wat `shm_size` bij Frigate
|
||||
doet, waarom Caddy poort 80 nodig heeft ook als je alles via HTTPS aanbiedt.
|
||||
|
||||
## 🎛️ Geavanceerde opties uit het zicht
|
||||
|
||||
Velden die je meestal met rust laat — database-wachtwoorden, PUID/PGID,
|
||||
USB-paden, bewaartermijnen — zitten nu achter een uitklapblok **"Geavanceerde
|
||||
opties"** met een teller erbij. Het installatieformulier begint daardoor met
|
||||
alleen de vragen die er echt toe doen.
|
||||
|
||||
Templates kunnen `advanced: true` op een veld of op een hele groep zetten. Een
|
||||
test bewaakt dat er per app altijd zichtbare velden overblijven, zodat niemand
|
||||
per ongeluk een volledig leeg formulier maakt.
|
||||
|
||||
---
|
||||
|
||||
# v0.6.20-beta — 31 apps erbij na een rondgang langs de verzamelsites
|
||||
|
||||
Van 65 naar 96 apps. Selectie op basis van awesome-selfhosted, selfh.st,
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.6.20-beta
|
||||
0.7.00-beta
|
||||
|
|
|
|||
|
|
@ -51,7 +51,8 @@
|
|||
"title": "Downloadmap",
|
||||
"default": "/mnt/downloads",
|
||||
"required": true,
|
||||
"description": "Zet deze op hetzelfde bestandssysteem als de mediamap, anders kopieert elke download in plaats van te verplaatsen"
|
||||
"description": "Zet deze op hetzelfde bestandssysteem als de mediamap, anders kopieert elke download in plaats van te verplaatsen",
|
||||
"help": "Zet deze map op hetzelfde bestandssysteem als je mediamap. Staan ze op verschillende schijven of volumes, dan kopieert elke voltooide download in plaats van te verplaatsen: traag, en je hebt alles tijdelijk dubbel."
|
||||
},
|
||||
{
|
||||
"name": "timezone",
|
||||
|
|
@ -66,7 +67,8 @@
|
|||
"title": "Gebruikers-ID (PUID)",
|
||||
"default": 1000,
|
||||
"required": true,
|
||||
"description": "`id -u` op je host; bepaalt de eigenaar van bestanden"
|
||||
"description": "`id -u` op je host; bepaalt de eigenaar van bestanden",
|
||||
"help": "Het gebruikersnummer waaronder de containers draaien. Klopt dit niet met de eigenaar van je mediamap, dan kunnen de apps er niet in schrijven. Vind het met `id -u` op je server."
|
||||
},
|
||||
{
|
||||
"name": "pgid",
|
||||
|
|
|
|||
|
|
@ -57,7 +57,8 @@
|
|||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||
"config": {
|
||||
"placeholder": "sterk-wachtwoord"
|
||||
}
|
||||
},
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "session_secret",
|
||||
|
|
@ -68,7 +69,8 @@
|
|||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||
"config": {
|
||||
"placeholder": "sterk-wachtwoord"
|
||||
}
|
||||
},
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "storage_key",
|
||||
|
|
@ -79,7 +81,8 @@
|
|||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||
"config": {
|
||||
"placeholder": "sterk-wachtwoord"
|
||||
}
|
||||
},
|
||||
"advanced": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,14 +54,16 @@
|
|||
"title": "Gebruikers-ID (PUID)",
|
||||
"default": 1000,
|
||||
"required": true,
|
||||
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host"
|
||||
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host",
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "pgid",
|
||||
"type": "int",
|
||||
"title": "Groeps-ID (PGID)",
|
||||
"default": 1000,
|
||||
"required": true
|
||||
"required": true,
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "secret_key",
|
||||
|
|
@ -80,7 +82,8 @@
|
|||
"title": "Toegestane hostnamen",
|
||||
"default": "*",
|
||||
"required": true,
|
||||
"description": "Bijvoorbeeld babybuddy.thuis.nl; * staat alles toe"
|
||||
"description": "Bijvoorbeeld babybuddy.thuis.nl; * staat alles toe",
|
||||
"advanced": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,14 +54,16 @@
|
|||
"title": "Gebruikers-ID (PUID)",
|
||||
"default": 1000,
|
||||
"required": true,
|
||||
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host"
|
||||
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host",
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "pgid",
|
||||
"type": "int",
|
||||
"title": "Groeps-ID (PGID)",
|
||||
"default": 1000,
|
||||
"required": true
|
||||
"required": true,
|
||||
"advanced": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,14 +54,16 @@
|
|||
"title": "Gebruikers-ID (PUID)",
|
||||
"default": 1000,
|
||||
"required": true,
|
||||
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host"
|
||||
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host",
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "pgid",
|
||||
"type": "int",
|
||||
"title": "Groeps-ID (PGID)",
|
||||
"default": 1000,
|
||||
"required": true
|
||||
"required": true,
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "db_password",
|
||||
|
|
@ -72,7 +74,8 @@
|
|||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||
"config": {
|
||||
"placeholder": "sterk-wachtwoord"
|
||||
}
|
||||
},
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "app_url",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@
|
|||
"type": "int",
|
||||
"title": "HTTP poort",
|
||||
"default": 80,
|
||||
"required": true
|
||||
"required": true,
|
||||
"help": "Caddy vraagt zelf certificaten aan bij Let's Encrypt en heeft daarvoor poort 80 nodig, ook als je alles via HTTPS aanbiedt."
|
||||
},
|
||||
{
|
||||
"name": "data_dir",
|
||||
|
|
|
|||
|
|
@ -54,14 +54,16 @@
|
|||
"title": "Gebruikers-ID (PUID)",
|
||||
"default": 1000,
|
||||
"required": true,
|
||||
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host"
|
||||
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host",
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "pgid",
|
||||
"type": "int",
|
||||
"title": "Groeps-ID (PGID)",
|
||||
"default": 1000,
|
||||
"required": true
|
||||
"required": true,
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "books_dir",
|
||||
|
|
|
|||
|
|
@ -54,14 +54,16 @@
|
|||
"title": "Gebruikers-ID (PUID)",
|
||||
"default": 1000,
|
||||
"required": true,
|
||||
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host"
|
||||
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host",
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "pgid",
|
||||
"type": "int",
|
||||
"title": "Groeps-ID (PGID)",
|
||||
"default": 1000,
|
||||
"required": true
|
||||
"required": true,
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "source_dir",
|
||||
|
|
|
|||
|
|
@ -57,7 +57,8 @@
|
|||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||
"config": {
|
||||
"placeholder": "sterk-wachtwoord"
|
||||
}
|
||||
},
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "app_key",
|
||||
|
|
@ -68,7 +69,8 @@
|
|||
"description": "Exact 32 tekens; gebruik de dobbelsteenknop en kort zo nodig in",
|
||||
"config": {
|
||||
"placeholder": "32-tekens-lange-sleutel"
|
||||
}
|
||||
},
|
||||
"advanced": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,8 @@
|
|||
"type": "int",
|
||||
"title": "RTSP poort",
|
||||
"default": 8554,
|
||||
"required": true
|
||||
"required": true,
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "shm_size",
|
||||
|
|
@ -61,7 +62,9 @@
|
|||
"title": "Gedeeld geheugen",
|
||||
"default": "128mb",
|
||||
"required": true,
|
||||
"description": "Verhoog dit bij meer dan twee camera's (ongeveer 64mb per camera)"
|
||||
"description": "Verhoog dit bij meer dan twee camera's (ongeveer 64mb per camera)",
|
||||
"advanced": true,
|
||||
"help": "Frigate houdt camerabeelden in gedeeld geheugen. Reken op ongeveer 64 MB per camera bovenop de standaard; te weinig leidt tot vastlopers zonder duidelijke foutmelding."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,14 +54,16 @@
|
|||
"title": "Gebruikers-ID (PUID)",
|
||||
"default": 1000,
|
||||
"required": true,
|
||||
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host"
|
||||
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host",
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "pgid",
|
||||
"type": "int",
|
||||
"title": "Groeps-ID (PGID)",
|
||||
"default": 1000,
|
||||
"required": true
|
||||
"required": true,
|
||||
"advanced": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,8 @@
|
|||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||
"config": {
|
||||
"placeholder": "sterk-wachtwoord"
|
||||
}
|
||||
},
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "app_url",
|
||||
|
|
|
|||
|
|
@ -3,9 +3,18 @@
|
|||
"metadata": {
|
||||
"name": "Immich",
|
||||
"description": "Zelfgehoste foto- en videobeheer met automatische back-up. Google Photos alternatief.",
|
||||
"tags": ["fotos", "media", "backup"],
|
||||
"icon": {"provider": "selfhst", "id": "immich"},
|
||||
"version": {"name": "release"}
|
||||
"tags": [
|
||||
"fotos",
|
||||
"media",
|
||||
"backup"
|
||||
],
|
||||
"icon": {
|
||||
"provider": "selfhst",
|
||||
"id": "immich"
|
||||
},
|
||||
"version": {
|
||||
"name": "release"
|
||||
}
|
||||
},
|
||||
"variables": [
|
||||
{
|
||||
|
|
@ -31,7 +40,8 @@
|
|||
"title": "Upload directory",
|
||||
"default": "/opt/serverup/appdata",
|
||||
"required": true,
|
||||
"description": "Basismap voor foto-uploads en database"
|
||||
"description": "Basismap voor foto-uploads en database",
|
||||
"help": "Hier komen je originele foto's en video's te staan. Reken op net zoveel ruimte als je telefoon en camera bij elkaar produceren, plus wat marge voor de miniaturen."
|
||||
},
|
||||
{
|
||||
"name": "timezone",
|
||||
|
|
@ -52,7 +62,10 @@
|
|||
"default": "immich_db_pass",
|
||||
"required": true,
|
||||
"description": "Wachtwoord voor de interne PostgreSQL database",
|
||||
"config": {"placeholder": "sterk-wachtwoord"}
|
||||
"config": {
|
||||
"placeholder": "sterk-wachtwoord"
|
||||
},
|
||||
"advanced": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,14 +53,16 @@
|
|||
"type": "str",
|
||||
"title": "Organisatie",
|
||||
"default": "thuis",
|
||||
"required": true
|
||||
"required": true,
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "bucket",
|
||||
"type": "str",
|
||||
"title": "Bucket",
|
||||
"default": "metingen",
|
||||
"required": true
|
||||
"required": true,
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "admin_user",
|
||||
|
|
|
|||
|
|
@ -57,7 +57,8 @@
|
|||
"description": "Willekeurige waarde voor sessies",
|
||||
"config": {
|
||||
"placeholder": "sterk-wachtwoord"
|
||||
}
|
||||
},
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "openai_key",
|
||||
|
|
@ -65,7 +66,8 @@
|
|||
"title": "OpenAI-sleutel (optioneel)",
|
||||
"default": "",
|
||||
"required": false,
|
||||
"description": "Alleen nodig als je automatisch labels wilt laten toekennen"
|
||||
"description": "Alleen nodig als je automatisch labels wilt laten toekennen",
|
||||
"advanced": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,14 +54,16 @@
|
|||
"title": "Gebruikers-ID (PUID)",
|
||||
"default": 1000,
|
||||
"required": true,
|
||||
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host"
|
||||
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host",
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "pgid",
|
||||
"type": "int",
|
||||
"title": "Groeps-ID (PGID)",
|
||||
"default": 1000,
|
||||
"required": true
|
||||
"required": true,
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "library_dir",
|
||||
|
|
|
|||
|
|
@ -75,7 +75,8 @@
|
|||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||
"config": {
|
||||
"placeholder": "sterk-wachtwoord"
|
||||
}
|
||||
},
|
||||
"advanced": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,8 @@
|
|||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||
"config": {
|
||||
"placeholder": "sterk-wachtwoord"
|
||||
}
|
||||
},
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "admin_email",
|
||||
|
|
|
|||
|
|
@ -54,14 +54,16 @@
|
|||
"title": "Gebruikers-ID (PUID)",
|
||||
"default": 1000,
|
||||
"required": true,
|
||||
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host"
|
||||
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host",
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "pgid",
|
||||
"type": "int",
|
||||
"title": "Groeps-ID (PGID)",
|
||||
"default": 1000,
|
||||
"required": true
|
||||
"required": true,
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "library_dir",
|
||||
|
|
|
|||
|
|
@ -46,7 +46,8 @@
|
|||
"type": "bool",
|
||||
"title": "N-gram data gebruiken",
|
||||
"default": false,
|
||||
"description": "Betere suggesties, maar vraagt enkele gigabytes aan schijfruimte"
|
||||
"description": "Betere suggesties, maar vraagt enkele gigabytes aan schijfruimte",
|
||||
"advanced": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,14 +54,16 @@
|
|||
"title": "Gebruikers-ID (PUID)",
|
||||
"default": 1000,
|
||||
"required": true,
|
||||
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host"
|
||||
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host",
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "pgid",
|
||||
"type": "int",
|
||||
"title": "Groeps-ID (PGID)",
|
||||
"default": 1000,
|
||||
"required": true
|
||||
"required": true,
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "downloads_dir",
|
||||
|
|
|
|||
|
|
@ -53,7 +53,9 @@
|
|||
"type": "bool",
|
||||
"title": "NVIDIA-GPU gebruiken",
|
||||
"default": false,
|
||||
"description": "Vereist de NVIDIA Container Toolkit op de host"
|
||||
"description": "Vereist de NVIDIA Container Toolkit op de host",
|
||||
"advanced": true,
|
||||
"help": "Met een NVIDIA-kaart draaien modellen tientallen keren sneller. Vereist wel dat de NVIDIA Container Toolkit op de host geïnstalleerd is; zonder die toolkit start de container niet."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,13 @@
|
|||
"title": "Ollama-adres",
|
||||
"default": "http://ollama:11434",
|
||||
"required": true,
|
||||
"description": "Wijs naar je Ollama-container of een adres in je netwerk"
|
||||
"description": "Wijs naar je Ollama-container of een adres in je netwerk",
|
||||
"connect": {
|
||||
"app": "ollama",
|
||||
"scheme": "http",
|
||||
"port": 11434
|
||||
},
|
||||
"help": "Het adres waar de taalmodellen draaien. Heb je de Ollama-app geïnstalleerd, kies die dan hier; je kunt ook naar een Ollama op een andere machine wijzen."
|
||||
},
|
||||
{
|
||||
"name": "secret_key",
|
||||
|
|
|
|||
|
|
@ -75,7 +75,8 @@
|
|||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||
"config": {
|
||||
"placeholder": "sterk-wachtwoord"
|
||||
}
|
||||
},
|
||||
"advanced": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,8 @@
|
|||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||
"config": {
|
||||
"placeholder": "sterk-wachtwoord"
|
||||
}
|
||||
},
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "secret_key",
|
||||
|
|
@ -68,7 +69,8 @@
|
|||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||
"config": {
|
||||
"placeholder": "sterk-wachtwoord"
|
||||
}
|
||||
},
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "base_url",
|
||||
|
|
|
|||
|
|
@ -54,7 +54,8 @@
|
|||
"title": "Bewaartermijn",
|
||||
"default": "30d",
|
||||
"required": true,
|
||||
"description": "Hoe lang meetwaarden bewaard blijven, bv. 30d of 1y"
|
||||
"description": "Hoe lang meetwaarden bewaard blijven, bv. 30d of 1y",
|
||||
"advanced": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,8 @@
|
|||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||
"config": {
|
||||
"placeholder": "sterk-wachtwoord"
|
||||
}
|
||||
},
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "secret_key",
|
||||
|
|
@ -68,7 +69,8 @@
|
|||
"description": "Willekeurige waarde voor sessieversleuteling",
|
||||
"config": {
|
||||
"placeholder": "willekeurige-lange-string"
|
||||
}
|
||||
},
|
||||
"advanced": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,8 @@
|
|||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||
"config": {
|
||||
"placeholder": "sterk-wachtwoord"
|
||||
}
|
||||
},
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "encryption_key",
|
||||
|
|
@ -68,14 +69,17 @@
|
|||
"description": "Beschermt je Tesla-token",
|
||||
"config": {
|
||||
"placeholder": "sterk-wachtwoord"
|
||||
}
|
||||
},
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "grafana_port",
|
||||
"type": "int",
|
||||
"title": "Grafana poort",
|
||||
"default": 3014,
|
||||
"required": true
|
||||
"required": true,
|
||||
"help": "TeslaMate levert kant-en-klare dashboards mee in een eigen Grafana. Dit is de poort waarop je die bekijkt, los van je eventuele andere Grafana.",
|
||||
"advanced": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,8 @@
|
|||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||
"config": {
|
||||
"placeholder": "sterk-wachtwoord"
|
||||
}
|
||||
},
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "app_url",
|
||||
|
|
|
|||
|
|
@ -61,7 +61,8 @@
|
|||
"title": "Publiek adres",
|
||||
"default": "vpn.example.com",
|
||||
"required": true,
|
||||
"description": "Het adres of IP waarop je van buitenaf binnenkomt"
|
||||
"description": "Het adres of IP waarop je van buitenaf binnenkomt",
|
||||
"help": "Het adres waarop je van buitenaf binnenkomt: je vaste IP, of een naam via een dynamische-DNS-dienst. Klopt dit niet, dan werken de QR-codes wel maar kunnen je apparaten geen verbinding maken."
|
||||
},
|
||||
{
|
||||
"name": "password_hash",
|
||||
|
|
|
|||
|
|
@ -57,7 +57,8 @@
|
|||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||
"config": {
|
||||
"placeholder": "sterk-wachtwoord"
|
||||
}
|
||||
},
|
||||
"advanced": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,8 @@
|
|||
"title": "USB-adapter",
|
||||
"default": "/dev/ttyACM0",
|
||||
"required": true,
|
||||
"description": "Pad naar je Zigbee-stick; vind hem met `ls -l /dev/serial/by-id/`"
|
||||
"description": "Pad naar je Zigbee-stick; vind hem met `ls -l /dev/serial/by-id/`",
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "mqtt_server",
|
||||
|
|
@ -62,7 +63,13 @@
|
|||
"title": "MQTT-server",
|
||||
"default": "mqtt://mosquitto:1883",
|
||||
"required": true,
|
||||
"description": "Adres van je MQTT-broker (zie de Mosquitto-app)"
|
||||
"description": "Adres van je MQTT-broker (zie de Mosquitto-app)",
|
||||
"connect": {
|
||||
"app": "mosquitto",
|
||||
"scheme": "mqtt",
|
||||
"port": 1883
|
||||
},
|
||||
"help": "Zigbee2MQTT stuurt alles wat je apparaten melden naar een MQTT-broker; Home Assistant leest daar weer uit. Heb je de Mosquitto-app geïnstalleerd, kies die dan hier. Een eigen broker elders vul je handmatig in."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1109,6 +1109,12 @@ def api_store_install():
|
|||
bp_values = d.get("values", {}) # boilerplate variable values
|
||||
net_name = (d.get("network") or "").strip() # leeg = poorten op de host
|
||||
net_ip = (d.get("ip") or "").strip()
|
||||
# Aan het gedeelde netwerk hangen, zodat deze stack andere apps op naam
|
||||
# kan bereiken (en andersom).
|
||||
verbinden = d.get("connect_apps")
|
||||
if verbinden is None:
|
||||
verbinden = bool(cfg.load().get("CONNECT_APPS_DEFAULT", True))
|
||||
gedeeld_net = (cfg.load().get("SHARED_NETWORK") or "serverup").strip()
|
||||
if not stack:
|
||||
return jsonify(ok=False, msg="stack vereist")
|
||||
|
||||
|
|
@ -1250,6 +1256,26 @@ def api_store_install():
|
|||
jobs.log(qq, "dim", "Poortmappings verwijderd — de app draait op "
|
||||
"zijn eigen poort op dit adres.")
|
||||
|
||||
# Aan het gedeelde netwerk hangen. Zonder dit zit elke stack in
|
||||
# zijn eigen compose-netwerk en vindt geen enkele app een andere.
|
||||
if verbinden and gedeeld_net:
|
||||
ok_net, fout_net = networks.ensure_shared(gedeeld_net)
|
||||
if not ok_net:
|
||||
jobs.log(qq, "warn",
|
||||
f"Gedeeld netwerk '{gedeeld_net}' niet beschikbaar: "
|
||||
f"{fout_net} — de stack start zonder koppeling.")
|
||||
else:
|
||||
cf = docker.find_compose(dest)
|
||||
try:
|
||||
cf.write_text(compose_transform.add_shared_network(
|
||||
cf.read_text(encoding="utf-8"), gedeeld_net),
|
||||
encoding="utf-8")
|
||||
jobs.log(qq, "ok",
|
||||
f"Gekoppeld aan netwerk '{gedeeld_net}' — andere "
|
||||
"apps zijn bereikbaar op hun containernaam.")
|
||||
except compose_transform.TransformError as e:
|
||||
jobs.log(qq, "warn", f"Koppelen mislukt: {e}")
|
||||
|
||||
# Valideer het resultaat vóór we proberen te starten, zodat een
|
||||
# fout in het template een duidelijke melding geeft.
|
||||
ok_cfg, err_cfg = docker.validate_compose(dest)
|
||||
|
|
@ -1279,6 +1305,7 @@ def api_store_install():
|
|||
# geen andere installatie hetzelfde IP voorgesteld.
|
||||
"network": net_name,
|
||||
"ip": net_ip if net else "",
|
||||
"connected": bool(verbinden and gedeeld_net),
|
||||
}, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
|
@ -1475,6 +1502,43 @@ def api_networks():
|
|||
drivers=list(networks.DRIVERS))
|
||||
|
||||
|
||||
@app.route("/api/apps/connectable")
|
||||
def api_connectable():
|
||||
"""Ge<0x00ef>nstalleerde apps waar een nieuwe app aan gekoppeld kan worden.
|
||||
|
||||
Per stack de containernamen zoals ze binnen het gedeelde netwerk op te
|
||||
lossen zijn, plus uit welke app-template hij komt. Daarmee kan de
|
||||
installatiemodal een keuzelijst tonen in plaats van de gebruiker zelf een
|
||||
adres te laten typen.
|
||||
"""
|
||||
lib = Path(cfg.load()["LIBRARY_DIR"]).resolve()
|
||||
uit = []
|
||||
if lib.is_dir():
|
||||
for d in sorted(lib.iterdir()):
|
||||
if not d.is_dir() or d.name.startswith(".") or not docker.has_compose(d):
|
||||
continue
|
||||
meta = {}
|
||||
mf = d / ".serverup.json"
|
||||
if mf.exists():
|
||||
try:
|
||||
meta = json.loads(mf.read_text("utf-8"))
|
||||
except Exception:
|
||||
meta = {}
|
||||
containers = [{"name": c["name"], "service": c.get("service", ""),
|
||||
"ports": c.get("ports", "")}
|
||||
for c in docker.compose_ps(d, name=d.name) if c.get("name")]
|
||||
uit.append({
|
||||
"stack": d.name,
|
||||
"source": meta.get("source", ""), # uit welke app-template
|
||||
"name": meta.get("name") or d.name,
|
||||
"ip": meta.get("ip", ""),
|
||||
"containers": containers or [{"name": d.name, "service": "", "ports": ""}],
|
||||
})
|
||||
c = cfg.load()
|
||||
return jsonify(apps=uit, shared_network=c.get("SHARED_NETWORK", "serverup"),
|
||||
connect_default=bool(c.get("CONNECT_APPS_DEFAULT", True)))
|
||||
|
||||
|
||||
@app.route("/api/networks/range", methods=["POST"])
|
||||
def api_networks_range():
|
||||
"""Zet "van adres X tot adres Y" om in bruikbare CIDR-blokken."""
|
||||
|
|
|
|||
|
|
@ -56,6 +56,11 @@ DEFAULTS = {
|
|||
"AUTO_SYNC_ON_BOOT": False,
|
||||
# Macvlan/ipvlan-netwerken waarmee stacks een eigen IP-adres krijgen.
|
||||
"NETWORKS": [],
|
||||
# Gedeeld bridge-netwerk waarop stacks elkaar op containernaam vinden.
|
||||
# Zonder dit zit elke stack in zijn eigen compose-netwerk en kan een app
|
||||
# geen andere app bereiken.
|
||||
"SHARED_NETWORK": "serverup",
|
||||
"CONNECT_APPS_DEFAULT": True,
|
||||
# Backups. KEEP=0 of MAX_AGE=0 schakelt die regel uit; de nieuwste backup
|
||||
# van een stack wordt nooit opgeruimd.
|
||||
"BACKUP_KEEP": 5,
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ def fields(d: Path) -> list[dict]:
|
|||
"placeholder": "", "description": grp.get("description", ""),
|
||||
"is_group_toggle": True,
|
||||
})
|
||||
groep_geavanceerd = bool(grp.get("advanced"))
|
||||
for item in grp.get("items", []) or []:
|
||||
cfg = item.get("config") or {}
|
||||
out.append({
|
||||
|
|
@ -108,6 +109,17 @@ def fields(d: Path) -> list[dict]:
|
|||
"needs": item.get("needs") or [],
|
||||
"placeholder": cfg.get("placeholder", ""),
|
||||
"description": item.get("description", ""),
|
||||
# Geavanceerde velden verdwijnen achter een uitklapblok, zodat
|
||||
# het formulier bij het installeren kort blijft.
|
||||
"advanced": bool(item.get("advanced")) or groep_geavanceerd,
|
||||
# Uitgebreide toelichting voor het info-venster; valt terug op
|
||||
# description als die er niet is.
|
||||
"help": item.get("help", ""),
|
||||
# Verwijst naar een andere app: de interface biedt dan een
|
||||
# keuzelijst met wat er al ge?nstalleerd is in plaats van een
|
||||
# leeg tekstveld. {"app": "mosquitto", "scheme": "mqtt",
|
||||
# "port": 1883, "path": ""}
|
||||
"connect": item.get("connect") or None,
|
||||
})
|
||||
return out
|
||||
|
||||
|
|
|
|||
|
|
@ -72,6 +72,48 @@ def to_dedicated_ip(compose_text: str, network: str, ip: str,
|
|||
allow_unicode=True, width=120)
|
||||
|
||||
|
||||
def add_shared_network(compose_text: str, network: str) -> str:
|
||||
"""Hang elke service ook aan een gedeeld netwerk.
|
||||
|
||||
Elke stack draait als eigen compose-project en krijgt daarmee zijn eigen
|
||||
netwerk. Twee stacks kunnen elkaar dan niet op servicenaam bereiken — een
|
||||
app als Zigbee2MQTT vindt de MQTT-broker uit een andere stack dus niet.
|
||||
Door ze allemaal aan één gedeeld bridge-netwerk te hangen, lost de
|
||||
container-naam wél op tussen stacks.
|
||||
|
||||
Bestaande netwerken (zoals een macvlan met vast IP) blijven staan; dit komt
|
||||
er naast.
|
||||
"""
|
||||
try:
|
||||
doc = yaml.safe_load(compose_text)
|
||||
except yaml.YAMLError as e:
|
||||
raise TransformError(f"compose is geen geldige YAML: {e}") from e
|
||||
if not isinstance(doc, dict) or not isinstance(doc.get("services"), dict):
|
||||
raise TransformError("compose bevat geen services")
|
||||
|
||||
for svc in doc["services"].values():
|
||||
if not isinstance(svc, dict):
|
||||
continue
|
||||
# `networks:` mag een lijst of een mapping zijn; allebei aanvullen.
|
||||
huidig = svc.get("networks")
|
||||
if huidig is None:
|
||||
svc["networks"] = [network]
|
||||
elif isinstance(huidig, list):
|
||||
if network not in huidig:
|
||||
huidig.append(network)
|
||||
elif isinstance(huidig, dict):
|
||||
huidig.setdefault(network, None)
|
||||
|
||||
netwerken = doc.get("networks")
|
||||
if not isinstance(netwerken, dict):
|
||||
netwerken = {}
|
||||
netwerken.setdefault(network, {"external": True})
|
||||
doc["networks"] = netwerken
|
||||
|
||||
return yaml.safe_dump(doc, sort_keys=False, default_flow_style=False,
|
||||
allow_unicode=True, width=120)
|
||||
|
||||
|
||||
def published_ports(compose_text: str) -> list[str]:
|
||||
"""Alle host-poorten die dit compose-bestand publiceert (voor de UI)."""
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -502,6 +502,29 @@ def create(net: dict) -> tuple[bool, str]:
|
|||
return True, "Netwerk aangemaakt."
|
||||
|
||||
|
||||
def ensure_shared(name: str) -> tuple[bool, str]:
|
||||
"""Zorg dat het gedeelde bridge-netwerk bestaat waarop stacks elkaar vinden.
|
||||
|
||||
Anders dan de macvlan-netwerken heeft dit er geen configuratie voor nodig:
|
||||
een gewoon bridge-netwerk volstaat, want het gaat alleen om naamresolutie
|
||||
tussen containers van verschillende compose-projecten.
|
||||
"""
|
||||
if not _NAME_RE.match(name or ""):
|
||||
return False, "Ongeldige netwerknaam."
|
||||
if exists(name):
|
||||
return True, ""
|
||||
r = docker._run([docker._bin(), "network", "create", "-d", "bridge", name],
|
||||
timeout=30)
|
||||
if r.returncode != 0:
|
||||
melding = (r.stderr or r.stdout).strip()
|
||||
# Bij gelijktijdige installaties kan een ander proces hem net hebben
|
||||
# aangemaakt; dat is geen fout.
|
||||
if "already exists" in melding.lower() or exists(name):
|
||||
return True, ""
|
||||
return False, melding[:300]
|
||||
return True, ""
|
||||
|
||||
|
||||
def remove(name: str) -> tuple[bool, str]:
|
||||
if not _NAME_RE.match(name or ""):
|
||||
return False, "Ongeldige netwerknaam."
|
||||
|
|
|
|||
|
|
@ -1119,6 +1119,37 @@ tailwind.config = {
|
|||
<p class="text-[11px] text-dm dark:text-dm-dark mt-1" x-text="t('instance_hint')">Wijzig voor meerdere installaties</p>
|
||||
</div>
|
||||
|
||||
<!-- Uitleg bij het veld waar je op het info-icoon klikte -->
|
||||
<div x-show="fieldInfo" x-cloak
|
||||
class="rounded-xl border border-ac/30 bg-ac/5 px-3.5 py-3">
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="mdi mdi-information-outline text-ac dark:text-ac-dark shrink-0"></span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs font-bold text-wh dark:text-wh-dark" x-text="(fieldInfo||{}).title"></div>
|
||||
<p class="text-[11px] text-t2 dark:text-t2-dark mt-0.5"
|
||||
x-text="(fieldInfo||{}).help || (fieldInfo||{}).description"></p>
|
||||
<p class="text-[10px] font-mono text-dm dark:text-dm-dark mt-1"
|
||||
x-show="(fieldInfo||{}).default !== null && (fieldInfo||{}).default !== undefined && (fieldInfo||{}).default !== ''"
|
||||
x-text="t('default_value') + ': ' + (fieldInfo||{}).default"></p>
|
||||
</div>
|
||||
<button @click="fieldInfo = null" class="text-t2 dark:text-t2-dark hover:text-tx">
|
||||
<span class="mdi mdi-close"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Koppelen aan andere apps -->
|
||||
<label class="flex items-start gap-2.5 cursor-pointer">
|
||||
<input type="checkbox" :checked="install.connect_apps !== false"
|
||||
@change="install.connect_apps = $event.target.checked"
|
||||
class="w-5 h-5 mt-0.5 rounded border-bd dark:border-bd-dark text-ac shrink-0">
|
||||
<span class="min-w-0">
|
||||
<span class="text-sm text-tx dark:text-tx-dark" x-text="t('connect_apps')">Verbinden met andere apps</span>
|
||||
<span class="block text-[11px] text-dm dark:text-dm-dark"
|
||||
x-text="t('connect_apps_hint', {net: sharedNetwork})"></span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<!-- Netwerk: poorten op de host, of een eigen IP-adres (macvlan/ipvlan) -->
|
||||
<div>
|
||||
<label class="form-label" x-text="t('network')">Netwerk</label>
|
||||
|
|
@ -1154,22 +1185,49 @@ tailwind.config = {
|
|||
</label>
|
||||
</template>
|
||||
</summary>
|
||||
<div class="px-3.5 py-3 grid sm:grid-cols-2 gap-3" x-show="!grp.toggle || install.values[grp.toggle]">
|
||||
<template x-for="f in grp.items" :key="f.name">
|
||||
<div class="px-3.5 py-3" x-show="!grp.toggle || install.values[grp.toggle]">
|
||||
<div class="grid sm:grid-cols-2 gap-3">
|
||||
<template x-for="f in grp.items.filter(x => !x.advanced)" :key="f.name">
|
||||
<!-- De groepsschakelaar staat al in de kop; niet nog eens tonen -->
|
||||
<div x-show="!f.is_group_toggle && bpFieldVisible(f)"
|
||||
:class="f.type==='bool' ? 'sm:col-span-2' : ''">
|
||||
<label class="form-label flex items-center gap-1"><span x-text="f.title"></span><span x-show="f.required" class="text-er">*</span></label>
|
||||
<template x-if="f.type === 'enum'">
|
||||
<label class="form-label flex items-center gap-1">
|
||||
<span x-text="f.title"></span>
|
||||
<span x-show="f.required" class="text-er">*</span>
|
||||
<!-- Info-venster met de volledige uitleg bij dit veld -->
|
||||
<button type="button" x-show="f.help || f.description"
|
||||
@click="fieldInfo = (fieldInfo && fieldInfo.name === f.name) ? null : f"
|
||||
class="text-t2 dark:text-t2-dark hover:text-ac dark:hover:text-ac-dark"
|
||||
:title="t('what_does_this_do')">
|
||||
<span class="mdi mdi-information-outline text-sm"></span>
|
||||
</button>
|
||||
</label>
|
||||
<!-- Koppeling met een al geïnstalleerde app -->
|
||||
<template x-if="f.connect">
|
||||
<div class="space-y-1.5">
|
||||
<select class="form-input" x-model="install.values[f.name]">
|
||||
<template x-for="doel in connectTargets(f)" :key="doel.value">
|
||||
<option :value="doel.value" x-text="doel.label"></option>
|
||||
</template>
|
||||
<option value="__handmatig__" x-text="t('enter_manually')">Handmatig invullen</option>
|
||||
</select>
|
||||
<input x-show="install.values[f.name] === '__handmatig__'"
|
||||
class="form-input font-mono !text-xs" :placeholder="f.placeholder"
|
||||
@input="install.values[f.name] = $event.target.value">
|
||||
<p x-show="!connectTargets(f).length" class="text-[11px] text-wr"
|
||||
x-text="t('no_connectable_app', {app: (f.connect || {}).app || ''})"></p>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!f.connect && f.type === 'enum'">
|
||||
<select class="form-input" x-model="install.values[f.name]"><template x-for="opt in f.options" :key="opt"><option :value="opt" x-text="opt"></option></template></select>
|
||||
</template>
|
||||
<template x-if="f.type === 'bool'">
|
||||
<template x-if="!f.connect && f.type === 'bool'">
|
||||
<label class="inline-flex items-center gap-2 cursor-pointer"><input type="checkbox" :checked="!!install.values[f.name]" @change="install.values[f.name] = $event.target.checked" class="rounded border-bd dark:border-bd-dark text-ac w-5 h-5"><span class="text-xs text-t2 dark:text-t2-dark" x-text="f.description"></span></label>
|
||||
</template>
|
||||
<template x-if="f.type === 'int'"><input type="number" class="form-input" x-model.number="install.values[f.name]" :placeholder="f.placeholder"></template>
|
||||
<template x-if="f.type === 'str' && !f.secret"><input class="form-input" x-model="install.values[f.name]" :placeholder="f.placeholder"></template>
|
||||
<template x-if="!f.connect && f.type === 'int'"><input type="number" class="form-input" x-model.number="install.values[f.name]" :placeholder="f.placeholder"></template>
|
||||
<template x-if="!f.connect && f.type === 'str' && !f.secret"><input class="form-input" x-model="install.values[f.name]" :placeholder="f.placeholder"></template>
|
||||
<!-- Geheim veld: genereerknop, zodat niemand 'admin123' invult -->
|
||||
<template x-if="f.type === 'str' && f.secret">
|
||||
<template x-if="!f.connect && f.type === 'str' && f.secret">
|
||||
<div class="flex gap-2">
|
||||
<input class="form-input font-mono !text-xs" x-model="install.values[f.name]" :placeholder="f.placeholder">
|
||||
<button type="button" class="btn-sm shrink-0" :title="t('generate_value')"
|
||||
|
|
@ -1182,6 +1240,69 @@ tailwind.config = {
|
|||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Verborgen menu: alles wat je meestal met rust laat -->
|
||||
<details x-show="grp.items.some(x => x.advanced)"
|
||||
class="mt-3 rounded-lg border border-bd dark:border-bd-dark overflow-hidden">
|
||||
<summary class="px-3 py-2 cursor-pointer text-[11px] font-bold text-t2 dark:text-t2-dark bg-s2/40 dark:bg-s2-dark/40 flex items-center gap-1.5">
|
||||
<span class="mdi mdi-tune"></span>
|
||||
<span x-text="t('advanced_options')">Geavanceerde opties</span>
|
||||
<span class="badge badge-dim ml-auto" x-text="grp.items.filter(x => x.advanced).length"></span>
|
||||
</summary>
|
||||
<div class="px-3 py-3 grid sm:grid-cols-2 gap-3">
|
||||
<template x-for="f in grp.items.filter(x => x.advanced)" :key="f.name">
|
||||
<div x-show="bpFieldVisible(f)" :class="f.type==='bool' ? 'sm:col-span-2' : ''">
|
||||
<label class="form-label flex items-center gap-1">
|
||||
<span x-text="f.title"></span>
|
||||
<span x-show="f.required" class="text-er">*</span>
|
||||
<!-- Info-venster met de volledige uitleg bij dit veld -->
|
||||
<button type="button" x-show="f.help || f.description"
|
||||
@click="fieldInfo = (fieldInfo && fieldInfo.name === f.name) ? null : f"
|
||||
class="text-t2 dark:text-t2-dark hover:text-ac dark:hover:text-ac-dark"
|
||||
:title="t('what_does_this_do')">
|
||||
<span class="mdi mdi-information-outline text-sm"></span>
|
||||
</button>
|
||||
</label>
|
||||
<!-- Koppeling met een al geïnstalleerde app -->
|
||||
<template x-if="f.connect">
|
||||
<div class="space-y-1.5">
|
||||
<select class="form-input" x-model="install.values[f.name]">
|
||||
<template x-for="doel in connectTargets(f)" :key="doel.value">
|
||||
<option :value="doel.value" x-text="doel.label"></option>
|
||||
</template>
|
||||
<option value="__handmatig__" x-text="t('enter_manually')">Handmatig invullen</option>
|
||||
</select>
|
||||
<input x-show="install.values[f.name] === '__handmatig__'"
|
||||
class="form-input font-mono !text-xs" :placeholder="f.placeholder"
|
||||
@input="install.values[f.name] = $event.target.value">
|
||||
<p x-show="!connectTargets(f).length" class="text-[11px] text-wr"
|
||||
x-text="t('no_connectable_app', {app: (f.connect || {}).app || ''})"></p>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!f.connect && f.type === 'enum'">
|
||||
<select class="form-input" x-model="install.values[f.name]"><template x-for="opt in f.options" :key="opt"><option :value="opt" x-text="opt"></option></template></select>
|
||||
</template>
|
||||
<template x-if="!f.connect && f.type === 'bool'">
|
||||
<label class="inline-flex items-center gap-2 cursor-pointer"><input type="checkbox" :checked="!!install.values[f.name]" @change="install.values[f.name] = $event.target.checked" class="rounded border-bd dark:border-bd-dark text-ac w-5 h-5"><span class="text-xs text-t2 dark:text-t2-dark" x-text="f.description"></span></label>
|
||||
</template>
|
||||
<template x-if="!f.connect && f.type === 'int'"><input type="number" class="form-input" x-model.number="install.values[f.name]" :placeholder="f.placeholder"></template>
|
||||
<template x-if="!f.connect && f.type === 'str' && !f.secret"><input class="form-input" x-model="install.values[f.name]" :placeholder="f.placeholder"></template>
|
||||
<!-- Geheim veld: genereerknop, zodat niemand 'admin123' invult -->
|
||||
<template x-if="!f.connect && f.type === 'str' && f.secret">
|
||||
<div class="flex gap-2">
|
||||
<input class="form-input font-mono !text-xs" x-model="install.values[f.name]" :placeholder="f.placeholder">
|
||||
<button type="button" class="btn-sm shrink-0" :title="t('generate_value')"
|
||||
@click="install.values[f.name] = randomSecret()"><span class="mdi mdi-dice-5-outline"></span></button>
|
||||
</div>
|
||||
</template>
|
||||
<p x-show="f.port_taken" class="text-[11px] text-wr mt-1"
|
||||
x-text="t('port_taken', {port: f.port_taken, suggested: f.suggested})"></p>
|
||||
<p x-show="f.description && f.type !== 'bool'" class="text-[11px] text-dm dark:text-dm-dark mt-1" x-text="f.description"></p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</details>
|
||||
</template>
|
||||
<details class="rounded-xl border border-bd dark:border-bd-dark overflow-hidden">
|
||||
|
|
@ -1415,6 +1536,9 @@ function app() {
|
|||
// Containers op deze host, ook die Server Up niet zelf beheert
|
||||
hostContainers: { containers: [], managed: 0, total: 0, running: 0 },
|
||||
|
||||
// Koppelbare apps en het info-venster in de installatiemodal
|
||||
connectApps: [], sharedNetwork: 'serverup', fieldInfo: null,
|
||||
|
||||
// Backups en updatestatus per stack
|
||||
bkStack: '', bkList: [], bkSettings: {},
|
||||
stackUpd: {}, checkingUpd: false,
|
||||
|
|
@ -1929,6 +2053,36 @@ function app() {
|
|||
this.loadNetworks();
|
||||
},
|
||||
|
||||
// ── Apps onderling koppelen ──────────────────────────────────────────────
|
||||
async loadConnectApps() {
|
||||
const d = await this.rpc('/api/apps/connectable');
|
||||
this.connectApps = d.apps || [];
|
||||
this.sharedNetwork = d.shared_network || 'serverup';
|
||||
if (this.install && this.install.connect_apps === undefined) {
|
||||
this.install.connect_apps = d.connect_default !== false;
|
||||
}
|
||||
},
|
||||
// Adressen van al geïnstalleerde apps die bij dit veld passen. Een veld met
|
||||
// {"connect": {"app": "mosquitto", "scheme": "mqtt", "port": 1883}} levert
|
||||
// mqtt://<containernaam>:1883 op voor elke geïnstalleerde Mosquitto.
|
||||
connectTargets(f) {
|
||||
const c = f.connect || {};
|
||||
const uit = [];
|
||||
for (const app of this.connectApps) {
|
||||
if (c.app && app.source !== c.app) continue;
|
||||
for (const ct of app.containers) {
|
||||
const schema = c.scheme ? c.scheme + '://' : '';
|
||||
const poort = c.port ? ':' + c.port : '';
|
||||
const pad = c.path || '';
|
||||
uit.push({
|
||||
value: schema + ct.name + poort + pad,
|
||||
label: app.name + ' → ' + ct.name + (c.port ? ' :' + c.port : ''),
|
||||
});
|
||||
}
|
||||
}
|
||||
return uit;
|
||||
},
|
||||
|
||||
// Cryptografisch willekeurige waarde voor admin-tokens en wachtwoorden.
|
||||
// Lokaal gegenereerd, dus het geheim gaat niet heen en weer over het net.
|
||||
randomSecret(bytes = 24) {
|
||||
|
|
@ -2028,7 +2182,9 @@ function app() {
|
|||
network: '', ip: ''
|
||||
};
|
||||
this.modal = 'install';
|
||||
this.fieldInfo = null;
|
||||
if (!this.netList.length) await this.loadNetworks();
|
||||
await this.loadConnectApps();
|
||||
const d = await this.rpc('/api/store/preview', { stack: this.install.stack, repo_id: rid });
|
||||
this.install.loading = false;
|
||||
if (!d.ok) { this.toast(d.msg || 'Preview faalde', 'er'); this.modal = null; return; }
|
||||
|
|
@ -2042,6 +2198,13 @@ function app() {
|
|||
else init[f.name] = '';
|
||||
});
|
||||
if (init.service_name !== undefined && !init.service_name) init.service_name = this.install.instance;
|
||||
// Wijst een veld naar een andere app en staat die al geïnstalleerd,
|
||||
// vul dan meteen het juiste adres in.
|
||||
for (const f of (d.fields || [])) {
|
||||
if (!f.connect) continue;
|
||||
const doelen = this.connectTargets(f);
|
||||
if (doelen.length) init[f.name] = doelen[0].value;
|
||||
}
|
||||
this.install.values = init;
|
||||
this.install.preview_files = d.preview_files || {};
|
||||
} else {
|
||||
|
|
@ -2083,6 +2246,7 @@ function app() {
|
|||
instance: this.install.instance || this.install.stack,
|
||||
};
|
||||
if (this.install.network) { body.network = this.install.network; body.ip = this.install.ip; }
|
||||
body.connect_apps = this.install.connect_apps !== false;
|
||||
if (this.install.format === 'boilerplate') body.values = this.install.values;
|
||||
else { body.env_overrides = this.install.envOverrides; body.image_tags = this.install.imageTags; }
|
||||
const d = await this.rpc('/api/store/install', body);
|
||||
|
|
|
|||
|
|
@ -246,5 +246,12 @@
|
|||
"of_which_managed": "of which {n} via Server Up",
|
||||
"other_containers": "Other containers",
|
||||
"other_containers_hint": "These containers run on this host but do not belong to a stack managed by Server Up — started manually, for example, or from before you began using Server Up. They do count towards the dashboard figures.",
|
||||
"this_app": "this app"
|
||||
"this_app": "this app",
|
||||
"advanced_options": "Advanced options",
|
||||
"what_does_this_do": "What does this setting do?",
|
||||
"default_value": "Default",
|
||||
"enter_manually": "Enter manually…",
|
||||
"no_connectable_app": "No {app} is installed yet. Install it first, or enter the address manually.",
|
||||
"connect_apps": "Connect to other apps",
|
||||
"connect_apps_hint": "Put this stack on the shared network '{net}' so it can reach other apps by name and vice versa. Without this, every stack stands alone."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -246,5 +246,12 @@
|
|||
"of_which_managed": "waarvan {n} via Server Up",
|
||||
"other_containers": "Overige containers",
|
||||
"other_containers_hint": "Deze containers draaien op deze host maar horen niet bij een stack die Server Up beheert — bijvoorbeeld handmatig gestart of van vóór je Server Up ging gebruiken. Ze tellen wel mee in de cijfers op het dashboard.",
|
||||
"this_app": "deze app"
|
||||
"this_app": "deze app",
|
||||
"advanced_options": "Geavanceerde opties",
|
||||
"what_does_this_do": "Wat doet deze instelling?",
|
||||
"default_value": "Standaard",
|
||||
"enter_manually": "Handmatig invullen…",
|
||||
"no_connectable_app": "Er is nog geen {app} geïnstalleerd. Installeer die eerst, of vul het adres handmatig in.",
|
||||
"connect_apps": "Verbinden met andere apps",
|
||||
"connect_apps_hint": "Zet deze stack op het gedeelde netwerk '{net}', zodat hij andere apps op hun naam kan bereiken en andersom. Zonder dit staat elke stack op zichzelf."
|
||||
}
|
||||
|
|
|
|||
142
tests/test_connect.py
Normal file
142
tests/test_connect.py
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
"""Apps onderling koppelen, geavanceerde velden en de uitleg per veld."""
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
WORTEL = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(WORTEL / "server-up"))
|
||||
|
||||
from core import boilerplates as bp
|
||||
from core import compose_transform as ct
|
||||
|
||||
from conftest import login
|
||||
|
||||
BASIS = """\
|
||||
services:
|
||||
web:
|
||||
image: nginx
|
||||
ports:
|
||||
- "8080:80"
|
||||
db:
|
||||
image: postgres
|
||||
"""
|
||||
|
||||
|
||||
# ── Gedeeld netwerk ──────────────────────────────────────────────────────────
|
||||
|
||||
def test_gedeeld_netwerk_komt_bij_elke_service():
|
||||
"""Zonder gedeeld netwerk zit elke stack in zijn eigen compose-netwerk en
|
||||
kan geen enkele app een andere op naam bereiken."""
|
||||
doc = yaml.safe_load(ct.add_shared_network(BASIS, "serverup"))
|
||||
for naam, svc in doc["services"].items():
|
||||
assert "serverup" in svc["networks"], naam
|
||||
assert doc["networks"]["serverup"] == {"external": True}
|
||||
|
||||
|
||||
def test_gedeeld_netwerk_naast_een_eigen_ip():
|
||||
"""Een stack met een macvlan-adres moet het gedeelde netwerk erbij krijgen,
|
||||
niet in plaats daarvan."""
|
||||
met_ip = ct.to_dedicated_ip(BASIS, "lan", "192.168.1.240")
|
||||
doc = yaml.safe_load(ct.add_shared_network(met_ip, "serverup"))
|
||||
web = doc["services"]["web"]["networks"]
|
||||
assert "lan" in web and "serverup" in web
|
||||
assert web["lan"] == {"ipv4_address": "192.168.1.240"}
|
||||
assert doc["networks"]["lan"]["external"] is True
|
||||
assert doc["networks"]["serverup"]["external"] is True
|
||||
|
||||
|
||||
def test_gedeeld_netwerk_is_idempotent():
|
||||
een = ct.add_shared_network(BASIS, "serverup")
|
||||
twee = ct.add_shared_network(een, "serverup")
|
||||
doc = yaml.safe_load(twee)
|
||||
assert doc["services"]["web"]["networks"].count("serverup") == 1
|
||||
|
||||
|
||||
def test_gedeeld_netwerk_bij_ongeldige_yaml():
|
||||
with pytest.raises(ct.TransformError):
|
||||
ct.add_shared_network("dit: [is geen: yaml", "serverup")
|
||||
|
||||
|
||||
# ── Veldopties ───────────────────────────────────────────────────────────────
|
||||
|
||||
def _veld(app: str, naam: str) -> dict:
|
||||
return next(f for f in bp.fields(WORTEL / "apps" / app) if f["name"] == naam)
|
||||
|
||||
|
||||
def test_koppelveld_wordt_doorgegeven():
|
||||
f = _veld("zigbee2mqtt", "mqtt_server")
|
||||
assert f["connect"] == {"app": "mosquitto", "scheme": "mqtt", "port": 1883}
|
||||
assert f["help"], "koppelveld hoort uitleg te hebben"
|
||||
|
||||
|
||||
def test_open_webui_wijst_naar_ollama():
|
||||
f = _veld("open-webui", "ollama_url")
|
||||
assert f["connect"]["app"] == "ollama"
|
||||
assert f["connect"]["port"] == 11434
|
||||
|
||||
|
||||
def test_geavanceerd_veld_wordt_gemarkeerd():
|
||||
assert _veld("zigbee2mqtt", "usb_device")["advanced"] is True
|
||||
assert _veld("zigbee2mqtt", "mqtt_server")["advanced"] is False
|
||||
|
||||
|
||||
def test_gewone_velden_blijven_zichtbaar():
|
||||
"""De basisvelden mogen niet per ongeluk achter het uitklapmenu verdwijnen."""
|
||||
for app in ("zigbee2mqtt", "immich", "frigate", "ollama"):
|
||||
velden = {f["name"]: f for f in bp.fields(WORTEL / "apps" / app)}
|
||||
assert velden["service_name"]["advanced"] is False
|
||||
if "port" in velden:
|
||||
assert velden["port"]["advanced"] is False
|
||||
|
||||
|
||||
def test_elke_app_houdt_zichtbare_velden_over():
|
||||
"""Een app waarvan alles geavanceerd is, zou een leeg formulier tonen."""
|
||||
for d in sorted((WORTEL / "apps").iterdir()):
|
||||
if not d.is_dir():
|
||||
continue
|
||||
velden = [f for f in bp.fields(d) if not f.get("is_group_toggle")]
|
||||
zichtbaar = [f for f in velden if not f["advanced"]]
|
||||
assert zichtbaar, f"{d.name}: alle velden staan als geavanceerd gemarkeerd"
|
||||
|
||||
|
||||
def test_koppelvelden_verwijzen_naar_bestaande_apps():
|
||||
"""Een connect-veld dat naar een app verwijst die niet bestaat, levert een
|
||||
lege keuzelijst op zonder dat iemand snapt waarom."""
|
||||
apps = {d.name for d in (WORTEL / "apps").iterdir() if d.is_dir()}
|
||||
for d in sorted((WORTEL / "apps").iterdir()):
|
||||
if not d.is_dir():
|
||||
continue
|
||||
for f in bp.fields(d):
|
||||
doel = (f.get("connect") or {}).get("app")
|
||||
if doel:
|
||||
assert doel in apps, f"{d.name}/{f['name']} verwijst naar '{doel}'"
|
||||
|
||||
|
||||
# ── Endpoint ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_connectable_endpoint(client, env, monkeypatch):
|
||||
from core import docker
|
||||
import json as _json
|
||||
login(client)
|
||||
stack = env["lib"] / "mosquitto"
|
||||
stack.mkdir(parents=True)
|
||||
(stack / "docker-compose.yml").write_text("services:\n a:\n image: x\n")
|
||||
(stack / ".serverup.json").write_text(_json.dumps(
|
||||
{"source": "mosquitto", "name": "Mosquitto MQTT"}))
|
||||
monkeypatch.setattr(docker, "compose_ps",
|
||||
lambda d, name=None: [{"name": "mosquitto", "service": "a",
|
||||
"ports": "1883/tcp", "running": True}])
|
||||
|
||||
d = client.get("/api/apps/connectable").get_json()
|
||||
assert d["shared_network"] == "serverup"
|
||||
app = next(a for a in d["apps"] if a["stack"] == "mosquitto")
|
||||
assert app["source"] == "mosquitto"
|
||||
assert app["containers"][0]["name"] == "mosquitto"
|
||||
|
||||
|
||||
def test_connectable_zonder_stacks(client):
|
||||
login(client)
|
||||
assert client.get("/api/apps/connectable").get_json()["apps"] == []
|
||||
Loading…
Reference in a new issue