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
10 changes: 5 additions & 5 deletions src/torq/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from datetime import datetime, timezone
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import FileResponse, StreamingResponse
from pydantic import BaseModel
from pydantic import BaseModel, Field

from torq.config import settings
from torq.db import models
Expand All @@ -19,13 +19,13 @@


class MachineIn(BaseModel):
id: str
model: str
location: str
id: str = Field(max_length=100)
model: str = Field(max_length=200)
location: str = Field(max_length=200)


class FaultIn(BaseModel):
fault_code: str
fault_code: str = Field(min_length=1, max_length=50)
machine: str = ""
context: str = ""
translate: bool = True
Expand Down
17 changes: 6 additions & 11 deletions src/torq/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,12 @@ def _ensure_llm_key(self):
if not self.llm_api_key:
self.llm_api_key = os.environ.get("OPENAI_API_KEY", "")
if not self.llm_api_key:
print("=" * 60)
print(" FATAL: No LLM API key found.")
print()
print(" Set LLM_API_KEY or OPENAI_API_KEY in your .env file")
print(" or export it as an environment variable.")
print()
print(" Example:")
print(" LLM_API_KEY=sk-... # DeepSeek (default)")
print(" OPENAI_API_KEY=sk-... # OpenAI-compatible")
print("=" * 60)
sys.exit(1)
import logging
logging.warning(
"No LLM API key found. "
"Set LLM_API_KEY or OPENAI_API_KEY in your .env file. "
"LLM-dependent features (diagnosis, translation) will fall back."
)
return self

# Vector DB (Qdrant)
Expand Down
5 changes: 4 additions & 1 deletion src/torq/dispatch/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ def _load_roster(shifts_file: Path | None = None) -> list[dict]:
shifts_file = shifts_file or settings.shifts_file
if not shifts_file.exists():
return []
return json.loads(shifts_file.read_text(encoding="utf-8"))
try:
return json.loads(shifts_file.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return []


def choose_technician(wo: WorkOrder, roster: list[dict] | None = None) -> dict | None:
Expand Down
25 changes: 25 additions & 0 deletions web/src/ErrorBoundary.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Component } from "react";

export default class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { error: null };
}

static getDerivedStateFromError(error) {
return { error };
}

render() {
if (this.state.error) {
return (
<div className="errorBoundary">
<h1>Something went wrong</h1>
<pre>{this.state.error.message}</pre>
<button onClick={() => window.location.reload()}>Reload page</button>
</div>
);
}
return this.props.children;
}
}
5 changes: 4 additions & 1 deletion web/src/main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App.jsx";
import ErrorBoundary from "./ErrorBoundary.jsx";
import { I18nProvider } from "./i18n";
import { ToastProvider } from "./toast.jsx";
import "./styles.css";
Expand All @@ -11,7 +12,9 @@ ReactDOM.createRoot(document.getElementById("root")).render(
<BrowserRouter>
<I18nProvider>
<ToastProvider>
<App />
<ErrorBoundary>
<App />
</ErrorBoundary>
</ToastProvider>
</I18nProvider>
</BrowserRouter>
Expand Down
11 changes: 11 additions & 0 deletions web/src/pages/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ function ActivityLog() {
/* ignore malformed frame */
}
});
es.onerror = () => setEntries((prev) => [...prev, { type: "disconnected", detail: "SSE lost" }]);
return () => {
alive = false;
es.close();
Expand Down Expand Up @@ -570,10 +571,14 @@ export default function Dashboard() {
const [machineDetail, setMachineDetail] = useState(null);
const [busy, setBusy] = useState(false);
const [loading, setLoading] = useState(true);
const [errored, setErrored] = useState(false);
const [statusFilter, setStatusFilter] = useState("");
const [searchQuery, setSearchQuery] = useState("");

const inflight = useRef(false);
const load = useCallback(async () => {
if (inflight.current) return;
inflight.current = true;
try {
const [m, p, a, ev, trend, fpm] = await Promise.all([
api.getMetrics(),
Expand All @@ -590,8 +595,12 @@ export default function Dashboard() {
setTrendData(trend);
setFpmData(fpm);
setLoading(false);
setErrored(false);
} catch (e) {
setLoading(false);
setErrored(true);
} finally {
inflight.current = false;
}
}, []);

Expand Down Expand Up @@ -657,6 +666,8 @@ export default function Dashboard() {
<p className={styles.sub}>{t("dashboard.subtitle")}</p>
</header>

{errored && <div className="dashboardError">Could not load data — check connection</div>}

<section className={styles.tiles}>
{loading ? (
<>
Expand Down
34 changes: 34 additions & 0 deletions web/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,37 @@ body {
animation: shimmer 1.5s ease-in-out infinite;
border-radius: 8px;
}

/* ── Error boundary ── */

.errorBoundary {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 2rem;
text-align: center;
}
.errorBoundary pre {
color: var(--text-secondary, #666);
max-width: 600px;
overflow-x: auto;
}
.errorBoundary button {
margin-top: 1rem;
padding: 0.6rem 1.5rem;
background: var(--accent, #2563eb);
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
}

/* ── Dashboard errored state ── */

.dashboardError {
text-align: center;
padding: 2rem;
color: var(--text-secondary, #666);
}
Loading