From c31f95155fef745c1a83ab1573d02290e2435b12 Mon Sep 17 00:00:00 2001 From: officer Date: Wed, 19 Aug 2026 09:58:30 +0000 Subject: [PATCH] make the connection settings reachable at /gitea/connection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The form appeared only when nothing was configured, so once a token was saved there was no way back to it — a member who mistyped theirs, or anyone rotating one, had nowhere to go. On a single-user machine that is an annoyance; on a company one it is the ordinary case, because every member has a token of their own to set. Uses the route pair the platform already generates for every plugin, so this needed no platform change. A plug icon beside the open-in-new-tab button, as a real Link rather than a button, because it is navigation and should be cmd-clickable. Saving returns to the frame — otherwise you are left looking at the form you just submitted, which reads as nothing having happened. The two halves were already right: the panel shows only the token field when isOwner is false, and getResolvedServiceCredentials gives a member the owner's URL with their OWN secret, never the owner's. --- web/GiteaConnection.tsx | 21 +++++++++++++++++---- web/GiteaFrame.tsx | 36 +++++++++++++++++++++++++----------- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/web/GiteaConnection.tsx b/web/GiteaConnection.tsx index 4ab3bbc..9a0ee49 100644 --- a/web/GiteaConnection.tsx +++ b/web/GiteaConnection.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { useNavigate } from 'react-router'; import { Loader2 } from 'lucide-react'; import { useClient } from 'hooks/useClient'; import { Button } from '@/components/ui/button'; @@ -34,6 +35,7 @@ type Props = { state: ConnectionState; onSaved: () => void }; export const GiteaConnection = ({ state, onSaved }: Props) => { const { put } = useClient(); + const navigate = useNavigate(); // Loopback is the right default and worth putting in front of someone: the sidecar fetches // SERVER-SIDE, so `localhost` means the machine Officer runs on. It needs no TLS and never leaves the // box — where the public URL would take the same traffic out and back. @@ -48,6 +50,9 @@ export const GiteaConnection = ({ state, onSaved }: Props) => { try { await put('/gitea/_config', state.isOwner === false ? { token } : { url, token }); onSaved(); + // Back to the frame. Saving from `/gitea/connection` otherwise leaves you looking at the form you + // just submitted, which reads as nothing having happened. + navigate('/gitea'); } catch (err) { setError(err instanceof Error ? err.message : 'could not save'); } finally { @@ -91,10 +96,18 @@ export const GiteaConnection = ({ state, onSaved }: Props) => { {error &&

{error}

} - +
+ + {/* Only once there is something to go back TO. */} + {state.configured && !state.unreadable ? ( + + ) : null} +
); }; diff --git a/web/GiteaFrame.tsx b/web/GiteaFrame.tsx index 677fdb0..3d120e6 100644 --- a/web/GiteaFrame.tsx +++ b/web/GiteaFrame.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; -import { ExternalLink, Loader2, TriangleAlert } from 'lucide-react'; +import { Link, useParams } from 'react-router'; +import { ExternalLink, Loader2, Plug, TriangleAlert } from 'lucide-react'; import { useClient } from 'hooks/useClient'; import { Button } from '@/components/ui/button'; import { GiteaConnection, type ConnectionState } from './GiteaConnection'; @@ -25,6 +26,11 @@ import { GiteaConnection, type ConnectionState } from './GiteaConnection'; const FRAME_SRC = '/api/gitea/_web/'; export const GiteaFrame = () => { + // `/gitea/connection` renders the settings instead of the frame. The plugin route pair gives this for + // free — `` and `/:section` are generated for every plugin — and it is the only way to + // change a token once one is set: the form used to appear ONLY when nothing was configured, so a + // member who mistyped theirs, or anyone rotating one, had nowhere to go. + const { section } = useParams(); const { get, post } = useClient(); const [state, setState] = useState<'opening' | 'ready' | 'failed'>('opening'); const [connection, setConnection] = useState(null); @@ -83,7 +89,8 @@ export const GiteaFrame = () => { ); } - if (connection && !connection.configured) { + // Unconfigured goes straight to the form; `/gitea/connection` opens it deliberately. + if (connection && (!connection.configured || section === 'connection')) { return setAttempt((n) => n + 1)} />; } @@ -92,15 +99,22 @@ export const GiteaFrame = () => { {/* The escape hatch, and it is not an admission of defeat: the whole point of proxying an app that already has a UI is that its own UI stays available. Anything the frame handles badly, this opens properly. */} - +
+ {/* A real link, not a button: it is navigation, and it should be cmd-clickable like any other. */} + + +