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. */}
+
+
+