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
1 change: 1 addition & 0 deletions .changelog/NEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@

- **[issue-2832] Split the boot-schema DDL into per-domain modules** — the ~1265-line inline `CREATE TABLE`/`CREATE INDEX`/trigger block in `ensureSchemaImpl()` (`server/lib/db.js`) is extracted into per-domain modules under `server/lib/db/schema/` (catalog, media, universes, writers-room, pipeline, privacy, tribe, audit, …), each exporting a statement array; `ensureSchemaImpl` is now a thin composer. Statement text and order are byte-identical, so the composed schema is unchanged.
- **[issue-2834] Split the ~1990-line `VideoGen.jsx` into hooks + subcomponents** — the client-side batch-queue orchestration moves into `useVideoGenQueue`, the pure model-memory / FFLF frame-budget / mode-compat helpers into `lib/videoGenParams.js`, and four presentational blocks into `components/videoGen/` (`RuntimeFingerprint`, `ModelRepairBanner`, `VideoPreviewPanel`, `VideoGenGallery`). The inline runtime-status `fetch()` now routes through a `getVideoGenRuntimeStatus` service wrapper, and the Seed input gains a proper `<label htmlFor>`/`id` pairing. Behavior and deep-link routing are unchanged.
- **[issue-2838] Route inline `fetch('/api/...')` calls through the client service layer** — the Prompt Manager's 13+ raw prompt/provider calls move into a new `services/apiPrompts.js` (stages, variables, job-skills), and the stray inline `/api/` fetches in `ImageGen`, `LocalSetupPanel`, `StagePromptModelPicker`, and `GsdTab` route through existing/new `apiX.js` wrappers. `useTheme`'s settings read/write now go through `apiSystem.getSettings`/`updateSettings` (silent, so the local-storage fallback stays the only error surface), which clears the last inline `/api/` fetch outside `services/`. Behavior-preserving; VideoGen's site was already routed through a service wrapper by the VideoGen split (#2834).
- **[issue-2835] Split the ~2875-line `ArcCanvas.jsx` into per-component files** — each of the 32 subcomponents/helpers (ArcHeader, EditorialRoadmapPanel, SeasonRow, IssueRow, VolumeCoversPanel, …) now lives in its own file under `client/src/components/pipeline/arcCanvas/`, with the shared severity-color map in `arcCanvas/shared.js`. `ArcCanvas.jsx` (default export) is now a thin 122-line composer and still re-exports `ArcRoadmapChart` so its public import path is unchanged. Pure mechanical, behavior-preserving refactor — no rendered-output change.
19 changes: 6 additions & 13 deletions client/src/components/apps/tabs/GsdTab.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -253,25 +253,18 @@ export default function GsdTab({ appId, repoPath }) {
setLoading(true);

// Fetch GSD status from documents endpoint (silent, no toast)
const docsResp = await fetch(`/api/apps/${appId}/documents`).catch(() => null);
const docsData = docsResp?.ok ? await docsResp.json().catch(() => null) : null;
const docsData = await api.getAppDocuments(appId, { silent: true }).catch(() => null);
const gsd = docsData?.gsd || {};
setGsdStatus(gsd);

// Only fetch project data if we have a roadmap + state (full project)
if (gsd.hasRoadmap && gsd.hasState) {
const [projectResp, phasesResp] = await Promise.all([
fetch(`/api/cos/gsd/projects/${appId}`).catch(() => null),
fetch(`/api/cos/gsd/projects/${appId}/phases`).catch(() => null),
const [projectData, phasesData] = await Promise.all([
api.getGsdProject(appId, { silent: true }).catch(() => null),
api.getGsdPhases(appId, { silent: true }).catch(() => null),
]);
if (projectResp?.ok) {
const data = await projectResp.json().catch(() => null);
setProject(data);
}
if (phasesResp?.ok) {
const data = await phasesResp.json().catch(() => null);
setPendingActions(data?.pendingActions || []);
}
if (projectData) setProject(projectData);
if (phasesData) setPendingActions(phasesData.pendingActions || []);
} else {
setProject(null);
setPendingActions([]);
Expand Down
28 changes: 9 additions & 19 deletions client/src/components/settings/LocalSetupPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import BrailleSpinner from '../BrailleSpinner';
import { usePrevious } from '../../hooks/usePrevious.js';
import { useInstallStream } from '../../hooks/useInstallStream.js';
import useMounted from '../../hooks/useMounted.js';
import { checkImageGenSetup, detectImageGenPython, createImageGenVenv } from '../../services/api';

export default function LocalSetupPanel({ pythonPath, onPythonPathChange, onPackagesChanged }) {
const [detecting, setDetecting] = useState(false);
Expand Down Expand Up @@ -54,10 +55,9 @@ export default function LocalSetupPanel({ pythonPath, onPythonPathChange, onPack
checkAbortRef.current = controller;
setChecking(true);
try {
const res = await fetch(`/api/image-gen/setup/check?pythonPath=${encodeURIComponent(path)}`, { signal: controller.signal });
const data = await checkImageGenSetup({ pythonPath: path, signal: controller.signal });
if (!mountedRef.current) return;
if (!res.ok) { setCheck(null); return; }
setCheck(await res.json());
setCheck(data);
} catch (err) {
// Aborted (unmount or superseded) — leave state alone.
if (err?.name === 'AbortError') return;
Expand Down Expand Up @@ -120,9 +120,9 @@ export default function LocalSetupPanel({ pythonPath, onPythonPathChange, onPack
const handleDetect = async () => {
setDetecting(true);
try {
const res = await fetch('/api/image-gen/setup/python');
if (!res.ok) { toast.error('Detection failed'); return; }
const { path } = await res.json();
const result = await detectImageGenPython();
if (!result) { toast.error('Detection failed'); return; }
const { path } = result;
if (path) {
onPythonPathChange(path);
toast.success(`Detected ${path}`);
Expand All @@ -147,21 +147,11 @@ export default function LocalSetupPanel({ pythonPath, onPythonPathChange, onPack
const handleCreateVenv = async () => {
setCreatingVenv(true);
try {
const res = await fetch('/api/image-gen/setup/create-venv', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
if (!res.ok) {
const json = await res.json().catch(() => ({}));
toast.error(json.error || 'Venv creation failed');
return;
}
const { pythonPath: venvPython } = await res.json();
const { pythonPath: venvPython } = await createImageGenVenv();
onPythonPathChange(venvPython);
toast.success(`Created venv at ${venvPython}`);
} catch {
toast.error('Failed to create venv');
} catch (err) {
toast.error(err.message || 'Failed to create venv');
} finally {
// Always clear so a fetch reject doesn't leave the button disabled.
setCreatingVenv(false);
Expand Down
19 changes: 10 additions & 9 deletions client/src/components/writers-room/StagePromptModelPicker.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
TIMEOUT_INPUT_MAX_MS,
TIMEOUT_INPUT_STEP_MS,
} from '../../utils/formatters';
import { getPrompt, savePrompt } from '../../services/apiPrompts';
import { getProviders } from '../../services/apiProviders';

/**
* Inline picker for a prompt stage's provider+model. Mirrors the Tier/Specific
Expand All @@ -30,8 +32,8 @@ export default function StagePromptModelPicker({ stageName, label = 'Stage LLM',
const controller = new AbortController();
const { signal } = controller;
Promise.all([
fetch(`/api/prompts/${encodeURIComponent(stageName)}`, { signal }).then((r) => (r.ok ? r.json() : null)).catch(() => null),
fetch('/api/providers', { signal }).then((r) => (r.ok ? r.json() : null)).catch(() => null),
getPrompt(stageName, { signal, silent: true }).catch(() => null),
getProviders({ signal, silent: true }).catch(() => null),
]).then(([s, p]) => {
if (cancelled) return;
// Normalize stage.timeout through parseTimeoutMs so a legacy on-disk
Expand Down Expand Up @@ -65,17 +67,16 @@ export default function StagePromptModelPicker({ stageName, label = 'Stage LLM',
if ('provider' in next) body.provider = next.provider ?? null;
if ('model' in next) body.model = next.model;
if ('timeout' in next) body.timeout = next.timeout;
const res = await fetch(`/api/prompts/${encodeURIComponent(stageName)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
let errMsg = null;
const ok = await savePrompt(stageName, body, { silent: true })
.then(() => true)
.catch((err) => { errMsg = err.message; return false; });
setSaving(false);
if (!res.ok) {
if (!ok) {
// Roll back the optimistic update so the picker UI doesn't show a
// provider/model that wasn't actually persisted server-side.
setStage(prevStage);
toast.error(`Failed to save ${label}: ${await res.text().catch(() => res.statusText)}`);
toast.error(`Failed to save ${label}: ${errMsg}`);
}
};

Expand Down
19 changes: 11 additions & 8 deletions client/src/hooks/useTheme.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
TIME_OF_DAY_AUTO_EVENT as CITY_TIME_OF_DAY_AUTO_EVENT,
} from './useCitySettings';
import { safeReadStorage, safeWriteStorage } from '../lib/safeStorage.js';
import { getSettings, updateSettings } from '../services/apiSystem.js';

const STORAGE_KEY = 'portos-theme';

Expand Down Expand Up @@ -61,8 +62,9 @@ export default function useTheme() {

useEffect(() => {
const controller = new AbortController();
fetch('/api/settings', { signal: controller.signal })
.then(r => r.ok ? r.json() : null)
// Silent — a failed read just falls back to localStorage (handled below),
// so the request() helper must not toast on top of our own warn.
getSettings({ silent: true, signal: controller.signal })
.then(settings => {
if (userPickedRef.current) return;
const serverTheme = settings?.theme ? normalizeThemeId(settings.theme) : null;
Expand All @@ -77,7 +79,9 @@ export default function useTheme() {
}
})
.catch((err) => {
if (err.name === 'AbortError') return;
// request() collapses an aborted fetch into a generic error, so check
// the controller too — an unmount/StrictMode remount isn't a failure.
if (controller.signal.aborted || err.name === 'AbortError') return;
console.warn(`⚠️ Theme fetch failed, using localStorage fallback: ${err.message}`);
});
return () => controller.abort();
Expand All @@ -92,11 +96,10 @@ export default function useTheme() {
setThemeId(normalized);
safeWriteStorage(STORAGE_KEY, normalized);
resetCityTimeOfDayOverride();
fetch('/api/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ theme: normalized }),
}).catch(() => console.warn('Theme sync to server failed'));
// Silent — the theme is already applied locally; a failed sync only warrants
// a console warning, not a toast on every theme switch.
updateSettings({ theme: normalized }, { silent: true })
.catch(() => console.warn('⚠️ Theme sync to server failed'));
}, []);

const toggleMode = useCallback(() => {
Expand Down
17 changes: 5 additions & 12 deletions client/src/pages/ImageGen.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@ import { useImageGenProgress } from '../hooks/useImageGenProgress';
import { useMediaJobSse } from '../hooks/useMediaJobSse';
import { useModelDownloadStatus } from '../hooks/useModelDownloadStatus';
import {
getImageGenStatus, generateImage, listImageModels, listLorasFull, listImageGallery,
getImageGenStatus, generateImage, generateImageMultipart, listImageModels, listLorasFull, listImageGallery,
cancelImageGen, deleteImage, setImageHidden, cleanGalleryImage, getActiveImageJob, getSettings,
buildFormData, listMediaJobs, regenerateGalleryImage, getRegenAvailability, removeImageWatermark,
getFlux2Status, getHfTokenStatus,
} from '../services/api';

// Multi-reference editing (FLUX.2 only) — 4 fixed slots, each carrying an
Expand Down Expand Up @@ -617,16 +618,13 @@ export default function ImageGen() {
const currentCompatKey = loraCompatKey(currentModel);

const refreshFlux2Status = useCallback((signal) => {
const qs = modelId ? `?modelId=${encodeURIComponent(modelId)}` : '';
return fetch(`/api/image-gen/setup/flux2-status${qs}`, { signal })
.then((r) => r.ok ? r.json() : null)
return getFlux2Status({ modelId, signal })
.then((s) => { if (s) setFlux2Status(s); })
.catch(() => {});
}, [modelId]);

const refreshHfTokenStatus = useCallback((signal) => {
return fetch('/api/image-gen/setup/hf-token-status', { signal })
.then((r) => r.ok ? r.json() : null)
return getHfTokenStatus({ signal })
.then((s) => { if (s) setHfTokenPresent(!!s.hfTokenPresent); })
.catch(() => {});
}, []);
Expand Down Expand Up @@ -763,12 +761,7 @@ export default function ImageGen() {
referenceStrengths: populatedRefs.map((s) => s.strength),
} : {};
const fd = buildFormData({ ...payload, ...initFields, ...refFields });
const res = await fetch('/api/image-gen/generate', { method: 'POST', body: fd });
if (!res.ok) {
const body = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
throw new Error(body.error || `HTTP ${res.status}`);
}
return { payload, data: await res.json() };
return { payload, data: await generateImageMultipart(fd, { silent: true }) };
}
return { payload, data: await generateImage(payload, { silent: true }) };
};
Expand Down
Loading