chore: archive inactive projects to apps-archived/

Move inactive projects out of active workspace:
- bauntown (community website)
- maerchenzauber (AI story generation)
- memoro (voice memo app)
- news (news aggregation)
- nutriphi (nutrition tracking)
- reader (reading app)
- uload (URL shortener)
- wisekeep (AI wisdom extraction)

Update CLAUDE.md documentation:
- Add presi to active projects
- Document archived projects section
- Update workspace configuration

Archived apps can be re-activated by moving back to apps/

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-11-29 07:03:59 +01:00
parent b97149ac12
commit 61d181fbc2
3148 changed files with 437 additions and 46640 deletions

View file

@ -0,0 +1,5 @@
<script lang="ts">
let { children } = $props();
</script>
{@render children()}

View file

@ -0,0 +1,46 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { ForgotPasswordPage } from '@manacore/shared-auth-ui';
import { ManaCoreLogo } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
import AppSlider from '$lib/components/AppSlider.svelte';
// German translations
const translations = {
title: 'Passwort vergessen',
subtitle: 'Gib deine E-Mail ein, um einen Reset-Link zu erhalten',
emailPlaceholder: 'E-Mail',
resetButton: 'Reset-Link senden',
sending: 'Wird gesendet...',
success: 'E-Mail gesendet!',
backToLogin: 'Zurück zur Anmeldung',
emailRequired: 'E-Mail ist erforderlich',
emailInvalid: 'Bitte gib eine gültige E-Mail-Adresse ein',
resetFailed: 'Zurücksetzen fehlgeschlagen',
resetSuccess: 'Bitte überprüfe deine E-Mails',
};
async function handleResetPassword(email: string) {
return authStore.resetPassword(email);
}
</script>
<svelte:head>
<title>Passwort vergessen | Wisekeep</title>
</svelte:head>
<ForgotPasswordPage
appName="Wisekeep"
logo={ManaCoreLogo}
primaryColor="#8b5cf6"
onResetPassword={handleResetPassword}
{goto}
loginPath="/login"
lightBackground="#f3e8ff"
darkBackground="#1e1b4b"
{translations}
>
{#snippet appSlider()}
<AppSlider />
{/snippet}
</ForgotPasswordPage>

View file

@ -0,0 +1,65 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { LoginPage } from '@manacore/shared-auth-ui';
import { ManaCoreLogo } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
import AppSlider from '$lib/components/AppSlider.svelte';
// Get redirect URL from query params
const redirectTo = $derived($page.url.searchParams.get('redirectTo') || '/dashboard');
// German translations
const translations = {
title: 'Anmelden',
subtitle: 'Melde dich mit deinem Konto an',
emailPlaceholder: 'E-Mail',
passwordPlaceholder: 'Passwort',
rememberMe: 'Angemeldet bleiben',
forgotPassword: 'Passwort vergessen?',
signInButton: 'Anmelden',
signingIn: 'Wird angemeldet...',
success: 'Erfolgreich!',
orDivider: 'oder',
noAccount: 'Noch kein Konto?',
createAccount: 'Jetzt registrieren',
skipToForm: 'Zum Login-Formular springen',
showPassword: 'Passwort anzeigen',
hidePassword: 'Passwort verbergen',
emailRequired: 'E-Mail ist erforderlich',
emailInvalid: 'Bitte gib eine gültige E-Mail-Adresse ein',
passwordRequired: 'Passwort ist erforderlich',
signInFailed: 'Anmeldung fehlgeschlagen',
googleSignInFailed: 'Google-Anmeldung fehlgeschlagen',
signInSuccess: 'Erfolgreich angemeldet. Weiterleitung...',
googleSignInSuccess: 'Erfolgreich mit Google angemeldet. Weiterleitung...',
};
async function handleSignIn(email: string, password: string) {
return authStore.signIn(email, password);
}
</script>
<svelte:head>
<title>Anmelden | Wisekeep</title>
</svelte:head>
<LoginPage
appName="Wisekeep"
logo={ManaCoreLogo}
primaryColor="#8b5cf6"
onSignIn={handleSignIn}
{goto}
enableGoogle={false}
enableApple={false}
successRedirect={redirectTo}
registerPath="/register"
forgotPasswordPath="/forgot-password"
lightBackground="#f3e8ff"
darkBackground="#1e1b4b"
{translations}
>
{#snippet appSlider()}
<AppSlider />
{/snippet}
</LoginPage>

View file

@ -0,0 +1,60 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { RegisterPage } from '@manacore/shared-auth-ui';
import { ManaCoreLogo } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
import AppSlider from '$lib/components/AppSlider.svelte';
// German translations
const translations = {
title: 'Registrieren',
subtitle: 'Erstelle dein Konto',
emailPlaceholder: 'E-Mail',
passwordPlaceholder: 'Passwort',
confirmPasswordPlaceholder: 'Passwort wiederholen',
signUpButton: 'Registrieren',
signingUp: 'Wird registriert...',
success: 'Erfolgreich!',
orDivider: 'oder',
hasAccount: 'Bereits ein Konto?',
signIn: 'Jetzt anmelden',
skipToForm: 'Zum Registrierungsformular springen',
showPassword: 'Passwort anzeigen',
hidePassword: 'Passwort verbergen',
emailRequired: 'E-Mail ist erforderlich',
emailInvalid: 'Bitte gib eine gültige E-Mail-Adresse ein',
passwordRequired: 'Passwort ist erforderlich',
passwordMinLength: 'Passwort muss mindestens 8 Zeichen haben',
passwordsNotMatch: 'Passwörter stimmen nicht überein',
signUpFailed: 'Registrierung fehlgeschlagen',
signUpSuccess: 'Erfolgreich registriert. Weiterleitung...',
verificationRequired: 'Bitte überprüfe deine E-Mails zur Bestätigung',
};
async function handleSignUp(email: string, password: string) {
return authStore.signUp(email, password);
}
</script>
<svelte:head>
<title>Registrieren | Wisekeep</title>
</svelte:head>
<RegisterPage
appName="Wisekeep"
logo={ManaCoreLogo}
primaryColor="#8b5cf6"
onSignUp={handleSignUp}
{goto}
enableGoogle={false}
enableApple={false}
successRedirect="/"
loginPath="/login"
lightBackground="#f3e8ff"
darkBackground="#1e1b4b"
{translations}
>
{#snippet appSlider()}
<AppSlider />
{/snippet}
</RegisterPage>

View file

@ -0,0 +1,13 @@
/**
* Protected routes layout server
* Auth checking is done client-side via Mana Core Auth
*/
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ url }) => {
// Return the current path for client-side redirect logic
return {
pathname: url.pathname,
};
};

View file

@ -0,0 +1,124 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { authStore } from '$lib/stores/auth.svelte';
import { initWebSocket, cleanup, isConnected } from '$lib/stores/jobs';
import type { LayoutData } from './$types';
let { children, data }: { children: any; data: LayoutData } = $props();
let isChecking = $state(true);
// Check auth on mount and redirect if not authenticated
onMount(async () => {
let shouldRedirect = false;
try {
await authStore.initialize();
shouldRedirect = !authStore.isAuthenticated;
if (!shouldRedirect) {
// Initialize WebSocket after auth check
initWebSocket();
}
} catch (error) {
console.error('Protected layout init error:', error);
shouldRedirect = true;
}
// Always set isChecking to false
isChecking = false;
if (shouldRedirect) {
const redirectTo = encodeURIComponent(data.pathname || '/dashboard');
goto(`/login?redirectTo=${redirectTo}`);
}
// Return cleanup function
return () => cleanup();
});
async function handleSignOut() {
await authStore.signOut();
goto('/login');
}
</script>
{#if isChecking}
<!-- Loading state while checking auth -->
<div class="min-h-screen bg-gray-50 flex items-center justify-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600"></div>
</div>
{:else}
<div class="min-h-screen flex flex-col">
<header class="bg-white shadow-sm border-b">
<div class="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
<a href="/dashboard" class="text-xl font-bold text-purple-600">Wisekeep</a>
<nav class="flex items-center gap-6">
<a
href="/dashboard"
class="transition-colors {$page.url.pathname === '/dashboard'
? 'text-purple-600 font-medium'
: 'text-gray-600 hover:text-gray-900'}"
>
Dashboard
</a>
<a
href="/transcribe"
class="transition-colors {$page.url.pathname === '/transcribe'
? 'text-purple-600 font-medium'
: 'text-gray-600 hover:text-gray-900'}"
>
Transcribe
</a>
<a
href="/transcripts"
class="transition-colors {$page.url.pathname === '/transcripts'
? 'text-purple-600 font-medium'
: 'text-gray-600 hover:text-gray-900'}"
>
Transcripts
</a>
<a
href="/playlists"
class="transition-colors {$page.url.pathname === '/playlists'
? 'text-purple-600 font-medium'
: 'text-gray-600 hover:text-gray-900'}"
>
Playlists
</a>
</nav>
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full {$isConnected ? 'bg-green-500' : 'bg-red-500'}"></span>
<span class="text-sm text-gray-500">
{$isConnected ? 'Connected' : 'Disconnected'}
</span>
</div>
{#if authStore.user}
<span class="text-sm text-gray-600 hidden sm:block">
{authStore.user.email}
</span>
{/if}
<button
onclick={handleSignOut}
class="px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
>
Abmelden
</button>
</div>
</div>
</header>
<main class="flex-1">
{@render children()}
</main>
<footer class="bg-gray-100 border-t py-4">
<div class="max-w-7xl mx-auto px-4 text-center text-sm text-gray-500">
Wisekeep - AI-powered wisdom extraction from video
</div>
</footer>
</div>
{/if}

View file

@ -0,0 +1,100 @@
<script lang="ts">
import { onMount } from 'svelte';
import { api, type Stats } from '$lib/api/client';
import { activeJobs, jobList } from '$lib/stores/jobs';
let stats: Stats | null = $state(null);
let loading = $state(true);
let error = $state<string | null>(null);
onMount(async () => {
try {
stats = await api.getStats();
const jobs = await api.getAllJobs();
// Initialize jobs store with existing jobs
jobs.forEach((job) => {
jobList; // trigger reactivity
});
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to load stats';
} finally {
loading = false;
}
});
</script>
<svelte:head>
<title>Dashboard | Wisekeep</title>
</svelte:head>
<div class="max-w-7xl mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-8">Dashboard</h1>
{#if loading}
<div class="text-gray-500">Loading...</div>
{:else if error}
<div class="bg-red-50 text-red-600 p-4 rounded-lg">{error}</div>
{:else if stats}
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div class="bg-white p-6 rounded-lg shadow-sm border">
<div class="text-sm text-gray-500 mb-1">Total Transcripts</div>
<div class="text-3xl font-bold text-purple-600">{stats.totalTranscripts}</div>
</div>
<div class="bg-white p-6 rounded-lg shadow-sm border">
<div class="text-sm text-gray-500 mb-1">Storage Used</div>
<div class="text-3xl font-bold">{stats.totalSizeMB} MB</div>
</div>
<div class="bg-white p-6 rounded-lg shadow-sm border">
<div class="text-sm text-gray-500 mb-1">Active Jobs</div>
<div class="text-3xl font-bold text-yellow-600">{stats.activeJobs}</div>
</div>
<div class="bg-white p-6 rounded-lg shadow-sm border">
<div class="text-sm text-gray-500 mb-1">Completed</div>
<div class="text-3xl font-bold text-green-600">{stats.completedJobs}</div>
</div>
</div>
{/if}
<div class="bg-white rounded-lg shadow-sm border p-6 mb-8">
<h2 class="text-xl font-semibold mb-4">Quick Start</h2>
<a
href="/transcribe"
class="inline-flex items-center px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition"
>
Start New Transcription
</a>
</div>
{#if $activeJobs.length > 0}
<div class="bg-white rounded-lg shadow-sm border p-6">
<h2 class="text-xl font-semibold mb-4">Active Jobs</h2>
<div class="space-y-4">
{#each $activeJobs as job (job.id)}
<div class="border rounded-lg p-4">
<div class="flex justify-between items-start mb-2">
<div>
<div class="font-medium">{job.videoInfo?.title || job.url}</div>
<div class="text-sm text-gray-500">{job.videoInfo?.channel || 'Loading...'}</div>
</div>
<span
class="px-2 py-1 text-xs rounded-full
{job.status === 'downloading' ? 'bg-blue-100 text-blue-700' : ''}
{job.status === 'transcribing' ? 'bg-yellow-100 text-yellow-700' : ''}
{job.status === 'pending' ? 'bg-gray-100 text-gray-700' : ''}"
>
{job.status}
</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div
class="bg-purple-600 h-2 rounded-full transition-all"
style="width: {job.progress}%"
></div>
</div>
<div class="text-sm text-gray-500 mt-1">{job.progress}%</div>
</div>
{/each}
</div>
</div>
{/if}
</div>

View file

@ -0,0 +1,64 @@
<script lang="ts">
import { onMount } from 'svelte';
import { api, type Playlist } from '$lib/api/client';
let playlists = $state<Playlist[]>([]);
let loading = $state(true);
let error = $state<string | null>(null);
onMount(async () => {
try {
playlists = await api.getPlaylists();
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to load playlists';
} finally {
loading = false;
}
});
const groupedPlaylists = $derived(() => {
const grouped: Record<string, Playlist[]> = {};
for (const playlist of playlists) {
if (!grouped[playlist.category]) {
grouped[playlist.category] = [];
}
grouped[playlist.category].push(playlist);
}
return grouped;
});
</script>
<svelte:head>
<title>Playlists | Wisekeep</title>
</svelte:head>
<div class="max-w-7xl mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-8">Playlists</h1>
{#if loading}
<div class="text-gray-500">Loading...</div>
{:else if error}
<div class="bg-red-50 text-red-600 p-4 rounded-lg">{error}</div>
{:else if playlists.length === 0}
<div class="bg-gray-50 rounded-lg p-8 text-center">
<p class="text-gray-500">No playlists yet</p>
</div>
{:else}
{#each Object.entries(groupedPlaylists()) as [category, categoryPlaylists]}
<div class="mb-8">
<h2 class="text-xl font-semibold mb-4 capitalize">{category}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{#each categoryPlaylists as playlist}
<div class="bg-white rounded-lg shadow-sm border p-4">
<h3 class="font-medium">{playlist.name}</h3>
{#if playlist.description}
<p class="text-sm text-gray-500 mt-1">{playlist.description}</p>
{/if}
<p class="text-xs text-gray-400 mt-2">{playlist.urlCount} URLs</p>
</div>
{/each}
</div>
</div>
{/each}
{/if}
</div>

View file

@ -0,0 +1,136 @@
<script lang="ts">
import { api } from '$lib/api/client';
import { addJob } from '$lib/stores/jobs';
import { goto } from '$app/navigation';
let url = $state('');
let language = $state('de');
let provider = $state<'openai' | 'local'>('openai');
let model = $state<'tiny' | 'base' | 'small' | 'medium' | 'large'>('base');
let loading = $state(false);
let error = $state<string | null>(null);
const languages = [
{ code: 'de', name: 'German' },
{ code: 'en', name: 'English' },
{ code: 'es', name: 'Spanish' },
{ code: 'fr', name: 'French' },
{ code: 'it', name: 'Italian' },
{ code: 'pt', name: 'Portuguese' },
{ code: 'ja', name: 'Japanese' },
{ code: 'ko', name: 'Korean' },
{ code: 'zh', name: 'Chinese' },
];
const models = [
{ value: 'tiny', label: 'Tiny (39 MB, ~10x speed)' },
{ value: 'base', label: 'Base (74 MB, ~7x speed)' },
{ value: 'small', label: 'Small (244 MB, ~4x speed)' },
{ value: 'medium', label: 'Medium (769 MB, ~2x speed)' },
{ value: 'large', label: 'Large (1.5 GB, best accuracy)' },
];
async function handleSubmit(e: Event) {
e.preventDefault();
error = null;
loading = true;
try {
const job = await api.createJob({
url,
language,
provider,
model: provider === 'local' ? model : undefined,
});
addJob(job);
goto('/');
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to start transcription';
} finally {
loading = false;
}
}
</script>
<svelte:head>
<title>New Transcription | Wisekeep</title>
</svelte:head>
<div class="max-w-2xl mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-8">New Transcription</h1>
{#if error}
<div class="bg-red-50 text-red-600 p-4 rounded-lg mb-6">{error}</div>
{/if}
<form onsubmit={handleSubmit} class="bg-white rounded-lg shadow-sm border p-6 space-y-6">
<div>
<label for="url" class="block text-sm font-medium text-gray-700 mb-2"> YouTube URL </label>
<input
type="url"
id="url"
bind:value={url}
placeholder="https://www.youtube.com/watch?v=..."
required
class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
/>
</div>
<div>
<label for="language" class="block text-sm font-medium text-gray-700 mb-2"> Language </label>
<select
id="language"
bind:value={language}
class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
>
{#each languages as lang}
<option value={lang.code}>{lang.name}</option>
{/each}
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2"> Transcription Provider </label>
<div class="flex gap-4">
<label class="flex items-center gap-2">
<input type="radio" bind:group={provider} value="openai" />
<span>OpenAI Whisper API</span>
</label>
<label class="flex items-center gap-2">
<input type="radio" bind:group={provider} value="local" />
<span>Local Whisper</span>
</label>
</div>
<p class="text-sm text-gray-500 mt-1">
{provider === 'openai'
? 'Fast, cloud-based transcription (~$0.006/min)'
: 'Free, requires local Whisper installation'}
</p>
</div>
{#if provider === 'local'}
<div>
<label for="model" class="block text-sm font-medium text-gray-700 mb-2">
Whisper Model
</label>
<select
id="model"
bind:value={model}
class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
>
{#each models as m}
<option value={m.value}>{m.label}</option>
{/each}
</select>
</div>
{/if}
<button
type="submit"
disabled={loading || !url}
class="w-full py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Starting...' : 'Start Transcription'}
</button>
</form>
</div>

View file

@ -0,0 +1,70 @@
<script lang="ts">
import { onMount } from 'svelte';
import { api } from '$lib/api/client';
import { jobList } from '$lib/stores/jobs';
let loading = $state(true);
onMount(async () => {
try {
const jobs = await api.getAllJobs();
// Jobs are managed via the store
} finally {
loading = false;
}
});
const completedJobs = $derived($jobList.filter((j) => j.status === 'completed'));
</script>
<svelte:head>
<title>Transcripts | Wisekeep</title>
</svelte:head>
<div class="max-w-7xl mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-8">Transcripts</h1>
{#if loading}
<div class="text-gray-500">Loading...</div>
{:else if completedJobs.length === 0}
<div class="bg-gray-50 rounded-lg p-8 text-center">
<p class="text-gray-500 mb-4">No transcripts yet</p>
<a
href="/transcribe"
class="inline-flex items-center px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
>
Create your first transcript
</a>
</div>
{:else}
<div class="grid gap-4">
{#each completedJobs as job (job.id)}
<div class="bg-white rounded-lg shadow-sm border p-4">
<div class="flex justify-between items-start">
<div>
<h3 class="font-medium">{job.videoInfo?.title || 'Untitled'}</h3>
<p class="text-sm text-gray-500">{job.videoInfo?.channel || 'Unknown channel'}</p>
<p class="text-xs text-gray-400 mt-1">
Completed: {new Date(job.completedAt || '').toLocaleString()}
</p>
</div>
<span class="px-2 py-1 text-xs bg-green-100 text-green-700 rounded-full">
Completed
</span>
</div>
{#if job.transcriptText}
<details class="mt-4">
<summary class="cursor-pointer text-sm text-purple-600 hover:text-purple-700">
View transcript
</summary>
<pre
class="mt-2 p-4 bg-gray-50 rounded text-sm whitespace-pre-wrap overflow-auto max-h-96">
{job.transcriptText}
</pre>
</details>
{/if}
</div>
{/each}
</div>
{/if}
</div>

View file

@ -0,0 +1,9 @@
<script lang="ts">
import '../app.css';
let { children } = $props();
</script>
<div class="min-h-screen bg-gray-50">
{@render children()}
</div>

View file

@ -0,0 +1,27 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import { authStore } from '$lib/stores/auth.svelte';
onMount(async () => {
await authStore.initialize();
if (authStore.isAuthenticated) {
goto('/dashboard', { replaceState: true });
} else {
goto('/login', { replaceState: true });
}
});
</script>
<svelte:head>
<title>Wisekeep - AI Wisdom Extraction</title>
</svelte:head>
<div class="flex items-center justify-center min-h-screen">
<div class="text-center">
<div
class="animate-spin w-10 h-10 border-4 border-purple-500 border-r-transparent rounded-full mx-auto"
></div>
<p class="mt-4 text-gray-600">Wird geladen...</p>
</div>
</div>