Run the SPA without a backend (local mode)

The app can now operate backend-agnostically: a production build still
asks on first start, but /setup also offers 'Continue without a backend'
(stored as the sentinel 'none'), and the shell adapts:

- Local mode shows only the e621-facing pages: Online (search, post view,
  favorites, blacklist editor, direct downloads) and Pools. Library,
  uploads, duplicates, stats, users, follows and similarity are hidden
  from the nav and palette and render a 'backend needed' state when
  reached directly; /detail/<e621 id> still works while /detail/J-x asks
  for a backend.
- e621 credentials are stored in this browser (j621.e621) and the
  Account page becomes a credentials-only screen; the store reads/writes
  locally instead of /api/auth/e621/.
- The header replaces the status pill and login/user area with an e621
  credentials button and a 'Setup Backend' button; the footer shows
  'Local mode — e621 features only' with the same entry point.
- In-library lookups (badges/browse markers) are skipped without a
  backend; 'Download to client' links straight to the e621 file instead
  of the backend proxy; follow buttons and palette follow toggles are
  hidden; api() fails fast with a clear message if something slips
  through.

Mode logic lives in lib/backend.ts (URL / '' same-origin / 'none') with
its matrix verified in Node; tsc, oxlint and the build are clean.
This commit is contained in:
2026-09-18 00:00:48 -05:00
parent 3bd5bc73dd
commit a904abdf20
19 changed files with 412 additions and 155 deletions
+11 -2
View File
@@ -5,6 +5,7 @@ import { Button, Spinner, inputClass } from "@/components/ui";
import { cn } from "@/lib/cn";
import {
DEFAULT_BACKEND_URL,
NO_BACKEND,
getBackendUrl,
getStoredBackendUrl,
normalizeBackendUrl,
@@ -57,7 +58,10 @@ async function testBackend(base: string): Promise<TestResult> {
*/
export function SetupPage() {
const stored = getStoredBackendUrl();
const [value, setValue] = useState(stored ?? DEFAULT_BACKEND_URL);
const localMode = stored === NO_BACKEND;
const [value, setValue] = useState(
stored === null || stored === NO_BACKEND ? DEFAULT_BACKEND_URL : stored,
);
const [testing, setTesting] = useState(false);
const [result, setResult] = useState<TestResult>(null);
@@ -101,7 +105,9 @@ export function SetupPage() {
<p className="mt-3 text-sm leading-relaxed text-ctp-subtext0">
Enter the origin this app should call for its API. Leave it blank
when the app and the API are served from the same domain.
when the app and the API are served from the same domain. Without a
backend the app runs in local mode: e621 browsing only, with
credentials kept in this browser.
</p>
<label className="mt-5 flex flex-col gap-1.5">
@@ -154,6 +160,9 @@ export function SetupPage() {
Use this server
</Button>
) : null}
<Button variant="ghost" onClick={() => save(NO_BACKEND)}>
{localMode ? "Keep running without a backend" : "Continue without a backend"}
</Button>
</div>
<p className="mt-4 text-xs leading-relaxed text-ctp-overlay0">