chore: archive 25 standalone web apps, move wisekeep to apps-archived

All standalone SvelteKit web apps have been superseded by the unified
ManaCore app (apps/manacore/apps/web). Moved to web-archived/ within
each project to preserve history while removing from active workspace.

Archived: calc, cards, chat, citycorners, contacts, context, guides,
inventar, moodlit, mukke, news, nutriphi, photos, picture, planta,
presi, questions, skilltree, storage, times, zitare, todo, calendar,
uload, memoro

Moved to apps-archived/: wisekeep (not integrated, inactive)

Kept active: manacore (unified), matrix, manavoxel, arcade (separate containers)

Server, landing, and package directories remain active for each project.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-02 20:14:29 +02:00
parent 373976a11b
commit 2eb1a0cd76
1994 changed files with 278 additions and 1310 deletions

View file

@ -0,0 +1,30 @@
export interface Config {
port: number;
databaseUrl: string;
manaAuthUrl: string;
cors: { origins: string[] };
groqApiKey: string;
whisperModel: string;
}
export function loadConfig(): Config {
const requiredEnv = (key: string, fallback?: string): string => {
const value = process.env[key] || fallback;
if (!value) throw new Error(`Missing required env var: ${key}`);
return value;
};
return {
port: parseInt(process.env.PORT || '3072', 10),
databaseUrl: requiredEnv(
'DATABASE_URL',
'postgresql://manacore:devpassword@localhost:5432/mana_sync'
),
manaAuthUrl: requiredEnv('MANA_CORE_AUTH_URL', 'http://localhost:3001'),
cors: {
origins: (process.env.CORS_ORIGINS || 'http://localhost:5173').split(','),
},
groqApiKey: process.env.GROQ_API_KEY || '',
whisperModel: process.env.WHISPER_MODEL || 'whisper-large-v3-turbo',
};
}

View file

@ -0,0 +1,28 @@
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { loadConfig } from './config';
import { errorHandler } from './middleware/error-handler';
import { jwtAuth } from './middleware/jwt-auth';
import { TranscribeService } from './services/transcribe';
import { healthRoutes } from './routes/health';
import { createTranscribeRoutes } from './routes/transcribe';
const config = loadConfig();
const transcribeService = new TranscribeService(config);
const app = new Hono();
app.onError(errorHandler);
app.use('*', cors({ origin: config.cors.origins, credentials: true }));
// Public
app.route('/health', healthRoutes);
// Protected
app.use('/api/v1/*', jwtAuth(config.manaAuthUrl));
app.route('/api/v1/transcribe', createTranscribeRoutes(transcribeService));
export default {
port: config.port,
fetch: app.fetch,
};

View file

@ -0,0 +1,19 @@
import { HTTPException } from 'hono/http-exception';
export class NotFoundError extends HTTPException {
constructor(message = 'Not found') {
super(404, { message });
}
}
export class BadRequestError extends HTTPException {
constructor(message = 'Bad request') {
super(400, { message });
}
}
export class UnauthorizedError extends HTTPException {
constructor(message = 'Unauthorized') {
super(401, { message });
}
}

View file

@ -0,0 +1,11 @@
import type { ErrorHandler } from 'hono';
import { HTTPException } from 'hono/http-exception';
export const errorHandler: ErrorHandler = (err, c) => {
if (err instanceof HTTPException) {
return c.json({ statusCode: err.status, message: err.message }, err.status);
}
console.error('Unhandled error:', err);
return c.json({ statusCode: 500, message: 'Internal server error' }, 500);
};

View file

@ -0,0 +1,46 @@
import type { MiddlewareHandler } from 'hono';
import { createRemoteJWKSet, jwtVerify } from 'jose';
import { UnauthorizedError } from '../lib/errors';
export interface AuthUser {
userId: string;
email: string;
role: string;
}
let jwks: ReturnType<typeof createRemoteJWKSet> | null = null;
function getJwks(authUrl: string) {
if (!jwks) {
jwks = createRemoteJWKSet(new URL('/api/auth/jwks', authUrl));
}
return jwks;
}
export function jwtAuth(authUrl: string): MiddlewareHandler {
return async (c, next) => {
const authHeader = c.req.header('Authorization');
if (!authHeader?.startsWith('Bearer ')) {
throw new UnauthorizedError('Missing or invalid Authorization header');
}
const token = authHeader.slice(7);
try {
const { payload } = await jwtVerify(token, getJwks(authUrl), {
issuer: authUrl,
audience: 'manacore',
});
const user: AuthUser = {
userId: payload.sub || '',
email: (payload.email as string) || '',
role: (payload.role as string) || 'user',
};
c.set('user', user);
await next();
} catch {
throw new UnauthorizedError('Invalid or expired token');
}
};
}

View file

@ -0,0 +1,10 @@
import { Hono } from 'hono';
export const healthRoutes = new Hono().get('/', (c) =>
c.json({
status: 'ok',
service: 'wisekeep-server',
runtime: 'bun',
timestamp: new Date().toISOString(),
})
);

View file

@ -0,0 +1,25 @@
import { Hono } from 'hono';
import type { TranscribeService } from '../services/transcribe';
import type { AuthUser } from '../middleware/jwt-auth';
export function createTranscribeRoutes(transcribeService: TranscribeService) {
return new Hono<{ Variables: { user: AuthUser } }>().post('/', async (c) => {
const { url, language } = await c.req.json<{ url: string; language?: string }>();
if (!url) return c.json({ error: 'URL is required' }, 400);
const result = await transcribeService.transcribe(url, language || 'de');
// Return result — client saves to local-first store
return c.json({
id: crypto.randomUUID(),
url,
title: result.title,
channel: result.channel,
duration: result.duration,
transcript: result.transcript,
language: result.language,
model: result.model,
status: 'completed',
});
});
}

View file

@ -0,0 +1,60 @@
import OpenAI from 'openai';
import { $ } from 'bun';
import { unlink } from 'node:fs/promises';
import { createReadStream } from 'node:fs';
import type { Config } from '../config';
export interface TranscriptionResult {
title: string;
channel: string;
duration: number;
transcript: string;
language: string;
model: string;
}
export class TranscribeService {
private groq: OpenAI | null;
constructor(private config: Config) {
this.groq = config.groqApiKey
? new OpenAI({ apiKey: config.groqApiKey, baseURL: 'https://api.groq.com/openai/v1' })
: null;
}
async transcribe(url: string, language = 'de'): Promise<TranscriptionResult> {
if (!this.groq) throw new Error('Groq API key not configured');
// 1. Get video info
const infoResult =
await $`yt-dlp --print "%(title)s|||%(channel)s|||%(duration)s" --no-download ${url}`.text();
const [title, channel, durationStr] = infoResult.trim().split('|||');
const duration = parseInt(durationStr) || 0;
// 2. Download audio to temp file
const tempFile = `/tmp/wisekeep-${Date.now()}.mp3`;
await $`yt-dlp -x --audio-format mp3 --audio-quality 5 -o ${tempFile} ${url}`;
try {
// 3. Transcribe with Groq (use ReadStream for OpenAI SDK compat)
const transcription = await this.groq.audio.transcriptions.create({
file: createReadStream(tempFile) as unknown as Parameters<
typeof this.groq.audio.transcriptions.create
>[0]['file'],
model: this.config.whisperModel,
language,
});
return {
title: title || 'Unknown',
channel: channel || 'Unknown',
duration,
transcript: transcription.text,
language,
model: this.config.whisperModel,
};
} finally {
await unlink(tempFile).catch(() => {});
}
}
}