fix: chunk upload race — always return the completed response with filepath; add campaign name to setup wizard
All checks were successful
Build and Push / build (push) Successful in 1m14s

This commit is contained in:
KansaiGaijin
2026-07-30 15:49:21 +12:00
parent 24fd6f16d2
commit cef9178d8d
3 changed files with 38 additions and 4 deletions

View File

@@ -20,6 +20,9 @@ services:
# Set to "true" after onboarding completes.
NAT20_ONBOARDING_COMPLETED: "false"
# Campaign name is set during the setup wizard (step 2).
# You can also rename it later in Settings → Campaigns.
# ── Transcription ─────────────────────────────────────
NAT20_WHISPER_MODEL: medium
NAT20_WHISPER_COMPUTE_TYPE: int8

View File

@@ -100,6 +100,17 @@ export const campaignApi = {
};
export const api = {
// Campaign
updateCampaign: async (id: string, body: Partial<Pick<Campaign, 'name' | 'description'>>): Promise<Campaign> => {
const res = await fetch(`${BASE_URL}/campaigns/${encodeURIComponent(id)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error('Failed to update campaign');
return res.json();
},
// Settings
getSettings: async (): Promise<GlobalSettings> => {
const res = await fetch(`${BASE_URL}/settings`);
@@ -241,6 +252,7 @@ export const api = {
};
let lastResult: any = null;
let completedResult: any = null;
for (let i = 0; i < totalChunks; i += CONCURRENCY) {
const batch = [];
for (let j = i; j < Math.min(i + CONCURRENCY, totalChunks); j++) {
@@ -248,9 +260,12 @@ export const api = {
}
const results = await Promise.all(batch);
lastResult = results[results.length - 1];
if (!completedResult) {
completedResult = results.find(r => r.status === 'completed');
}
onProgress(Math.round((Math.min(i + CONCURRENCY, totalChunks) / totalChunks) * 100));
}
return lastResult;
return completedResult || lastResult;
},
// Files

View File

@@ -20,18 +20,22 @@ const DEFAULT_SETTINGS: Record<string, string> = {
export default function Setup({ onComplete }: { onComplete: () => void }) {
const [step, setStep] = useState(1);
const [campaignName, setCampaignName] = useState("Default Campaign");
const [settings, setSettings] = useState<Record<string, string>>(DEFAULT_SETTINGS);
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
// On first load, try to fetch existing settings from the "default" campaign
// (which is always created by init_db).
// On first load, try to fetch existing settings and name from the "default" campaign.
useEffect(() => {
api.getCampaignSettings('default')
.then(data => {
if (data) setSettings(prev => ({ ...prev, ...data }));
})
.catch(() => {});
fetch('/api/campaigns/default')
.then(r => r.ok ? r.json() : null)
.then(c => { if (c?.name) setCampaignName(c.name); })
.catch(() => {});
}, []);
const handleNext = () => setStep(prev => prev + 1);
@@ -41,6 +45,7 @@ export default function Setup({ onComplete }: { onComplete: () => void }) {
setSaving(true);
setError("");
try {
await api.updateCampaign('default', { name: campaignName });
await api.updateCampaignSettings('default', settings);
await api.updateSettings({ onboarding_completed: true });
onComplete();
@@ -112,7 +117,18 @@ export default function Setup({ onComplete }: { onComplete: () => void }) {
{step === 2 && (
<div className="space-y-6">
<div>
<h2 className="text-xl font-semibold mb-2">2. Summarization Engine (LLM)</h2>
<h2 className="text-xl font-semibold mb-2">Campaign Name</h2>
<p className="text-slate-400 text-sm mb-4">Give your campaign a name you can change it later in Settings.</p>
<input
type="text"
value={campaignName}
onChange={e => setCampaignName(e.target.value)}
placeholder="My Campaign"
className="w-full bg-slate-900 border border-slate-700 rounded px-3 py-2 text-white"
/>
</div>
<div className="border-t border-slate-700 pt-4">
<h2 className="text-xl font-semibold mb-2">Summarization Engine (LLM)</h2>
<p className="text-slate-400 text-sm mb-4">Choose where your post-transcription formatting and smart summaries are generated.</p>
</div>