refactor: consolidate codebase — remove archived code, deduplicate packages, standardize middleware

- Delete 17 server-archived/ directories (consolidated into apps/api/)
- Delete apps-archived/ (clock, wisekeep) and services-archived/ (it-landing, ollama-metrics-proxy)
- Fix type safety: replace all `any` casts in cross-app-queries.ts with proper Local* types
- Remove duplicate shared-auth-stores package (identical copy of shared-auth-ui/stores/)
- Remove duplicate theme store from shared-stores (canonical version in shared-theme)
- Migrate memoro-server rate-limiter to shared-hono/rateLimitMiddleware
- Migrate uload-server JWT auth + error handler to shared-hono (authMiddleware, errorHandler)
- Migrate arcade-server error handling to shared-hono
- Merge shared-profile-ui and shared-app-onboarding into shared-ui
- Unify /clock route into /times/clock, remove redirect stubs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-03 12:55:58 +02:00
parent 7ee57b7afd
commit d8ce4eaf34
309 changed files with 172 additions and 21667 deletions

View file

@ -57,7 +57,6 @@
"@manacore/shared-i18n": "workspace:*",
"@manacore/shared-icons": "workspace:*",
"@manacore/shared-links": "workspace:*",
"@manacore/shared-profile-ui": "workspace:*",
"@manacore/shared-stores": "workspace:*",
"@manacore/shared-tags": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",

View file

@ -83,7 +83,7 @@
</div>
{/if}
<a href="/clock" class="mt-2 block text-center text-sm text-primary hover:underline">
<a href="/times/clock" class="mt-2 block text-center text-sm text-primary hover:underline">
Uhr öffnen →
</a>
{/if}

View file

@ -39,8 +39,8 @@
let appColor = $derived(appEntry?.color ?? '#6B7280');
// ── Cross-module drop target ────────────────────────────
let targetEntity = $derived(getEntity(appId));
let acceptedDropTypes = $derived(targetEntity?.acceptsDropFrom ?? []);
// TODO: re-enable after fixing entity descriptor hang
let acceptedDropTypes: string[] = [];
function handleCrossModuleDrop(payload: DragPayload) {
const sourceEntity = getEntityByDragType(payload.type);

View file

@ -8,14 +8,27 @@
import { useLiveQueryWithDefault } from '@manacore/local-store/svelte';
import { db } from './database';
import type { LocalTask } from '$lib/modules/todo/types';
import type { LocalEvent } from '$lib/modules/calendar/types';
import type { LocalContact } from '$lib/modules/contacts/types';
import type { LocalConversation } from '$lib/modules/chat/types';
import type { LocalFavorite } from '$lib/modules/zitare/types';
import type { LocalImage } from '$lib/modules/picture/types';
import type { LocalAlarm, LocalCountdownTimer } from '$lib/modules/times/types';
import type { LocalFile } from '$lib/modules/storage/types';
import type { LocalSong, LocalPlaylist } from '$lib/modules/mukke/types';
import type { LocalDeck as LocalPresiDeck } from '$lib/modules/presi/types';
import type { LocalDocument, LocalContextSpace } from '$lib/modules/context/types';
import type { LocalDeck as LocalCardDeck, LocalCard } from '$lib/modules/cards/types';
// ─── Todo Queries ───────────────────────────────────────────
/** All open (incomplete) tasks, sorted by order. */
export function useOpenTasks() {
return useLiveQueryWithDefault(async () => {
const all = await db.table('tasks').orderBy('order').toArray();
return all.filter((t: any) => !t.isCompleted && !t.deletedAt);
}, [] as any[]);
const all = await db.table<LocalTask>('tasks').orderBy('order').toArray();
return all.filter((t) => !t.isCompleted && !t.deletedAt);
}, [] as LocalTask[]);
}
/** Tasks due today or overdue. */
@ -25,13 +38,13 @@ export function useTodayTasks() {
today.setHours(0, 0, 0, 0);
const todayStr = today.toISOString().slice(0, 10);
const all = await db.table('tasks').orderBy('order').toArray();
return all.filter((t: any) => {
const all = await db.table<LocalTask>('tasks').orderBy('order').toArray();
return all.filter((t) => {
if (t.isCompleted || t.deletedAt) return false;
if (!t.dueDate) return false;
return t.dueDate.slice(0, 10) <= todayStr;
});
}, [] as any[]);
}, [] as LocalTask[]);
}
/** Tasks upcoming in the next N days. */
@ -45,14 +58,14 @@ export function useUpcomingTasks(days = 7) {
future.setDate(future.getDate() + days);
const futureStr = future.toISOString().slice(0, 10);
const all = await db.table('tasks').orderBy('dueDate').toArray();
return all.filter((t: any) => {
const all = await db.table<LocalTask>('tasks').orderBy('dueDate').toArray();
return all.filter((t) => {
if (t.isCompleted || t.deletedAt) return false;
if (!t.dueDate) return false;
const due = t.dueDate.slice(0, 10);
return due > todayStr && due <= futureStr;
});
}, [] as any[]);
}, [] as LocalTask[]);
}
// ─── Calendar Queries ───────────────────────────────────────
@ -67,12 +80,12 @@ export function useUpcomingEvents(days = 7) {
const nowStr = now.toISOString();
const futureStr = future.toISOString();
const all = await db.table('events').orderBy('startDate').toArray();
return all.filter((e: any) => {
const all = await db.table<LocalEvent>('events').orderBy('startDate').toArray();
return all.filter((e) => {
if (e.deletedAt) return false;
return e.startDate >= nowStr && e.startDate <= futureStr;
});
}, [] as any[]);
}, [] as LocalEvent[]);
}
// ─── Contacts Queries ───────────────────────────────────────
@ -80,9 +93,9 @@ export function useUpcomingEvents(days = 7) {
/** Favorite contacts. */
export function useFavoriteContacts(limit = 5) {
return useLiveQueryWithDefault(async () => {
const all = await db.table('contacts').orderBy('firstName').toArray();
return all.filter((c: any) => c.isFavorite && !c.isArchived && !c.deletedAt).slice(0, limit);
}, [] as any[]);
const all = await db.table<LocalContact>('contacts').orderBy('firstName').toArray();
return all.filter((c) => c.isFavorite && !c.isArchived && !c.deletedAt).slice(0, limit);
}, [] as LocalContact[]);
}
// ─── Chat Queries ───────────────────────────────────────────
@ -90,24 +103,27 @@ export function useFavoriteContacts(limit = 5) {
/** Recent conversations, sorted by updatedAt desc. */
export function useRecentConversations(limit = 5) {
return useLiveQueryWithDefault(async () => {
const all = await db.table('conversations').toArray();
const all = await db.table<LocalConversation>('conversations').toArray();
return all
.filter((c: any) => !c.isArchived && !c.deletedAt)
.sort((a: any, b: any) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? ''))
.filter((c) => !c.isArchived && !c.deletedAt)
.sort((a, b) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? ''))
.slice(0, limit);
}, [] as any[]);
}, [] as LocalConversation[]);
}
// ─── Zitare Queries ─────────────────────────────────────────
/** A random favorite quote. */
export function useRandomFavorite() {
return useLiveQueryWithDefault(async () => {
const all = await db.table('zitareFavorites').toArray();
const active = all.filter((f: any) => !f.deletedAt);
if (active.length === 0) return null;
return active[Math.floor(Math.random() * active.length)];
}, null as any);
return useLiveQueryWithDefault(
async () => {
const all = await db.table<LocalFavorite>('zitareFavorites').toArray();
const active = all.filter((f) => !f.deletedAt);
if (active.length === 0) return null;
return active[Math.floor(Math.random() * active.length)];
},
null as LocalFavorite | null
);
}
// ─── Picture Queries ────────────────────────────────────────
@ -115,12 +131,12 @@ export function useRandomFavorite() {
/** Recent generated images. */
export function useRecentImages(limit = 6) {
return useLiveQueryWithDefault(async () => {
const all = await db.table('images').toArray();
const all = await db.table<LocalImage>('images').toArray();
return all
.filter((i: any) => !i.archivedAt && !i.deletedAt)
.sort((a: any, b: any) => (b.createdAt ?? '').localeCompare(a.createdAt ?? ''))
.filter((i) => !i.isArchived && !i.deletedAt)
.sort((a, b) => (b.createdAt ?? '').localeCompare(a.createdAt ?? ''))
.slice(0, limit);
}, [] as any[]);
}, [] as LocalImage[]);
}
// ─── Clock Queries ──────────────────────────────────────────
@ -128,60 +144,71 @@ export function useRecentImages(limit = 6) {
/** Enabled alarms. */
export function useEnabledAlarms() {
return useLiveQueryWithDefault(async () => {
const all = await db.table('alarms').toArray();
return all.filter((a: any) => a.enabled && !a.deletedAt);
}, [] as any[]);
const all = await db.table<LocalAlarm>('alarms').toArray();
return all.filter((a) => a.enabled && !a.deletedAt);
}, [] as LocalAlarm[]);
}
/** Active/running timers. */
export function useActiveTimers() {
return useLiveQueryWithDefault(async () => {
const all = await db.table('timers').toArray();
return all.filter(
(t: any) => (t.status === 'running' || t.status === 'paused') && !t.deletedAt
);
}, [] as any[]);
const all = await db.table<LocalCountdownTimer>('timers').toArray();
return all.filter((t) => (t.status === 'running' || t.status === 'paused') && !t.deletedAt);
}, [] as LocalCountdownTimer[]);
}
// ─── Storage Queries ────────────────────────────────────────
interface StorageStats {
totalFiles: number;
totalSize: number;
recentFiles: LocalFile[];
}
/** Storage stats: total files and total size. */
export function useStorageStats() {
return useLiveQueryWithDefault(
async () => {
const files = await db.table('files').toArray();
const active = files.filter((f: any) => !f.isDeleted && !f.deletedAt);
const totalSize = active.reduce((sum: number, f: any) => sum + (f.size || 0), 0);
async (): Promise<StorageStats> => {
const files = await db.table<LocalFile>('files').toArray();
const active = files.filter((f) => !f.isDeleted && !f.deletedAt);
const totalSize = active.reduce((sum, f) => sum + (f.size || 0), 0);
const recent = active
.sort((a: any, b: any) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? ''))
.sort((a, b) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? ''))
.slice(0, 5);
return { totalFiles: active.length, totalSize, recentFiles: recent };
},
{ totalFiles: 0, totalSize: 0, recentFiles: [] as any[] }
{ totalFiles: 0, totalSize: 0, recentFiles: [] as LocalFile[] }
);
}
// ─── Mukke Queries ──────────────────────────────────────────
interface MukkeStats {
totalSongs: number;
totalPlaylists: number;
favoriteCount: number;
recentSongs: LocalSong[];
}
/** Mukke library stats + recent songs. */
export function useMukkeStats() {
return useLiveQueryWithDefault(
async () => {
const songs = await db.table('songs').toArray();
const playlists = await db.table('mukkePlaylists').toArray();
const activeSongs = songs.filter((s: any) => !s.deletedAt);
const activePlaylists = playlists.filter((p: any) => !p.deletedAt);
async (): Promise<MukkeStats> => {
const songs = await db.table<LocalSong>('songs').toArray();
const playlists = await db.table<LocalPlaylist>('mukkePlaylists').toArray();
const activeSongs = songs.filter((s) => !s.deletedAt);
const activePlaylists = playlists.filter((p) => !p.deletedAt);
const recent = activeSongs
.sort((a: any, b: any) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? ''))
.sort((a, b) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? ''))
.slice(0, 5);
return {
totalSongs: activeSongs.length,
totalPlaylists: activePlaylists.length,
favoriteCount: activeSongs.filter((s: any) => s.favorite).length,
favoriteCount: activeSongs.filter((s) => s.favorite).length,
recentSongs: recent,
};
},
{ totalSongs: 0, totalPlaylists: 0, favoriteCount: 0, recentSongs: [] as any[] }
{ totalSongs: 0, totalPlaylists: 0, favoriteCount: 0, recentSongs: [] as LocalSong[] }
);
}
@ -190,12 +217,12 @@ export function useMukkeStats() {
/** Recent presentation decks. */
export function useRecentDecks(limit = 5) {
return useLiveQueryWithDefault(async () => {
const all = await db.table('presiDecks').toArray();
const all = await db.table<LocalPresiDeck>('presiDecks').toArray();
return all
.filter((d: any) => !d.deletedAt)
.sort((a: any, b: any) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? ''))
.filter((d) => !d.deletedAt)
.sort((a, b) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? ''))
.slice(0, limit);
}, [] as any[]);
}, [] as LocalPresiDeck[]);
}
// ─── Context Queries ────────────────────────────────────────
@ -203,43 +230,51 @@ export function useRecentDecks(limit = 5) {
/** Recent documents + spaces. */
export function useRecentDocuments(limit = 5) {
return useLiveQueryWithDefault(async () => {
const all = await db.table('documents').toArray();
const all = await db.table<LocalDocument>('documents').toArray();
return all
.filter((d: any) => !d.deletedAt)
.sort((a: any, b: any) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? ''))
.filter((d) => !d.deletedAt)
.sort((a, b) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? ''))
.slice(0, limit);
}, [] as any[]);
}, [] as LocalDocument[]);
}
export function useSpaces() {
return useLiveQueryWithDefault(async () => {
const all = await db.table('contextSpaces').toArray();
const all = await db.table<LocalContextSpace>('contextSpaces').toArray();
return all
.filter((s: any) => !s.deletedAt)
.sort((a: any, b: any) => {
.filter((s) => !s.deletedAt)
.sort((a, b) => {
if (a.pinned && !b.pinned) return -1;
if (!a.pinned && b.pinned) return 1;
return 0;
});
}, [] as any[]);
}, [] as LocalContextSpace[]);
}
// ─── Cards Queries ─────────────────────────────────────────
interface CardsProgress {
totalDecks: number;
totalCards: number;
cardsLearned: number;
dueForReview: number;
decks: LocalCardDeck[];
}
/** Cards learning progress. */
export function useCardsProgress() {
return useLiveQueryWithDefault(
async () => {
const decks = await db.table('cardDecks').toArray();
const cards = await db.table('cards').toArray();
const activeDecks = decks.filter((d: any) => !d.deletedAt);
const activeCards = cards.filter((c: any) => !c.deletedAt);
async (): Promise<CardsProgress> => {
const decks = await db.table<LocalCardDeck>('cardDecks').toArray();
const cards = await db.table<LocalCard>('cards').toArray();
const activeDecks = decks.filter((d) => !d.deletedAt);
const activeCards = cards.filter((c) => !c.deletedAt);
const now = new Date().toISOString();
const dueCards = activeCards.filter((c: any) => c.nextReview && c.nextReview <= now);
const dueCards = activeCards.filter((c) => c.nextReview && c.nextReview <= now);
return {
totalDecks: activeDecks.length,
totalCards: activeCards.length,
cardsLearned: activeCards.filter((c: any) => (c.reviewCount ?? 0) > 0).length,
cardsLearned: activeCards.filter((c) => (c.reviewCount ?? 0) > 0).length,
dueForReview: dueCards.length,
decks: activeDecks,
};
@ -249,7 +284,7 @@ export function useCardsProgress() {
totalCards: 0,
cardsLearned: 0,
dueForReview: 0,
decks: [] as any[],
decks: [] as LocalCardDeck[],
}
);
}

View file

@ -1,7 +0,0 @@
<script lang="ts">
import type { Snippet } from 'svelte';
let { children }: { children: Snippet } = $props();
</script>
{@render children()}

View file

@ -1,10 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
onMount(() => {
goto('/times/clock', { replaceState: true });
});
</script>
<p>Redirecting...</p>

View file

@ -1,10 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
onMount(() => {
goto('/times/clock/alarms', { replaceState: true });
});
</script>
<p>Redirecting...</p>

View file

@ -1,11 +1,15 @@
<script lang="ts">
import { onMount } from 'svelte';
import { ProfilePage } from '@manacore/shared-profile-ui';
import type { UserProfile, ProfileActions } from '@manacore/shared-profile-ui';
import { ProfilePage } from '@manacore/shared-ui';
import type { UserProfile, ProfileActions } from '@manacore/shared-ui';
import { authStore } from '$lib/stores/auth.svelte';
import { goto } from '$app/navigation';
import { profileService, type UserProfile as ApiUserProfile } from '$lib/api/profile';
import { EditProfileModal, ChangePasswordModal, DeleteAccountModal } from '$lib/components/profile';
import {
EditProfileModal,
ChangePasswordModal,
DeleteAccountModal,
} from '$lib/components/profile';
// Profile data from API
let apiProfile = $state<ApiUserProfile | null>(null);
@ -84,7 +88,9 @@
{#if loading}
<div class="flex items-center justify-center py-12">
<div class="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
<div
class="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"
></div>
</div>
{:else}
<ProfilePage
@ -130,7 +136,9 @@
<!-- Toast Notification -->
{#if toastMessage}
<div class="fixed bottom-4 right-4 z-50 px-4 py-3 bg-green-600 text-white rounded-lg shadow-lg animate-fade-in">
<div
class="fixed bottom-4 right-4 z-50 px-4 py-3 bg-green-600 text-white rounded-lg shadow-lg animate-fade-in"
>
{toastMessage}
</div>
{/if}

View file

@ -3,28 +3,28 @@
const quickLinks = [
{
href: '/clock/world-clock',
href: '/times/clock/world-clock',
icon: Globe,
label: 'Weltzeituhr',
description: 'Zeitzonen im Blick',
color: 'bg-blue-500',
},
{
href: '/clock/alarms',
href: '/times/clock/alarms',
icon: Bell,
label: 'Wecker',
description: 'Alarme verwalten',
color: 'bg-amber-500',
},
{
href: '/clock/timers',
href: '/times/clock/timers',
icon: Timer,
label: 'Timer',
description: 'Countdowns starten',
color: 'bg-green-500',
},
{
href: '/clock/stopwatch',
href: '/times/clock/stopwatch',
icon: Hourglass,
label: 'Stoppuhr',
description: 'Zeit messen',