mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
feat(llm-playground): add production deployment with auth
- Add Dockerfile for multi-stage Docker build - Add mana-core-auth integration with login/register pages - Add auth store using Svelte 5 runes - Add protected route layout with auth guard - Add health endpoint for container health checks - Add runtime URL injection via hooks.server.ts - Add logout button to header - Update docker-compose.macmini.yml with llm-playground service - Update cloudflared-config.yml with playground.mana.how route - Update mana-llm CORS config for playground domain - Update generate-env.mjs with auth URL variable Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
8207d38ca5
commit
fdba0e3425
19 changed files with 859 additions and 577 deletions
|
|
@ -36,6 +36,12 @@ ingress:
|
|||
- hostname: nutriphi-api.mana.how
|
||||
service: http://localhost:3023
|
||||
|
||||
# LLM Services
|
||||
- hostname: playground.mana.how
|
||||
service: http://localhost:5190
|
||||
- hostname: llm.mana.how
|
||||
service: http://localhost:3025
|
||||
|
||||
# Monitoring & Tools
|
||||
- hostname: grafana.mana.how
|
||||
service: http://localhost:3100
|
||||
|
|
|
|||
|
|
@ -735,6 +735,37 @@ services:
|
|||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
# ============================================
|
||||
# LLM Playground (Web)
|
||||
# ============================================
|
||||
|
||||
llm-playground:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: services/llm-playground/Dockerfile
|
||||
container_name: llm-playground
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5190:5190"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=5190
|
||||
- PUBLIC_MANA_CORE_AUTH_URL=http://mana-core-auth:3001
|
||||
- PUBLIC_MANA_CORE_AUTH_URL_CLIENT=https://auth.mana.how
|
||||
- PUBLIC_MANA_LLM_URL=http://host.docker.internal:3025
|
||||
- PUBLIC_MANA_LLM_URL_CLIENT=https://llm.mana.how
|
||||
depends_on:
|
||||
mana-core-auth:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:5190/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=true"
|
||||
|
||||
# ============================================
|
||||
# Monitoring Stack
|
||||
# ============================================
|
||||
|
|
|
|||
867
pnpm-lock.yaml
generated
867
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -639,6 +639,7 @@ const APP_CONFIGS = [
|
|||
path: 'services/llm-playground/.env',
|
||||
vars: {
|
||||
PUBLIC_MANA_LLM_URL: (env) => env.MANA_LLM_URL || 'http://localhost:3025',
|
||||
PUBLIC_MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL || 'http://localhost:3001',
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
|||
9
services/llm-playground/.dockerignore
Normal file
9
services/llm-playground/.dockerignore
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
node_modules
|
||||
.svelte-kit
|
||||
.git
|
||||
*.md
|
||||
.env*
|
||||
!.env.example
|
||||
dist
|
||||
coverage
|
||||
.DS_Store
|
||||
69
services/llm-playground/Dockerfile
Normal file
69
services/llm-playground/Dockerfile
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
# Build arguments for SvelteKit static env vars
|
||||
ARG PUBLIC_MANA_CORE_AUTH_URL=http://mana-core-auth:3001
|
||||
ARG PUBLIC_MANA_LLM_URL=http://mana-llm:3025
|
||||
|
||||
# Set as environment variables for build
|
||||
ENV PUBLIC_MANA_CORE_AUTH_URL=$PUBLIC_MANA_CORE_AUTH_URL
|
||||
ENV PUBLIC_MANA_LLM_URL=$PUBLIC_MANA_LLM_URL
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy root workspace files
|
||||
COPY pnpm-workspace.yaml ./
|
||||
COPY package.json ./
|
||||
COPY pnpm-lock.yaml ./
|
||||
|
||||
# Copy shared packages needed by llm-playground
|
||||
COPY packages/shared-auth ./packages/shared-auth
|
||||
|
||||
# Copy llm-playground service
|
||||
COPY services/llm-playground ./services/llm-playground
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Build shared packages that need building
|
||||
WORKDIR /app/packages/shared-auth
|
||||
RUN pnpm build || true
|
||||
|
||||
# Build the web app
|
||||
WORKDIR /app/services/llm-playground
|
||||
RUN pnpm exec svelte-kit sync
|
||||
RUN pnpm build
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
# Keep same directory structure as builder so pnpm symlinks resolve correctly
|
||||
WORKDIR /app/services/llm-playground
|
||||
|
||||
# Copy the pnpm store that symlinks point to (at /app/node_modules/.pnpm)
|
||||
COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm
|
||||
|
||||
# Copy the app's node_modules (contains symlinks to the pnpm store)
|
||||
COPY --from=builder /app/services/llm-playground/node_modules ./node_modules
|
||||
|
||||
# Copy built application
|
||||
COPY --from=builder /app/services/llm-playground/build ./build
|
||||
COPY --from=builder /app/services/llm-playground/package.json ./
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5190
|
||||
|
||||
# Set environment variables
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=5190
|
||||
ENV HOST=0.0.0.0
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:5190/health || exit 1
|
||||
|
||||
# Run the app
|
||||
CMD ["node", "build"]
|
||||
|
|
@ -21,6 +21,7 @@
|
|||
"vite": "^7.1.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@manacore/shared-auth": "workspace:*",
|
||||
"marked": "^17.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
services/llm-playground/src/app.d.ts
vendored
5
services/llm-playground/src/app.d.ts
vendored
|
|
@ -8,6 +8,11 @@ declare global {
|
|||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
|
||||
interface Window {
|
||||
__PUBLIC_MANA_CORE_AUTH_URL__?: string;
|
||||
__PUBLIC_MANA_LLM_URL__?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
|
|
|
|||
18
services/llm-playground/src/hooks.server.ts
Normal file
18
services/llm-playground/src/hooks.server.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import type { Handle } from '@sveltejs/kit';
|
||||
|
||||
const PUBLIC_MANA_CORE_AUTH_URL_CLIENT =
|
||||
process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || '';
|
||||
const PUBLIC_MANA_LLM_URL_CLIENT =
|
||||
process.env.PUBLIC_MANA_LLM_URL_CLIENT || process.env.PUBLIC_MANA_LLM_URL || '';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
return resolve(event, {
|
||||
transformPageChunk: ({ html }) => {
|
||||
const envScript = `<script>
|
||||
window.__PUBLIC_MANA_CORE_AUTH_URL__ = "${PUBLIC_MANA_CORE_AUTH_URL_CLIENT}";
|
||||
window.__PUBLIC_MANA_LLM_URL__ = "${PUBLIC_MANA_LLM_URL_CLIENT}";
|
||||
</script>`;
|
||||
return html.replace('<head>', `<head>${envScript}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -6,11 +6,21 @@ import type {
|
|||
StreamChunk,
|
||||
} from '$lib/types';
|
||||
import { env } from '$env/dynamic/public';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
const API_BASE = env.PUBLIC_MANA_LLM_URL || 'http://localhost:3025';
|
||||
function getApiBase(): string {
|
||||
if (browser) {
|
||||
return (
|
||||
(window as unknown as { __PUBLIC_MANA_LLM_URL__?: string }).__PUBLIC_MANA_LLM_URL__ ||
|
||||
env.PUBLIC_MANA_LLM_URL ||
|
||||
'http://localhost:3025'
|
||||
);
|
||||
}
|
||||
return env.PUBLIC_MANA_LLM_URL || 'http://localhost:3025';
|
||||
}
|
||||
|
||||
export async function getHealth(): Promise<HealthResponse> {
|
||||
const response = await fetch(`${API_BASE}/health`);
|
||||
const response = await fetch(`${getApiBase()}/health`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Health check failed: ${response.statusText}`);
|
||||
}
|
||||
|
|
@ -18,7 +28,7 @@ export async function getHealth(): Promise<HealthResponse> {
|
|||
}
|
||||
|
||||
export async function getModels(): Promise<ModelsResponse> {
|
||||
const response = await fetch(`${API_BASE}/v1/models`);
|
||||
const response = await fetch(`${getApiBase()}/v1/models`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch models: ${response.statusText}`);
|
||||
}
|
||||
|
|
@ -28,7 +38,7 @@ export async function getModels(): Promise<ModelsResponse> {
|
|||
export async function sendCompletion(
|
||||
request: ChatCompletionRequest
|
||||
): Promise<ChatCompletionResponse> {
|
||||
const response = await fetch(`${API_BASE}/v1/chat/completions`, {
|
||||
const response = await fetch(`${getApiBase()}/v1/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...request, stream: false }),
|
||||
|
|
@ -42,7 +52,7 @@ export async function sendCompletion(
|
|||
export async function* streamCompletion(
|
||||
request: ChatCompletionRequest
|
||||
): AsyncGenerator<string, void, unknown> {
|
||||
const response = await fetch(`${API_BASE}/v1/chat/completions`, {
|
||||
const response = await fetch(`${getApiBase()}/v1/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...request, stream: true }),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { getHealth } from '$lib/api/llm';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let healthStatus = $state<'loading' | 'healthy' | 'error'>('loading');
|
||||
|
|
@ -75,5 +76,13 @@
|
|||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onclick={() => authStore.signOut()}
|
||||
class="rounded px-3 py-1.5 text-sm transition-colors hover:bg-white/10"
|
||||
style="color: var(--color-text-muted);"
|
||||
title="Abmelden"
|
||||
>
|
||||
Abmelden
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
|||
105
services/llm-playground/src/lib/stores/auth.svelte.ts
Normal file
105
services/llm-playground/src/lib/stores/auth.svelte.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import { browser } from '$app/environment';
|
||||
import { goto } from '$app/navigation';
|
||||
import { initializeWebAuth, type AuthService, type UserData } from '@manacore/shared-auth';
|
||||
|
||||
let user = $state<UserData | null>(null);
|
||||
let loading = $state(true);
|
||||
let initialized = $state(false);
|
||||
|
||||
let _authService: AuthService | null = null;
|
||||
|
||||
function getAuthUrl(): string {
|
||||
if (!browser) return '';
|
||||
return (
|
||||
(window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__ ||
|
||||
import.meta.env.PUBLIC_MANA_CORE_AUTH_URL ||
|
||||
'http://localhost:3001'
|
||||
);
|
||||
}
|
||||
|
||||
function getLlmUrl(): string {
|
||||
if (!browser) return '';
|
||||
return (
|
||||
(window as unknown as { __PUBLIC_MANA_LLM_URL__?: string }).__PUBLIC_MANA_LLM_URL__ ||
|
||||
import.meta.env.PUBLIC_MANA_LLM_URL ||
|
||||
'http://localhost:3025'
|
||||
);
|
||||
}
|
||||
|
||||
function getAuthService(): AuthService | null {
|
||||
if (!browser) return null;
|
||||
if (!_authService) {
|
||||
const auth = initializeWebAuth({
|
||||
baseUrl: getAuthUrl(),
|
||||
backendUrl: getLlmUrl(),
|
||||
});
|
||||
_authService = auth.authService;
|
||||
}
|
||||
return _authService;
|
||||
}
|
||||
|
||||
export const authStore = {
|
||||
get user() {
|
||||
return user;
|
||||
},
|
||||
get loading() {
|
||||
return loading;
|
||||
},
|
||||
get initialized() {
|
||||
return initialized;
|
||||
},
|
||||
get isAuthenticated() {
|
||||
return !!user;
|
||||
},
|
||||
|
||||
async initialize() {
|
||||
if (initialized || !browser) return;
|
||||
loading = true;
|
||||
try {
|
||||
const authService = getAuthService();
|
||||
if (authService) {
|
||||
const currentUser = await authService.getUserFromToken();
|
||||
user = currentUser;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth initialization failed:', error);
|
||||
user = null;
|
||||
} finally {
|
||||
loading = false;
|
||||
initialized = true;
|
||||
}
|
||||
},
|
||||
|
||||
async signIn(email: string, password: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) throw new Error('Auth not initialized');
|
||||
const result = await authService.signIn(email, password);
|
||||
if (result.success) {
|
||||
const currentUser = await authService.getUserFromToken();
|
||||
user = currentUser;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
async signUp(email: string, password: string, name?: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) throw new Error('Auth not initialized');
|
||||
return authService.signUp(email, password, name);
|
||||
},
|
||||
|
||||
async signOut() {
|
||||
const authService = getAuthService();
|
||||
if (authService) {
|
||||
await authService.signOut();
|
||||
}
|
||||
user = null;
|
||||
goto('/login');
|
||||
},
|
||||
|
||||
async getValidToken(): Promise<string | null> {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return null;
|
||||
return authService.getAppToken();
|
||||
},
|
||||
};
|
||||
11
services/llm-playground/src/routes/(auth)/+layout.svelte
Normal file
11
services/llm-playground/src/routes/(auth)/+layout.svelte
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
let { children }: { children: Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="min-h-screen flex items-center justify-center"
|
||||
style="background-color: var(--color-bg);"
|
||||
>
|
||||
{@render children()}
|
||||
</div>
|
||||
109
services/llm-playground/src/routes/(auth)/login/+page.svelte
Normal file
109
services/llm-playground/src/routes/(auth)/login/+page.svelte
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let email = $state('');
|
||||
let password = $state('');
|
||||
let error = $state('');
|
||||
let isLoading = $state(false);
|
||||
|
||||
const redirectTo = $derived($page.url.searchParams.get('redirectTo') || '/');
|
||||
|
||||
onMount(async () => {
|
||||
await authStore.initialize();
|
||||
if (authStore.isAuthenticated) {
|
||||
goto(redirectTo);
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
error = '';
|
||||
isLoading = true;
|
||||
|
||||
try {
|
||||
const result = await authStore.signIn(email, password);
|
||||
if (result.success) {
|
||||
goto(redirectTo);
|
||||
} else {
|
||||
error = result.error || 'Login fehlgeschlagen';
|
||||
}
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Login fehlgeschlagen';
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="w-full max-w-md p-8 rounded-xl border"
|
||||
style="background-color: var(--color-surface); border-color: var(--color-border);"
|
||||
>
|
||||
<h1 class="text-2xl font-bold mb-6" style="color: var(--color-text);">LLM Playground</h1>
|
||||
<p class="mb-6" style="color: var(--color-text-muted);">
|
||||
Melde dich an, um den Playground zu nutzen.
|
||||
</p>
|
||||
|
||||
{#if error}
|
||||
<div
|
||||
class="mb-4 p-3 rounded-lg text-sm"
|
||||
style="background-color: var(--color-error-bg, rgba(239, 68, 68, 0.1)); border: 1px solid var(--color-error, #ef4444); color: var(--color-error, #ef4444);"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form onsubmit={handleSubmit} class="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
for="email"
|
||||
class="block text-sm font-medium mb-1"
|
||||
style="color: var(--color-text-muted);">E-Mail</label
|
||||
>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
bind:value={email}
|
||||
required
|
||||
class="w-full px-4 py-2 rounded-lg focus:outline-none focus:ring-2"
|
||||
style="background-color: var(--color-bg); border: 1px solid var(--color-border); color: var(--color-text);"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="password"
|
||||
class="block text-sm font-medium mb-1"
|
||||
style="color: var(--color-text-muted);">Passwort</label
|
||||
>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
bind:value={password}
|
||||
required
|
||||
class="w-full px-4 py-2 rounded-lg focus:outline-none focus:ring-2"
|
||||
style="background-color: var(--color-bg); border: 1px solid var(--color-border); color: var(--color-text);"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
class="w-full py-2 px-4 font-medium rounded-lg transition-colors disabled:opacity-50"
|
||||
style="background-color: var(--color-primary); color: white;"
|
||||
>
|
||||
{isLoading ? 'Wird angemeldet...' : 'Anmelden'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="mt-6 text-center text-sm" style="color: var(--color-text-muted);">
|
||||
Noch kein Konto? <a
|
||||
href="/register"
|
||||
class="hover:underline"
|
||||
style="color: var(--color-primary);">Registrieren</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
129
services/llm-playground/src/routes/(auth)/register/+page.svelte
Normal file
129
services/llm-playground/src/routes/(auth)/register/+page.svelte
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
let email = $state('');
|
||||
let password = $state('');
|
||||
let name = $state('');
|
||||
let error = $state('');
|
||||
let success = $state(false);
|
||||
let isLoading = $state(false);
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
error = '';
|
||||
isLoading = true;
|
||||
|
||||
try {
|
||||
const result = await authStore.signUp(email, password, name || undefined);
|
||||
if (result.success) {
|
||||
success = true;
|
||||
} else {
|
||||
error = result.error || 'Registrierung fehlgeschlagen';
|
||||
}
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Registrierung fehlgeschlagen';
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="w-full max-w-md p-8 rounded-xl border"
|
||||
style="background-color: var(--color-surface); border-color: var(--color-border);"
|
||||
>
|
||||
<h1 class="text-2xl font-bold mb-6" style="color: var(--color-text);">Registrieren</h1>
|
||||
|
||||
{#if success}
|
||||
<div
|
||||
class="p-4 rounded-lg"
|
||||
style="background-color: var(--color-success-bg, rgba(34, 197, 94, 0.1)); border: 1px solid var(--color-success, #22c55e);"
|
||||
>
|
||||
<p style="color: var(--color-success, #22c55e);">
|
||||
Registrierung erfolgreich! Du kannst dich jetzt anmelden.
|
||||
</p>
|
||||
<a
|
||||
href="/login"
|
||||
class="hover:underline mt-2 inline-block"
|
||||
style="color: var(--color-primary);">Zum Login</a
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
{#if error}
|
||||
<div
|
||||
class="mb-4 p-3 rounded-lg text-sm"
|
||||
style="background-color: var(--color-error-bg, rgba(239, 68, 68, 0.1)); border: 1px solid var(--color-error, #ef4444); color: var(--color-error, #ef4444);"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form onsubmit={handleSubmit} class="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
for="name"
|
||||
class="block text-sm font-medium mb-1"
|
||||
style="color: var(--color-text-muted);">Name (optional)</label
|
||||
>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
bind:value={name}
|
||||
class="w-full px-4 py-2 rounded-lg focus:outline-none focus:ring-2"
|
||||
style="background-color: var(--color-bg); border: 1px solid var(--color-border); color: var(--color-text);"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="email"
|
||||
class="block text-sm font-medium mb-1"
|
||||
style="color: var(--color-text-muted);">E-Mail</label
|
||||
>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
bind:value={email}
|
||||
required
|
||||
class="w-full px-4 py-2 rounded-lg focus:outline-none focus:ring-2"
|
||||
style="background-color: var(--color-bg); border: 1px solid var(--color-border); color: var(--color-text);"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="password"
|
||||
class="block text-sm font-medium mb-1"
|
||||
style="color: var(--color-text-muted);">Passwort</label
|
||||
>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
bind:value={password}
|
||||
required
|
||||
minlength="8"
|
||||
class="w-full px-4 py-2 rounded-lg focus:outline-none focus:ring-2"
|
||||
style="background-color: var(--color-bg); border: 1px solid var(--color-border); color: var(--color-text);"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
class="w-full py-2 px-4 font-medium rounded-lg transition-colors disabled:opacity-50"
|
||||
style="background-color: var(--color-primary); color: white;"
|
||||
>
|
||||
{isLoading ? 'Wird registriert...' : 'Registrieren'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="mt-6 text-center text-sm" style="color: var(--color-text-muted);">
|
||||
Bereits ein Konto? <a
|
||||
href="/login"
|
||||
class="hover:underline"
|
||||
style="color: var(--color-primary);">Anmelden</a
|
||||
>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
let { children }: { children: Snippet } = $props();
|
||||
let isChecking = $state(true);
|
||||
|
||||
onMount(async () => {
|
||||
await authStore.initialize();
|
||||
if (!authStore.isAuthenticated) {
|
||||
const currentPath = $page.url.pathname;
|
||||
goto(`/login?redirectTo=${encodeURIComponent(currentPath)}`);
|
||||
return;
|
||||
}
|
||||
isChecking = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if isChecking}
|
||||
<div
|
||||
class="min-h-screen flex items-center justify-center"
|
||||
style="background-color: var(--color-bg);"
|
||||
>
|
||||
<div
|
||||
class="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2"
|
||||
style="border-color: var(--color-primary);"
|
||||
></div>
|
||||
</div>
|
||||
{:else}
|
||||
{@render children()}
|
||||
{/if}
|
||||
10
services/llm-playground/src/routes/health/+server.ts
Normal file
10
services/llm-playground/src/routes/health/+server.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const GET: RequestHandler = async () => {
|
||||
return json({
|
||||
status: 'healthy',
|
||||
service: 'llm-playground',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
};
|
||||
|
|
@ -33,7 +33,7 @@ class Settings(BaseSettings):
|
|||
cache_ttl: int = 3600
|
||||
|
||||
# CORS
|
||||
cors_origins: str = "http://localhost:5173,http://localhost:5190,https://mana.how"
|
||||
cors_origins: str = "http://localhost:5173,http://localhost:5190,https://mana.how,https://playground.mana.how"
|
||||
|
||||
@property
|
||||
def cors_origins_list(self) -> list[str]:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue