Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions csv-table-hub-main/src/__tests__/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,26 @@ describe("requireSession (route guard)", () => {
expect(() => requireSession()).not.toThrow();
});
});

describe("API unreachable (#118)", () => {
const down = () => vi.fn(async () => { throw new TypeError("Failed to fetch"); });

it("sign-in says the API can't be reached instead of 'Failed to fetch'", async () => {
vi.stubGlobal("fetch", down());
await expect(signIn("acme", "alice", "pw")).rejects.toThrow(/Can't reach the API at .*Is it running\?/);
expect(getSession()).toBeNull();
});

it("other API calls say the same and keep the session", async () => {
store(session());
vi.stubGlobal("fetch", down());
await expect(listCsvFiles()).rejects.toThrow(/Can't reach the API/);
expect(getSession()?.token).toBe("tok-123");
});

it("a cancelled request is not reported as the API being down", async () => {
const abort = new DOMException("The operation was aborted.", "AbortError");
vi.stubGlobal("fetch", vi.fn(async () => { throw abort; }));
await expect(signIn("acme", "alice", "pw")).rejects.toBe(abort);
});
});
4 changes: 2 additions & 2 deletions csv-table-hub-main/src/lib/csv.functions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { z } from "zod";
import type { QueryClient } from "@tanstack/react-query";
import { apiBase, authHeaders, clearSession } from "@/lib/session";
import { apiBase, authHeaders, callApi, clearSession } from "@/lib/session";

const columnTypeSchema = z.enum(["int8", "numeric", "date", "timestamptz", "boolean", "text"]);
export type ColumnType = z.infer<typeof columnTypeSchema>;
Expand Down Expand Up @@ -102,7 +102,7 @@ const previewRowsSchema = z.object({
});

async function apiFetch<T>(path: string, schema: z.ZodType<T>, options?: RequestInit): Promise<T> {
const response = await fetch(`${apiBase}${path}`, {
const response = await callApi(`${apiBase}${path}`, {
...options,
headers: {
"content-type": "application/json",
Expand Down
19 changes: 16 additions & 3 deletions csv-table-hub-main/src/lib/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ const KEY = "te.session";

export const apiBase = (import.meta.env.VITE_API_BASE ?? "").replace(/\/$/, "");

/** fetch() that says when the API can't be reached (#118), instead of the
* browser's bare "Failed to fetch". Aborts (cancelled uploads) pass through. */
export async function callApi(url: string, init?: RequestInit): Promise<Response> {
try {
return await fetch(url, init);
} catch (err) {
if (err instanceof TypeError) {
throw new Error(`Can't reach the API at ${apiBase || "this site"}. Is it running?`);
}
throw err;
}
}

export function getSession(now: Date = new Date()): Session | null {
try {
const raw = sessionStorage.getItem(KEY);
Expand All @@ -43,7 +56,7 @@ export function clearSession(): void {

/** Sign in with a local account. Throws with the server's message on failure. */
export async function signIn(tenant: string, username: string, password: string): Promise<Session> {
const res = await fetch(`${apiBase}/api/auth/login`, {
const res = await callApi(`${apiBase}/api/auth/login`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ tenant, username, password }),
Expand Down Expand Up @@ -72,7 +85,7 @@ export async function signOut(): Promise<void> {
const s = getSession();
clearSession();
if (s) {
await fetch(`${apiBase}/api/auth/logout`, {
await callApi(`${apiBase}/api/auth/logout`, {
method: "POST",
headers: { Authorization: `Bearer ${s.token}` },
}).catch(() => undefined);
Expand All @@ -92,7 +105,7 @@ export function requireSession(): void {

/** Change your own password (#117). This session stays; the server ends all others. */
export async function changePassword(currentPassword: string, newPassword: string): Promise<void> {
const res = await fetch(`${apiBase}/api/auth/password`, {
const res = await callApi(`${apiBase}/api/auth/password`, {
method: "POST",
headers: { "content-type": "application/json", ...authHeaders() },
body: JSON.stringify({ currentPassword, newPassword }),
Expand Down
3 changes: 2 additions & 1 deletion docs/QUICKSTART.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,10 @@ Restart-Service postgresql-x64-18
|---|---|---|
| `password authentication failed` | Wrong `PGPASSWORD` | Reset password using admin steps above |
| `Connection refused` on port 5433 | PG 18 not running | Open Services → start `postgresql-x64-18` |
| `alembic.ini not found` on API start | Expected — no Alembic in this repo | Safe to ignore |
| Frontend shows "This page didn't load" | API not running | Start the API first (Terminal 1) |
| `ERR_CONNECTION_REFUSED` in browser console | API stopped | Restart `uvicorn` |
| "Can't reach the API at http://localhost:8000. Is it running?" | API not running | Start it with `.\scripts\start-api.ps1` |
| `start-api.ps1` says "Port 8000 is already in use by PID …" | An older API is still running, maybe in a window you've forgotten (#118) | Close that window, or run the `taskkill` line it prints, then start again |
| `npm run dev` fails with "Missing script" | Wrong directory | Must run from `csv-table-hub-main\`, not repo root |
| Deployment shows `[✗] failed for dev` | DB already exists | False positive — run `bash build/deploy_all.sh dev` to confirm |

Expand Down
14 changes: 14 additions & 0 deletions scripts/start-api.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ if (-not $env:API_KEY) {
Write-Host "API_KEY not set - every endpoint is unauthenticated (fine for local dev)." -ForegroundColor Yellow
}

# #118: refuse to start a second API. Uvicorn can print "running" even when an
# older API already holds the port, and then every request goes to the old one.
$busy = Get-NetTCPConnection -LocalPort 8000 -State Listen -ErrorAction SilentlyContinue
if ($busy) {
foreach ($b in $busy) {
$p = Get-Process -Id $b.OwningProcess -ErrorAction SilentlyContinue
Write-Host ("Port 8000 is already in use by PID {0} ({1}, started {2})." -f $b.OwningProcess, $p.ProcessName, $p.StartTime) -ForegroundColor Red
}
Write-Host "Another API is probably still running. Close its window, or stop it with:" -ForegroundColor Red
Write-Host (" taskkill /PID {0} /T /F" -f $busy[0].OwningProcess) -ForegroundColor Red
exit 1
}

Set-Location (Join-Path $PSScriptRoot "..")
$host.UI.RawUI.WindowTitle = "API (port 8000)"
Write-Host "API starting on http://localhost:8000 (docs: /docs)" -ForegroundColor Green
python -m uvicorn api.main:app --reload --port 8000
1 change: 1 addition & 0 deletions scripts/start-frontend.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ if (-not (Test-Path "node_modules")) {
npm install
}

$host.UI.RawUI.WindowTitle = "Frontend (port 8080)"
Write-Host "Frontend starting on http://localhost:8080" -ForegroundColor Green
npm run dev
Loading