Phase goal: Reuse the minimal GoQueue / SpringQueue (base) React-TS dashboard, but adapt it to PyQueue’s stricter, more explicit FastAPI contract (schemas, enums, DI-driven service wiring).
Key theme: Backend is the source of truth — the frontend “bends” to match PyQueue’s request/response shapes and vocabulary.
- Integrating the existing “Queue Dashboard” frontend (Vite + React + TS) against the PyQueue backend API.
- Hardening the API boundary so the frontend consumes exact DTOs returned by FastAPI.
- Cleaning up mismatches that were “loose” in earlier projects (raw strings, camelCase, implied semantics).
- A UI redesign or a PyQueuePro frontend build.
- Deep UX improvements, filtering systems, or advanced visualizations.
- Persistence, observability, multi-tenant rooms, auth, etc. (those belong to later phases / PyQueuePro).
PyQueue was reorganized into a monorepo to cleanly separate concerns:
/PyQueue-Backend
- /enums
- /models
- /schemas
- /system
- main.py
/PyQueue-Frontend
- /src
- /components
-- JobDisplay.tsx
-- JobsList.tsx
-- LoadingSpinner.tsx
- /utility
-- api.ts
-- types.ts
- App.tsx
- App.css
- main.tsx
Reasoning: This separation clarifies that:
- Backend owns the contract and runtime logic.
- Frontend is an external client consuming that contract.
- Each side can be hosted independently (Railway backend, Netlify frontend) with environment-driven configuration.
Earlier projects (GoQueue / SpringQueue base) were more permissive:
- task types were passed as raw strings
- response shapes often matched UI preferences
- enum vocabulary and casing were sometimes implicit
PyQueue is more explicit:
- FastAPI schemas define the public contract (
TaskResponse) - internal model fields differ from external API fields
- enum values are canonical, stable strings
POST /api/enqueueGET /api/jobsGET /api/jobs/{job_id}POST /api/jobs/{job_id}/retryDELETE /api/jobs/{job_id}POST /api/clear
In PyQueue, the API contract is backend-defined and FastAPI-native, so responses favor snake_case and explicit serialization choices (ISO timestamps).
Example:
- Frontend (old):
createdAt - Backend (PyQueue):
created_at(ISO-8601 string)
The fix: Frontend types and component reads were updated to match the exact backend DTO.
PyQueue uses enums for TaskType and TaskStatus. The backend emits canonical enum values:
- Status values:
"QUEUED","INPROGRESS","COMPLETED","FAILED" - Type values:
"EMAIL","REPORT","DATACLEANUP","SMS","NEWSLETTER","TAKESLONG","FAIL","FAILABS","TEST"
Earlier projects used UI-friendly strings (e.g., "fail-absolute", "data-cleanup", "failed"), which diverged from PyQueue’s contract.
The fix: Introduce a translation layer in api.ts that maps UI values to backend enum values, while keeping backend vocabulary strict.
In GoQueue and SpringQueue base, the frontend’s Task shape included UI-driven fields and casing conventions. PyQueue formalized the contract, so the frontend had to stop “inventing” types and instead reflect the wire format.
export interface Task {
id: string;
payload: string;
type: string; // backend enum value (e.g. "EMAIL")
status: string; // backend enum value (e.g. "FAILED")
attempts: number;
max_retries: number;
created_at: string; // ISO-8601 timestamp (PyQueue-specific)
}Both work in TypeScript, but interface communicates:
- “this is a stable DTO / contract shape”
- easier future extension via
extends(e.g.,TaskView extends Task { ... })
This matches the intent of a backend-defined schema.
api.ts became the formal boundary adapter between UI and backend, responsible for:
- translating UI-friendly inputs into backend-valid request payloads
- preserving backend responses verbatim (no implicit reshaping)
- ensuring the backend remains the source of truth
The UI still uses dropdown values like "fail-absolute" for human readability. PyQueue expects "FAILABS". The mapping resolves that discrepancy without weakening backend strictness.
const TASK_TYPE_MAP: Record<string, string> = {
"email": "EMAIL",
"report": "REPORT",
"data-cleanup": "DATACLEANUP",
"sms": "SMS",
"newsletter": "NEWSLETTER",
"takes-long": "TAKESLONG",
"fail": "FAIL",
"fail-absolute": "FAILABS",
};Enqueue now uses:
body: JSON.stringify({
payload,
t_type: backendType, // backend enum value
})Important: t_type is the backend request key, matching EnqueueRequest in FastAPI.
Phase 4’s component-level work largely fell into two buckets:
Earlier frontends checked status like:
"failed"(lowercase)
PyQueue emits:
"FAILED"(uppercase enum value)
So checks must become:
job?.status === "FAILED"This is crucial because:
- it preserves backend ownership of meaning
- avoids accidental “semantic drift” across clients
PyQueue returns:
created_atas ISO-8601 string
The frontend should:
- keep
created_atuntouched intypes.ts - format only at render time (presentation layer)
Example (used in JobDisplay):
<div>
<b>Created At:</b>{" "}
{job ? new Date(job.created_at).toLocaleString() : ""}
</div>Why this matters: It keeps the API boundary clean, and the UI flexible.
This component had the highest mismatch risk because it reads many fields and contained status-driven behavior (Retry button).
Key updates:
-
updated field reads:
maxRetries→max_retriescreatedAt→created_at(formatted)
-
updated Retry visibility:
"failed"→"FAILED"
It also aligned to the updated API contract that exposes:
attemptsmax_retries
JobsList mostly displays:
idstatus
Because it doesn’t interpret status semantics or format timestamps, it required little/no changes beyond type alignment.
App.tsx primarily:
- orchestrates state and event handlers
- calls
api.ts - passes selected jobs into children
Most meaningful changes were copy / branding (later polish), not core integration.
During Phase 4, a prior deviation to not expose attempts and max_retries externally was "walked back" (now they are exposed in API responses, etc):
attemptsmax_retries
Even though this minimal dashboard is intentionally simple, PyQueue is meant to lead toward PyQueuePro, where the UI is explicitly introspection-focused (similar to SpringQueuePro’s “inspect task details”).
So these were updated:
TaskResponseto include these fieldstask_to_responsemapper to serialize them- frontend
Taskinterface to match
This preserved the backend-first rule:
- frontend does not invent attempts/max retries
- backend explicitly chooses to expose them in the contract
Because Phase 4 is specifically about integration and hosting readiness, environment-driven configuration on both sides were configured.
In api.ts:
const API_BASE = import.meta.env.VITE_API_BASE.replace(/\/+$/, "");Local .env example:
VITE_API_BASE=http://127.0.0.1:8000
Netlify will set VITE_API_BASE to the Railway backend URL.
FastAPI does not automatically read .env, so the backend must explicitly load env vars in local development, and Railway will inject them in production.
Key usage: controlling allow_origins for CORS via env var(s).
Typical pattern:
- define
FRONTEND_ORIGIN(orFRONTEND_ORIGINS) - use it to set
allow_origins=[...]
Principle: Backend owns who may call it — the frontend does not “decide” CORS.
Styling in Phase 4 was intentionally minor compared to the core work, but was present to signify a degree of separation from GoQueue and SpringQueue (base edition). Here is what it encompassed):
-
move toward a FastAPI/teal accent (
#009688) -
lighten the palette to be more vibrant (less “dark dashboard”)
-
optionally introduce CSS variables for a small color system:
- background
- accent
- hover
- shadow
At the end of Phase 4, PyQueue has:
- A working Vite + React + TypeScript dashboard
- Strict contract alignment with the FastAPI backend
- Enum vocabulary mismatch resolved cleanly in the boundary layer (
api.ts) - DTO correctness enforced via
types.ts - Status handling and timestamp formatting fixed (B3/B4)
- Environment-variable driven configuration for deployment
- A coherent “backend-first” integration story consistent with SpringQueuePro practices
-
DTOs are contracts, not conveniences
- PyQueue’s schemas forced a clean split between internal models and API shape.
-
A boundary adapter is non-negotiable
api.tsis where UI-friendly inputs become backend-valid requests.
-
Enums demand vocabulary discipline
- A backend emitting
"FAILED"means the frontend must not assume"failed".
- A backend emitting
-
Timestamps are a wire format problem + a UI formatting problem
- Keep ISO strings in the contract; format only at render time.
-
Deployment forces correctness
- env vars + CORS become first-class concerns once frontend and backend are hosted separately.