plotlink-ows 0.1.15 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +185 -93
  2. package/app/db.ts +1 -1
  3. package/app/lib/paths.ts +0 -2
  4. package/app/lib/publish.ts +257 -44
  5. package/app/prisma/schema.prisma +0 -36
  6. package/app/routes/dashboard.ts +105 -57
  7. package/app/routes/publish.ts +107 -25
  8. package/app/routes/settings.ts +194 -0
  9. package/app/routes/stories.ts +223 -0
  10. package/app/routes/terminal.ts +258 -0
  11. package/app/routes/wallet.ts +40 -10
  12. package/app/server.ts +35 -81
  13. package/app/web/App.tsx +4 -6
  14. package/app/web/components/Dashboard.tsx +98 -79
  15. package/app/web/components/Layout.tsx +70 -103
  16. package/app/web/components/PreviewPanel.tsx +388 -0
  17. package/app/web/components/Settings.tsx +210 -67
  18. package/app/web/components/StoriesPage.tsx +270 -0
  19. package/app/web/components/StoryBrowser.tsx +161 -0
  20. package/app/web/components/TerminalPanel.tsx +428 -0
  21. package/app/web/components/WalletCard.tsx +14 -8
  22. package/app/web/dist/assets/index-BuOxhUWG.css +32 -0
  23. package/app/web/dist/assets/index-De8CpT47.js +129 -0
  24. package/app/web/dist/index.html +3 -3
  25. package/app/web/dist/plotlink-logo.svg +5 -0
  26. package/app/web/public/plotlink-logo.svg +5 -0
  27. package/app/web/styles.css +18 -0
  28. package/bin/plotlink-ows.js +18 -62
  29. package/package.json +15 -6
  30. package/scripts/fix-index-status.ts +93 -0
  31. package/app/lib/llm-client.ts +0 -265
  32. package/app/lib/writer-prompt.ts +0 -44
  33. package/app/routes/chat.ts +0 -135
  34. package/app/routes/config.ts +0 -210
  35. package/app/routes/oauth.ts +0 -150
  36. package/app/web/components/Chat.tsx +0 -272
  37. package/app/web/components/LLMSetup.tsx +0 -291
  38. package/app/web/components/Publish.tsx +0 -245
  39. package/app/web/dist/assets/index-C9kXlYO_.css +0 -2
  40. package/app/web/dist/assets/index-CJiiaLHs.js +0 -9
@@ -0,0 +1,258 @@
1
+ import { Hono } from "hono";
2
+ import * as pty from "node-pty";
3
+ import path from "path";
4
+ import fs from "fs";
5
+ import { fileURLToPath } from "url";
6
+ import { randomUUID } from "crypto";
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const STORIES_DIR = path.join(__dirname, "..", "..", "stories");
10
+ const MAX_SESSIONS = 5;
11
+ const SESSION_FILE = path.join(__dirname, "..", "..", "data", "terminal-sessions.json");
12
+
13
+ const terminal = new Hono();
14
+
15
+ // Active PTY sessions keyed by story name
16
+ const ptySessions = new Map<
17
+ string,
18
+ { term: pty.IPty; ws: WebSocket | null; state: "running" | "stopped"; sessionId: string }
19
+ >();
20
+
21
+ function safeName(name: string): string | null {
22
+ if (!name || name.includes("..") || name.includes("/") || name.includes("\\") || name.startsWith(".")) {
23
+ return null;
24
+ }
25
+ return name;
26
+ }
27
+
28
+ /** Load stored session UUIDs from disk */
29
+ function loadSessionMap(): Record<string, string> {
30
+ try {
31
+ if (fs.existsSync(SESSION_FILE)) {
32
+ return JSON.parse(fs.readFileSync(SESSION_FILE, "utf-8"));
33
+ }
34
+ } catch { /* ignore */ }
35
+ return {};
36
+ }
37
+
38
+ /** Save session UUIDs to disk */
39
+ function saveSessionMap(map: Record<string, string>) {
40
+ try {
41
+ const dir = path.dirname(SESSION_FILE);
42
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
43
+ fs.writeFileSync(SESSION_FILE, JSON.stringify(map, null, 2) + "\n");
44
+ } catch { /* ignore */ }
45
+ }
46
+
47
+ function spawnPty(storyName: string, opts?: { sessionId?: string; resume?: boolean }) {
48
+ const storyDir = path.join(STORIES_DIR, storyName);
49
+ if (!fs.existsSync(storyDir)) fs.mkdirSync(storyDir, { recursive: true });
50
+ const shell = process.env.SHELL || "/bin/zsh";
51
+
52
+ // Determine session ID
53
+ const sessionMap = loadSessionMap();
54
+ let sessionId: string;
55
+
56
+ // Build Claude CLI command with session flags
57
+ // Note: no --cwd flag — Claude CLI uses process cwd, set via pty.spawn({ cwd: storyDir })
58
+ let claudeCmd = "claude";
59
+ if (opts?.resume && sessionMap[storyName]) {
60
+ // Resume: reuse stored session
61
+ sessionId = sessionMap[storyName];
62
+ claudeCmd += ` --resume "${sessionId}"`;
63
+ } else {
64
+ // Fresh: always generate new UUID
65
+ sessionId = opts?.sessionId || randomUUID();
66
+ claudeCmd += ` --session-id "${sessionId}"`;
67
+ }
68
+
69
+ const term = pty.spawn(shell, ["-l", "-c", claudeCmd], {
70
+ name: "xterm-256color",
71
+ cols: 120,
72
+ rows: 30,
73
+ cwd: storyDir,
74
+ env: process.env as Record<string, string>,
75
+ });
76
+
77
+ // Persist session ID
78
+ sessionMap[storyName] = sessionId;
79
+ saveSessionMap(sessionMap);
80
+
81
+ const isResume = !!opts?.resume;
82
+ const spawnTime = Date.now();
83
+ const session = { term, ws: null as WebSocket | null, state: "running" as const, sessionId };
84
+ ptySessions.set(storyName, session);
85
+
86
+ term.onExit(({ exitCode }) => {
87
+ const s = ptySessions.get(storyName);
88
+ if (s?.term !== term) return;
89
+
90
+ // If a resumed session exits quickly (< 5s), signal client to auto-reconnect fresh
91
+ const elapsed = Date.now() - spawnTime;
92
+ if (isResume && elapsed < 5000 && exitCode !== 0) {
93
+ console.log(`Resume for "${storyName}" failed (exit ${exitCode} in ${elapsed}ms), signaling fresh fallback`);
94
+ ptySessions.delete(storyName);
95
+ if (s.ws && s.ws.readyState <= 1) {
96
+ // Close code 4000 = resume-failed, client should auto-reconnect fresh
97
+ s.ws.close(4000, "resume-failed");
98
+ }
99
+ s.ws = null;
100
+ return;
101
+ }
102
+
103
+ s.state = "stopped";
104
+ if (s.ws && s.ws.readyState <= 1) {
105
+ s.ws.close(1000, `exited:${exitCode}`);
106
+ }
107
+ s.ws = null;
108
+ });
109
+
110
+ return session;
111
+ }
112
+
113
+ /** POST /api/terminal/spawn — spawn Claude CLI for a story */
114
+ terminal.post("/spawn", async (c) => {
115
+ const body = await c.req.json<{ storyName?: string; resume?: boolean }>().catch(() => ({}));
116
+ const storyName = safeName(body.storyName || "default");
117
+ if (!storyName) return c.json({ error: "Invalid story name" }, 400);
118
+
119
+ const existing = ptySessions.get(storyName);
120
+ if (existing?.term && existing.state === "running") {
121
+ return c.json({ ok: true, pid: existing.term.pid, storyName, sessionId: existing.sessionId, reused: true });
122
+ }
123
+
124
+ // Enforce max concurrent sessions
125
+ const running = [...ptySessions.values()].filter((s) => s.state === "running").length;
126
+ if (running >= MAX_SESSIONS) {
127
+ return c.json({ error: `Max ${MAX_SESSIONS} concurrent sessions`, ok: false }, 429);
128
+ }
129
+
130
+ try {
131
+ const session = spawnPty(storyName, { resume: body.resume });
132
+ return c.json({ ok: true, pid: session.term.pid, storyName, sessionId: session.sessionId });
133
+ } catch (err: unknown) {
134
+ const message = err instanceof Error ? err.message : "Failed to spawn PTY";
135
+ return c.json({ ok: false, error: message }, 500);
136
+ }
137
+ });
138
+
139
+ /** GET /api/terminal/session/:storyName — get stored session ID for a story */
140
+ terminal.get("/session/:storyName", (c) => {
141
+ const storyName = safeName(c.req.param("storyName"));
142
+ if (!storyName) return c.json({ error: "Invalid story name" }, 400);
143
+
144
+ const sessionMap = loadSessionMap();
145
+ const sessionId = sessionMap[storyName] || null;
146
+ const active = ptySessions.get(storyName);
147
+
148
+ return c.json({
149
+ sessionId,
150
+ running: !!active && active.state === "running",
151
+ });
152
+ });
153
+
154
+ /** DELETE /api/terminal/:storyName — kill a story's PTY */
155
+ terminal.delete("/:storyName", (c) => {
156
+ const storyName = safeName(c.req.param("storyName"));
157
+ if (!storyName) return c.json({ error: "Invalid story name" }, 400);
158
+
159
+ const session = ptySessions.get(storyName);
160
+ if (session?.term && session.state === "running") {
161
+ session.term.kill();
162
+ session.state = "stopped";
163
+ ptySessions.delete(storyName);
164
+ return c.json({ ok: true });
165
+ }
166
+ ptySessions.delete(storyName);
167
+ return c.json({ ok: true, message: "not running" });
168
+ });
169
+
170
+ /** POST /api/terminal/stop — kill PTY (legacy, kills default) */
171
+ terminal.post("/stop", (c) => {
172
+ const session = ptySessions.get("default");
173
+ if (session?.term && session.state === "running") {
174
+ session.term.kill();
175
+ session.state = "stopped";
176
+ return c.json({ ok: true });
177
+ }
178
+ return c.json({ ok: true, message: "not running" });
179
+ });
180
+
181
+ /** GET /api/terminal/status — list all sessions */
182
+ terminal.get("/status", (c) => {
183
+ const sessions: Record<string, { running: boolean; pid: number | null; sessionId: string }> = {};
184
+ for (const [name, session] of ptySessions) {
185
+ sessions[name] = {
186
+ running: session.state === "running",
187
+ pid: session.term?.pid ?? null,
188
+ sessionId: session.sessionId,
189
+ };
190
+ }
191
+ return c.json({ sessions });
192
+ });
193
+
194
+ /**
195
+ * Attach a raw WebSocket to a story's PTY session.
196
+ * Called from server.ts WebSocket upgrade handler.
197
+ */
198
+ export function attachTerminalWs(ws: WebSocket, storyName?: string, resume?: boolean) {
199
+ const name = storyName && safeName(storyName) ? storyName : "default";
200
+ let session = ptySessions.get(name);
201
+
202
+ // Lazy spawn if no PTY exists
203
+ if (!session || session.state !== "running") {
204
+ // Enforce max concurrent sessions
205
+ const running = [...ptySessions.values()].filter((s) => s.state === "running").length;
206
+ if (running >= MAX_SESSIONS) {
207
+ ws.close(1013, "max-sessions");
208
+ return;
209
+ }
210
+
211
+ try {
212
+ session = spawnPty(name, { resume });
213
+ } catch (err) {
214
+ console.error("PTY spawn failed:", err);
215
+ ws.close(1011, "pty-spawn-failed");
216
+ return;
217
+ }
218
+ }
219
+
220
+ // Replace previous WS
221
+ if (session.ws && session.ws !== ws && session.ws.readyState <= 1) {
222
+ session.ws.close(1000, "replaced");
223
+ }
224
+ session.ws = ws;
225
+
226
+ // PTY output → browser
227
+ const dataDisposable = session.term.onData((data: string) => {
228
+ if (ws.readyState === WebSocket.OPEN) {
229
+ ws.send(data);
230
+ }
231
+ });
232
+
233
+ // Browser input → PTY
234
+ ws.addEventListener("message", (event: MessageEvent) => {
235
+ if (!session?.term || session.state !== "running") return;
236
+ const str = typeof event.data === "string" ? event.data : event.data.toString();
237
+ try {
238
+ const parsed = JSON.parse(str);
239
+ if (parsed.type === "resize" && parsed.cols && parsed.rows) {
240
+ session.term.resize(parsed.cols, parsed.rows);
241
+ return;
242
+ }
243
+ } catch {
244
+ // Not JSON — raw input
245
+ }
246
+ session.term.write(str);
247
+ });
248
+
249
+ // Cleanup on close (keep PTY running)
250
+ ws.addEventListener("close", () => {
251
+ dataDisposable.dispose();
252
+ if (session?.ws === ws) {
253
+ session.ws = null;
254
+ }
255
+ });
256
+ }
257
+
258
+ export { terminal as terminalRoutes };
@@ -34,22 +34,50 @@ wallet.get("/", async (c) => {
34
34
 
35
35
  const address = getBaseAddress(plotlinkWallet);
36
36
 
37
- // Fetch USDC balance on Base via RPC
37
+ // Fetch balances on Base via RPC
38
+ let ethBalance = "0";
38
39
  let usdcBalance = "0";
40
+ let plotBalance = "0";
41
+ const rpcUrl = process.env.NEXT_PUBLIC_RPC_URL || "https://mainnet.base.org";
42
+
39
43
  if (address) {
44
+ const addrPadded = "000000000000000000000000" + address.slice(2).toLowerCase();
45
+ const balanceOfSig = "0x70a08231" + addrPadded;
46
+
40
47
  try {
41
- const USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // USDC on Base mainnet
42
- const balanceOfSig = "0x70a08231000000000000000000000000" + address.slice(2).toLowerCase();
43
- const rpcUrl = process.env.NEXT_PUBLIC_RPC_URL || "https://mainnet.base.org";
44
- const res = await fetch(rpcUrl, {
48
+ // ETH balance
49
+ const ethRes = await fetch(rpcUrl, {
50
+ method: "POST",
51
+ headers: { "Content-Type": "application/json" },
52
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_getBalance", params: [address, "latest"] }),
53
+ });
54
+ const ethData = await ethRes.json() as { result?: string };
55
+ if (ethData.result && ethData.result !== "0x" && ethData.result !== "0x0") {
56
+ ethBalance = (Number(BigInt(ethData.result)) / 1e18).toFixed(6);
57
+ }
58
+
59
+ // USDC balance (6 decimals)
60
+ const USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
61
+ const usdcRes = await fetch(rpcUrl, {
62
+ method: "POST",
63
+ headers: { "Content-Type": "application/json" },
64
+ body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "eth_call", params: [{ to: USDC_BASE, data: balanceOfSig }, "latest"] }),
65
+ });
66
+ const usdcData = await usdcRes.json() as { result?: string };
67
+ if (usdcData.result && usdcData.result !== "0x") {
68
+ usdcBalance = (Number(BigInt(usdcData.result)) / 1e6).toFixed(2);
69
+ }
70
+
71
+ // PLOT balance (18 decimals)
72
+ const PLOT = "0x4F567DACBF9D15A6acBe4A47FC2Ade0719Fb63C4";
73
+ const plotRes = await fetch(rpcUrl, {
45
74
  method: "POST",
46
75
  headers: { "Content-Type": "application/json" },
47
- body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_call", params: [{ to: USDC_BASE, data: balanceOfSig }, "latest"] }),
76
+ body: JSON.stringify({ jsonrpc: "2.0", id: 3, method: "eth_call", params: [{ to: PLOT, data: balanceOfSig }, "latest"] }),
48
77
  });
49
- const data = await res.json() as { result?: string };
50
- if (data.result && data.result !== "0x") {
51
- const raw = BigInt(data.result);
52
- usdcBalance = (Number(raw) / 1e6).toFixed(2); // USDC has 6 decimals
78
+ const plotData = await plotRes.json() as { result?: string };
79
+ if (plotData.result && plotData.result !== "0x") {
80
+ plotBalance = (Number(BigInt(plotData.result)) / 1e18).toFixed(4);
53
81
  }
54
82
  } catch { /* balance fetch best-effort */ }
55
83
  }
@@ -59,7 +87,9 @@ wallet.get("/", async (c) => {
59
87
  walletId: plotlinkWallet.id,
60
88
  name: plotlinkWallet.name,
61
89
  address,
90
+ ethBalance,
62
91
  usdcBalance,
92
+ plotBalance,
63
93
  accounts: plotlinkWallet.accounts,
64
94
  });
65
95
  } catch (err: unknown) {
package/app/server.ts CHANGED
@@ -10,15 +10,14 @@ dotenv.config({ path: ENV_FILE });
10
10
  import { Hono } from "hono";
11
11
  import { cors } from "hono/cors";
12
12
  import { serve } from "@hono/node-server";
13
- import { createNodeWebSocket } from "@hono/node-ws";
14
13
  import { serveStatic } from "@hono/node-server/serve-static";
15
14
  import { authRoutes, requireAuth } from "./routes/auth";
16
- import { configRoutes } from "./routes/config";
17
15
  import { walletRoutes } from "./routes/wallet";
18
- import { oauthRoutes } from "./routes/oauth";
19
- import { chatRoutes } from "./routes/chat";
20
16
  import { publishRoutes } from "./routes/publish";
21
17
  import { dashboardRoutes } from "./routes/dashboard";
18
+ import { terminalRoutes, attachTerminalWs } from "./routes/terminal";
19
+ import { storiesRoutes } from "./routes/stories";
20
+ import { settingsRoutes } from "./routes/settings";
22
21
  import { initDb } from "./db";
23
22
  import { execSync } from "child_process";
24
23
  import fs from "fs";
@@ -26,98 +25,28 @@ import fs from "fs";
26
25
  const __dirname = __dirnamePre;
27
26
 
28
27
  const app = new Hono();
29
- const { upgradeWebSocket, injectWebSocket } = createNodeWebSocket({ app });
30
-
31
28
  // CORS for local dev
32
29
  app.use("/*", cors({ origin: "http://localhost:5173", credentials: true }));
33
30
 
34
31
  // API routes
35
32
  app.route("/api/auth", authRoutes);
36
33
  // Protected routes
37
- app.use("/api/config/*", requireAuth);
38
34
  app.use("/api/wallet/*", requireAuth);
39
- // OAuth: protect start/status but NOT callback (provider redirects without auth)
40
- app.use("/api/oauth/:provider/start", requireAuth);
41
- app.use("/api/oauth/:provider/status", requireAuth);
42
- app.route("/api/config", configRoutes);
43
35
  app.route("/api/wallet", walletRoutes);
44
- app.route("/api/oauth", oauthRoutes);
45
- app.use("/api/chat/*", requireAuth);
46
36
  app.use("/api/publish/*", requireAuth);
47
- app.route("/api/chat", chatRoutes);
48
37
  app.route("/api/publish", publishRoutes);
49
38
  app.use("/api/dashboard/*", requireAuth);
50
39
  app.route("/api/dashboard", dashboardRoutes);
40
+ app.use("/api/terminal/*", requireAuth);
41
+ app.route("/api/terminal", terminalRoutes);
42
+ app.use("/api/stories/*", requireAuth);
43
+ app.route("/api/stories", storiesRoutes);
44
+ app.use("/api/settings/*", requireAuth);
45
+ app.route("/api/settings", settingsRoutes);
51
46
 
52
47
  // Health check
53
48
  app.get("/api/health", (c) => c.json({ status: "ok" }));
54
49
 
55
- // WebSocket chat endpoint (auth via query param since WS can't send headers)
56
- app.get(
57
- "/ws/chat",
58
- upgradeWebSocket((c) => {
59
- const wsToken = new URL(c.req.url).searchParams.get("token");
60
- let authenticated = false;
61
-
62
- return {
63
- async onOpen(_event, ws) {
64
- if (!wsToken) { ws.close(4001, "Missing token"); return; }
65
- const { db } = await import("./db");
66
- const session = await db.session.findUnique({ where: { token: wsToken } });
67
- if (!session || session.expiresAt < new Date()) { ws.close(4001, "Invalid token"); return; }
68
- authenticated = true;
69
- },
70
- async onMessage(event, ws) {
71
- if (!authenticated) { ws.close(4001, "Not authenticated"); return; }
72
- try {
73
- const data = JSON.parse(String(event.data));
74
- if (data.type === "message" && data.sessionId && data.content) {
75
- const { db } = await import("./db");
76
- const { streamChat } = await import("./lib/llm-client");
77
- const { WRITER_SYSTEM_PROMPT } = await import("./lib/writer-prompt");
78
-
79
- // Save user message
80
- await db.message.create({
81
- data: { sessionId: data.sessionId, role: "user", content: data.content },
82
- });
83
-
84
- // Build context
85
- const messages = await db.message.findMany({
86
- where: { sessionId: data.sessionId },
87
- orderBy: { createdAt: "asc" },
88
- });
89
-
90
- const chatMessages = [
91
- { role: "system" as const, content: WRITER_SYSTEM_PROMPT },
92
- ...messages.map((m: { role: string; content: string }) => ({
93
- role: m.role as "user" | "assistant" | "system",
94
- content: m.content,
95
- })),
96
- ];
97
-
98
- // Stream response
99
- let fullResponse = "";
100
- for await (const chunk of streamChat(chatMessages)) {
101
- fullResponse += chunk;
102
- ws.send(JSON.stringify({ type: "chunk", content: chunk }));
103
- }
104
-
105
- // Save assistant message
106
- await db.message.create({
107
- data: { sessionId: data.sessionId, role: "assistant", content: fullResponse },
108
- });
109
-
110
- ws.send(JSON.stringify({ type: "done" }));
111
- }
112
- } catch (err: unknown) {
113
- const message = err instanceof Error ? err.message : "WebSocket error";
114
- ws.send(JSON.stringify({ type: "error", message }));
115
- }
116
- },
117
- };
118
- }),
119
- );
120
-
121
50
  // In production, serve the built frontend
122
51
  const distPath = path.join(__dirname, "web", "dist");
123
52
  if (fs.existsSync(distPath)) {
@@ -143,12 +72,37 @@ async function start() {
143
72
  // Initialize database connection
144
73
  await initDb();
145
74
 
75
+ // Ensure stories directory exists
76
+ const storiesDir = path.join(__dirname, "..", "stories");
77
+ if (!fs.existsSync(storiesDir)) fs.mkdirSync(storiesDir, { recursive: true });
78
+
146
79
  const port = Number(process.env.APP_PORT) || 7777;
147
80
  const server = serve({ fetch: app.fetch, port }, (info) => {
148
81
  console.log(`\n PlotLink OWS running at http://localhost:${info.port}\n`);
149
82
  });
150
83
 
151
- injectWebSocket(server);
84
+ // Terminal WebSocket: raw WS on /ws/terminal (bypasses Hono for raw PTY relay)
85
+ const { WebSocketServer } = await import("ws");
86
+ const wss = new WebSocketServer({ noServer: true });
87
+ // server from serve() IS an http.Server
88
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
89
+ (server as any).on("upgrade", (req: any, socket: any, head: any) => {
90
+ const url = new URL(req.url || "", `http://localhost:${port}`);
91
+ if (url.pathname === "/ws/terminal") {
92
+ // Auth check: verify token from query params
93
+ const wsToken = url.searchParams.get("token");
94
+ if (!wsToken) { socket.destroy(); return; }
95
+ import("./db").then(async ({ db }) => {
96
+ const session = await db.session.findUnique({ where: { token: wsToken } });
97
+ if (!session || session.expiresAt < new Date()) { socket.destroy(); return; }
98
+ const story = url.searchParams.get("story") || undefined;
99
+ const resume = url.searchParams.get("resume") === "true";
100
+ wss.handleUpgrade(req, socket, head, (ws) => {
101
+ attachTerminalWs(ws as unknown as WebSocket, story, resume);
102
+ });
103
+ }).catch(() => socket.destroy());
104
+ }
105
+ });
152
106
  }
153
107
 
154
108
  start().catch(console.error);
package/app/web/App.tsx CHANGED
@@ -3,8 +3,6 @@ import { Login } from "./components/Login";
3
3
  import { Setup } from "./components/Setup";
4
4
  import { Layout } from "./components/Layout";
5
5
 
6
- const API_BASE = "http://localhost:7777";
7
-
8
6
  export function App() {
9
7
  const [token, setToken] = useState<string | null>(() =>
10
8
  localStorage.getItem("ows-token"),
@@ -14,7 +12,7 @@ export function App() {
14
12
 
15
13
  useEffect(() => {
16
14
  // Check if passphrase is configured (first-run detection)
17
- fetch(`${API_BASE}/api/auth/status`)
15
+ fetch(`/api/auth/status`)
18
16
  .then((r) => r.json())
19
17
  .then((d) => setConfigured(d.configured))
20
18
  .catch(() => setConfigured(null));
@@ -26,7 +24,7 @@ export function App() {
26
24
  setChecking(false);
27
25
  return;
28
26
  }
29
- fetch(`${API_BASE}/api/auth/verify`, {
27
+ fetch(`/api/auth/verify`, {
30
28
  headers: { Authorization: `Bearer ${token}` },
31
29
  })
32
30
  .then((r) => {
@@ -44,7 +42,7 @@ export function App() {
44
42
 
45
43
  const handleLogin = async (passphrase: string): Promise<string | null> => {
46
44
  try {
47
- const res = await fetch(`${API_BASE}/api/auth/login`, {
45
+ const res = await fetch(`/api/auth/login`, {
48
46
  method: "POST",
49
47
  headers: { "Content-Type": "application/json" },
50
48
  body: JSON.stringify({ passphrase }),
@@ -61,7 +59,7 @@ export function App() {
61
59
 
62
60
  const handleSetup = async (passphrase: string): Promise<string | null> => {
63
61
  try {
64
- const res = await fetch(`${API_BASE}/api/auth/setup`, {
62
+ const res = await fetch(`/api/auth/setup`, {
65
63
  method: "POST",
66
64
  headers: { "Content-Type": "application/json" },
67
65
  body: JSON.stringify({ passphrase }),