chore: delete 25 web-archived directories, remove stale stubs, clean workspace config

- Delete all 25 apps/*/apps/web-archived/ directories (superseded by unified ManaCore app)
- Remove stale +page.server.ts stubs from teams, organizations, settings (always returned empty data)
- Simplify teams and organizations pages to static empty-state (no server load dependency)
- Delete empty apps/context/apps/mobile/components/variants/index.ts
- Remove commented-out apps-archived entries from pnpm-workspace.yaml

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-03 13:03:49 +02:00
parent e1077e261f
commit 6ced238571
1940 changed files with 41 additions and 223288 deletions

View file

@ -1,2 +0,0 @@
# News Hub Web App Configuration
PUBLIC_NEWS_API_URL=http://localhost:3000

View file

@ -1,17 +0,0 @@
// @ts-check
import {
baseConfig,
typescriptConfig,
svelteConfig,
prettierConfig,
} from '@manacore/eslint-config';
export default [
{
ignores: ['dist/**', '.svelte-kit/**', 'node_modules/**'],
},
...baseConfig,
...typescriptConfig,
...svelteConfig,
...prettierConfig,
];

View file

@ -1,33 +0,0 @@
{
"name": "@news/web",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
},
"devDependencies": {
"@sveltejs/adapter-node": "^5.0.0",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^5.0.4",
"@tailwindcss/vite": "^4.1.11",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^6.0.0"
},
"dependencies": {
"@manacore/local-store": "workspace:*",
"@manacore/shared-auth": "workspace:*",
"@manacore/shared-auth-stores": "workspace:*",
"@manacore/shared-auth-ui": "workspace:*",
"@manacore/shared-branding": "workspace:*",
"@manacore/shared-ui": "workspace:*",
"svelte-sonner": "^1.0.5"
}
}

View file

@ -1,8 +0,0 @@
@import "tailwindcss";
@import "@manacore/shared-tailwind/themes.css";
/* Scan shared packages for Tailwind classes */
@source "../../../../packages/shared-ui/src";
@source "../../../../packages/shared-auth-ui/src";
@source "../../../../packages/shared-branding/src";
@source "../../../../packages/shared-theme-ui/src";

View file

@ -1,11 +0,0 @@
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

View file

@ -1,12 +0,0 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View file

@ -1,11 +0,0 @@
<script lang="ts">
let { size = 48, color = '#10b981' }: { size?: number; color?: string } = $props();
</script>
<svg width={size} height={size} viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" rx="22" fill={color} />
<rect x="22" y="25" width="56" height="50" rx="4" stroke="white" stroke-width="4" fill="none" />
<line x1="30" y1="38" x2="55" y2="38" stroke="white" stroke-width="3" stroke-linecap="round" />
<line x1="30" y1="48" x2="70" y2="48" stroke="white" stroke-width="3" stroke-linecap="round" />
<line x1="30" y1="58" x2="65" y2="58" stroke="white" stroke-width="3" stroke-linecap="round" />
</svg>

View file

@ -1,39 +0,0 @@
import type { LocalArticle, LocalCategory } from './local-store';
export const guestCategories: LocalCategory[] = [
{ id: 'cat-tech', name: 'Technologie', slug: 'technologie', color: '#3b82f6', order: 0 },
{ id: 'cat-science', name: 'Wissenschaft', slug: 'wissenschaft', color: '#10b981', order: 1 },
{ id: 'cat-world', name: 'Welt', slug: 'welt', color: '#f59e0b', order: 2 },
{ id: 'cat-business', name: 'Wirtschaft', slug: 'wirtschaft', color: '#8b5cf6', order: 3 },
];
export const guestArticles: LocalArticle[] = [
{
id: 'demo-1',
type: 'feed',
sourceOrigin: 'ai',
title: 'Willkommen bei News Hub!',
excerpt: 'Dein persönlicher Nachrichtenleser mit KI-Zusammenfassungen und Read-Later Funktion.',
content:
'News Hub kombiniert KI-kuratierte Nachrichten mit deiner persönlichen Leseliste. Speichere Artikel von jeder Website, lese sie offline und entdecke neue Perspektiven.',
categoryId: 'cat-tech',
isArchived: false,
wordCount: 42,
readingTimeMinutes: 1,
publishedAt: new Date().toISOString(),
},
{
id: 'demo-2',
type: 'saved',
sourceOrigin: 'user_saved',
title: 'Beispiel: Gespeicherter Artikel',
excerpt:
'So sieht ein gespeicherter Artikel aus. Nutze die Browser-Extension um Artikel zu speichern.',
originalUrl: 'https://example.com',
content:
'Dies ist ein Beispiel für einen Artikel, den du über die Browser-Extension oder die Web-App gespeichert hast.',
isArchived: false,
wordCount: 30,
readingTimeMinutes: 1,
},
];

View file

@ -1,50 +0,0 @@
import { createLocalStore, type BaseRecord } from '@manacore/local-store';
const SYNC_SERVER_URL = import.meta.env.PUBLIC_SYNC_SERVER_URL || 'http://localhost:3050';
export interface LocalArticle extends BaseRecord {
type: 'feed' | 'summary' | 'in_depth' | 'saved';
sourceOrigin: 'ai' | 'user_saved';
title: string;
content?: string | null;
htmlContent?: string | null;
excerpt?: string | null;
originalUrl?: string | null;
author?: string | null;
siteName?: string | null;
imageUrl?: string | null;
wordCount?: number | null;
readingTimeMinutes?: number | null;
categoryId?: string | null;
isArchived: boolean;
publishedAt?: string | null;
}
export interface LocalCategory extends BaseRecord {
name: string;
slug: string;
color?: string | null;
order: number;
}
import { guestArticles, guestCategories } from './guest-seed';
export const newsStore = createLocalStore({
appId: 'news',
collections: [
{
name: 'articles',
indexes: ['type', 'sourceOrigin', 'isArchived', 'categoryId', '[type+isArchived]'],
guestSeed: guestArticles,
},
{
name: 'categories',
indexes: ['slug', 'order'],
guestSeed: guestCategories,
},
],
sync: { serverUrl: SYNC_SERVER_URL },
});
export const articleCollection = newsStore.collection<LocalArticle>('articles');
export const categoryCollection = newsStore.collection<LocalCategory>('categories');

View file

@ -1,5 +0,0 @@
import { createManaAuthStore } from '@manacore/shared-auth-stores';
export const authStore = createManaAuthStore({
devBackendPort: 3071,
});

View file

@ -1,127 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { PillNavigation } from '@manacore/shared-ui';
import type { PillNavItem } from '@manacore/shared-ui';
import { getPillAppItems, getManaApp } from '@manacore/shared-branding';
import { AuthGate, GuestWelcomeModal, SessionExpiredBanner } from '@manacore/shared-auth-ui';
import { shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
import { authStore } from '$lib/stores/auth.svelte';
import { newsStore } from '$lib/data/local-store';
let { children } = $props();
let appItems = $derived(getPillAppItems(undefined, undefined, undefined, authStore.user?.tier));
let userEmail = $derived(authStore.isAuthenticated ? (authStore.user?.email ?? '') : '');
const navItems: PillNavItem[] = [
{ href: '/feed', label: 'Feed', icon: 'rss' },
{ href: '/saved', label: 'Gespeichert', icon: 'bookmark' },
{ href: '/settings', label: 'Einstellungen', icon: 'settings' },
];
let isCollapsed = $state(false);
let isDark = $state(true);
let showGuestWelcome = $state(false);
function handleKeydown(event: KeyboardEvent) {
const target = event.target as HTMLElement;
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)
return;
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
const num = parseInt(event.key);
const routes = ['/feed', '/saved', '/settings'];
if (num >= 1 && num <= 3) {
event.preventDefault();
goto(routes[num - 1]);
}
}
}
function handleCollapsedChange(collapsed: boolean) {
isCollapsed = collapsed;
localStorage?.setItem('news-nav-collapsed', String(collapsed));
}
function handleToggleTheme() {
isDark = !isDark;
document.documentElement.classList.toggle('dark', isDark);
localStorage?.setItem('news-dark-mode', String(isDark));
}
async function handleLogout() {
newsStore.stopSync();
await authStore.signOut();
goto('/auth/login');
}
function handleAuthReady() {
if (authStore.isAuthenticated) {
newsStore.startSync(() => authStore.getValidToken());
}
if (!authStore.isAuthenticated && shouldShowGuestWelcome('news')) {
showGuestWelcome = true;
}
const savedCollapsed = localStorage?.getItem('news-nav-collapsed');
if (savedCollapsed === 'true') isCollapsed = true;
const savedDark = localStorage?.getItem('news-dark-mode');
isDark = savedDark !== 'false'; // default dark
document.documentElement.classList.toggle('dark', isDark);
}
</script>
<svelte:window onkeydown={handleKeydown} />
<AuthGate
{authStore}
{goto}
allowGuest={true}
onReady={handleAuthReady}
requiredTier={getManaApp('news')?.requiredTier}
appName={getManaApp('news')?.name}
>
<div class="flex min-h-screen flex-col">
<PillNavigation
items={navItems}
currentPath={$page.url.pathname}
appName="News"
homeRoute="/feed"
onLogout={handleLogout}
onToggleTheme={handleToggleTheme}
{isDark}
{isCollapsed}
onCollapsedChange={handleCollapsedChange}
showThemeToggle={true}
primaryColor="#10b981"
showAppSwitcher={true}
{appItems}
{userEmail}
settingsHref="/settings"
>
{#snippet logo()}
<span class="text-xl">📰</span>
<span class="pill-label font-bold">News</span>
{/snippet}
</PillNavigation>
<main class="main-content flex-1 transition-all duration-300 {isCollapsed ? '' : 'pt-20'}">
<div class="container mx-auto px-4 py-8">
{@render children()}
</div>
</main>
</div>
<GuestWelcomeModal
appId="news"
visible={showGuestWelcome}
onClose={() => (showGuestWelcome = false)}
onLogin={() => goto('/auth/login')}
onRegister={() => goto('/auth/register')}
locale="de"
/>
{#if authStore.isAuthenticated}
<SessionExpiredBanner locale="de" loginHref="/auth/login" />
{/if}
</AuthGate>

View file

@ -1,103 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
const NEWS_SERVER = import.meta.env.PUBLIC_NEWS_SERVER_URL || 'http://localhost:3071';
let articles = $state<Record<string, unknown>[]>([]);
let loading = $state(true);
let selectedType = $state<string>('');
async function loadArticles() {
loading = true;
try {
const params = new URLSearchParams();
if (selectedType) params.set('type', selectedType);
params.set('limit', '30');
const res = await fetch(`${NEWS_SERVER}/api/v1/feed?${params}`);
if (res.ok) articles = await res.json();
} catch {
// Server offline
}
loading = false;
}
function changeType(type: string) {
selectedType = type;
loadArticles();
}
onMount(loadArticles);
</script>
<div class="mx-auto max-w-4xl">
<div class="mb-6 flex items-center justify-between">
<h1 class="text-3xl font-bold">Feed</h1>
<div class="flex gap-1">
{#each [{ value: '', label: 'Alle' }, { value: 'feed', label: 'News' }, { value: 'summary', label: 'Summaries' }, { value: 'in_depth', label: 'In-Depth' }] as tab}
<button
onclick={() => changeType(tab.value)}
class="rounded-md px-3 py-1.5 text-sm font-medium transition-colors {selectedType ===
tab.value
? 'bg-emerald-600 text-white'
: 'bg-gray-800 text-gray-300 hover:bg-gray-700'}"
>
{tab.label}
</button>
{/each}
</div>
</div>
{#if loading}
<div class="space-y-4">
{#each Array(5) as _}
<div class="h-24 animate-pulse rounded-xl bg-gray-800"></div>
{/each}
</div>
{:else if articles.length === 0}
<div class="rounded-xl border-2 border-dashed border-gray-700 p-12 text-center">
<p class="text-lg font-medium text-gray-400">Noch keine Artikel im Feed</p>
<p class="mt-1 text-sm text-gray-500">
AI-kuratierte Nachrichten erscheinen hier automatisch.
</p>
</div>
{:else}
<div class="space-y-4">
{#each articles as article}
<a
href="/feed/{article.id}"
class="block rounded-xl border border-gray-800 bg-gray-900 p-5 transition-all hover:border-gray-700 hover:bg-gray-800/80"
>
<div class="flex gap-4">
{#if article.imageUrl}
<img
src={String(article.imageUrl)}
alt=""
class="h-20 w-28 shrink-0 rounded-lg object-cover"
/>
{/if}
<div class="min-w-0 flex-1">
<div class="mb-1 flex items-center gap-2">
{#if article.type === 'summary'}
<span class="rounded bg-blue-900 px-1.5 py-0.5 text-xs text-blue-300"
>Summary</span
>
{:else if article.type === 'in_depth'}
<span class="rounded bg-purple-900 px-1.5 py-0.5 text-xs text-purple-300"
>In-Depth</span
>
{/if}
{#if article.readingTimeMinutes}
<span class="text-xs text-gray-500">{article.readingTimeMinutes} Min.</span>
{/if}
</div>
<h2 class="truncate text-lg font-semibold text-gray-100">{article.title}</h2>
{#if article.excerpt}
<p class="mt-1 line-clamp-2 text-sm text-gray-400">{article.excerpt}</p>
{/if}
</div>
</div>
</a>
{/each}
</div>
{/if}
</div>

View file

@ -1,173 +0,0 @@
<script lang="ts">
import { useLiveQuery } from '@manacore/local-store/svelte';
import { articleCollection } from '$lib/data/local-store';
import type { LocalArticle } from '$lib/data/local-store';
import { authStore } from '$lib/stores/auth.svelte';
import { toast } from 'svelte-sonner';
import { Archive, Trash } from '@manacore/shared-icons';
const NEWS_SERVER = import.meta.env.PUBLIC_NEWS_SERVER_URL || 'http://localhost:3071';
const savedArticles = useLiveQuery(() =>
articleCollection.getAll({ sourceOrigin: 'user_saved' })
);
let saveUrl = $state('');
let saving = $state(false);
let showArchived = $state(false);
let filteredArticles = $derived.by(() => {
const all = savedArticles.value ?? [];
return showArchived ? all : all.filter((a) => !a.isArchived);
});
async function saveFromUrl() {
if (!saveUrl) return;
saving = true;
try {
const token = authStore.isAuthenticated ? await authStore.getValidToken() : null;
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = await fetch(`${NEWS_SERVER}/api/v1/extract/preview`, {
method: 'POST',
headers,
body: JSON.stringify({ url: saveUrl }),
});
if (!res.ok) throw new Error('Extraction failed');
const extracted = await res.json();
await articleCollection.insert({
id: crypto.randomUUID(),
type: 'saved',
sourceOrigin: 'user_saved',
title: extracted.title,
content: extracted.content,
htmlContent: extracted.htmlContent,
excerpt: extracted.excerpt,
originalUrl: saveUrl,
author: extracted.byline,
siteName: extracted.siteName,
wordCount: extracted.wordCount,
readingTimeMinutes: extracted.readingTimeMinutes,
isArchived: false,
});
toast.success(`"${extracted.title}" gespeichert`);
saveUrl = '';
} catch {
toast.error('Artikel konnte nicht extrahiert werden');
}
saving = false;
}
async function toggleArchive(article: LocalArticle) {
await articleCollection.update(article.id, { isArchived: !article.isArchived });
toast.success(article.isArchived ? 'Wiederhergestellt' : 'Archiviert');
}
async function deleteArticle(article: LocalArticle) {
if (!confirm(`"${article.title}" löschen?`)) return;
await articleCollection.delete(article.id);
toast.success('Gelöscht');
}
</script>
<div class="mx-auto max-w-4xl">
<h1 class="mb-6 text-3xl font-bold">Gespeicherte Artikel</h1>
<!-- Save URL Form -->
<div class="mb-6 rounded-xl border border-gray-800 bg-gray-900 p-5">
<div class="flex gap-3">
<input
type="url"
bind:value={saveUrl}
placeholder="https://example.com/article — URL einfügen und speichern"
class="flex-1 rounded-lg border border-gray-700 bg-gray-800 px-4 py-3 text-gray-100 placeholder-gray-500 focus:border-emerald-500 focus:outline-none"
onkeydown={(e) => e.key === 'Enter' && saveFromUrl()}
/>
<button
onclick={saveFromUrl}
disabled={!saveUrl || saving}
class="rounded-lg bg-emerald-600 px-6 py-3 font-medium text-white hover:bg-emerald-700 disabled:opacity-50"
>
{saving ? 'Wird gespeichert...' : 'Speichern'}
</button>
</div>
</div>
<!-- Filter -->
<div class="mb-4">
<label class="flex cursor-pointer items-center gap-2 text-sm text-gray-400">
<input type="checkbox" bind:checked={showArchived} class="rounded" />
Archivierte anzeigen
</label>
</div>
<!-- Articles List -->
{#if savedArticles.loading}
<div class="space-y-3">
{#each Array(3) as _}
<div class="h-20 animate-pulse rounded-xl bg-gray-800"></div>
{/each}
</div>
{:else if filteredArticles.length === 0}
<div class="rounded-xl border-2 border-dashed border-gray-700 p-12 text-center">
<p class="text-lg font-medium text-gray-400">Noch keine gespeicherten Artikel</p>
<p class="mt-1 text-sm text-gray-500">
Füge eine URL oben ein oder nutze die Browser-Extension.
</p>
</div>
{:else}
<div class="space-y-3">
{#each filteredArticles as article (article.id)}
<div
class="group rounded-xl border border-gray-800 bg-gray-900 p-4 transition-all hover:border-gray-700 {article.isArchived
? 'opacity-60'
: ''}"
>
<div class="flex items-start justify-between">
<div class="min-w-0 flex-1">
<h3 class="truncate font-semibold text-gray-100">{article.title}</h3>
{#if article.excerpt}
<p class="mt-1 line-clamp-2 text-sm text-gray-400">{article.excerpt}</p>
{/if}
<div class="mt-2 flex items-center gap-3 text-xs text-gray-500">
{#if article.siteName}
<span>{article.siteName}</span>
{/if}
{#if article.readingTimeMinutes}
<span>{article.readingTimeMinutes} Min.</span>
{/if}
{#if article.originalUrl}
<a
href={article.originalUrl}
target="_blank"
class="text-emerald-500 hover:underline">Original</a
>
{/if}
</div>
</div>
<div class="ml-4 flex items-center gap-1">
<button
onclick={() => toggleArchive(article)}
class="rounded-lg p-2 text-gray-500 opacity-0 transition-all hover:bg-gray-800 hover:text-gray-300 group-hover:opacity-100"
title={article.isArchived ? 'Wiederherstellen' : 'Archivieren'}
>
<Archive size={16} />
</button>
<button
onclick={() => deleteArticle(article)}
class="rounded-lg p-2 text-gray-500 opacity-0 transition-all hover:bg-gray-800 hover:text-red-400 group-hover:opacity-100"
title="Löschen"
>
<Trash size={16} />
</button>
</div>
</div>
</div>
{/each}
</div>
{/if}
</div>

View file

@ -1,37 +0,0 @@
<script lang="ts">
import '../app.css';
import { onMount } from 'svelte';
import { Toaster } from 'svelte-sonner';
import { authStore } from '$lib/stores/auth.svelte';
import { newsStore } from '$lib/data/local-store';
let { children } = $props();
let loading = $state(true);
onMount(async () => {
await authStore.initialize();
await newsStore.initialize();
loading = false;
});
</script>
{#if loading}
<div class="flex min-h-screen items-center justify-center bg-gray-950">
<div
class="inline-block h-10 w-10 animate-spin rounded-full border-4 border-solid border-emerald-500 border-r-transparent"
></div>
</div>
{:else}
<div class="min-h-screen bg-gray-950 text-gray-100">
{@render children()}
</div>
{/if}
<Toaster
position="bottom-right"
expand={false}
richColors
closeButton
duration={4000}
visibleToasts={3}
/>

View file

@ -1,6 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
onMount(() => goto('/feed'));
</script>

View file

@ -1,27 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { LoginPage } from '@manacore/shared-auth-ui';
import NewsLogo from '$lib/components/NewsLogo.svelte';
import { authStore } from '$lib/stores/auth.svelte';
async function handleSignIn(email: string, password: string) {
return authStore.signIn(email, password);
}
</script>
<LoginPage
appName="News Hub"
logo={NewsLogo}
primaryColor="#10b981"
onSignIn={handleSignIn}
onResendVerification={(email) => authStore.resendVerificationEmail(email)}
passkeyAvailable={authStore.isPasskeyAvailable()}
onSignInWithPasskey={() => authStore.signInWithPasskey()}
onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)}
onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)}
onSendMagicLink={(email) => authStore.sendMagicLink(email)}
{goto}
successRedirect="/feed"
registerPath="/auth/register"
forgotPasswordPath="/auth/forgot-password"
/>

View file

@ -1,20 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { RegisterPage } from '@manacore/shared-auth-ui';
import NewsLogo from '$lib/components/NewsLogo.svelte';
import { authStore } from '$lib/stores/auth.svelte';
async function handleSignUp(email: string, password: string) {
return authStore.signUp(email, password);
}
</script>
<RegisterPage
appName="News Hub"
logo={NewsLogo}
primaryColor="#10b981"
onSignUp={handleSignUp}
{goto}
successRedirect="/feed"
loginPath="/auth/login"
/>

View file

@ -1,12 +0,0 @@
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: [vitePreprocess()],
kit: {
adapter: adapter(),
},
};
export default config;

View file

@ -1,14 +0,0 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
}

View file

@ -1,7 +0,0 @@
import { sveltekit } from '@sveltejs/kit/vite';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
});