diff --git a/web/src/routes/api/admin/ai/providers/swap/+server.ts b/web/src/routes/api/admin/ai/providers/swap/+server.ts new file mode 100644 index 0000000..d24451a --- /dev/null +++ b/web/src/routes/api/admin/ai/providers/swap/+server.ts @@ -0,0 +1,46 @@ +import { json, error } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { sql } from '$lib/server/db'; + +/** POST — bytt prioritet mellom to providers (atomisk) */ +export const POST: RequestHandler = async ({ request, locals }) => { + if (!locals.workspace || !locals.user) error(401); + + const { id_a, id_b } = await request.json(); + if (!id_a || !id_b) error(400, 'id_a og id_b kreves'); + + // Atomisk swap via CTE — unngår UNIQUE-constraint-brudd + const rows = await sql` + WITH swap AS ( + SELECT + a.id AS id_a, a.priority AS pri_a, + b.id AS id_b, b.priority AS pri_b + FROM ai_model_providers a, ai_model_providers b + WHERE a.id = ${id_a}::uuid AND b.id = ${id_b}::uuid + AND a.alias_id = b.alias_id + ), + update_a AS ( + UPDATE ai_model_providers SET priority = -1, updated_at = now() + WHERE id = (SELECT id_a FROM swap) + ), + update_b AS ( + UPDATE ai_model_providers SET priority = (SELECT pri_a FROM swap), updated_at = now() + WHERE id = (SELECT id_b FROM swap) + ) + UPDATE ai_model_providers SET priority = (SELECT pri_b FROM swap), updated_at = now() + WHERE id = (SELECT id_a FROM swap) + RETURNING id + `; + + if (rows.length === 0) error(400, 'Kunne ikke bytte — sjekk at begge tilhører samme alias'); + + // Returner oppdaterte providers for aliaset + const updated = await sql` + SELECT id, alias_id, priority, litellm_model, api_key_env, is_active, extra_params + FROM ai_model_providers + WHERE alias_id = (SELECT alias_id FROM ai_model_providers WHERE id = ${id_a}::uuid) + ORDER BY priority ASC + `; + + return json(updated); +}; diff --git a/web/src/routes/server-admin/ai/+page.svelte b/web/src/routes/server-admin/ai/+page.svelte index 3b1ca96..7e48e46 100644 --- a/web/src/routes/server-admin/ai/+page.svelte +++ b/web/src/routes/server-admin/ai/+page.svelte @@ -479,6 +479,31 @@ } } + async function swapProviders(providerA: Provider, providerB: Provider) { + saving = providerA.id; + errorMsg = ''; + try { + const res = await fetch('/api/admin/ai/providers/swap', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id_a: providerA.id, id_b: providerB.id }) + }); + if (!res.ok) throw new Error('Feil ved bytte'); + const updated: Provider[] = await res.json(); + // Oppdater providers-listen med nye prioriteter + for (const u of updated) { + const existing = providers.find(p => p.id === u.id); + if (existing) existing.priority = u.priority; + } + providers = [...providers]; + markSaved(providerA.id); + } catch { + errorMsg = 'Kunne ikke bytte rekkefølge'; + } finally { + saving = null; + } + } + async function toggleProvider(provider: Provider) { saving = provider.id; errorMsg = ''; @@ -894,10 +919,26 @@ {#if expandedAlias === alias.id}
- {#each ap as provider (provider.id)} + {#each ap as provider, idx (provider.id)} {@const keyDisabled = !isKeyEnabled(provider.api_key_env)}
- #{provider.priority} + + + + + + #{provider.priority} + {provider.litellm_model} {provider.api_key_env}{#if keyDisabled} (av){/if} @@ -1560,7 +1601,35 @@ .col-pri { color: #8b92a5; font-size: 0.75rem; - width: 30px; + width: 60px; + display: flex; + align-items: center; + gap: 0.25rem; + } + + .pri-arrows { + display: flex; + flex-direction: column; + gap: 0; + } + + .pri-btn { + background: none; + border: none; + color: #6c7086; + font-size: 0.55rem; + cursor: pointer; + padding: 0; + line-height: 1; + } + + .pri-btn:hover:not(:disabled) { + color: #cdd6f4; + } + + .pri-btn:disabled { + opacity: 0.2; + cursor: default; } .col-model {