diff --git a/.env.example b/.env.example index 628b986..d164f51 100644 --- a/.env.example +++ b/.env.example @@ -46,3 +46,6 @@ BACKEND_SSH=mih00@mih00.hostsharing.net # WERKBAUM_TAIGA_API_URL in die Server-Umgebung (/env) nach. # Fehlt sie, bleibt das Feature aus und die Server-Zeile unangetastet. #TAIGA_API_URL=https://plan-api.hostsharing.net/api/v1 +# Basis-URL des Taiga-FRONTENDS (nicht aus der API-URL ableitbar) — nur für +# die Ticket-Links des Editors (Strg+Klick öffnet #US-123 in Taiga). +#TAIGA_WEB_URL=https://plan.hostsharing.net diff --git a/backend/src/main/kotlin/de/werkbaum/api/DocumentsController.kt b/backend/src/main/kotlin/de/werkbaum/api/DocumentsController.kt index 4952c69..586b38c 100644 --- a/backend/src/main/kotlin/de/werkbaum/api/DocumentsController.kt +++ b/backend/src/main/kotlin/de/werkbaum/api/DocumentsController.kt @@ -65,6 +65,9 @@ class DocumentsController( // Feature-Meldung des Taiga-Proxys (D91): Der Editor zeigt die // Ticket-Aktionen nur, wo ein konfiguriertes Backend antwortet. taiga = taigaProperties.configured, + // Für die Ticket-Links des Editors (D91-Nachtrag 5); ohne + // abschließenden Schrägstrich, damit der Client schlicht anhängt. + taigaWeb = taigaProperties.webUrl.trimEnd('/').ifBlank { null }, ) ) diff --git a/backend/src/main/kotlin/de/werkbaum/integration/taiga/TaigaProperties.kt b/backend/src/main/kotlin/de/werkbaum/integration/taiga/TaigaProperties.kt index 2e9fcd2..ef74590 100644 --- a/backend/src/main/kotlin/de/werkbaum/integration/taiga/TaigaProperties.kt +++ b/backend/src/main/kotlin/de/werkbaum/integration/taiga/TaigaProperties.kt @@ -21,6 +21,15 @@ data class TaigaProperties( */ val apiUrl: String = "", + /** + * Basis-URL des Taiga-**Frontends** (`https://plan.hostsharing.net`) — + * nicht aus der API-URL ableitbar, bei der Zielinstanz liegen beide auf + * verschiedenen Hosts. Nur für die Links des Editors (`GET /info` meldet + * sie als `taigaWeb`); leer heißt: keine Links, die Anlage funktioniert + * trotzdem. + */ + val webUrl: String = "", + /** * Login-Typ für `POST /auth`: `ldap` (LDAP-Plugin, so die Zielinstanz) * oder `normal`. Nur der Auth-Endpunkt braucht ihn; bei der angekündigten diff --git a/backend/src/main/resources/application.yaml b/backend/src/main/resources/application.yaml index c09bf3d..4a0f5c4 100644 --- a/backend/src/main/resources/application.yaml +++ b/backend/src/main/resources/application.yaml @@ -56,6 +56,9 @@ werkbaum: # bei der Zielinstanz z. B. https://plan-api.hostsharing.net/api/v1 taiga: api-url: ${WERKBAUM_TAIGA_API_URL:} + # Basis-URL des Taiga-FRONTENDS (z. B. https://plan.hostsharing.net) - + # nicht aus der API-URL ableitbar. Nur fuer die Ticket-Links des Editors. + web-url: ${WERKBAUM_TAIGA_WEB_URL:} # Login-Typ der Instanz fuer POST /taiga/auth: "ldap" (LDAP-Plugin, # plan.hostsharing.net) oder "normal". auth-type: ${WERKBAUM_TAIGA_AUTH_TYPE:ldap} diff --git a/backend/src/main/resources/openapi/api.yaml b/backend/src/main/resources/openapi/api.yaml index e0321cc..6e22f1a 100644 --- a/backend/src/main/resources/openapi/api.yaml +++ b/backend/src/main/resources/openapi/api.yaml @@ -872,6 +872,14 @@ components: true, wenn eine Taiga-Instanz konfiguriert ist (`werkbaum.taiga.api-url`) - der Editor zeigt die Ticket-Aktionen im Knoten-Fenster nur dann (D91). + taigaWeb: + type: string + description: > + Basis-URL des Taiga-Frontends (`werkbaum.taiga.web-url`), ohne + abschliessenden Schraegstrich - der Editor baut daraus die Links + zum Oeffnen einer Ticket-Referenz + (`/project//us/` bzw. `.../task/`). + Fehlt, wenn nicht konfiguriert; das Oeffnen entfaellt dann. TaigaAuthRequest: type: object diff --git a/backend/src/test/kotlin/de/werkbaum/api/TaigaApiTest.kt b/backend/src/test/kotlin/de/werkbaum/api/TaigaApiTest.kt index 37ea330..6a168db 100644 --- a/backend/src/test/kotlin/de/werkbaum/api/TaigaApiTest.kt +++ b/backend/src/test/kotlin/de/werkbaum/api/TaigaApiTest.kt @@ -41,6 +41,9 @@ class TaigaApiTest { val result = client.get().uri("/api/v1/info").exchange().returnResult(String::class.java) result.status.value() shouldBe 200 result.responseBody!! shouldContain "\"taiga\":true" + // Die Web-Basis für die Ticket-Links (D91-Nachtrag 5) — ohne den + // konfigurierten Schrägstrich am Ende. + result.responseBody!! shouldContain "\"taigaWeb\":\"https://plan.example.test\"" } @Test @@ -154,6 +157,8 @@ class TaigaApiTest { fun taigaUrl(registry: DynamicPropertyRegistry) { startStub() registry.add("werkbaum.taiga.api-url") { "http://127.0.0.1:${stub.address.port}/api/v1" } + // Mit abschließendem Schrägstrich — /info muss ihn abstreifen. + registry.add("werkbaum.taiga.web-url") { "https://plan.example.test/" } } } } diff --git a/backend/src/test/kotlin/de/werkbaum/api/TaigaDisabledTest.kt b/backend/src/test/kotlin/de/werkbaum/api/TaigaDisabledTest.kt index 3b2d9a0..f02c72d 100644 --- a/backend/src/test/kotlin/de/werkbaum/api/TaigaDisabledTest.kt +++ b/backend/src/test/kotlin/de/werkbaum/api/TaigaDisabledTest.kt @@ -2,6 +2,7 @@ package de.werkbaum.api import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldNotContain import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient @@ -36,6 +37,9 @@ class TaigaDisabledTest { val result = client.get().uri("/api/v1/info").exchange().returnResult(String::class.java) result.status.value() shouldBe 200 result.responseBody!! shouldContain "\"taiga\":false" + // Ohne konfigurierte Web-Basis gibt es keine Ticket-Links — und keine + // leere Zeichenkette, an die ein Client Pfade anhängen könnte. + result.responseBody!! shouldNotContain "\"taigaWeb\":\"" } @Test diff --git a/scripts/deploy-backend.sh b/scripts/deploy-backend.sh index 307efbf..770450b 100755 --- a/scripts/deploy-backend.sh +++ b/scripts/deploy-backend.sh @@ -74,9 +74,11 @@ DIR="$(env_value BACKEND_DIR)"; DIR="${DIR:-opt/werkbaum}" JDK_DIR="$(env_value BACKEND_JDK_DIR)"; JDK_DIR="${JDK_DIR:-opt/jdk21}" PORT="$(env_value BACKEND_PORT)"; PORT="${PORT:-9080}" XMX="$(env_value BACKEND_XMX)"; XMX="${XMX:-192m}" -# Taiga-Proxy (D91): API-URL aus der lokalen .env; der Deploy zieht sie als -# WERKBAUM_TAIGA_API_URL in die Server-Umgebung nach (leer = nichts anfassen). +# Taiga-Proxy (D91): API- und Web-URL aus der lokalen .env; der Deploy zieht +# sie als WERKBAUM_TAIGA_API_URL / WERKBAUM_TAIGA_WEB_URL in die +# Server-Umgebung nach (leer = nichts anfassen). TAIGA_API_URL="$(env_value TAIGA_API_URL)" +TAIGA_WEB_URL="$(env_value TAIGA_WEB_URL)" # Derselbe Pfad in DREI Schreibweisen, weil ihn drei Werkzeuge lesen: # @@ -150,7 +152,7 @@ ssh "$SSH_TARGET" "mkdir -p \"$DIR_SH\" \"\$HOME/.config/systemd/user\"" rsync -az --chmod=F644 "$STAGE/werkbaum-backend.service" \ "$SSH_TARGET:~/.config/systemd/user/werkbaum-backend.service" -ssh "$SSH_TARGET" DIR="$DIR_SH" PORT="$PORT" RESTART="$RESTART" TAIGA_URL="$TAIGA_API_URL" 'bash -s' <<'REMOTE' +ssh "$SSH_TARGET" DIR="$DIR_SH" PORT="$PORT" RESTART="$RESTART" TAIGA_URL="$TAIGA_API_URL" TAIGA_WEB="$TAIGA_WEB_URL" 'bash -s' <<'REMOTE' set -euo pipefail DIR="$(eval echo "$DIR")" @@ -184,6 +186,8 @@ WERKBAUM_MASTER_PASSWORD_HASH= # Zielinstanz liegt sie auf einem eigenen Host. Leer = Feature aus; # GET /api/v1/info meldet es (taiga). #WERKBAUM_TAIGA_API_URL=https://plan-api.hostsharing.net/api/v1 +# Basis-URL des Taiga-FRONTENDS — nur fuer die Ticket-Links des Editors. +#WERKBAUM_TAIGA_WEB_URL=https://plan.hostsharing.net ENV chmod 600 "$DIR/env" echo " ! $DIR/env angelegt — Master-Passwort-Hash dort eintragen," @@ -203,6 +207,14 @@ if [ -n "${TAIGA_URL:-}" ]; then fi echo " WERKBAUM_TAIGA_API_URL -> ${TAIGA_URL}" fi +if [ -n "${TAIGA_WEB:-}" ]; then + if grep -q '^WERKBAUM_TAIGA_WEB_URL=' "$DIR/env"; then + sed -i "s#^WERKBAUM_TAIGA_WEB_URL=.*#WERKBAUM_TAIGA_WEB_URL=${TAIGA_WEB}#" "$DIR/env" + else + printf 'WERKBAUM_TAIGA_WEB_URL=%s\n' "$TAIGA_WEB" >> "$DIR/env" + fi + echo " WERKBAUM_TAIGA_WEB_URL -> ${TAIGA_WEB}" +fi if ! loginctl show-user "$(id -un)" -p Linger 2>/dev/null | grep -q 'Linger=yes'; then echo " ! Linger ist nicht gesetzt — der Dienst endet mit der Sitzung."