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
|
# 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,
|
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",
|
"title": "Downloadmap",
|
||||||
"default": "/mnt/downloads",
|
"default": "/mnt/downloads",
|
||||||
"required": true,
|
"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",
|
"name": "timezone",
|
||||||
|
|
@ -66,7 +67,8 @@
|
||||||
"title": "Gebruikers-ID (PUID)",
|
"title": "Gebruikers-ID (PUID)",
|
||||||
"default": 1000,
|
"default": 1000,
|
||||||
"required": true,
|
"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",
|
"name": "pgid",
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,8 @@
|
||||||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"advanced": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "session_secret",
|
"name": "session_secret",
|
||||||
|
|
@ -68,7 +69,8 @@
|
||||||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"advanced": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "storage_key",
|
"name": "storage_key",
|
||||||
|
|
@ -79,7 +81,8 @@
|
||||||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"advanced": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,14 +54,16 @@
|
||||||
"title": "Gebruikers-ID (PUID)",
|
"title": "Gebruikers-ID (PUID)",
|
||||||
"default": 1000,
|
"default": 1000,
|
||||||
"required": true,
|
"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",
|
"name": "pgid",
|
||||||
"type": "int",
|
"type": "int",
|
||||||
"title": "Groeps-ID (PGID)",
|
"title": "Groeps-ID (PGID)",
|
||||||
"default": 1000,
|
"default": 1000,
|
||||||
"required": true
|
"required": true,
|
||||||
|
"advanced": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "secret_key",
|
"name": "secret_key",
|
||||||
|
|
@ -80,7 +82,8 @@
|
||||||
"title": "Toegestane hostnamen",
|
"title": "Toegestane hostnamen",
|
||||||
"default": "*",
|
"default": "*",
|
||||||
"required": true,
|
"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)",
|
"title": "Gebruikers-ID (PUID)",
|
||||||
"default": 1000,
|
"default": 1000,
|
||||||
"required": true,
|
"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",
|
"name": "pgid",
|
||||||
"type": "int",
|
"type": "int",
|
||||||
"title": "Groeps-ID (PGID)",
|
"title": "Groeps-ID (PGID)",
|
||||||
"default": 1000,
|
"default": 1000,
|
||||||
"required": true
|
"required": true,
|
||||||
|
"advanced": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,14 +54,16 @@
|
||||||
"title": "Gebruikers-ID (PUID)",
|
"title": "Gebruikers-ID (PUID)",
|
||||||
"default": 1000,
|
"default": 1000,
|
||||||
"required": true,
|
"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",
|
"name": "pgid",
|
||||||
"type": "int",
|
"type": "int",
|
||||||
"title": "Groeps-ID (PGID)",
|
"title": "Groeps-ID (PGID)",
|
||||||
"default": 1000,
|
"default": 1000,
|
||||||
"required": true
|
"required": true,
|
||||||
|
"advanced": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "db_password",
|
"name": "db_password",
|
||||||
|
|
@ -72,7 +74,8 @@
|
||||||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"advanced": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "app_url",
|
"name": "app_url",
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,8 @@
|
||||||
"type": "int",
|
"type": "int",
|
||||||
"title": "HTTP poort",
|
"title": "HTTP poort",
|
||||||
"default": 80,
|
"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",
|
"name": "data_dir",
|
||||||
|
|
|
||||||
|
|
@ -54,14 +54,16 @@
|
||||||
"title": "Gebruikers-ID (PUID)",
|
"title": "Gebruikers-ID (PUID)",
|
||||||
"default": 1000,
|
"default": 1000,
|
||||||
"required": true,
|
"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",
|
"name": "pgid",
|
||||||
"type": "int",
|
"type": "int",
|
||||||
"title": "Groeps-ID (PGID)",
|
"title": "Groeps-ID (PGID)",
|
||||||
"default": 1000,
|
"default": 1000,
|
||||||
"required": true
|
"required": true,
|
||||||
|
"advanced": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "books_dir",
|
"name": "books_dir",
|
||||||
|
|
|
||||||
|
|
@ -54,14 +54,16 @@
|
||||||
"title": "Gebruikers-ID (PUID)",
|
"title": "Gebruikers-ID (PUID)",
|
||||||
"default": 1000,
|
"default": 1000,
|
||||||
"required": true,
|
"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",
|
"name": "pgid",
|
||||||
"type": "int",
|
"type": "int",
|
||||||
"title": "Groeps-ID (PGID)",
|
"title": "Groeps-ID (PGID)",
|
||||||
"default": 1000,
|
"default": 1000,
|
||||||
"required": true
|
"required": true,
|
||||||
|
"advanced": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "source_dir",
|
"name": "source_dir",
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,8 @@
|
||||||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"advanced": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "app_key",
|
"name": "app_key",
|
||||||
|
|
@ -68,7 +69,8 @@
|
||||||
"description": "Exact 32 tekens; gebruik de dobbelsteenknop en kort zo nodig in",
|
"description": "Exact 32 tekens; gebruik de dobbelsteenknop en kort zo nodig in",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "32-tekens-lange-sleutel"
|
"placeholder": "32-tekens-lange-sleutel"
|
||||||
}
|
},
|
||||||
|
"advanced": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,8 @@
|
||||||
"type": "int",
|
"type": "int",
|
||||||
"title": "RTSP poort",
|
"title": "RTSP poort",
|
||||||
"default": 8554,
|
"default": 8554,
|
||||||
"required": true
|
"required": true,
|
||||||
|
"advanced": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "shm_size",
|
"name": "shm_size",
|
||||||
|
|
@ -61,7 +62,9 @@
|
||||||
"title": "Gedeeld geheugen",
|
"title": "Gedeeld geheugen",
|
||||||
"default": "128mb",
|
"default": "128mb",
|
||||||
"required": true,
|
"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)",
|
"title": "Gebruikers-ID (PUID)",
|
||||||
"default": 1000,
|
"default": 1000,
|
||||||
"required": true,
|
"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",
|
"name": "pgid",
|
||||||
"type": "int",
|
"type": "int",
|
||||||
"title": "Groeps-ID (PGID)",
|
"title": "Groeps-ID (PGID)",
|
||||||
"default": 1000,
|
"default": 1000,
|
||||||
"required": true
|
"required": true,
|
||||||
|
"advanced": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,8 @@
|
||||||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"advanced": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "app_url",
|
"name": "app_url",
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,18 @@
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"name": "Immich",
|
"name": "Immich",
|
||||||
"description": "Zelfgehoste foto- en videobeheer met automatische back-up. Google Photos alternatief.",
|
"description": "Zelfgehoste foto- en videobeheer met automatische back-up. Google Photos alternatief.",
|
||||||
"tags": ["fotos", "media", "backup"],
|
"tags": [
|
||||||
"icon": {"provider": "selfhst", "id": "immich"},
|
"fotos",
|
||||||
"version": {"name": "release"}
|
"media",
|
||||||
|
"backup"
|
||||||
|
],
|
||||||
|
"icon": {
|
||||||
|
"provider": "selfhst",
|
||||||
|
"id": "immich"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"name": "release"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"variables": [
|
"variables": [
|
||||||
{
|
{
|
||||||
|
|
@ -31,7 +40,8 @@
|
||||||
"title": "Upload directory",
|
"title": "Upload directory",
|
||||||
"default": "/opt/serverup/appdata",
|
"default": "/opt/serverup/appdata",
|
||||||
"required": true,
|
"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",
|
"name": "timezone",
|
||||||
|
|
@ -52,7 +62,10 @@
|
||||||
"default": "immich_db_pass",
|
"default": "immich_db_pass",
|
||||||
"required": true,
|
"required": true,
|
||||||
"description": "Wachtwoord voor de interne PostgreSQL database",
|
"description": "Wachtwoord voor de interne PostgreSQL database",
|
||||||
"config": {"placeholder": "sterk-wachtwoord"}
|
"config": {
|
||||||
|
"placeholder": "sterk-wachtwoord"
|
||||||
|
},
|
||||||
|
"advanced": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,14 +53,16 @@
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"title": "Organisatie",
|
"title": "Organisatie",
|
||||||
"default": "thuis",
|
"default": "thuis",
|
||||||
"required": true
|
"required": true,
|
||||||
|
"advanced": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "bucket",
|
"name": "bucket",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"title": "Bucket",
|
"title": "Bucket",
|
||||||
"default": "metingen",
|
"default": "metingen",
|
||||||
"required": true
|
"required": true,
|
||||||
|
"advanced": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "admin_user",
|
"name": "admin_user",
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,8 @@
|
||||||
"description": "Willekeurige waarde voor sessies",
|
"description": "Willekeurige waarde voor sessies",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"advanced": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "openai_key",
|
"name": "openai_key",
|
||||||
|
|
@ -65,7 +66,8 @@
|
||||||
"title": "OpenAI-sleutel (optioneel)",
|
"title": "OpenAI-sleutel (optioneel)",
|
||||||
"default": "",
|
"default": "",
|
||||||
"required": false,
|
"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)",
|
"title": "Gebruikers-ID (PUID)",
|
||||||
"default": 1000,
|
"default": 1000,
|
||||||
"required": true,
|
"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",
|
"name": "pgid",
|
||||||
"type": "int",
|
"type": "int",
|
||||||
"title": "Groeps-ID (PGID)",
|
"title": "Groeps-ID (PGID)",
|
||||||
"default": 1000,
|
"default": 1000,
|
||||||
"required": true
|
"required": true,
|
||||||
|
"advanced": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "library_dir",
|
"name": "library_dir",
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,8 @@
|
||||||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"advanced": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,8 @@
|
||||||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"advanced": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "admin_email",
|
"name": "admin_email",
|
||||||
|
|
|
||||||
|
|
@ -54,14 +54,16 @@
|
||||||
"title": "Gebruikers-ID (PUID)",
|
"title": "Gebruikers-ID (PUID)",
|
||||||
"default": 1000,
|
"default": 1000,
|
||||||
"required": true,
|
"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",
|
"name": "pgid",
|
||||||
"type": "int",
|
"type": "int",
|
||||||
"title": "Groeps-ID (PGID)",
|
"title": "Groeps-ID (PGID)",
|
||||||
"default": 1000,
|
"default": 1000,
|
||||||
"required": true
|
"required": true,
|
||||||
|
"advanced": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "library_dir",
|
"name": "library_dir",
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,8 @@
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
"title": "N-gram data gebruiken",
|
"title": "N-gram data gebruiken",
|
||||||
"default": false,
|
"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)",
|
"title": "Gebruikers-ID (PUID)",
|
||||||
"default": 1000,
|
"default": 1000,
|
||||||
"required": true,
|
"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",
|
"name": "pgid",
|
||||||
"type": "int",
|
"type": "int",
|
||||||
"title": "Groeps-ID (PGID)",
|
"title": "Groeps-ID (PGID)",
|
||||||
"default": 1000,
|
"default": 1000,
|
||||||
"required": true
|
"required": true,
|
||||||
|
"advanced": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "downloads_dir",
|
"name": "downloads_dir",
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,9 @@
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
"title": "NVIDIA-GPU gebruiken",
|
"title": "NVIDIA-GPU gebruiken",
|
||||||
"default": false,
|
"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",
|
"title": "Ollama-adres",
|
||||||
"default": "http://ollama:11434",
|
"default": "http://ollama:11434",
|
||||||
"required": true,
|
"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",
|
"name": "secret_key",
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,8 @@
|
||||||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"advanced": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,8 @@
|
||||||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"advanced": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "secret_key",
|
"name": "secret_key",
|
||||||
|
|
@ -68,7 +69,8 @@
|
||||||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"advanced": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "base_url",
|
"name": "base_url",
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,8 @@
|
||||||
"title": "Bewaartermijn",
|
"title": "Bewaartermijn",
|
||||||
"default": "30d",
|
"default": "30d",
|
||||||
"required": true,
|
"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",
|
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"advanced": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "secret_key",
|
"name": "secret_key",
|
||||||
|
|
@ -68,7 +69,8 @@
|
||||||
"description": "Willekeurige waarde voor sessieversleuteling",
|
"description": "Willekeurige waarde voor sessieversleuteling",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "willekeurige-lange-string"
|
"placeholder": "willekeurige-lange-string"
|
||||||
}
|
},
|
||||||
|
"advanced": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,8 @@
|
||||||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"advanced": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "encryption_key",
|
"name": "encryption_key",
|
||||||
|
|
@ -68,14 +69,17 @@
|
||||||
"description": "Beschermt je Tesla-token",
|
"description": "Beschermt je Tesla-token",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"advanced": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "grafana_port",
|
"name": "grafana_port",
|
||||||
"type": "int",
|
"type": "int",
|
||||||
"title": "Grafana poort",
|
"title": "Grafana poort",
|
||||||
"default": 3014,
|
"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",
|
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"advanced": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "app_url",
|
"name": "app_url",
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,8 @@
|
||||||
"title": "Publiek adres",
|
"title": "Publiek adres",
|
||||||
"default": "vpn.example.com",
|
"default": "vpn.example.com",
|
||||||
"required": true,
|
"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",
|
"name": "password_hash",
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,8 @@
|
||||||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"advanced": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,8 @@
|
||||||
"title": "USB-adapter",
|
"title": "USB-adapter",
|
||||||
"default": "/dev/ttyACM0",
|
"default": "/dev/ttyACM0",
|
||||||
"required": true,
|
"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",
|
"name": "mqtt_server",
|
||||||
|
|
@ -62,7 +63,13 @@
|
||||||
"title": "MQTT-server",
|
"title": "MQTT-server",
|
||||||
"default": "mqtt://mosquitto:1883",
|
"default": "mqtt://mosquitto:1883",
|
||||||
"required": true,
|
"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
|
bp_values = d.get("values", {}) # boilerplate variable values
|
||||||
net_name = (d.get("network") or "").strip() # leeg = poorten op de host
|
net_name = (d.get("network") or "").strip() # leeg = poorten op de host
|
||||||
net_ip = (d.get("ip") or "").strip()
|
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:
|
if not stack:
|
||||||
return jsonify(ok=False, msg="stack vereist")
|
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 "
|
jobs.log(qq, "dim", "Poortmappings verwijderd — de app draait op "
|
||||||
"zijn eigen poort op dit adres.")
|
"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
|
# Valideer het resultaat vóór we proberen te starten, zodat een
|
||||||
# fout in het template een duidelijke melding geeft.
|
# fout in het template een duidelijke melding geeft.
|
||||||
ok_cfg, err_cfg = docker.validate_compose(dest)
|
ok_cfg, err_cfg = docker.validate_compose(dest)
|
||||||
|
|
@ -1279,6 +1305,7 @@ def api_store_install():
|
||||||
# geen andere installatie hetzelfde IP voorgesteld.
|
# geen andere installatie hetzelfde IP voorgesteld.
|
||||||
"network": net_name,
|
"network": net_name,
|
||||||
"ip": net_ip if net else "",
|
"ip": net_ip if net else "",
|
||||||
|
"connected": bool(verbinden and gedeeld_net),
|
||||||
}, ensure_ascii=False, indent=2), encoding="utf-8")
|
}, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
@ -1475,6 +1502,43 @@ def api_networks():
|
||||||
drivers=list(networks.DRIVERS))
|
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"])
|
@app.route("/api/networks/range", methods=["POST"])
|
||||||
def api_networks_range():
|
def api_networks_range():
|
||||||
"""Zet "van adres X tot adres Y" om in bruikbare CIDR-blokken."""
|
"""Zet "van adres X tot adres Y" om in bruikbare CIDR-blokken."""
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,11 @@ DEFAULTS = {
|
||||||
"AUTO_SYNC_ON_BOOT": False,
|
"AUTO_SYNC_ON_BOOT": False,
|
||||||
# Macvlan/ipvlan-netwerken waarmee stacks een eigen IP-adres krijgen.
|
# Macvlan/ipvlan-netwerken waarmee stacks een eigen IP-adres krijgen.
|
||||||
"NETWORKS": [],
|
"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
|
# Backups. KEEP=0 of MAX_AGE=0 schakelt die regel uit; de nieuwste backup
|
||||||
# van een stack wordt nooit opgeruimd.
|
# van een stack wordt nooit opgeruimd.
|
||||||
"BACKUP_KEEP": 5,
|
"BACKUP_KEEP": 5,
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ def fields(d: Path) -> list[dict]:
|
||||||
"placeholder": "", "description": grp.get("description", ""),
|
"placeholder": "", "description": grp.get("description", ""),
|
||||||
"is_group_toggle": True,
|
"is_group_toggle": True,
|
||||||
})
|
})
|
||||||
|
groep_geavanceerd = bool(grp.get("advanced"))
|
||||||
for item in grp.get("items", []) or []:
|
for item in grp.get("items", []) or []:
|
||||||
cfg = item.get("config") or {}
|
cfg = item.get("config") or {}
|
||||||
out.append({
|
out.append({
|
||||||
|
|
@ -108,6 +109,17 @@ def fields(d: Path) -> list[dict]:
|
||||||
"needs": item.get("needs") or [],
|
"needs": item.get("needs") or [],
|
||||||
"placeholder": cfg.get("placeholder", ""),
|
"placeholder": cfg.get("placeholder", ""),
|
||||||
"description": item.get("description", ""),
|
"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
|
return out
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,48 @@ def to_dedicated_ip(compose_text: str, network: str, ip: str,
|
||||||
allow_unicode=True, width=120)
|
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]:
|
def published_ports(compose_text: str) -> list[str]:
|
||||||
"""Alle host-poorten die dit compose-bestand publiceert (voor de UI)."""
|
"""Alle host-poorten die dit compose-bestand publiceert (voor de UI)."""
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -502,6 +502,29 @@ def create(net: dict) -> tuple[bool, str]:
|
||||||
return True, "Netwerk aangemaakt."
|
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]:
|
def remove(name: str) -> tuple[bool, str]:
|
||||||
if not _NAME_RE.match(name or ""):
|
if not _NAME_RE.match(name or ""):
|
||||||
return False, "Ongeldige netwerknaam."
|
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>
|
<p class="text-[11px] text-dm dark:text-dm-dark mt-1" x-text="t('instance_hint')">Wijzig voor meerdere installaties</p>
|
||||||
</div>
|
</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) -->
|
<!-- Netwerk: poorten op de host, of een eigen IP-adres (macvlan/ipvlan) -->
|
||||||
<div>
|
<div>
|
||||||
<label class="form-label" x-text="t('network')">Netwerk</label>
|
<label class="form-label" x-text="t('network')">Netwerk</label>
|
||||||
|
|
@ -1154,22 +1185,49 @@ tailwind.config = {
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="px-3.5 py-3 grid sm:grid-cols-2 gap-3" x-show="!grp.toggle || install.values[grp.toggle]">
|
<div class="px-3.5 py-3" x-show="!grp.toggle || install.values[grp.toggle]">
|
||||||
<template x-for="f in grp.items" :key="f.name">
|
<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 -->
|
<!-- De groepsschakelaar staat al in de kop; niet nog eens tonen -->
|
||||||
<div x-show="!f.is_group_toggle && bpFieldVisible(f)"
|
<div x-show="!f.is_group_toggle && bpFieldVisible(f)"
|
||||||
:class="f.type==='bool' ? 'sm:col-span-2' : ''">
|
: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>
|
<label class="form-label flex items-center gap-1">
|
||||||
<template x-if="f.type === 'enum'">
|
<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>
|
<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>
|
||||||
<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>
|
<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>
|
||||||
<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.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.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 === 'str' && !f.secret"><input class="form-input" x-model="install.values[f.name]" :placeholder="f.placeholder"></template>
|
||||||
<!-- Geheim veld: genereerknop, zodat niemand 'admin123' invult -->
|
<!-- 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">
|
<div class="flex gap-2">
|
||||||
<input class="form-input font-mono !text-xs" x-model="install.values[f.name]" :placeholder="f.placeholder">
|
<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')"
|
<button type="button" class="btn-sm shrink-0" :title="t('generate_value')"
|
||||||
|
|
@ -1182,6 +1240,69 @@ tailwind.config = {
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</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>
|
</details>
|
||||||
</template>
|
</template>
|
||||||
<details class="rounded-xl border border-bd dark:border-bd-dark overflow-hidden">
|
<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
|
// Containers op deze host, ook die Server Up niet zelf beheert
|
||||||
hostContainers: { containers: [], managed: 0, total: 0, running: 0 },
|
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
|
// Backups en updatestatus per stack
|
||||||
bkStack: '', bkList: [], bkSettings: {},
|
bkStack: '', bkList: [], bkSettings: {},
|
||||||
stackUpd: {}, checkingUpd: false,
|
stackUpd: {}, checkingUpd: false,
|
||||||
|
|
@ -1929,6 +2053,36 @@ function app() {
|
||||||
this.loadNetworks();
|
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.
|
// Cryptografisch willekeurige waarde voor admin-tokens en wachtwoorden.
|
||||||
// Lokaal gegenereerd, dus het geheim gaat niet heen en weer over het net.
|
// Lokaal gegenereerd, dus het geheim gaat niet heen en weer over het net.
|
||||||
randomSecret(bytes = 24) {
|
randomSecret(bytes = 24) {
|
||||||
|
|
@ -2028,7 +2182,9 @@ function app() {
|
||||||
network: '', ip: ''
|
network: '', ip: ''
|
||||||
};
|
};
|
||||||
this.modal = 'install';
|
this.modal = 'install';
|
||||||
|
this.fieldInfo = null;
|
||||||
if (!this.netList.length) await this.loadNetworks();
|
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 });
|
const d = await this.rpc('/api/store/preview', { stack: this.install.stack, repo_id: rid });
|
||||||
this.install.loading = false;
|
this.install.loading = false;
|
||||||
if (!d.ok) { this.toast(d.msg || 'Preview faalde', 'er'); this.modal = null; return; }
|
if (!d.ok) { this.toast(d.msg || 'Preview faalde', 'er'); this.modal = null; return; }
|
||||||
|
|
@ -2042,6 +2198,13 @@ function app() {
|
||||||
else init[f.name] = '';
|
else init[f.name] = '';
|
||||||
});
|
});
|
||||||
if (init.service_name !== undefined && !init.service_name) init.service_name = this.install.instance;
|
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.values = init;
|
||||||
this.install.preview_files = d.preview_files || {};
|
this.install.preview_files = d.preview_files || {};
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -2083,6 +2246,7 @@ function app() {
|
||||||
instance: this.install.instance || this.install.stack,
|
instance: this.install.instance || this.install.stack,
|
||||||
};
|
};
|
||||||
if (this.install.network) { body.network = this.install.network; body.ip = this.install.ip; }
|
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;
|
if (this.install.format === 'boilerplate') body.values = this.install.values;
|
||||||
else { body.env_overrides = this.install.envOverrides; body.image_tags = this.install.imageTags; }
|
else { body.env_overrides = this.install.envOverrides; body.image_tags = this.install.imageTags; }
|
||||||
const d = await this.rpc('/api/store/install', body);
|
const d = await this.rpc('/api/store/install', body);
|
||||||
|
|
|
||||||
|
|
@ -246,5 +246,12 @@
|
||||||
"of_which_managed": "of which {n} via Server Up",
|
"of_which_managed": "of which {n} via Server Up",
|
||||||
"other_containers": "Other containers",
|
"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.",
|
"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",
|
"of_which_managed": "waarvan {n} via Server Up",
|
||||||
"other_containers": "Overige containers",
|
"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.",
|
"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