From 4d390be5afdc60dbeb257e33aa7bf13e17335988 Mon Sep 17 00:00:00 2001 From: Till JS Date: Sun, 29 Mar 2026 19:28:11 +0200 Subject: [PATCH] feat(news): migrate from archive to local-first + Hono architecture - Move from apps-archived/ to apps/ - Delete NestJS API, Docker files, old docs, browser extension - Create Hono/Bun server with content extraction (Mozilla Readability) and AI feed API reading from mana-sync's sync_changes - Create local-first store (articles, categories) with guest seed data - Rewrite web app: Feed page, Saved articles with URL extraction, auth pages using shared-auth-ui, AuthGate with guest mode - Add news to shared-branding (app icon, mana-apps registry) - Add CLAUDE.md, dev scripts, root CLAUDE.md entry - 0 type errors on both server and web Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 1 + .../news/MigrationPlan-Unified-App.md | 1526 -------------- apps-archived/news/README.md | 187 -- apps-archived/news/apps/api/nest-cli.json | 8 - apps-archived/news/apps/api/package.json | 38 - apps-archived/news/apps/api/src/app.module.ts | 24 - .../api/src/articles/articles.controller.ts | 70 - .../apps/api/src/articles/articles.module.ts | 12 - .../apps/api/src/articles/articles.service.ts | 134 -- .../news/apps/api/src/auth/auth.controller.ts | 88 - .../news/apps/api/src/auth/auth.module.ts | 10 - .../news/apps/api/src/auth/auth.service.ts | 150 -- .../src/categories/categories.controller.ts | 12 - .../api/src/categories/categories.module.ts | 10 - .../api/src/categories/categories.service.ts | 25 - .../decorators/current-user.decorator.ts | 6 - .../apps/api/src/common/guards/auth.guard.ts | 31 - .../content-extraction.controller.ts | 46 - .../content-extraction.module.ts | 13 - .../content-extraction.service.ts | 76 - .../apps/api/src/database/database.module.ts | 26 - apps-archived/news/apps/api/src/main.ts | 37 - .../apps/api/src/users/users.controller.ts | 56 - .../news/apps/api/src/users/users.module.ts | 12 - .../news/apps/api/src/users/users.service.ts | 47 - apps-archived/news/apps/api/tsconfig.json | 25 - .../news/apps/landing/tailwind.config.mjs | 39 - apps-archived/news/apps/web/package.json | 41 - apps-archived/news/apps/web/src/app.d.ts | 33 - .../news/apps/web/src/lib/api/feedback.ts | 15 - .../news/apps/web/src/lib/services/api.ts | 93 - .../apps/web/src/lib/stores/auth.svelte.ts | 81 - .../web/src/routes/(protected)/+layout.svelte | 154 -- .../src/routes/(protected)/apps/+page.svelte | 17 - .../src/routes/(protected)/feed/+page.svelte | 96 - .../routes/(protected)/feedback/+page.svelte | 11 - .../routes/(protected)/in-depth/+page.svelte | 73 - .../src/routes/(protected)/saved/+page.svelte | 89 - .../routes/(protected)/summaries/+page.svelte | 72 - .../news/apps/web/src/routes/+layout.svelte | 17 - .../news/apps/web/src/routes/+page.svelte | 31 - .../web/src/routes/auth/login/+page.svelte | 86 - .../web/src/routes/auth/register/+page.svelte | 99 - apps-archived/news/apps/web/vite.config.ts | 25 - apps-archived/news/docker/docker-compose.yml | 36 - apps-archived/news/docker/init.sql | 6 - .../news/packages/browser-extension/README.md | 133 -- .../packages/browser-extension/background.js | 64 - .../packages/browser-extension/content.js | 91 - .../packages/browser-extension/debug.html | 28 - .../news/packages/browser-extension/debug.js | 51 - .../packages/browser-extension/manifest.json | 30 - .../packages/browser-extension/popup.html | 165 -- .../news/packages/browser-extension/popup.js | 178 -- apps-archived/uload/.dockerignore | 32 - apps-archived/uload/.env.example | 36 - apps-archived/uload/.env.production.example | 20 - apps-archived/uload/.env.stripe.example | 17 - apps-archived/uload/.gitignore | 43 - apps-archived/uload/CLAUDE.md | 132 -- apps-archived/uload/Dockerfile | 73 - apps-archived/uload/README.md | 151 -- apps-archived/uload/apps/backend/.env.example | 22 - apps-archived/uload/apps/backend/Dockerfile | 65 - .../uload/apps/backend/nest-cli.json | 8 - apps-archived/uload/apps/backend/package.json | 76 - .../uload/apps/backend/src/app.module.ts | 75 - .../backend/src/config/validation.schema.ts | 26 - .../src/controllers/analytics.controller.ts | 95 - .../src/controllers/health.controller.ts | 31 - .../src/controllers/links.controller.ts | 128 -- .../src/controllers/redirect.controller.ts | 103 - .../backend/src/database/database.module.ts | 30 - .../database/repositories/click.repository.ts | 150 -- .../src/database/repositories/index.ts | 2 - .../database/repositories/link.repository.ts | 136 -- apps-archived/uload/apps/backend/src/main.ts | 47 - .../backend/src/services/analytics.service.ts | 99 - .../backend/src/services/links.service.ts | 138 -- .../backend/src/services/redirect.service.ts | 74 - .../uload/apps/backend/tsconfig.json | 23 - .../uload/apps/landing/astro.config.mjs | 20 - apps-archived/uload/apps/landing/package.json | 25 - .../apps/landing/src/components/Footer.astro | 114 -- .../landing/src/components/HeroSection.astro | 195 -- .../landing/src/components/Navigation.astro | 86 - .../src/content/blog/link-tracking-guide.md | 92 - .../content/blog/psychologie-kurzer-urls.md | 76 - .../uload/apps/landing/src/content/config.ts | 17 - apps-archived/uload/apps/landing/src/env.d.ts | 2 - .../apps/landing/src/layouts/BaseLayout.astro | 59 - .../landing/src/layouts/LegalLayout.astro | 28 - .../uload/apps/landing/src/pages/about.astro | 130 -- .../uload/apps/landing/src/pages/agb.astro | 76 - .../apps/landing/src/pages/blog/[slug].astro | 95 - .../apps/landing/src/pages/blog/index.astro | 69 - .../apps/landing/src/pages/datenschutz.astro | 91 - .../apps/landing/src/pages/features.astro | 169 -- .../apps/landing/src/pages/impressum.astro | 63 - .../uload/apps/landing/src/pages/index.astro | 235 --- .../apps/landing/src/pages/sicherheit.astro | 202 -- .../uload/apps/landing/src/styles/global.css | 92 - .../uload/apps/landing/tailwind.config.mjs | 48 - .../uload/apps/landing/tsconfig.json | 11 - apps-archived/uload/apps/web/.env.example | 36 - .../uload/apps/web/.env.production.example | 20 - .../uload/apps/web/.env.stripe.example | 17 - apps-archived/uload/apps/web/.npmrc | 1 - apps-archived/uload/apps/web/.prettierignore | 9 - apps-archived/uload/apps/web/.prettierrc | 16 - .../uload/apps/web/drizzle.config.ts | 14 - .../apps/web/drizzle/0000_material_puma.sql | 227 --- .../apps/web/drizzle/meta/0000_snapshot.json | 1762 ----------------- .../uload/apps/web/drizzle/meta/_journal.json | 13 - apps-archived/uload/apps/web/e2e/demo.test.ts | 6 - apps-archived/uload/apps/web/eslint.config.js | 40 - apps-archived/uload/apps/web/package.json | 76 - .../uload/apps/web/playwright.config.ts | 9 - apps-archived/uload/apps/web/src/app.css | 162 -- apps-archived/uload/apps/web/src/app.d.ts | 34 - apps-archived/uload/apps/web/src/app.html | 103 - .../src/content/authors/till-schneider.json | 11 - .../src/content/blog/link-tracking-guide.md | 157 -- .../content/blog/psychologie-kurzer-urls.md | 184 -- .../uload/apps/web/src/content/config.ts | 64 - apps-archived/uload/apps/web/src/demo.spec.ts | 7 - .../uload/apps/web/src/hooks.server.ts | 153 -- .../ab-testing/components/HeroABTest.svelte | 251 --- .../web/src/lib/ab-testing/config/variants.ts | 208 -- .../src/lib/ab-testing/service/HashManager.ts | 209 -- .../apps/web/src/lib/actions/clickOutside.ts | 16 - .../apps/web/src/lib/actions/touch.test.ts | 202 -- .../uload/apps/web/src/lib/actions/touch.ts | 343 ---- .../uload/apps/web/src/lib/analytics.ts | 145 -- .../uload/apps/web/src/lib/api/feedback.ts | 15 - .../uload/apps/web/src/lib/assets/favicon.svg | 1 - .../uload/apps/web/src/lib/auth-helper.ts | 146 -- .../uload/apps/web/src/lib/cache.test.ts | 219 -- apps-archived/uload/apps/web/src/lib/cache.ts | 93 - .../src/lib/components/AccountSwitcher.svelte | 180 -- .../apps/web/src/lib/components/Button.svelte | 48 - .../web/src/lib/components/DataTable.svelte | 186 -- .../web/src/lib/components/Dropdown.svelte | 197 -- .../src/lib/components/FloatingSidebar.svelte | 644 ------ .../apps/web/src/lib/components/Footer.svelte | 201 -- .../lib/components/LanguageSwitcher.svelte | 92 - .../src/lib/components/LinkUsageBar.svelte | 92 - .../src/lib/components/MobileSidebar.svelte | 306 --- .../web/src/lib/components/Navigation.svelte | 840 -------- .../lib/components/NotificationBell.svelte | 270 --- .../components/SimpleAccountSwitcher.svelte | 145 -- .../web/src/lib/components/StatsBar.svelte | 159 -- .../web/src/lib/components/TagBadge.svelte | 75 - .../web/src/lib/components/TagCard.svelte | 175 -- .../web/src/lib/components/TagList.svelte | 100 - .../web/src/lib/components/TagListItem.svelte | 413 ---- .../web/src/lib/components/TagSelector.svelte | 207 -- .../web/src/lib/components/TagStats.svelte | 290 --- .../src/lib/components/ThemeDropdown.svelte | 160 -- .../src/lib/components/UpgradeButton.svelte | 75 - .../web/src/lib/components/ViewToggle.svelte | 62 - .../lib/components/WorkspaceSwitcher.svelte | 202 -- .../src/lib/components/blog/BlogCard.svelte | 146 -- .../src/lib/components/cards/BaseCard.svelte | 73 - .../lib/components/cards/CardEditor.svelte | 541 ----- .../lib/components/cards/CardRenderer.svelte | 285 --- .../lib/components/cards/CustomCard.svelte | 156 -- .../lib/components/cards/ModularCard.svelte | 236 --- .../components/cards/ProfileCardItem.svelte | 149 -- .../components/cards/SafeCardRenderer.svelte | 234 --- .../lib/components/cards/TemplateCard.svelte | 190 -- .../components/cards/editor/CodeEditor.svelte | 102 - .../cards/editor/ModuleEditor.svelte | 363 ---- .../cards/editor/TemplateEditor.svelte | 202 -- .../cards/modules/ActionsModule.svelte | 55 - .../cards/modules/ContentModule.svelte | 72 - .../cards/modules/FooterModule.svelte | 53 - .../cards/modules/HeaderModule.svelte | 60 - .../cards/modules/LinksModule.svelte | 125 -- .../cards/modules/MediaModule.svelte | 44 - .../cards/modules/StatsModule.svelte | 49 - .../web/src/lib/components/cards/types.ts | 275 --- .../lib/components/gdpr/CookieBanner.svelte | 328 --- .../lib/components/landing/BlogSection.svelte | 118 -- .../components/landing/FeatureShowcase.svelte | 553 ------ .../lib/components/landing/HeroSection.svelte | 228 --- .../components/landing/PricingSection.svelte | 270 --- .../components/landing/TargetAudience.svelte | 487 ----- .../components/landing/Testimonials.svelte | 221 --- .../components/landing/TrustSignals.svelte | 270 --- .../src/lib/components/links/LinkCard.svelte | 533 ----- .../components/links/LinkCardCompact.svelte | 234 --- .../components/links/LinkCreationCard.svelte | 340 ---- .../components/links/LinkCreationForm.svelte | 1002 ---------- .../src/lib/components/links/LinkList.svelte | 200 -- .../lib/components/links/LinkListItem.svelte | 541 ----- .../src/lib/components/links/LinkStats.svelte | 314 --- .../components/mobile/InstallPWABanner.svelte | 233 --- .../lib/components/security/TOTPSetup.svelte | 411 ---- .../src/lib/components/tags/TagStats.svelte | 409 ---- .../templates/CreateTemplateModal.svelte | 322 --- .../components/templates/TemplateCard.svelte | 227 --- .../templates/TemplatePreviewModal.svelte | 267 --- .../uload/apps/web/src/lib/content/index.ts | 186 -- .../uload/apps/web/src/lib/db/index.ts | 24 - .../uload/apps/web/src/lib/db/schema.ts | 413 ---- apps-archived/uload/apps/web/src/lib/email.ts | 222 --- .../uload/apps/web/src/lib/gdpr/compliance.ts | 422 ---- .../uload/apps/web/src/lib/i18n/index.ts | 60 - .../apps/web/src/lib/i18n/locales/de.json | 28 - .../apps/web/src/lib/i18n/locales/en.json | 144 -- .../apps/web/src/lib/i18n/locales/es.json | 28 - .../apps/web/src/lib/i18n/locales/fr.json | 28 - .../apps/web/src/lib/i18n/locales/it.json | 28 - apps-archived/uload/apps/web/src/lib/index.ts | 1 - .../web/src/lib/layouts/BlogLayout.svelte | 263 --- .../web/src/lib/layouts/DefaultLayout.svelte | 14 - .../uload/apps/web/src/lib/locale.ts | 42 - .../apps/web/src/lib/pocketbase-client.ts | 29 - .../uload/apps/web/src/lib/pocketbase.spec.ts | 145 -- .../uload/apps/web/src/lib/pocketbase.ts | 202 -- apps-archived/uload/apps/web/src/lib/pwa.ts | 172 -- .../uload/apps/web/src/lib/qrcode.ts | 175 -- .../apps/web/src/lib/schemas/cardSchemas.ts | 256 --- .../lib/scripts/update-links-collection.js | 87 - .../uload/apps/web/src/lib/security/totp.ts | 284 --- .../web/src/lib/server/cache-middleware.ts | 96 - .../apps/web/src/lib/server/linkCache.ts | 216 -- .../apps/web/src/lib/server/rate-limiter.ts | 357 ---- .../apps/web/src/lib/server/redis-improved.ts | 120 -- .../uload/apps/web/src/lib/server/redis.ts | 137 -- .../uload/apps/web/src/lib/server/stripe.ts | 23 - .../web/src/lib/services/cardConverter.ts | 494 ----- .../web/src/lib/services/cardSanitizer.ts | 400 ---- .../apps/web/src/lib/services/cardService.ts | 153 -- .../web/src/lib/services/cardValidator.ts | 454 ----- .../apps/web/src/lib/services/email-sender.ts | 113 -- .../uload/apps/web/src/lib/services/email.ts | 276 --- .../apps/web/src/lib/services/iframePool.ts | 211 -- .../apps/web/src/lib/services/link-limits.ts | 152 -- .../web/src/lib/services/moduleEventBus.ts | 266 --- .../web/src/lib/services/pocketbase-email.ts | 121 -- .../uload/apps/web/src/lib/services/toast.ts | 173 -- .../src/lib/services/unifiedCardService.ts | 617 ------ .../uload/apps/web/src/lib/storage.ts | 141 -- .../uload/apps/web/src/lib/stores/accounts.ts | 146 -- .../web/src/lib/stores/activeWorkspace.ts | 145 -- .../uload/apps/web/src/lib/stores/cards.ts | 433 ---- .../apps/web/src/lib/stores/notifications.ts | 152 -- .../apps/web/src/lib/stores/unifiedCards.ts | 557 ------ .../apps/web/src/lib/stores/viewModes.ts | 63 - .../apps/web/src/lib/stores/workspaces.ts | 236 --- .../apps/web/src/lib/stripe-translations.ts | 272 --- .../uload/apps/web/src/lib/theme.svelte.ts | 3 - .../uload/apps/web/src/lib/themes/README.md | 123 -- .../uload/apps/web/src/lib/themes/presets.ts | 207 -- .../apps/web/src/lib/themes/theme-store.ts | 181 -- .../uload/apps/web/src/lib/types/accounts.ts | 146 -- .../uload/apps/web/src/lib/username.spec.ts | 171 -- .../uload/apps/web/src/lib/username.ts | 107 - .../apps/web/src/lib/utils/reserved-slugs.ts | 684 ------- .../uload/apps/web/src/paraglide/messages.ts | 175 -- .../web/src/routes/(app)/+layout.server.ts | 94 - .../apps/web/src/routes/(app)/+layout.svelte | 191 -- .../web/src/routes/(app)/apps/+page.svelte | 17 - .../src/routes/(app)/feedback/+page.svelte | 11 - .../web/src/routes/(app)/my/+layout.server.ts | 12 - .../web/src/routes/(app)/my/+page.server.ts | 9 - .../apps/web/src/routes/(app)/my/+page.svelte | 1 - .../(app)/my/analytics/[id]/+page.server.ts | 141 -- .../(app)/my/analytics/[id]/+page.svelte | 341 ---- .../src/routes/(app)/my/cards/+page.server.ts | 12 - .../src/routes/(app)/my/cards/+page.svelte | 403 ---- .../routes/(app)/my/cards/+page.svelte.backup | 482 ----- .../(app)/my/cards/builder/+page.server.ts | 13 - .../(app)/my/cards/builder/+page.svelte | 676 ------- .../src/routes/(app)/my/links/+page.server.ts | 515 ----- .../src/routes/(app)/my/links/+page.svelte | 653 ------ .../routes/(app)/my/links/debug/+page.svelte | 96 - .../src/routes/(app)/my/tags/+page.server.ts | 281 --- .../web/src/routes/(app)/my/tags/+page.svelte | 473 ----- .../routes/(app)/my/tags/page.server.spec.ts | 425 ---- .../src/routes/(app)/pricing/+page.server.ts | 12 - .../web/src/routes/(app)/pricing/+page.svelte | 337 ---- .../src/routes/(app)/settings/+page.server.ts | 182 -- .../src/routes/(app)/settings/+page.svelte | 784 -------- .../(app)/settings/team/+page.server.ts | 240 --- .../routes/(app)/settings/team/+page.svelte | 285 --- .../(app)/settings/workspaces/+page.server.ts | 98 - .../(app)/settings/workspaces/+page.svelte | 224 --- .../settings/workspaces/[id]/+page.server.ts | 242 --- .../settings/workspaces/[id]/+page.svelte | 420 ---- .../settings/workspaces/new/+page.server.ts | 130 -- .../settings/workspaces/new/+page.svelte | 276 --- .../(app)/setup-username/+page.server.ts | 145 -- .../routes/(app)/setup-username/+page.svelte | 217 -- .../(app)/template-store/+page.server.ts | 25 - .../routes/(app)/template-store/+page.svelte | 314 --- .../apps/web/src/routes/(auth)/+layout.svelte | 15 - .../(auth)/forgot-password/+page.server.ts | 52 - .../(auth)/forgot-password/+page.svelte | 43 - .../src/routes/(auth)/login/+page.server.ts | 54 - .../web/src/routes/(auth)/login/+page.svelte | 62 - .../routes/(auth)/register/+page.server.ts | 260 --- .../src/routes/(auth)/register/+page.svelte | 89 - .../routes/(auth)/register/register.test.ts | 87 - .../(auth)/reset-password/+page.server.ts | 57 - .../routes/(auth)/reset-password/+page.svelte | 186 -- .../(auth)/verify-email/+page.server.ts | 84 - .../routes/(auth)/verify-email/+page.svelte | 31 - .../apps/web/src/routes/+layout.server.ts | 7 - .../uload/apps/web/src/routes/+layout.svelte | 69 - .../uload/apps/web/src/routes/+page.server.ts | 173 -- .../uload/apps/web/src/routes/+page.svelte | 612 ------ .../web/src/routes/[...slug]/+page.server.ts | 196 -- .../web/src/routes/[...slug]/+page.svelte | 67 - .../src/routes/api/check-username/+server.ts | 45 - .../apps/web/src/routes/api/health/+server.ts | 10 - .../src/routes/api/redis-status/+server.ts | 42 - .../src/routes/api/stripe/checkout/+server.ts | 123 -- .../src/routes/api/stripe/webhook/+server.ts | 244 --- .../web/src/routes/api/test-pb/+server.ts | 42 - .../apps/web/src/routes/api/verify/+server.ts | 21 - .../apps/web/src/routes/api/vote/+server.ts | 88 - .../src/routes/checkout/success/+page.svelte | 144 -- .../apps/web/src/routes/health/+server.ts | 39 - .../apps/web/src/routes/offline/+page.svelte | 130 -- .../src/routes/p/[username]/+page.server.ts | 108 - .../web/src/routes/p/[username]/+page.svelte | 277 --- .../apps/web/src/routes/page.svelte.spec.ts | 13 - .../apps/web/src/routes/preview/+page.svelte | 50 - .../web/src/routes/sitemap.xml/+server.ts | 54 - .../routes/team/accept-invite/+page.server.ts | 72 - .../routes/team/accept-invite/+page.svelte | 107 - .../apps/web/src/routes/test-redis/+server.ts | 192 -- .../src/routes/u/[username]/+page.server.ts | 40 - .../web/src/routes/u/[username]/+page.svelte | 70 - .../w/[workspace]/[...code]/+page.server.ts | 163 -- .../w/[workspace]/[...code]/+page.svelte | 120 -- .../apps/web/src/tests/factories/index.ts | 165 -- .../apps/web/src/tests/mocks/pocketbase.ts | 107 - .../uload/apps/web/src/tests/setup.ts | 240 --- .../web/static/icons/apple-touch-icon.svg | 4 - .../apps/web/static/icons/icon-128x128.svg | 4 - .../apps/web/static/icons/icon-144x144.svg | 4 - .../apps/web/static/icons/icon-152x152.svg | 4 - .../apps/web/static/icons/icon-192x192.svg | 4 - .../apps/web/static/icons/icon-384x384.svg | 4 - .../apps/web/static/icons/icon-512x512.svg | 4 - .../apps/web/static/icons/icon-72x72.svg | 4 - .../apps/web/static/icons/icon-96x96.svg | 4 - .../static/icons/icon-maskable-192x192.svg | 5 - .../static/icons/icon-maskable-512x512.svg | 5 - .../uload/apps/web/static/manifest.json | 132 -- .../uload/apps/web/static/robots.txt | 3 - apps-archived/uload/apps/web/static/sw.js | 244 --- apps-archived/uload/apps/web/svelte.config.js | 47 - .../uload/apps/web/tailwind.config.js | 24 - apps-archived/uload/apps/web/tsconfig.json | 19 - apps-archived/uload/apps/web/vite.config.ts | 87 - .../uload/apps/web/vitest-setup-client.ts | 2 - apps-archived/uload/backend/.gitignore | 11 - apps-archived/uload/backend/CHANGELOG.md | 427 ---- apps-archived/uload/backend/LICENSE.md | 17 - .../uload/backend/MANUAL_SCHEMA_SETUP.md | 135 -- .../uload/backend/download-pocketbase.sh | 24 - .../pb_hooks/team_invitations.pb.js.disabled | 292 --- apps-archived/uload/backend/pb_schema.json | 141 -- apps-archived/uload/docker-compose.prod.yml | 98 - apps-archived/uload/docker-compose.yml | 93 - .../docs/ACCOUNT_SHARING_SIMPLIFICATION.md | 264 --- .../uload/docs/ADD_MISSING_FIELDS.md | 72 - .../uload/docs/ADMIN_ACCESS_GUIDE.md | 125 -- apps-archived/uload/docs/AUTH_FIX_SUMMARY.md | 65 - .../docs/COMPLETE-POCKETBASE-DEV-SETUP.md | 482 ----- .../uload/docs/DATABASE_SWITCHING.md | 107 - apps-archived/uload/docs/DEPLOYMENT.md | 441 ----- apps-archived/uload/docs/DEPLOYMENT_FIX.md | 53 - apps-archived/uload/docs/DEPLOYMENT_GUIDE.md | 217 -- .../uload/docs/DEPLOYMENT_LESSONS_LEARNED.md | 314 --- .../uload/docs/DOMAIN_SETUP_ULO_AD.md | 362 ---- apps-archived/uload/docs/EMAIL_SETUP.md | 109 - apps-archived/uload/docs/FINAL_SOLUTION.md | 74 - apps-archived/uload/docs/MCP-DUAL-SETUP.md | 171 -- .../uload/docs/MIGRATION_COMPLETE.md | 121 -- apps-archived/uload/docs/MIGRATION_GUIDE.md | 452 ----- .../uload/docs/PARAGLIDE_SVELTEKIT_GUIDE.md | 395 ---- apps-archived/uload/docs/POCKETBASE_ADMIN.md | 65 - apps-archived/uload/docs/README_DEPLOYMENT.md | 135 -- apps-archived/uload/docs/REDIS-SETUP.md | 113 -- .../uload/docs/SECURITY_BEST_PRACTICES.md | 160 -- .../uload/docs/SETUP_POCKETBASE_SERVER.md | 119 -- .../docs/STRIPE_INTEGRATION_EXPLAINED.md | 240 --- .../uload/docs/WORKSPACE_MIGRATION_REPORT.md | 310 --- .../docs/ab-testing-multilingual-plan.md | 224 --- .../docs/ab-testing-multilingual-test.md | 141 -- .../uload/docs/auth-implementation-notes.md | 189 -- .../docs/blog/BLOG-INTEGRATION-PROPOSALS.md | 447 ----- .../blog/HYBRID-MDSVEX-COLLECTIONS-GUIDE.md | 874 -------- .../uload/docs/blog/IMPLEMENTATION-PLAN.md | 433 ---- .../docs/blog/STATIC-MARKDOWN-BLOG-GUIDE.md | 702 ------- .../docs/blog/SVELTE5-HYBRID-BLOG-GUIDE.md | 948 --------- .../docs/blog/die-psychologie-kurzer-urls.md | 274 --- apps-archived/uload/docs/brevo-setup.md | 236 --- .../uload/docs/card-architecture-v2.md | 187 -- apps-archived/uload/docs/cards/README.md | 91 - .../uload/docs/cards/api-reference.md | 539 ----- .../uload/docs/cards/architecture.md | 276 --- apps-archived/uload/docs/cards/components.md | 350 ---- apps-archived/uload/docs/cards/examples.md | 579 ------ .../docs/cards/implementation-comparison.md | 304 --- apps-archived/uload/docs/cards/modules.md | 408 ---- .../docs/cards/server-side-html-cards.md | 551 ------ apps-archived/uload/docs/cards/templates.md | 458 ----- apps-archived/uload/docs/cards/themes.md | 420 ---- .../docs/cards/unified-cards-migration.md | 278 --- .../uload/docs/database-refactoring-plan.md | 331 ---- .../20250815-15:08-feature-overview.md | 285 --- .../docs/features/ab-testing-test-urls.md | 56 - .../features/abc-testing-implementation.md | 712 ------- .../docs/features/downtime-prevention-plan.md | 631 ------ .../features/features/unified-card-system.md | 498 ----- .../mail/COMPLETE-EMAIL-SETUP-GUIDE.md | 495 ----- .../docs/features/mail/SMTP-SETUP-SECURE.md | 86 - .../mail/email-templates-bilingual.md | 765 ------- .../mail/email-templates-simplified.md | 470 ----- .../docs/features/mail/email-templates.md | 584 ------ .../features/mail/multilingual-email-plan.md | 529 ----- .../pocketbase/POCKETBASE-DEV-SETUP-PLAN.md | 225 --- .../pocketbase/POCKETBASE-MANUAL-SETUP.md | 129 -- .../pocketbase/pocketbase-local-setup.md | 246 --- .../features/pocketbase/pocketbase-setup.md | 123 -- .../uload/docs/features/profile-background.md | 60 - .../features/projects-shared-usernames.md | 287 --- .../redis_docs/reddis-ressourcen-hosting.md | 513 ----- .../features/redis_docs/reddis-vorteile.md | 185 -- .../features/redis_docs/redis-architecture.md | 302 --- .../features/redis_docs/redis-local-setup.md | 317 --- .../features/redis_docs/redis-quickstart.md | 125 -- .../referral/referral-tracking-system.md | 411 ---- .../referral-vs-promo-code-comparison.md | 1033 ---------- ...voucher-code-system-community-affiliate.md | 943 --------- .../voucher-implementation-approaches.md | 501 ----- .../docs/features/toast-integration-status.md | 246 --- .../docs/features/toast-notifications.md | 261 --- .../marketing/Educational-Content-Posts.md | 485 ----- ...tional-Sprecher-Script-Psychologie-URLs.md | 71 - .../uload/docs/marketing/Facebook.md | 160 -- .../marketing/Instagram-Content-Strategie.md | 233 --- .../docs/marketing/Instagram-Story-Scripte.md | 131 -- .../docs/marketing/Landing-Page-Guidelines.md | 230 --- .../Marktpositionierung+Zielgruppen.md | 268 --- .../uload/docs/marketing/VideoScripts.md | 166 -- .../docs/marketing/VideoScripts_Fließtext.md | 77 - .../docs/marketing/hero-image-prompts.md | 188 -- .../uload/docs/marketing/landing-page.md | 272 --- .../monetize/link-limits-implementation.md | 298 --- .../uload/docs/plan-public-profiles.md | 122 -- .../docs/plans/development-roadmap-2025.md | 972 --------- .../reports/app-stability-testing-strategy.md | 568 ------ .../reports/card-architecture-analysis.md | 333 ---- .../docs/reports/cards-feature-analysis.md | 212 -- .../uload/docs/reports/cards-profile-fix.md | 92 - ...ase-evaluation-pocketbase-vs-postgresql.md | 166 -- .../reports/database-optimization-report.md | 366 ---- .../uload/docs/reports/email-setup-options.md | 461 ----- .../reports/empfehlungen-und-aktionsplan.md | 454 ----- .../docs/reports/homepage-abc-testing-plan.md | 591 ------ .../ip-geolocation-implementation-options.md | 238 --- .../docs/reports/landing-page-strategie.md | 256 --- .../marketing-seite-implementierungsplan.md | 676 ------- .../reports/marketing-website-architektur.md | 1339 ------------- ...gration-effort-pocketbase-to-postgresql.md | 405 ---- .../naming-convention-migration-plan.md | 439 ---- .../performance-vergleich-uload-shlink.md | 525 ----- .../self-hosted-geolocation-solutions.md | 335 ---- .../uload/docs/setup-local-pocketbase.md | 59 - .../docs/stripe/CLAUDE_CODE_MCP_SETUP.md | 207 -- .../uload/docs/stripe/CODE_SNIPPETS.md | 786 -------- .../uload/docs/stripe/IMPLEMENTATION_GUIDE.md | 713 ------- .../uload/docs/stripe/MCP_QUICK_SETUP.md | 136 -- .../uload/docs/stripe/MCP_SERVER_GUIDE.md | 413 ---- .../docs/stripe/Monetization_Overview.md | 141 -- .../uload/docs/stripe/QUICK_START.md | 276 --- .../docs/stripe/STRIPE_SETUP_SIMPLEST_WAY.md | 327 --- .../uload/docs/stripe/deployment-setup.md | 29 - .../uload/docs/stripe/testing-checklist.md | 195 -- apps-archived/uload/docs/tags-feature.md | 146 -- .../uload/docs/testing/test-pages.md | 83 - .../uload/docs/umami-custom-events.md | 364 ---- apps-archived/uload/docs/umami-setup.md | 87 - .../uload/scripts/apply-db-optimizations.sh | 44 - .../uload/scripts/check-prod-redis.sh | 46 - apps-archived/uload/scripts/create-admin.sh | 12 - .../uload/scripts/create-collections.mjs | 425 ---- .../scripts/create-default-templates.cjs | 320 --- .../create-unified-cards-collection.js | 389 ---- apps-archived/uload/scripts/debug-auth.mjs | 106 - .../uload/scripts/extract-templates.js | 41 - .../uload/scripts/fix-field-names.sh | 23 - apps-archived/uload/scripts/fix-imports.sh | 25 - .../uload/scripts/fix-links-collection.sh | 86 - .../uload/scripts/generate-pwa-icons.js | 65 - .../uload/scripts/generate-pwa-icons.mjs | 72 - .../uload/scripts/migrate-links-collection.sh | 63 - apps-archived/uload/scripts/migrate-links.js | 80 - .../scripts/migrate-to-username-prefix.js | 76 - .../uload/scripts/migrate-to-workspaces.js | 156 -- .../uload/scripts/optimize-database.sql | 69 - apps-archived/uload/scripts/seed-local-db.js | 244 --- .../uload/scripts/setup-test-user.mjs | 59 - apps-archived/uload/scripts/test-email.js | 54 - apps-archived/uload/scripts/test-env.js | 30 - apps-archived/uload/scripts/test-env.mjs | 34 - .../uload/scripts/test-local-redis.mjs | 48 - .../uload/scripts/test-pb-connection.js | 35 - apps-archived/uload/scripts/test-pb-prod.js | 40 - apps-archived/uload/scripts/test-pb.js | 35 - .../uload/scripts/test-pocketbase.js | 71 - .../uload/scripts/test-prod-pocketbase.js | 138 -- .../uload/scripts/test-redis-cache.js | 232 --- .../uload/scripts/test-redis-cache.mjs | 268 --- .../uload/scripts/test-registration.js | 64 - .../uload/scripts/test-url-variants.js | 76 - {apps-archived => apps}/news/.env.example | 0 {apps-archived => apps}/news/.gitignore | 0 apps/news/CLAUDE.md | 67 + .../news/apps/landing/astro.config.mjs | 7 +- .../news/apps/landing/package.json | 0 .../apps/landing/src/components/Footer.astro | 0 .../landing/src/components/Navigation.astro | 0 .../apps/landing/src/layouts/Layout.astro | 0 .../news/apps/landing/src/pages/index.astro | 0 .../news/apps/landing/src/styles/global.css | 0 apps/news/apps/landing/tailwind.config.mjs | 37 + .../news/apps/landing/tsconfig.json | 0 apps/news/apps/server/package.json | 24 + apps/news/apps/server/src/config.ts | 26 + apps/news/apps/server/src/db/connection.ts | 14 + apps/news/apps/server/src/index.ts | 46 + apps/news/apps/server/src/lib/errors.ts | 19 + .../server/src/middleware/error-handler.ts | 11 + .../apps/server/src/middleware/jwt-auth.ts | 46 + apps/news/apps/server/src/routes/extract.ts | 38 + apps/news/apps/server/src/routes/feed.ts | 21 + apps/news/apps/server/src/routes/health.ts | 10 + apps/news/apps/server/src/services/extract.ts | 50 + apps/news/apps/server/src/services/feed.ts | 71 + apps/news/apps/server/tsconfig.json | 16 + .../news/apps/web/.env.example | 0 .../news/apps/web/eslint.config.js | 0 apps/news/apps/web/package.json | 33 + .../news/apps/web/src/app.css | 0 apps/news/apps/web/src/app.d.ts | 11 + .../news/apps/web/src/app.html | 0 .../web/src/lib/components/NewsLogo.svelte | 11 + apps/news/apps/web/src/lib/data/guest-seed.ts | 39 + .../news/apps/web/src/lib/data/local-store.ts | 50 + .../apps/web/src/lib/stores/auth.svelte.ts | 5 + .../web/src/routes/(protected)/+layout.svelte | 120 ++ .../src/routes/(protected)/feed/+page.svelte | 103 + .../src/routes/(protected)/saved/+page.svelte | 186 ++ apps/news/apps/web/src/routes/+layout.svelte | 37 + apps/news/apps/web/src/routes/+page.svelte | 6 + .../web/src/routes/auth/login/+page.svelte | 27 + .../web/src/routes/auth/register/+page.svelte | 20 + .../news/apps/web/svelte.config.js | 5 +- .../news/apps/web/tsconfig.json | 0 apps/news/apps/web/vite.config.ts | 7 + apps/news/package.json | 8 + package.json | 5 + packages/shared-branding/src/app-icons.ts | 3 + packages/shared-branding/src/mana-apps.ts | 17 + pnpm-lock.yaml | 201 +- 574 files changed, 1385 insertions(+), 100253 deletions(-) delete mode 100644 apps-archived/news/MigrationPlan-Unified-App.md delete mode 100644 apps-archived/news/README.md delete mode 100644 apps-archived/news/apps/api/nest-cli.json delete mode 100644 apps-archived/news/apps/api/package.json delete mode 100644 apps-archived/news/apps/api/src/app.module.ts delete mode 100644 apps-archived/news/apps/api/src/articles/articles.controller.ts delete mode 100644 apps-archived/news/apps/api/src/articles/articles.module.ts delete mode 100644 apps-archived/news/apps/api/src/articles/articles.service.ts delete mode 100644 apps-archived/news/apps/api/src/auth/auth.controller.ts delete mode 100644 apps-archived/news/apps/api/src/auth/auth.module.ts delete mode 100644 apps-archived/news/apps/api/src/auth/auth.service.ts delete mode 100644 apps-archived/news/apps/api/src/categories/categories.controller.ts delete mode 100644 apps-archived/news/apps/api/src/categories/categories.module.ts delete mode 100644 apps-archived/news/apps/api/src/categories/categories.service.ts delete mode 100644 apps-archived/news/apps/api/src/common/decorators/current-user.decorator.ts delete mode 100644 apps-archived/news/apps/api/src/common/guards/auth.guard.ts delete mode 100644 apps-archived/news/apps/api/src/content-extraction/content-extraction.controller.ts delete mode 100644 apps-archived/news/apps/api/src/content-extraction/content-extraction.module.ts delete mode 100644 apps-archived/news/apps/api/src/content-extraction/content-extraction.service.ts delete mode 100644 apps-archived/news/apps/api/src/database/database.module.ts delete mode 100644 apps-archived/news/apps/api/src/main.ts delete mode 100644 apps-archived/news/apps/api/src/users/users.controller.ts delete mode 100644 apps-archived/news/apps/api/src/users/users.module.ts delete mode 100644 apps-archived/news/apps/api/src/users/users.service.ts delete mode 100644 apps-archived/news/apps/api/tsconfig.json delete mode 100644 apps-archived/news/apps/landing/tailwind.config.mjs delete mode 100644 apps-archived/news/apps/web/package.json delete mode 100644 apps-archived/news/apps/web/src/app.d.ts delete mode 100644 apps-archived/news/apps/web/src/lib/api/feedback.ts delete mode 100644 apps-archived/news/apps/web/src/lib/services/api.ts delete mode 100644 apps-archived/news/apps/web/src/lib/stores/auth.svelte.ts delete mode 100644 apps-archived/news/apps/web/src/routes/(protected)/+layout.svelte delete mode 100644 apps-archived/news/apps/web/src/routes/(protected)/apps/+page.svelte delete mode 100644 apps-archived/news/apps/web/src/routes/(protected)/feed/+page.svelte delete mode 100644 apps-archived/news/apps/web/src/routes/(protected)/feedback/+page.svelte delete mode 100644 apps-archived/news/apps/web/src/routes/(protected)/in-depth/+page.svelte delete mode 100644 apps-archived/news/apps/web/src/routes/(protected)/saved/+page.svelte delete mode 100644 apps-archived/news/apps/web/src/routes/(protected)/summaries/+page.svelte delete mode 100644 apps-archived/news/apps/web/src/routes/+layout.svelte delete mode 100644 apps-archived/news/apps/web/src/routes/+page.svelte delete mode 100644 apps-archived/news/apps/web/src/routes/auth/login/+page.svelte delete mode 100644 apps-archived/news/apps/web/src/routes/auth/register/+page.svelte delete mode 100644 apps-archived/news/apps/web/vite.config.ts delete mode 100644 apps-archived/news/docker/docker-compose.yml delete mode 100644 apps-archived/news/docker/init.sql delete mode 100644 apps-archived/news/packages/browser-extension/README.md delete mode 100644 apps-archived/news/packages/browser-extension/background.js delete mode 100644 apps-archived/news/packages/browser-extension/content.js delete mode 100644 apps-archived/news/packages/browser-extension/debug.html delete mode 100644 apps-archived/news/packages/browser-extension/debug.js delete mode 100644 apps-archived/news/packages/browser-extension/manifest.json delete mode 100644 apps-archived/news/packages/browser-extension/popup.html delete mode 100644 apps-archived/news/packages/browser-extension/popup.js delete mode 100644 apps-archived/uload/.dockerignore delete mode 100644 apps-archived/uload/.env.example delete mode 100644 apps-archived/uload/.env.production.example delete mode 100644 apps-archived/uload/.env.stripe.example delete mode 100644 apps-archived/uload/.gitignore delete mode 100644 apps-archived/uload/CLAUDE.md delete mode 100644 apps-archived/uload/Dockerfile delete mode 100644 apps-archived/uload/README.md delete mode 100644 apps-archived/uload/apps/backend/.env.example delete mode 100644 apps-archived/uload/apps/backend/Dockerfile delete mode 100644 apps-archived/uload/apps/backend/nest-cli.json delete mode 100644 apps-archived/uload/apps/backend/package.json delete mode 100644 apps-archived/uload/apps/backend/src/app.module.ts delete mode 100644 apps-archived/uload/apps/backend/src/config/validation.schema.ts delete mode 100644 apps-archived/uload/apps/backend/src/controllers/analytics.controller.ts delete mode 100644 apps-archived/uload/apps/backend/src/controllers/health.controller.ts delete mode 100644 apps-archived/uload/apps/backend/src/controllers/links.controller.ts delete mode 100644 apps-archived/uload/apps/backend/src/controllers/redirect.controller.ts delete mode 100644 apps-archived/uload/apps/backend/src/database/database.module.ts delete mode 100644 apps-archived/uload/apps/backend/src/database/repositories/click.repository.ts delete mode 100644 apps-archived/uload/apps/backend/src/database/repositories/index.ts delete mode 100644 apps-archived/uload/apps/backend/src/database/repositories/link.repository.ts delete mode 100644 apps-archived/uload/apps/backend/src/main.ts delete mode 100644 apps-archived/uload/apps/backend/src/services/analytics.service.ts delete mode 100644 apps-archived/uload/apps/backend/src/services/links.service.ts delete mode 100644 apps-archived/uload/apps/backend/src/services/redirect.service.ts delete mode 100644 apps-archived/uload/apps/backend/tsconfig.json delete mode 100644 apps-archived/uload/apps/landing/astro.config.mjs delete mode 100644 apps-archived/uload/apps/landing/package.json delete mode 100644 apps-archived/uload/apps/landing/src/components/Footer.astro delete mode 100644 apps-archived/uload/apps/landing/src/components/HeroSection.astro delete mode 100644 apps-archived/uload/apps/landing/src/components/Navigation.astro delete mode 100644 apps-archived/uload/apps/landing/src/content/blog/link-tracking-guide.md delete mode 100644 apps-archived/uload/apps/landing/src/content/blog/psychologie-kurzer-urls.md delete mode 100644 apps-archived/uload/apps/landing/src/content/config.ts delete mode 100644 apps-archived/uload/apps/landing/src/env.d.ts delete mode 100644 apps-archived/uload/apps/landing/src/layouts/BaseLayout.astro delete mode 100644 apps-archived/uload/apps/landing/src/layouts/LegalLayout.astro delete mode 100644 apps-archived/uload/apps/landing/src/pages/about.astro delete mode 100644 apps-archived/uload/apps/landing/src/pages/agb.astro delete mode 100644 apps-archived/uload/apps/landing/src/pages/blog/[slug].astro delete mode 100644 apps-archived/uload/apps/landing/src/pages/blog/index.astro delete mode 100644 apps-archived/uload/apps/landing/src/pages/datenschutz.astro delete mode 100644 apps-archived/uload/apps/landing/src/pages/features.astro delete mode 100644 apps-archived/uload/apps/landing/src/pages/impressum.astro delete mode 100644 apps-archived/uload/apps/landing/src/pages/index.astro delete mode 100644 apps-archived/uload/apps/landing/src/pages/sicherheit.astro delete mode 100644 apps-archived/uload/apps/landing/src/styles/global.css delete mode 100644 apps-archived/uload/apps/landing/tailwind.config.mjs delete mode 100644 apps-archived/uload/apps/landing/tsconfig.json delete mode 100644 apps-archived/uload/apps/web/.env.example delete mode 100644 apps-archived/uload/apps/web/.env.production.example delete mode 100644 apps-archived/uload/apps/web/.env.stripe.example delete mode 100644 apps-archived/uload/apps/web/.npmrc delete mode 100644 apps-archived/uload/apps/web/.prettierignore delete mode 100644 apps-archived/uload/apps/web/.prettierrc delete mode 100644 apps-archived/uload/apps/web/drizzle.config.ts delete mode 100644 apps-archived/uload/apps/web/drizzle/0000_material_puma.sql delete mode 100644 apps-archived/uload/apps/web/drizzle/meta/0000_snapshot.json delete mode 100644 apps-archived/uload/apps/web/drizzle/meta/_journal.json delete mode 100644 apps-archived/uload/apps/web/e2e/demo.test.ts delete mode 100644 apps-archived/uload/apps/web/eslint.config.js delete mode 100644 apps-archived/uload/apps/web/package.json delete mode 100644 apps-archived/uload/apps/web/playwright.config.ts delete mode 100644 apps-archived/uload/apps/web/src/app.css delete mode 100644 apps-archived/uload/apps/web/src/app.d.ts delete mode 100644 apps-archived/uload/apps/web/src/app.html delete mode 100644 apps-archived/uload/apps/web/src/content/authors/till-schneider.json delete mode 100644 apps-archived/uload/apps/web/src/content/blog/link-tracking-guide.md delete mode 100644 apps-archived/uload/apps/web/src/content/blog/psychologie-kurzer-urls.md delete mode 100644 apps-archived/uload/apps/web/src/content/config.ts delete mode 100644 apps-archived/uload/apps/web/src/demo.spec.ts delete mode 100644 apps-archived/uload/apps/web/src/hooks.server.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/ab-testing/components/HeroABTest.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/ab-testing/config/variants.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/ab-testing/service/HashManager.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/actions/clickOutside.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/actions/touch.test.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/actions/touch.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/analytics.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/api/feedback.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/assets/favicon.svg delete mode 100644 apps-archived/uload/apps/web/src/lib/auth-helper.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/cache.test.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/cache.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/components/AccountSwitcher.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/Button.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/DataTable.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/Dropdown.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/FloatingSidebar.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/Footer.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/LanguageSwitcher.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/LinkUsageBar.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/MobileSidebar.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/Navigation.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/NotificationBell.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/SimpleAccountSwitcher.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/StatsBar.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/TagBadge.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/TagCard.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/TagList.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/TagListItem.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/TagSelector.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/TagStats.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/ThemeDropdown.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/UpgradeButton.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/ViewToggle.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/WorkspaceSwitcher.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/blog/BlogCard.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/cards/BaseCard.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/cards/CardEditor.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/cards/CardRenderer.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/cards/CustomCard.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/cards/ModularCard.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/cards/ProfileCardItem.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/cards/SafeCardRenderer.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/cards/TemplateCard.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/cards/editor/CodeEditor.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/cards/editor/ModuleEditor.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/cards/editor/TemplateEditor.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/cards/modules/ActionsModule.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/cards/modules/ContentModule.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/cards/modules/FooterModule.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/cards/modules/HeaderModule.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/cards/modules/LinksModule.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/cards/modules/MediaModule.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/cards/modules/StatsModule.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/cards/types.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/components/gdpr/CookieBanner.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/landing/BlogSection.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/landing/FeatureShowcase.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/landing/HeroSection.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/landing/PricingSection.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/landing/TargetAudience.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/landing/Testimonials.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/landing/TrustSignals.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/links/LinkCard.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/links/LinkCardCompact.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/links/LinkCreationCard.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/links/LinkCreationForm.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/links/LinkList.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/links/LinkListItem.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/links/LinkStats.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/mobile/InstallPWABanner.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/security/TOTPSetup.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/tags/TagStats.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/templates/CreateTemplateModal.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/templates/TemplateCard.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/components/templates/TemplatePreviewModal.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/content/index.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/db/index.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/db/schema.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/email.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/gdpr/compliance.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/i18n/index.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/i18n/locales/de.json delete mode 100644 apps-archived/uload/apps/web/src/lib/i18n/locales/en.json delete mode 100644 apps-archived/uload/apps/web/src/lib/i18n/locales/es.json delete mode 100644 apps-archived/uload/apps/web/src/lib/i18n/locales/fr.json delete mode 100644 apps-archived/uload/apps/web/src/lib/i18n/locales/it.json delete mode 100644 apps-archived/uload/apps/web/src/lib/index.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/layouts/BlogLayout.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/layouts/DefaultLayout.svelte delete mode 100644 apps-archived/uload/apps/web/src/lib/locale.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/pocketbase-client.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/pocketbase.spec.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/pocketbase.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/pwa.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/qrcode.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/schemas/cardSchemas.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/scripts/update-links-collection.js delete mode 100644 apps-archived/uload/apps/web/src/lib/security/totp.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/server/cache-middleware.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/server/linkCache.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/server/rate-limiter.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/server/redis-improved.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/server/redis.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/server/stripe.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/services/cardConverter.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/services/cardSanitizer.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/services/cardService.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/services/cardValidator.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/services/email-sender.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/services/email.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/services/iframePool.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/services/link-limits.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/services/moduleEventBus.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/services/pocketbase-email.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/services/toast.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/services/unifiedCardService.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/storage.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/stores/accounts.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/stores/activeWorkspace.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/stores/cards.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/stores/notifications.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/stores/unifiedCards.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/stores/viewModes.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/stores/workspaces.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/stripe-translations.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/theme.svelte.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/themes/README.md delete mode 100644 apps-archived/uload/apps/web/src/lib/themes/presets.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/themes/theme-store.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/types/accounts.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/username.spec.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/username.ts delete mode 100644 apps-archived/uload/apps/web/src/lib/utils/reserved-slugs.ts delete mode 100644 apps-archived/uload/apps/web/src/paraglide/messages.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/+layout.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/+layout.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/apps/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/feedback/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/my/+layout.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/my/+page.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/my/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/my/analytics/[id]/+page.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/my/analytics/[id]/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/my/cards/+page.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/my/cards/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/my/cards/+page.svelte.backup delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/my/cards/builder/+page.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/my/cards/builder/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/my/links/+page.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/my/links/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/my/links/debug/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/my/tags/+page.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/my/tags/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/my/tags/page.server.spec.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/pricing/+page.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/pricing/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/settings/+page.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/settings/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/settings/team/+page.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/settings/team/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/settings/workspaces/+page.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/settings/workspaces/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/settings/workspaces/[id]/+page.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/settings/workspaces/[id]/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/settings/workspaces/new/+page.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/settings/workspaces/new/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/setup-username/+page.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/setup-username/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/template-store/+page.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/(app)/template-store/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/(auth)/+layout.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/(auth)/forgot-password/+page.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/(auth)/forgot-password/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/(auth)/login/+page.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/(auth)/login/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/(auth)/register/+page.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/(auth)/register/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/(auth)/register/register.test.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/(auth)/reset-password/+page.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/(auth)/reset-password/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/(auth)/verify-email/+page.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/(auth)/verify-email/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/+layout.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/+layout.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/+page.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/[...slug]/+page.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/[...slug]/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/api/check-username/+server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/api/health/+server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/api/redis-status/+server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/api/stripe/checkout/+server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/api/stripe/webhook/+server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/api/test-pb/+server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/api/verify/+server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/api/vote/+server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/checkout/success/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/health/+server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/offline/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/p/[username]/+page.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/p/[username]/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/page.svelte.spec.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/preview/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/sitemap.xml/+server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/team/accept-invite/+page.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/team/accept-invite/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/test-redis/+server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/u/[username]/+page.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/u/[username]/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/routes/w/[workspace]/[...code]/+page.server.ts delete mode 100644 apps-archived/uload/apps/web/src/routes/w/[workspace]/[...code]/+page.svelte delete mode 100644 apps-archived/uload/apps/web/src/tests/factories/index.ts delete mode 100644 apps-archived/uload/apps/web/src/tests/mocks/pocketbase.ts delete mode 100644 apps-archived/uload/apps/web/src/tests/setup.ts delete mode 100644 apps-archived/uload/apps/web/static/icons/apple-touch-icon.svg delete mode 100644 apps-archived/uload/apps/web/static/icons/icon-128x128.svg delete mode 100644 apps-archived/uload/apps/web/static/icons/icon-144x144.svg delete mode 100644 apps-archived/uload/apps/web/static/icons/icon-152x152.svg delete mode 100644 apps-archived/uload/apps/web/static/icons/icon-192x192.svg delete mode 100644 apps-archived/uload/apps/web/static/icons/icon-384x384.svg delete mode 100644 apps-archived/uload/apps/web/static/icons/icon-512x512.svg delete mode 100644 apps-archived/uload/apps/web/static/icons/icon-72x72.svg delete mode 100644 apps-archived/uload/apps/web/static/icons/icon-96x96.svg delete mode 100644 apps-archived/uload/apps/web/static/icons/icon-maskable-192x192.svg delete mode 100644 apps-archived/uload/apps/web/static/icons/icon-maskable-512x512.svg delete mode 100644 apps-archived/uload/apps/web/static/manifest.json delete mode 100644 apps-archived/uload/apps/web/static/robots.txt delete mode 100644 apps-archived/uload/apps/web/static/sw.js delete mode 100644 apps-archived/uload/apps/web/svelte.config.js delete mode 100644 apps-archived/uload/apps/web/tailwind.config.js delete mode 100644 apps-archived/uload/apps/web/tsconfig.json delete mode 100644 apps-archived/uload/apps/web/vite.config.ts delete mode 100644 apps-archived/uload/apps/web/vitest-setup-client.ts delete mode 100644 apps-archived/uload/backend/.gitignore delete mode 100644 apps-archived/uload/backend/CHANGELOG.md delete mode 100644 apps-archived/uload/backend/LICENSE.md delete mode 100644 apps-archived/uload/backend/MANUAL_SCHEMA_SETUP.md delete mode 100755 apps-archived/uload/backend/download-pocketbase.sh delete mode 100644 apps-archived/uload/backend/pb_hooks/team_invitations.pb.js.disabled delete mode 100644 apps-archived/uload/backend/pb_schema.json delete mode 100644 apps-archived/uload/docker-compose.prod.yml delete mode 100644 apps-archived/uload/docker-compose.yml delete mode 100644 apps-archived/uload/docs/ACCOUNT_SHARING_SIMPLIFICATION.md delete mode 100644 apps-archived/uload/docs/ADD_MISSING_FIELDS.md delete mode 100644 apps-archived/uload/docs/ADMIN_ACCESS_GUIDE.md delete mode 100644 apps-archived/uload/docs/AUTH_FIX_SUMMARY.md delete mode 100644 apps-archived/uload/docs/COMPLETE-POCKETBASE-DEV-SETUP.md delete mode 100644 apps-archived/uload/docs/DATABASE_SWITCHING.md delete mode 100644 apps-archived/uload/docs/DEPLOYMENT.md delete mode 100644 apps-archived/uload/docs/DEPLOYMENT_FIX.md delete mode 100644 apps-archived/uload/docs/DEPLOYMENT_GUIDE.md delete mode 100644 apps-archived/uload/docs/DEPLOYMENT_LESSONS_LEARNED.md delete mode 100644 apps-archived/uload/docs/DOMAIN_SETUP_ULO_AD.md delete mode 100644 apps-archived/uload/docs/EMAIL_SETUP.md delete mode 100644 apps-archived/uload/docs/FINAL_SOLUTION.md delete mode 100644 apps-archived/uload/docs/MCP-DUAL-SETUP.md delete mode 100644 apps-archived/uload/docs/MIGRATION_COMPLETE.md delete mode 100644 apps-archived/uload/docs/MIGRATION_GUIDE.md delete mode 100644 apps-archived/uload/docs/PARAGLIDE_SVELTEKIT_GUIDE.md delete mode 100644 apps-archived/uload/docs/POCKETBASE_ADMIN.md delete mode 100644 apps-archived/uload/docs/README_DEPLOYMENT.md delete mode 100644 apps-archived/uload/docs/REDIS-SETUP.md delete mode 100644 apps-archived/uload/docs/SECURITY_BEST_PRACTICES.md delete mode 100644 apps-archived/uload/docs/SETUP_POCKETBASE_SERVER.md delete mode 100644 apps-archived/uload/docs/STRIPE_INTEGRATION_EXPLAINED.md delete mode 100644 apps-archived/uload/docs/WORKSPACE_MIGRATION_REPORT.md delete mode 100644 apps-archived/uload/docs/ab-testing-multilingual-plan.md delete mode 100644 apps-archived/uload/docs/ab-testing-multilingual-test.md delete mode 100644 apps-archived/uload/docs/auth-implementation-notes.md delete mode 100644 apps-archived/uload/docs/blog/BLOG-INTEGRATION-PROPOSALS.md delete mode 100644 apps-archived/uload/docs/blog/HYBRID-MDSVEX-COLLECTIONS-GUIDE.md delete mode 100644 apps-archived/uload/docs/blog/IMPLEMENTATION-PLAN.md delete mode 100644 apps-archived/uload/docs/blog/STATIC-MARKDOWN-BLOG-GUIDE.md delete mode 100644 apps-archived/uload/docs/blog/SVELTE5-HYBRID-BLOG-GUIDE.md delete mode 100644 apps-archived/uload/docs/blog/die-psychologie-kurzer-urls.md delete mode 100644 apps-archived/uload/docs/brevo-setup.md delete mode 100644 apps-archived/uload/docs/card-architecture-v2.md delete mode 100644 apps-archived/uload/docs/cards/README.md delete mode 100644 apps-archived/uload/docs/cards/api-reference.md delete mode 100644 apps-archived/uload/docs/cards/architecture.md delete mode 100644 apps-archived/uload/docs/cards/components.md delete mode 100644 apps-archived/uload/docs/cards/examples.md delete mode 100644 apps-archived/uload/docs/cards/implementation-comparison.md delete mode 100644 apps-archived/uload/docs/cards/modules.md delete mode 100644 apps-archived/uload/docs/cards/server-side-html-cards.md delete mode 100644 apps-archived/uload/docs/cards/templates.md delete mode 100644 apps-archived/uload/docs/cards/themes.md delete mode 100644 apps-archived/uload/docs/cards/unified-cards-migration.md delete mode 100644 apps-archived/uload/docs/database-refactoring-plan.md delete mode 100644 apps-archived/uload/docs/features/20250815-15:08-feature-overview.md delete mode 100644 apps-archived/uload/docs/features/ab-testing-test-urls.md delete mode 100644 apps-archived/uload/docs/features/abc-testing-implementation.md delete mode 100644 apps-archived/uload/docs/features/downtime-prevention-plan.md delete mode 100644 apps-archived/uload/docs/features/features/unified-card-system.md delete mode 100644 apps-archived/uload/docs/features/mail/COMPLETE-EMAIL-SETUP-GUIDE.md delete mode 100644 apps-archived/uload/docs/features/mail/SMTP-SETUP-SECURE.md delete mode 100644 apps-archived/uload/docs/features/mail/email-templates-bilingual.md delete mode 100644 apps-archived/uload/docs/features/mail/email-templates-simplified.md delete mode 100644 apps-archived/uload/docs/features/mail/email-templates.md delete mode 100644 apps-archived/uload/docs/features/mail/multilingual-email-plan.md delete mode 100644 apps-archived/uload/docs/features/pocketbase/POCKETBASE-DEV-SETUP-PLAN.md delete mode 100644 apps-archived/uload/docs/features/pocketbase/POCKETBASE-MANUAL-SETUP.md delete mode 100644 apps-archived/uload/docs/features/pocketbase/pocketbase-local-setup.md delete mode 100644 apps-archived/uload/docs/features/pocketbase/pocketbase-setup.md delete mode 100644 apps-archived/uload/docs/features/profile-background.md delete mode 100644 apps-archived/uload/docs/features/projects-shared-usernames.md delete mode 100644 apps-archived/uload/docs/features/redis_docs/reddis-ressourcen-hosting.md delete mode 100644 apps-archived/uload/docs/features/redis_docs/reddis-vorteile.md delete mode 100644 apps-archived/uload/docs/features/redis_docs/redis-architecture.md delete mode 100644 apps-archived/uload/docs/features/redis_docs/redis-local-setup.md delete mode 100644 apps-archived/uload/docs/features/redis_docs/redis-quickstart.md delete mode 100644 apps-archived/uload/docs/features/referral/referral-tracking-system.md delete mode 100644 apps-archived/uload/docs/features/referral/referral-vs-promo-code-comparison.md delete mode 100644 apps-archived/uload/docs/features/referral/voucher-code-system-community-affiliate.md delete mode 100644 apps-archived/uload/docs/features/referral/voucher-implementation-approaches.md delete mode 100644 apps-archived/uload/docs/features/toast-integration-status.md delete mode 100644 apps-archived/uload/docs/features/toast-notifications.md delete mode 100644 apps-archived/uload/docs/marketing/Educational-Content-Posts.md delete mode 100644 apps-archived/uload/docs/marketing/Educational-Sprecher-Script-Psychologie-URLs.md delete mode 100644 apps-archived/uload/docs/marketing/Facebook.md delete mode 100644 apps-archived/uload/docs/marketing/Instagram-Content-Strategie.md delete mode 100644 apps-archived/uload/docs/marketing/Instagram-Story-Scripte.md delete mode 100644 apps-archived/uload/docs/marketing/Landing-Page-Guidelines.md delete mode 100644 apps-archived/uload/docs/marketing/Marktpositionierung+Zielgruppen.md delete mode 100644 apps-archived/uload/docs/marketing/VideoScripts.md delete mode 100644 apps-archived/uload/docs/marketing/VideoScripts_Fließtext.md delete mode 100644 apps-archived/uload/docs/marketing/hero-image-prompts.md delete mode 100644 apps-archived/uload/docs/marketing/landing-page.md delete mode 100644 apps-archived/uload/docs/monetize/link-limits-implementation.md delete mode 100644 apps-archived/uload/docs/plan-public-profiles.md delete mode 100644 apps-archived/uload/docs/plans/development-roadmap-2025.md delete mode 100644 apps-archived/uload/docs/reports/app-stability-testing-strategy.md delete mode 100644 apps-archived/uload/docs/reports/card-architecture-analysis.md delete mode 100644 apps-archived/uload/docs/reports/cards-feature-analysis.md delete mode 100644 apps-archived/uload/docs/reports/cards-profile-fix.md delete mode 100644 apps-archived/uload/docs/reports/database-evaluation-pocketbase-vs-postgresql.md delete mode 100644 apps-archived/uload/docs/reports/database-optimization-report.md delete mode 100644 apps-archived/uload/docs/reports/email-setup-options.md delete mode 100644 apps-archived/uload/docs/reports/empfehlungen-und-aktionsplan.md delete mode 100644 apps-archived/uload/docs/reports/homepage-abc-testing-plan.md delete mode 100644 apps-archived/uload/docs/reports/ip-geolocation-implementation-options.md delete mode 100644 apps-archived/uload/docs/reports/landing-page-strategie.md delete mode 100644 apps-archived/uload/docs/reports/marketing-seite-implementierungsplan.md delete mode 100644 apps-archived/uload/docs/reports/marketing-website-architektur.md delete mode 100644 apps-archived/uload/docs/reports/migration-effort-pocketbase-to-postgresql.md delete mode 100644 apps-archived/uload/docs/reports/naming-convention-migration-plan.md delete mode 100644 apps-archived/uload/docs/reports/performance-vergleich-uload-shlink.md delete mode 100644 apps-archived/uload/docs/reports/self-hosted-geolocation-solutions.md delete mode 100644 apps-archived/uload/docs/setup-local-pocketbase.md delete mode 100644 apps-archived/uload/docs/stripe/CLAUDE_CODE_MCP_SETUP.md delete mode 100644 apps-archived/uload/docs/stripe/CODE_SNIPPETS.md delete mode 100644 apps-archived/uload/docs/stripe/IMPLEMENTATION_GUIDE.md delete mode 100644 apps-archived/uload/docs/stripe/MCP_QUICK_SETUP.md delete mode 100644 apps-archived/uload/docs/stripe/MCP_SERVER_GUIDE.md delete mode 100644 apps-archived/uload/docs/stripe/Monetization_Overview.md delete mode 100644 apps-archived/uload/docs/stripe/QUICK_START.md delete mode 100644 apps-archived/uload/docs/stripe/STRIPE_SETUP_SIMPLEST_WAY.md delete mode 100644 apps-archived/uload/docs/stripe/deployment-setup.md delete mode 100644 apps-archived/uload/docs/stripe/testing-checklist.md delete mode 100644 apps-archived/uload/docs/tags-feature.md delete mode 100644 apps-archived/uload/docs/testing/test-pages.md delete mode 100644 apps-archived/uload/docs/umami-custom-events.md delete mode 100644 apps-archived/uload/docs/umami-setup.md delete mode 100755 apps-archived/uload/scripts/apply-db-optimizations.sh delete mode 100755 apps-archived/uload/scripts/check-prod-redis.sh delete mode 100644 apps-archived/uload/scripts/create-admin.sh delete mode 100644 apps-archived/uload/scripts/create-collections.mjs delete mode 100755 apps-archived/uload/scripts/create-default-templates.cjs delete mode 100644 apps-archived/uload/scripts/create-unified-cards-collection.js delete mode 100644 apps-archived/uload/scripts/debug-auth.mjs delete mode 100644 apps-archived/uload/scripts/extract-templates.js delete mode 100755 apps-archived/uload/scripts/fix-field-names.sh delete mode 100755 apps-archived/uload/scripts/fix-imports.sh delete mode 100755 apps-archived/uload/scripts/fix-links-collection.sh delete mode 100644 apps-archived/uload/scripts/generate-pwa-icons.js delete mode 100644 apps-archived/uload/scripts/generate-pwa-icons.mjs delete mode 100755 apps-archived/uload/scripts/migrate-links-collection.sh delete mode 100644 apps-archived/uload/scripts/migrate-links.js delete mode 100644 apps-archived/uload/scripts/migrate-to-username-prefix.js delete mode 100644 apps-archived/uload/scripts/migrate-to-workspaces.js delete mode 100644 apps-archived/uload/scripts/optimize-database.sql delete mode 100755 apps-archived/uload/scripts/seed-local-db.js delete mode 100644 apps-archived/uload/scripts/setup-test-user.mjs delete mode 100644 apps-archived/uload/scripts/test-email.js delete mode 100644 apps-archived/uload/scripts/test-env.js delete mode 100644 apps-archived/uload/scripts/test-env.mjs delete mode 100644 apps-archived/uload/scripts/test-local-redis.mjs delete mode 100644 apps-archived/uload/scripts/test-pb-connection.js delete mode 100644 apps-archived/uload/scripts/test-pb-prod.js delete mode 100644 apps-archived/uload/scripts/test-pb.js delete mode 100644 apps-archived/uload/scripts/test-pocketbase.js delete mode 100644 apps-archived/uload/scripts/test-prod-pocketbase.js delete mode 100644 apps-archived/uload/scripts/test-redis-cache.js delete mode 100644 apps-archived/uload/scripts/test-redis-cache.mjs delete mode 100644 apps-archived/uload/scripts/test-registration.js delete mode 100644 apps-archived/uload/scripts/test-url-variants.js rename {apps-archived => apps}/news/.env.example (100%) rename {apps-archived => apps}/news/.gitignore (100%) create mode 100644 apps/news/CLAUDE.md rename {apps-archived => apps}/news/apps/landing/astro.config.mjs (64%) rename {apps-archived => apps}/news/apps/landing/package.json (100%) rename {apps-archived => apps}/news/apps/landing/src/components/Footer.astro (100%) rename {apps-archived => apps}/news/apps/landing/src/components/Navigation.astro (100%) rename {apps-archived => apps}/news/apps/landing/src/layouts/Layout.astro (100%) rename {apps-archived => apps}/news/apps/landing/src/pages/index.astro (100%) rename {apps-archived => apps}/news/apps/landing/src/styles/global.css (100%) create mode 100644 apps/news/apps/landing/tailwind.config.mjs rename {apps-archived => apps}/news/apps/landing/tsconfig.json (100%) create mode 100644 apps/news/apps/server/package.json create mode 100644 apps/news/apps/server/src/config.ts create mode 100644 apps/news/apps/server/src/db/connection.ts create mode 100644 apps/news/apps/server/src/index.ts create mode 100644 apps/news/apps/server/src/lib/errors.ts create mode 100644 apps/news/apps/server/src/middleware/error-handler.ts create mode 100644 apps/news/apps/server/src/middleware/jwt-auth.ts create mode 100644 apps/news/apps/server/src/routes/extract.ts create mode 100644 apps/news/apps/server/src/routes/feed.ts create mode 100644 apps/news/apps/server/src/routes/health.ts create mode 100644 apps/news/apps/server/src/services/extract.ts create mode 100644 apps/news/apps/server/src/services/feed.ts create mode 100644 apps/news/apps/server/tsconfig.json rename {apps-archived => apps}/news/apps/web/.env.example (100%) rename {apps-archived => apps}/news/apps/web/eslint.config.js (100%) create mode 100644 apps/news/apps/web/package.json rename {apps-archived => apps}/news/apps/web/src/app.css (100%) create mode 100644 apps/news/apps/web/src/app.d.ts rename {apps-archived => apps}/news/apps/web/src/app.html (100%) create mode 100644 apps/news/apps/web/src/lib/components/NewsLogo.svelte create mode 100644 apps/news/apps/web/src/lib/data/guest-seed.ts create mode 100644 apps/news/apps/web/src/lib/data/local-store.ts create mode 100644 apps/news/apps/web/src/lib/stores/auth.svelte.ts create mode 100644 apps/news/apps/web/src/routes/(protected)/+layout.svelte create mode 100644 apps/news/apps/web/src/routes/(protected)/feed/+page.svelte create mode 100644 apps/news/apps/web/src/routes/(protected)/saved/+page.svelte create mode 100644 apps/news/apps/web/src/routes/+layout.svelte create mode 100644 apps/news/apps/web/src/routes/+page.svelte create mode 100644 apps/news/apps/web/src/routes/auth/login/+page.svelte create mode 100644 apps/news/apps/web/src/routes/auth/register/+page.svelte rename {apps-archived => apps}/news/apps/web/svelte.config.js (70%) rename {apps-archived => apps}/news/apps/web/tsconfig.json (100%) create mode 100644 apps/news/apps/web/vite.config.ts create mode 100644 apps/news/package.json diff --git a/CLAUDE.md b/CLAUDE.md index 0c489b879..d54fb9504 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,6 +56,7 @@ For comprehensive guidelines on code patterns and conventions, see the `.claude/ | **traces** | City exploration | Backend, Mobile | | **taktik** | Time tracking | Web | | **uload** | URL shortener & link management | Server, Web, Landing | +| **news** | AI news reader & personal library | Server, Web, Landing | | **calc** | Calculator & converter | Web | | **playground** | LLM playground | Web | diff --git a/apps-archived/news/MigrationPlan-Unified-App.md b/apps-archived/news/MigrationPlan-Unified-App.md deleted file mode 100644 index ce0f1f6b1..000000000 --- a/apps-archived/news/MigrationPlan-Unified-App.md +++ /dev/null @@ -1,1526 +0,0 @@ -# Migrationsplan: Unified News Hub - -## Übersicht - -Migration von **ainews** + **kokon** zu einer vereinten App mit: -- **PostgreSQL** (Docker lokal, später Cloud) -- **Drizzle ORM** (Type-safe Database) -- **NestJS** (Backend API) -- **Better Auth** (Authentication) - ---- - -## Ziel-Architektur - -``` -news/ -├── apps/ -│ ├── mobile/ # React Native/Expo App (vereint) -│ └── api/ # NestJS Backend -├── packages/ -│ ├── database/ # Drizzle Schema + Migrations -│ ├── shared/ # Shared Types & Utilities -│ └── browser-extension/ # Chrome Extension -├── docker/ -│ └── docker-compose.yml # PostgreSQL + Dev Services -├── package.json # Monorepo Root (pnpm workspaces) -└── turbo.json # Turborepo Config (optional) -``` - -### Datenfluss - -``` -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ Mobile App │────▶│ NestJS API │────▶│ PostgreSQL │ -│ (Expo/RN) │◀────│ + Better Auth │◀────│ (Docker) │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ - │ │ - │ ┌────────┴────────┐ - │ │ │ -┌────────▼────────┐ │ ┌───────────▼───────────┐ -│ Browser Extension│─────┘ │ Content Extraction │ -│ (Chrome/Firefox) │ │ (Readability) │ -└─────────────────┘ └───────────────────────┘ -``` - ---- - -## Phase 1: Monorepo Setup - -### 1.1 Projekt-Struktur erstellen - -```bash -# Im news/ Ordner -pnpm init - -# Workspace-Struktur -mkdir -p apps/mobile apps/api packages/database packages/shared packages/browser-extension docker -``` - -### 1.2 Root `package.json` - -```json -{ - "name": "news-hub", - "private": true, - "scripts": { - "dev": "turbo run dev", - "build": "turbo run build", - "db:generate": "turbo run db:generate --filter=@news/database", - "db:migrate": "turbo run db:migrate --filter=@news/database", - "db:studio": "turbo run db:studio --filter=@news/database" - }, - "devDependencies": { - "turbo": "^2.0.0", - "typescript": "^5.4.0" - }, - "packageManager": "pnpm@9.0.0", - "workspaces": [ - "apps/*", - "packages/*" - ] -} -``` - -### 1.3 `turbo.json` - -```json -{ - "$schema": "https://turbo.build/schema.json", - "tasks": { - "build": { - "dependsOn": ["^build"], - "outputs": ["dist/**", ".next/**", "build/**"] - }, - "dev": { - "cache": false, - "persistent": true - }, - "db:generate": {}, - "db:migrate": {}, - "db:studio": { - "cache": false, - "persistent": true - } - } -} -``` - ---- - -## Phase 2: Docker Setup - -### 2.1 `docker/docker-compose.yml` - -```yaml -version: '3.9' - -services: - postgres: - image: postgres:16-alpine - container_name: news-hub-db - restart: unless-stopped - environment: - POSTGRES_USER: news - POSTGRES_PASSWORD: news_dev_password - POSTGRES_DB: news_hub - ports: - - "5432:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - - ./init.sql:/docker-entrypoint-initdb.d/init.sql - healthcheck: - test: ["CMD-SHELL", "pg_isready -U news -d news_hub"] - interval: 5s - timeout: 5s - retries: 5 - - # Optional: pgAdmin für DB-Verwaltung - pgadmin: - image: dpage/pgadmin4:latest - container_name: news-hub-pgadmin - restart: unless-stopped - environment: - PGADMIN_DEFAULT_EMAIL: admin@local.dev - PGADMIN_DEFAULT_PASSWORD: admin - ports: - - "5050:80" - depends_on: - - postgres - -volumes: - postgres_data: -``` - -### 2.2 `docker/init.sql` - -```sql --- Extensions -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- Für Textsuche - --- Grants -GRANT ALL PRIVILEGES ON DATABASE news_hub TO news; -``` - -### 2.3 Start-Script in Root `package.json` - -```json -{ - "scripts": { - "docker:up": "docker-compose -f docker/docker-compose.yml up -d", - "docker:down": "docker-compose -f docker/docker-compose.yml down", - "docker:logs": "docker-compose -f docker/docker-compose.yml logs -f" - } -} -``` - ---- - -## Phase 3: Database Package (Drizzle) - -### 3.1 `packages/database/package.json` - -```json -{ - "name": "@news/database", - "version": "1.0.0", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "scripts": { - "build": "tsup src/index.ts --format cjs,esm --dts", - "db:generate": "drizzle-kit generate", - "db:migrate": "drizzle-kit migrate", - "db:push": "drizzle-kit push", - "db:studio": "drizzle-kit studio" - }, - "dependencies": { - "drizzle-orm": "^0.36.0", - "postgres": "^3.4.0" - }, - "devDependencies": { - "drizzle-kit": "^0.28.0", - "tsup": "^8.0.0", - "typescript": "^5.4.0" - } -} -``` - -### 3.2 `packages/database/drizzle.config.ts` - -```typescript -import { defineConfig } from 'drizzle-kit'; - -export default defineConfig({ - schema: './src/schema/index.ts', - out: './drizzle', - dialect: 'postgresql', - dbCredentials: { - url: process.env.DATABASE_URL || 'postgresql://news:news_dev_password@localhost:5432/news_hub', - }, -}); -``` - -### 3.3 `packages/database/src/schema/index.ts` - -```typescript -export * from './users'; -export * from './articles'; -export * from './categories'; -export * from './interactions'; -export * from './auth'; -``` - -### 3.4 `packages/database/src/schema/users.ts` - -```typescript -import { pgTable, uuid, text, timestamp, boolean, pgEnum } from 'drizzle-orm/pg-core'; - -export const userTierEnum = pgEnum('user_tier', ['free', 'premium', 'enterprise']); -export const readingSpeedEnum = pgEnum('reading_speed', ['slow', 'normal', 'fast']); - -export const users = pgTable('users', { - id: uuid('id').primaryKey().defaultRandom(), - email: text('email').notNull().unique(), - name: text('name'), - avatarUrl: text('avatar_url'), - - // Preferences - tier: userTierEnum('tier').default('free').notNull(), - readingSpeed: readingSpeedEnum('reading_speed').default('normal').notNull(), - preferredCategories: text('preferred_categories').array(), - blockedSources: text('blocked_sources').array(), - - // Settings - onboardingCompleted: boolean('onboarding_completed').default(false).notNull(), - notificationSettings: text('notification_settings'), // JSON string - - // Timestamps - createdAt: timestamp('created_at').defaultNow().notNull(), - updatedAt: timestamp('updated_at').defaultNow().notNull(), -}); - -export type User = typeof users.$inferSelect; -export type NewUser = typeof users.$inferInsert; -``` - -### 3.5 `packages/database/src/schema/articles.ts` - -```typescript -import { pgTable, uuid, text, timestamp, boolean, integer, real, pgEnum, index } from 'drizzle-orm/pg-core'; -import { users } from './users'; -import { categories } from './categories'; - -export const articleTypeEnum = pgEnum('article_type', ['feed', 'summary', 'in_depth', 'saved']); -export const articleSourceEnum = pgEnum('article_source', ['ai', 'user_saved']); -export const summaryPeriodEnum = pgEnum('summary_period', ['morning', 'noon', 'evening', 'night']); - -export const articles = pgTable('articles', { - id: uuid('id').primaryKey().defaultRandom(), - - // Core fields - type: articleTypeEnum('type').notNull(), - sourceOrigin: articleSourceEnum('source_origin').default('ai').notNull(), - title: text('title').notNull(), - content: text('content').notNull(), - summary: text('summary'), - - // For user-saved articles - userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }), - originalUrl: text('original_url'), - parsedContent: text('parsed_content'), - isArchived: boolean('is_archived').default(false), - - // Metadata - categoryId: uuid('category_id').references(() => categories.id), - sourceUrl: text('source_url'), - sourceName: text('source_name'), - sourceDomain: text('source_domain'), - author: text('author'), - imageUrl: text('image_url'), - - // AI-generated metadata - aiTags: text('ai_tags').array(), - sentimentScore: real('sentiment_score'), - - // Reading metrics - readingTimeMinutes: integer('reading_time_minutes'), - wordCount: integer('word_count'), - - // Summary-specific fields - summaryDate: timestamp('summary_date'), - summaryPeriod: summaryPeriodEnum('summary_period'), - includedArticleIds: uuid('included_article_ids').array(), - - // In-depth specific fields - keyInsights: text('key_insights'), // JSON string - dataVisualizations: text('data_visualizations'), // JSON string - relatedArticleIds: uuid('related_article_ids').array(), - - // Timestamps - publishedAt: timestamp('published_at').defaultNow().notNull(), - createdAt: timestamp('created_at').defaultNow().notNull(), - updatedAt: timestamp('updated_at').defaultNow().notNull(), -}, (table) => ({ - // Indexes für schnelle Abfragen - typeIdx: index('articles_type_idx').on(table.type), - userIdx: index('articles_user_idx').on(table.userId), - sourceOriginIdx: index('articles_source_origin_idx').on(table.sourceOrigin), - publishedAtIdx: index('articles_published_at_idx').on(table.publishedAt), - categoryIdx: index('articles_category_idx').on(table.categoryId), -})); - -export type Article = typeof articles.$inferSelect; -export type NewArticle = typeof articles.$inferInsert; -``` - -### 3.6 `packages/database/src/schema/categories.ts` - -```typescript -import { pgTable, uuid, text, timestamp, integer } from 'drizzle-orm/pg-core'; - -export const categories = pgTable('categories', { - id: uuid('id').primaryKey().defaultRandom(), - name: text('name').notNull().unique(), - displayName: text('display_name').notNull(), - description: text('description'), - icon: text('icon'), - color: text('color'), - priority: integer('priority').default(0).notNull(), - - createdAt: timestamp('created_at').defaultNow().notNull(), -}); - -export type Category = typeof categories.$inferSelect; -export type NewCategory = typeof categories.$inferInsert; -``` - -### 3.7 `packages/database/src/schema/interactions.ts` - -```typescript -import { pgTable, uuid, timestamp, boolean, real, integer, index, unique } from 'drizzle-orm/pg-core'; -import { users } from './users'; -import { articles } from './articles'; - -export const userArticleInteractions = pgTable('user_article_interactions', { - id: uuid('id').primaryKey().defaultRandom(), - userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(), - articleId: uuid('article_id').references(() => articles.id, { onDelete: 'cascade' }).notNull(), - - // Interaction states - isRead: boolean('is_read').default(false).notNull(), - isSaved: boolean('is_saved').default(false).notNull(), - readProgress: real('read_progress').default(0), // 0.0 to 1.0 - rating: integer('rating'), // 1-5 - shareCount: integer('share_count').default(0).notNull(), - - // Timestamps - openedAt: timestamp('opened_at'), - readAt: timestamp('read_at'), - savedAt: timestamp('saved_at'), - ratedAt: timestamp('rated_at'), - createdAt: timestamp('created_at').defaultNow().notNull(), - updatedAt: timestamp('updated_at').defaultNow().notNull(), -}, (table) => ({ - // Unique constraint: ein User kann nur eine Interaction pro Artikel haben - userArticleUnique: unique('user_article_unique').on(table.userId, table.articleId), - // Indexes - userIdx: index('interactions_user_idx').on(table.userId), - articleIdx: index('interactions_article_idx').on(table.articleId), -})); - -export type UserArticleInteraction = typeof userArticleInteractions.$inferSelect; -export type NewUserArticleInteraction = typeof userArticleInteractions.$inferInsert; -``` - -### 3.8 `packages/database/src/schema/auth.ts` (Better Auth) - -```typescript -import { pgTable, uuid, text, timestamp, boolean, integer } from 'drizzle-orm/pg-core'; -import { users } from './users'; - -// Better Auth Sessions -export const sessions = pgTable('sessions', { - id: uuid('id').primaryKey().defaultRandom(), - userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(), - token: text('token').notNull().unique(), - expiresAt: timestamp('expires_at').notNull(), - ipAddress: text('ip_address'), - userAgent: text('user_agent'), - createdAt: timestamp('created_at').defaultNow().notNull(), - updatedAt: timestamp('updated_at').defaultNow().notNull(), -}); - -// Better Auth Accounts (OAuth providers) -export const accounts = pgTable('accounts', { - id: uuid('id').primaryKey().defaultRandom(), - userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(), - providerId: text('provider_id').notNull(), // 'email', 'google', 'apple', etc. - providerAccountId: text('provider_account_id').notNull(), - accessToken: text('access_token'), - refreshToken: text('refresh_token'), - accessTokenExpiresAt: timestamp('access_token_expires_at'), - refreshTokenExpiresAt: timestamp('refresh_token_expires_at'), - scope: text('scope'), - password: text('password'), // Hashed, only for email provider - createdAt: timestamp('created_at').defaultNow().notNull(), - updatedAt: timestamp('updated_at').defaultNow().notNull(), -}); - -// Better Auth Verification Tokens (Email verification, password reset) -export const verificationTokens = pgTable('verification_tokens', { - id: uuid('id').primaryKey().defaultRandom(), - userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(), - token: text('token').notNull().unique(), - type: text('type').notNull(), // 'email_verification', 'password_reset' - expiresAt: timestamp('expires_at').notNull(), - createdAt: timestamp('created_at').defaultNow().notNull(), -}); -``` - -### 3.9 `packages/database/src/index.ts` - -```typescript -import { drizzle } from 'drizzle-orm/postgres-js'; -import postgres from 'postgres'; -import * as schema from './schema'; - -export * from './schema'; - -const connectionString = process.env.DATABASE_URL || 'postgresql://news:news_dev_password@localhost:5432/news_hub'; - -// For query purposes -const queryClient = postgres(connectionString); -export const db = drizzle(queryClient, { schema }); - -// For migrations (uses different client settings) -export const createMigrationClient = () => { - const migrationClient = postgres(connectionString, { max: 1 }); - return drizzle(migrationClient, { schema }); -}; -``` - ---- - -## Phase 4: NestJS Backend - -### 4.1 `apps/api/package.json` - -```json -{ - "name": "@news/api", - "version": "1.0.0", - "scripts": { - "build": "nest build", - "dev": "nest start --watch", - "start": "nest start", - "start:prod": "node dist/main" - }, - "dependencies": { - "@nestjs/common": "^10.0.0", - "@nestjs/core": "^10.0.0", - "@nestjs/platform-fastify": "^10.0.0", - "@news/database": "workspace:*", - "better-auth": "^1.0.0", - "@mozilla/readability": "^0.5.0", - "jsdom": "^24.0.0", - "class-validator": "^0.14.0", - "class-transformer": "^0.5.1" - }, - "devDependencies": { - "@nestjs/cli": "^10.0.0", - "@types/node": "^20.0.0", - "typescript": "^5.4.0" - } -} -``` - -### 4.2 `apps/api/src/main.ts` - -```typescript -import { NestFactory } from '@nestjs/core'; -import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify'; -import { ValidationPipe } from '@nestjs/common'; -import { AppModule } from './app.module'; - -async function bootstrap() { - const app = await NestFactory.create( - AppModule, - new FastifyAdapter() - ); - - app.enableCors({ - origin: [ - 'http://localhost:8081', // Expo web - 'http://localhost:19006', // Expo web alt - 'exp://*', // Expo Go - ], - credentials: true, - }); - - app.useGlobalPipes(new ValidationPipe({ - whitelist: true, - transform: true, - })); - - await app.listen(3000, '0.0.0.0'); - console.log('API running on http://localhost:3000'); -} - -bootstrap(); -``` - -### 4.3 `apps/api/src/app.module.ts` - -```typescript -import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; -import { DatabaseModule } from './database/database.module'; -import { AuthModule } from './auth/auth.module'; -import { ArticlesModule } from './articles/articles.module'; -import { CategoriesModule } from './categories/categories.module'; -import { UsersModule } from './users/users.module'; -import { ContentExtractionModule } from './content-extraction/content-extraction.module'; - -@Module({ - imports: [ - ConfigModule.forRoot({ isGlobal: true }), - DatabaseModule, - AuthModule, - ArticlesModule, - CategoriesModule, - UsersModule, - ContentExtractionModule, - ], -}) -export class AppModule {} -``` - -### 4.4 `apps/api/src/database/database.module.ts` - -```typescript -import { Module, Global } from '@nestjs/common'; -import { db } from '@news/database'; - -export const DATABASE_CONNECTION = 'DATABASE_CONNECTION'; - -@Global() -@Module({ - providers: [ - { - provide: DATABASE_CONNECTION, - useValue: db, - }, - ], - exports: [DATABASE_CONNECTION], -}) -export class DatabaseModule {} -``` - -### 4.5 `apps/api/src/auth/auth.module.ts` (Better Auth) - -```typescript -import { Module } from '@nestjs/common'; -import { AuthController } from './auth.controller'; -import { AuthService } from './auth.service'; -import { BetterAuthService } from './better-auth.service'; - -@Module({ - controllers: [AuthController], - providers: [AuthService, BetterAuthService], - exports: [AuthService, BetterAuthService], -}) -export class AuthModule {} -``` - -### 4.6 `apps/api/src/auth/better-auth.service.ts` - -```typescript -import { Injectable } from '@nestjs/common'; -import { betterAuth } from 'better-auth'; -import { drizzleAdapter } from 'better-auth/adapters/drizzle'; -import { db, users, sessions, accounts, verificationTokens } from '@news/database'; - -@Injectable() -export class BetterAuthService { - public auth = betterAuth({ - database: drizzleAdapter(db, { - provider: 'pg', - schema: { - user: users, - session: sessions, - account: accounts, - verification: verificationTokens, - }, - }), - emailAndPassword: { - enabled: true, - requireEmailVerification: false, // Für MVP erstmal aus - }, - session: { - expiresIn: 60 * 60 * 24 * 7, // 7 days - updateAge: 60 * 60 * 24, // 1 day - }, - // Optional: OAuth providers - // socialProviders: { - // google: { - // clientId: process.env.GOOGLE_CLIENT_ID!, - // clientSecret: process.env.GOOGLE_CLIENT_SECRET!, - // }, - // apple: { - // clientId: process.env.APPLE_CLIENT_ID!, - // clientSecret: process.env.APPLE_CLIENT_SECRET!, - // }, - // }, - }); -} -``` - -### 4.7 `apps/api/src/auth/auth.controller.ts` - -```typescript -import { Controller, Post, Get, Body, Req, Res, UseGuards } from '@nestjs/common'; -import { FastifyRequest, FastifyReply } from 'fastify'; -import { BetterAuthService } from './better-auth.service'; - -@Controller('auth') -export class AuthController { - constructor(private betterAuth: BetterAuthService) {} - - @Post('signup') - async signUp( - @Body() body: { email: string; password: string; name?: string }, - @Req() req: FastifyRequest, - @Res() res: FastifyReply, - ) { - return this.betterAuth.auth.api.signUpEmail({ - body, - headers: req.headers as any, - }); - } - - @Post('signin') - async signIn( - @Body() body: { email: string; password: string }, - @Req() req: FastifyRequest, - ) { - return this.betterAuth.auth.api.signInEmail({ - body, - headers: req.headers as any, - }); - } - - @Post('signout') - async signOut(@Req() req: FastifyRequest) { - return this.betterAuth.auth.api.signOut({ - headers: req.headers as any, - }); - } - - @Get('session') - async getSession(@Req() req: FastifyRequest) { - return this.betterAuth.auth.api.getSession({ - headers: req.headers as any, - }); - } -} -``` - -### 4.8 `apps/api/src/articles/articles.module.ts` - -```typescript -import { Module } from '@nestjs/common'; -import { ArticlesController } from './articles.controller'; -import { ArticlesService } from './articles.service'; - -@Module({ - controllers: [ArticlesController], - providers: [ArticlesService], - exports: [ArticlesService], -}) -export class ArticlesModule {} -``` - -### 4.9 `apps/api/src/articles/articles.service.ts` - -```typescript -import { Injectable, Inject } from '@nestjs/common'; -import { eq, and, desc, sql } from 'drizzle-orm'; -import { DATABASE_CONNECTION } from '../database/database.module'; -import { db, articles, Article, NewArticle } from '@news/database'; - -@Injectable() -export class ArticlesService { - constructor(@Inject(DATABASE_CONNECTION) private db: typeof db) {} - - // AI-generierte Artikel (feed, summary, in_depth) - async getAIArticles(options: { - type?: 'feed' | 'summary' | 'in_depth'; - categoryId?: string; - limit?: number; - offset?: number; - }): Promise { - const { type, categoryId, limit = 20, offset = 0 } = options; - - let query = this.db - .select() - .from(articles) - .where(eq(articles.sourceOrigin, 'ai')) - .orderBy(desc(articles.publishedAt)) - .limit(limit) - .offset(offset); - - if (type) { - query = query.where(and( - eq(articles.sourceOrigin, 'ai'), - eq(articles.type, type) - )); - } - - if (categoryId) { - query = query.where(and( - eq(articles.sourceOrigin, 'ai'), - eq(articles.categoryId, categoryId) - )); - } - - return query; - } - - // User-gespeicherte Artikel - async getSavedArticles(userId: string, includeArchived = false): Promise { - const conditions = [ - eq(articles.sourceOrigin, 'user_saved'), - eq(articles.userId, userId), - ]; - - if (!includeArchived) { - conditions.push(eq(articles.isArchived, false)); - } - - return this.db - .select() - .from(articles) - .where(and(...conditions)) - .orderBy(desc(articles.createdAt)); - } - - // Artikel speichern (für Content Extraction) - async createSavedArticle(data: { - userId: string; - title: string; - content: string; - parsedContent: string; - originalUrl: string; - }): Promise
{ - const [article] = await this.db - .insert(articles) - .values({ - type: 'saved', - sourceOrigin: 'user_saved', - userId: data.userId, - title: data.title, - content: data.content, - parsedContent: data.parsedContent, - originalUrl: data.originalUrl, - isArchived: false, - }) - .returning(); - - return article; - } - - // Artikel archivieren - async archiveArticle(articleId: string, userId: string): Promise { - await this.db - .update(articles) - .set({ isArchived: true, updatedAt: new Date() }) - .where(and( - eq(articles.id, articleId), - eq(articles.userId, userId) - )); - } - - // Artikel löschen - async deleteArticle(articleId: string, userId: string): Promise { - await this.db - .delete(articles) - .where(and( - eq(articles.id, articleId), - eq(articles.userId, userId) - )); - } - - // Einzelnen Artikel laden - async getArticleById(articleId: string): Promise
{ - const [article] = await this.db - .select() - .from(articles) - .where(eq(articles.id, articleId)) - .limit(1); - - return article || null; - } -} -``` - -### 4.10 `apps/api/src/articles/articles.controller.ts` - -```typescript -import { Controller, Get, Post, Delete, Param, Query, Body, UseGuards, Req } from '@nestjs/common'; -import { ArticlesService } from './articles.service'; -import { AuthGuard } from '../auth/auth.guard'; - -@Controller('articles') -export class ArticlesController { - constructor(private articlesService: ArticlesService) {} - - // Öffentliche AI-Artikel - @Get() - async getArticles( - @Query('type') type?: 'feed' | 'summary' | 'in_depth', - @Query('categoryId') categoryId?: string, - @Query('limit') limit?: string, - @Query('offset') offset?: string, - ) { - return this.articlesService.getAIArticles({ - type, - categoryId, - limit: limit ? parseInt(limit) : 20, - offset: offset ? parseInt(offset) : 0, - }); - } - - // Einzelner Artikel - @Get(':id') - async getArticle(@Param('id') id: string) { - return this.articlesService.getArticleById(id); - } - - // Gespeicherte Artikel (Auth required) - @Get('saved') - @UseGuards(AuthGuard) - async getSavedArticles( - @Req() req: any, - @Query('includeArchived') includeArchived?: string, - ) { - return this.articlesService.getSavedArticles( - req.user.id, - includeArchived === 'true' - ); - } - - // Artikel archivieren - @Post(':id/archive') - @UseGuards(AuthGuard) - async archiveArticle(@Param('id') id: string, @Req() req: any) { - await this.articlesService.archiveArticle(id, req.user.id); - return { success: true }; - } - - // Artikel löschen - @Delete(':id') - @UseGuards(AuthGuard) - async deleteArticle(@Param('id') id: string, @Req() req: any) { - await this.articlesService.deleteArticle(id, req.user.id); - return { success: true }; - } -} -``` - -### 4.11 `apps/api/src/content-extraction/content-extraction.service.ts` - -```typescript -import { Injectable } from '@nestjs/common'; -import { Readability } from '@mozilla/readability'; -import { JSDOM } from 'jsdom'; -import { ArticlesService } from '../articles/articles.service'; - -export interface ExtractedContent { - title: string; - content: string; // Plain text - htmlContent: string; // Cleaned HTML - excerpt?: string; - byline?: string; - siteName?: string; -} - -@Injectable() -export class ContentExtractionService { - constructor(private articlesService: ArticlesService) {} - - async extractFromUrl(url: string): Promise { - // Fetch the page - const response = await fetch(url, { - headers: { - 'User-Agent': 'Mozilla/5.0 (compatible; NewsHub/1.0)', - }, - }); - - if (!response.ok) { - throw new Error(`Failed to fetch URL: ${response.status}`); - } - - const html = await response.text(); - - // Parse with JSDOM - const dom = new JSDOM(html, { url }); - const reader = new Readability(dom.window.document); - const article = reader.parse(); - - if (!article) { - throw new Error('Could not extract article content'); - } - - return { - title: article.title, - content: article.textContent, - htmlContent: article.content, - excerpt: article.excerpt, - byline: article.byline, - siteName: article.siteName, - }; - } - - async saveArticleFromUrl(userId: string, url: string) { - const extracted = await this.extractFromUrl(url); - - return this.articlesService.createSavedArticle({ - userId, - title: extracted.title, - content: extracted.content, - parsedContent: extracted.htmlContent, - originalUrl: url, - }); - } -} -``` - -### 4.12 `apps/api/src/content-extraction/content-extraction.controller.ts` - -```typescript -import { Controller, Post, Body, UseGuards, Req } from '@nestjs/common'; -import { ContentExtractionService } from './content-extraction.service'; -import { AuthGuard } from '../auth/auth.guard'; - -@Controller('extract') -export class ContentExtractionController { - constructor(private contentExtractionService: ContentExtractionService) {} - - @Post('save') - @UseGuards(AuthGuard) - async saveFromUrl(@Body('url') url: string, @Req() req: any) { - const article = await this.contentExtractionService.saveArticleFromUrl( - req.user.id, - url - ); - - return { success: true, article }; - } - - @Post('preview') - async previewUrl(@Body('url') url: string) { - const extracted = await this.contentExtractionService.extractFromUrl(url); - return extracted; - } -} -``` - ---- - -## Phase 5: Mobile App migrieren - -### 5.1 Apps zusammenführen - -```bash -# ainews nach apps/mobile verschieben -mv ainews apps/mobile - -# kokon-spezifische Dateien übernehmen -cp kokon/hooks/useArticles.ts apps/mobile/hooks/ -cp -r kokon/browser-extension packages/browser-extension -``` - -### 5.2 `apps/mobile/package.json` anpassen - -```json -{ - "name": "@news/mobile", - "dependencies": { - "@news/shared": "workspace:*", - // Supabase entfernen: - // "@supabase/supabase-js": "REMOVE", - // Stattdessen: - "better-auth/client": "^1.0.0" - } -} -``` - -### 5.3 Neuer API Client: `apps/mobile/services/api.ts` - -```typescript -const API_URL = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:3000'; - -class ApiClient { - private token: string | null = null; - - setToken(token: string | null) { - this.token = token; - } - - private async request( - endpoint: string, - options: RequestInit = {} - ): Promise { - const headers: HeadersInit = { - 'Content-Type': 'application/json', - ...options.headers, - }; - - if (this.token) { - headers['Authorization'] = `Bearer ${this.token}`; - } - - const response = await fetch(`${API_URL}${endpoint}`, { - ...options, - headers, - }); - - if (!response.ok) { - const error = await response.json().catch(() => ({})); - throw new Error(error.message || `API Error: ${response.status}`); - } - - return response.json(); - } - - // Articles - async getArticles(params?: { - type?: string; - categoryId?: string; - limit?: number; - offset?: number; - }) { - const query = new URLSearchParams(params as any).toString(); - return this.request(`/articles?${query}`); - } - - async getArticle(id: string) { - return this.request(`/articles/${id}`); - } - - async getSavedArticles(includeArchived = false) { - return this.request(`/articles/saved?includeArchived=${includeArchived}`); - } - - async archiveArticle(id: string) { - return this.request(`/articles/${id}/archive`, { method: 'POST' }); - } - - async deleteArticle(id: string) { - return this.request(`/articles/${id}`, { method: 'DELETE' }); - } - - // Content Extraction - async saveArticleFromUrl(url: string) { - return this.request('/extract/save', { - method: 'POST', - body: JSON.stringify({ url }), - }); - } - - // Auth - async signUp(email: string, password: string, name?: string) { - return this.request('/auth/signup', { - method: 'POST', - body: JSON.stringify({ email, password, name }), - }); - } - - async signIn(email: string, password: string) { - const result = await this.request<{ token: string; user: any }>('/auth/signin', { - method: 'POST', - body: JSON.stringify({ email, password }), - }); - this.setToken(result.token); - return result; - } - - async signOut() { - await this.request('/auth/signout', { method: 'POST' }); - this.setToken(null); - } - - async getSession() { - return this.request('/auth/session'); - } -} - -export const api = new ApiClient(); -``` - -### 5.4 Auth Context aktualisieren: `apps/mobile/contexts/AuthContext.tsx` - -```typescript -import React, { createContext, useContext, useEffect, useState } from 'react'; -import AsyncStorage from '@react-native-async-storage/async-storage'; -import { api } from '~/services/api'; - -interface User { - id: string; - email: string; - name?: string; -} - -interface AuthContextType { - user: User | null; - isLoading: boolean; - signIn: (email: string, password: string) => Promise; - signUp: (email: string, password: string, name?: string) => Promise; - signOut: () => Promise; -} - -const AuthContext = createContext(null); - -const TOKEN_KEY = 'auth_token'; - -export function AuthProvider({ children }: { children: React.ReactNode }) { - const [user, setUser] = useState(null); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - loadStoredAuth(); - }, []); - - async function loadStoredAuth() { - try { - const token = await AsyncStorage.getItem(TOKEN_KEY); - if (token) { - api.setToken(token); - const session = await api.getSession(); - setUser(session.user); - } - } catch (error) { - console.error('Failed to load auth:', error); - } finally { - setIsLoading(false); - } - } - - async function signIn(email: string, password: string) { - const result = await api.signIn(email, password); - await AsyncStorage.setItem(TOKEN_KEY, result.token); - setUser(result.user); - } - - async function signUp(email: string, password: string, name?: string) { - const result = await api.signUp(email, password, name); - await AsyncStorage.setItem(TOKEN_KEY, result.token); - setUser(result.user); - } - - async function signOut() { - await api.signOut(); - await AsyncStorage.removeItem(TOKEN_KEY); - setUser(null); - } - - return ( - - {children} - - ); -} - -export function useAuth() { - const context = useContext(AuthContext); - if (!context) { - throw new Error('useAuth must be used within AuthProvider'); - } - return context; -} -``` - -### 5.5 Article Store aktualisieren: `apps/mobile/store/articleStore.ts` - -```typescript -import { create } from 'zustand'; -import { api } from '~/services/api'; - -interface Article { - id: string; - type: 'feed' | 'summary' | 'in_depth' | 'saved'; - title: string; - content: string; - // ... weitere Felder -} - -interface ArticleState { - articles: Article[]; - savedArticles: Article[]; - isLoading: boolean; - isLoadingSaved: boolean; - - loadArticles: (type?: string) => Promise; - loadSavedArticles: () => Promise; - saveArticleFromUrl: (url: string) => Promise; - archiveArticle: (id: string) => Promise; -} - -export const useArticleStore = create((set, get) => ({ - articles: [], - savedArticles: [], - isLoading: false, - isLoadingSaved: false, - - loadArticles: async (type) => { - set({ isLoading: true }); - try { - const articles = await api.getArticles({}); - set({ articles, isLoading: false }); - } catch (error) { - console.error('Failed to load articles:', error); - set({ isLoading: false }); - } - }, - - loadSavedArticles: async () => { - set({ isLoadingSaved: true }); - try { - const savedArticles = await api.getSavedArticles(); - set({ savedArticles, isLoadingSaved: false }); - } catch (error) { - console.error('Failed to load saved articles:', error); - set({ isLoadingSaved: false }); - } - }, - - saveArticleFromUrl: async (url: string) => { - try { - await api.saveArticleFromUrl(url); - get().loadSavedArticles(); - return true; - } catch (error) { - console.error('Failed to save article:', error); - return false; - } - }, - - archiveArticle: async (id: string) => { - // Optimistic update - set(state => ({ - savedArticles: state.savedArticles.filter(a => a.id !== id) - })); - - try { - await api.archiveArticle(id); - } catch (error) { - // Rollback - get().loadSavedArticles(); - } - }, -})); -``` - ---- - -## Phase 6: Browser Extension anpassen - -### 6.1 `packages/browser-extension/manifest.json` - -```json -{ - "manifest_version": 3, - "name": "News Hub - Save Article", - "version": "1.0.0", - "description": "Speichere Artikel in deiner News Hub Bibliothek", - "permissions": ["activeTab", "storage"], - "host_permissions": ["http://localhost:3000/*"], - "action": { - "default_popup": "popup.html", - "default_title": "In News Hub speichern" - }, - "background": { - "service_worker": "background.js" - } -} -``` - -### 6.2 `packages/browser-extension/popup.js` - -```javascript -const API_URL = 'http://localhost:3000'; - -document.addEventListener('DOMContentLoaded', async () => { - const pageTitle = document.getElementById('pageTitle'); - const pageUrl = document.getElementById('pageUrl'); - const saveButton = document.getElementById('saveButton'); - const status = document.getElementById('status'); - const loginNotice = document.getElementById('loginNotice'); - - let currentTab = null; - let authToken = null; - - // Get current tab - const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); - currentTab = tab; - pageTitle.textContent = tab.title || 'Untitled'; - pageUrl.textContent = tab.url; - - // Check auth from storage - const stored = await chrome.storage.local.get(['news_hub_token']); - authToken = stored.news_hub_token; - - if (authToken) { - saveButton.disabled = false; - loginNotice.style.display = 'none'; - saveArticle(); - } else { - loginNotice.style.display = 'block'; - saveButton.disabled = true; - } - - async function saveArticle() { - if (!currentTab || !authToken) return; - - saveButton.disabled = true; - status.textContent = 'Speichert...'; - - try { - const response = await fetch(`${API_URL}/extract/save`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${authToken}`, - }, - body: JSON.stringify({ url: currentTab.url }), - }); - - if (response.ok) { - status.textContent = 'Gespeichert!'; - setTimeout(() => window.close(), 1500); - } else { - throw new Error('Failed to save'); - } - } catch (error) { - status.textContent = 'Fehler beim Speichern'; - saveButton.disabled = false; - } - } - - saveButton.addEventListener('click', saveArticle); -}); -``` - ---- - -## Phase 7: Environment & Scripts - -### 7.1 `.env.example` (Root) - -```env -# Database -DATABASE_URL=postgresql://news:news_dev_password@localhost:5432/news_hub - -# API -API_PORT=3000 -API_URL=http://localhost:3000 - -# Better Auth -BETTER_AUTH_SECRET=your-secret-key-here - -# Mobile App -EXPO_PUBLIC_API_URL=http://localhost:3000 -``` - -### 7.2 Root `package.json` Scripts - -```json -{ - "scripts": { - "dev": "turbo run dev", - "dev:api": "turbo run dev --filter=@news/api", - "dev:mobile": "turbo run dev --filter=@news/mobile", - "build": "turbo run build", - "docker:up": "docker-compose -f docker/docker-compose.yml up -d", - "docker:down": "docker-compose -f docker/docker-compose.yml down", - "db:generate": "pnpm --filter @news/database db:generate", - "db:migrate": "pnpm --filter @news/database db:migrate", - "db:push": "pnpm --filter @news/database db:push", - "db:studio": "pnpm --filter @news/database db:studio" - } -} -``` - ---- - -## Migrations-Checkliste - -### Vorbereitung -- [ ] pnpm installieren (falls nicht vorhanden) -- [ ] Docker Desktop installiert -- [ ] Node.js 20+ installiert - -### Phase 1: Monorepo -- [ ] Root package.json erstellt -- [ ] turbo.json erstellt -- [ ] Ordnerstruktur angelegt -- [ ] `pnpm install` erfolgreich - -### Phase 2: Docker -- [ ] docker-compose.yml erstellt -- [ ] `pnpm docker:up` startet PostgreSQL -- [ ] pgAdmin erreichbar unter localhost:5050 - -### Phase 3: Database Package -- [ ] Drizzle Schema definiert -- [ ] `pnpm db:generate` erfolgreich -- [ ] `pnpm db:push` erstellt Tabellen -- [ ] `pnpm db:studio` zeigt Tabellen - -### Phase 4: NestJS Backend -- [ ] NestJS Projekt erstellt -- [ ] Better Auth konfiguriert -- [ ] Auth Endpoints funktionieren -- [ ] Articles Endpoints funktionieren -- [ ] Content Extraction funktioniert - -### Phase 5: Mobile App -- [ ] ainews nach apps/mobile verschoben -- [ ] Supabase-Abhängigkeiten entfernt -- [ ] API Client erstellt -- [ ] Auth Context aktualisiert -- [ ] Store aktualisiert -- [ ] App startet und verbindet mit API - -### Phase 6: Browser Extension -- [ ] Extension nach packages/browser-extension -- [ ] API URL angepasst -- [ ] Funktioniert mit neuem Backend - -### Phase 7: Testing -- [ ] User Registration -- [ ] User Login -- [ ] Artikel laden (Feed) -- [ ] Artikel speichern via URL -- [ ] Artikel speichern via Extension -- [ ] Artikel archivieren - ---- - -## Empfohlene Reihenfolge zum Starten - -```bash -# 1. Monorepo initialisieren -cd news -pnpm init -# package.json anpassen (workspaces) - -# 2. Docker starten -pnpm docker:up - -# 3. Database Package erstellen & migrieren -cd packages/database -pnpm install -pnpm db:push - -# 4. API entwickeln & testen -cd apps/api -pnpm install -pnpm dev -# Test: curl http://localhost:3000/articles - -# 5. Mobile App migrieren -mv ainews apps/mobile -cd apps/mobile -# Supabase entfernen, API Client einbauen -pnpm dev - -# 6. Extension anpassen -cd packages/browser-extension -# URLs anpassen, testen -``` - ---- - -## Vorteile der neuen Architektur - -| Aspekt | Vorher (Supabase) | Nachher (Eigenes Backend) | -|--------|-------------------|---------------------------| -| **Kontrolle** | Abhängig von Supabase | Volle Kontrolle | -| **Kosten** | Pay-per-use | Fixkosten (oder gratis lokal) | -| **Flexibilität** | Supabase-Limits | Unbegrenzt skalierbar | -| **Type Safety** | Manuell generiert | Drizzle: Schema = Types | -| **Migrations** | Supabase Dashboard | Drizzle-Kit: Versioniert | -| **Testing** | Schwierig lokal | Docker: Identisch zu Prod | -| **Auth** | Supabase Auth | Better Auth: Flexibel | - ---- - -## Nächste Schritte - -1. **Entscheidung**: Plan OK? Anpassungen nötig? -2. **Phase 1 starten**: Monorepo Setup -3. **Parallel**: Docker & Database Package - -Soll ich mit der Implementierung beginnen? diff --git a/apps-archived/news/README.md b/apps-archived/news/README.md deleted file mode 100644 index e5087aa63..000000000 --- a/apps-archived/news/README.md +++ /dev/null @@ -1,187 +0,0 @@ -# News Hub - -A unified news reading platform combining AI-curated news with personal article saving capabilities. - -## Architecture - -``` -news/ -├── apps/ -│ ├── mobile/ # React Native/Expo App -│ └── api/ # NestJS Backend -├── packages/ -│ ├── database/ # Drizzle ORM Schema -│ ├── shared/ # Shared utilities -│ └── browser-extension/ # Chrome Extension -└── docker/ # PostgreSQL Docker setup -``` - -## Tech Stack - -| Component | Technology | -| ------------ | --------------------------- | -| **Database** | PostgreSQL 16 (Docker) | -| **ORM** | Drizzle | -| **Backend** | NestJS + Fastify | -| **Auth** | Custom JWT Auth | -| **Mobile** | React Native / Expo | -| **State** | Zustand | -| **Styling** | NativeWind (Tailwind) | -| **Monorepo** | pnpm workspaces + Turborepo | - -## Getting Started - -### Prerequisites - -- Node.js 20+ -- pnpm 9+ -- Docker Desktop - -### Setup - -```bash -# 1. Install dependencies -pnpm install - -# 2. Start PostgreSQL -pnpm docker:up - -# 3. Push database schema -pnpm db:push - -# 4. Start API server -pnpm dev:api - -# 5. Start mobile app (in another terminal) -pnpm dev:mobile -``` - -### Available Scripts - -```bash -# Development -pnpm dev # Start all services -pnpm dev:api # Start API only -pnpm dev:mobile # Start mobile app only - -# Database -pnpm db:push # Push schema to database -pnpm db:generate # Generate migrations -pnpm db:migrate # Run migrations -pnpm db:studio # Open Drizzle Studio - -# Docker -pnpm docker:up # Start PostgreSQL -pnpm docker:down # Stop PostgreSQL -pnpm docker:logs # View logs - -# Build -pnpm build # Build all packages -``` - -## Environment Variables - -Create a `.env` file in the root directory: - -```env -# Database -DATABASE_URL=postgresql://news:news_dev_password@localhost:5432/news_hub - -# API -API_PORT=3000 -API_URL=http://localhost:3000 - -# Better Auth Secret -BETTER_AUTH_SECRET=your-secret-key - -# Mobile App -EXPO_PUBLIC_API_URL=http://localhost:3000 -``` - -## Features - -### News Feed (AI-Generated) - -- **Feed**: Quick news updates with infinite scroll -- **Summaries**: 4 daily summaries (morning, noon, evening, night) -- **In-Depth**: Detailed analysis articles - -### Personal Library (Read Later) - -- Save articles from any URL -- Browser extension for one-click saving -- Content extraction with Readability -- Archive and organize articles - -## API Endpoints - -### Auth - -- `POST /auth/signup` - Create account -- `POST /auth/signin` - Sign in -- `POST /auth/signout` - Sign out -- `GET /auth/session` - Get current session - -### Articles - -- `GET /articles` - Get AI articles (public) -- `GET /articles/:id` - Get single article -- `GET /articles/saved/list` - Get saved articles (auth required) -- `POST /articles/:id/archive` - Archive article -- `DELETE /articles/:id` - Delete article - -### Content Extraction - -- `POST /extract/save` - Save article from URL (auth required) -- `POST /extract/preview` - Preview URL extraction (public) - -### Categories - -- `GET /categories` - Get all categories - -### Users - -- `GET /users/me` - Get current user -- `PATCH /users/me` - Update profile -- `PATCH /users/me/onboarding` - Complete onboarding - -## Browser Extension - -The browser extension is located in `packages/browser-extension/`. - -### Installation (Development) - -1. Go to `chrome://extensions/` -2. Enable "Developer mode" -3. Click "Load unpacked" -4. Select the `packages/browser-extension` folder - -## Database Schema - -### Tables - -- `users` - User accounts and preferences -- `articles` - All articles (AI-generated and user-saved) -- `categories` - Article categories -- `user_article_interactions` - Reading progress, ratings, bookmarks -- `sessions` - Auth sessions -- `accounts` - Auth providers -- `verifications` - Email verification tokens - -## Development - -### Adding a new API endpoint - -1. Create service in `apps/api/src/{module}/{module}.service.ts` -2. Create controller in `apps/api/src/{module}/{module}.controller.ts` -3. Add module to `app.module.ts` - -### Adding a new database table - -1. Create schema in `packages/database/src/schema/{table}.ts` -2. Export from `packages/database/src/schema/index.ts` -3. Run `pnpm db:push` to update database - -## License - -Private diff --git a/apps-archived/news/apps/api/nest-cli.json b/apps-archived/news/apps/api/nest-cli.json deleted file mode 100644 index 95538fb90..000000000 --- a/apps-archived/news/apps/api/nest-cli.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/nest-cli", - "collection": "@nestjs/schematics", - "sourceRoot": "src", - "compilerOptions": { - "deleteOutDir": true - } -} diff --git a/apps-archived/news/apps/api/package.json b/apps-archived/news/apps/api/package.json deleted file mode 100644 index 2b2144f2f..000000000 --- a/apps-archived/news/apps/api/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "@news/api", - "version": "1.0.0", - "private": true, - "scripts": { - "build": "nest build", - "dev": "nest start --watch", - "start:dev": "nest start --watch", - "start": "nest start", - "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix" - }, - "dependencies": { - "@manacore/shared-utils": "workspace:*", - "@manacore/shared-types": "workspace:*", - "@nestjs/common": "^10.4.0", - "@nestjs/config": "^3.3.0", - "@nestjs/core": "^10.4.0", - "@nestjs/platform-fastify": "^10.4.0", - "@manacore/news-database": "workspace:*", - "drizzle-orm": "^0.36.0", - "postgres": "^3.4.5", - "@mozilla/readability": "^0.5.0", - "jsdom": "^25.0.0", - "class-validator": "^0.14.1", - "class-transformer": "^0.5.1", - "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1" - }, - "devDependencies": { - "@nestjs/cli": "^10.4.0", - "@nestjs/schematics": "^10.2.0", - "@types/jsdom": "^21.1.0", - "@types/node": "^22.0.0", - "typescript": "^5.6.0" - } -} diff --git a/apps-archived/news/apps/api/src/app.module.ts b/apps-archived/news/apps/api/src/app.module.ts deleted file mode 100644 index 68c3c5d07..000000000 --- a/apps-archived/news/apps/api/src/app.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; -import { DatabaseModule } from './database/database.module'; -import { AuthModule } from './auth/auth.module'; -import { ArticlesModule } from './articles/articles.module'; -import { CategoriesModule } from './categories/categories.module'; -import { UsersModule } from './users/users.module'; -import { ContentExtractionModule } from './content-extraction/content-extraction.module'; - -@Module({ - imports: [ - ConfigModule.forRoot({ - isGlobal: true, - envFilePath: '../../.env', - }), - DatabaseModule, - AuthModule, - ArticlesModule, - CategoriesModule, - UsersModule, - ContentExtractionModule, - ], -}) -export class AppModule {} diff --git a/apps-archived/news/apps/api/src/articles/articles.controller.ts b/apps-archived/news/apps/api/src/articles/articles.controller.ts deleted file mode 100644 index 68a21e9ca..000000000 --- a/apps-archived/news/apps/api/src/articles/articles.controller.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Controller, Get, Post, Delete, Param, Query, UseGuards } from '@nestjs/common'; -import { ArticlesService } from './articles.service'; -import { AuthGuard } from '../common/guards/auth.guard'; -import { CurrentUser } from '../common/decorators/current-user.decorator'; -import { User } from '@manacore/news-database'; - -@Controller('articles') -export class ArticlesController { - constructor(private articlesService: ArticlesService) {} - - // Public: Get AI-generated articles - @Get() - async getArticles( - @Query('type') type?: 'feed' | 'summary' | 'in_depth', - @Query('categoryId') categoryId?: string, - @Query('limit') limit?: string, - @Query('offset') offset?: string - ) { - return this.articlesService.getAIArticles({ - type, - categoryId, - limit: limit ? parseInt(limit, 10) : 20, - offset: offset ? parseInt(offset, 10) : 0, - }); - } - - // Public: Get single article - @Get(':id') - async getArticle(@Param('id') id: string) { - const article = await this.articlesService.getArticleById(id); - if (!article) { - return { error: 'Article not found' }; - } - return article; - } - - // Protected: Get user's saved articles - @Get('saved/list') - @UseGuards(AuthGuard) - async getSavedArticles( - @CurrentUser() user: User, - @Query('includeArchived') includeArchived?: string - ) { - return this.articlesService.getSavedArticles(user.id, includeArchived === 'true'); - } - - // Protected: Archive article - @Post(':id/archive') - @UseGuards(AuthGuard) - async archiveArticle(@Param('id') id: string, @CurrentUser() user: User) { - await this.articlesService.archiveArticle(id, user.id); - return { success: true }; - } - - // Protected: Unarchive article - @Post(':id/unarchive') - @UseGuards(AuthGuard) - async unarchiveArticle(@Param('id') id: string, @CurrentUser() user: User) { - await this.articlesService.unarchiveArticle(id, user.id); - return { success: true }; - } - - // Protected: Delete article - @Delete(':id') - @UseGuards(AuthGuard) - async deleteArticle(@Param('id') id: string, @CurrentUser() user: User) { - await this.articlesService.deleteArticle(id, user.id); - return { success: true }; - } -} diff --git a/apps-archived/news/apps/api/src/articles/articles.module.ts b/apps-archived/news/apps/api/src/articles/articles.module.ts deleted file mode 100644 index 0dc988fd8..000000000 --- a/apps-archived/news/apps/api/src/articles/articles.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ArticlesController } from './articles.controller'; -import { ArticlesService } from './articles.service'; -import { AuthModule } from '../auth/auth.module'; - -@Module({ - imports: [AuthModule], - controllers: [ArticlesController], - providers: [ArticlesService], - exports: [ArticlesService], -}) -export class ArticlesModule {} diff --git a/apps-archived/news/apps/api/src/articles/articles.service.ts b/apps-archived/news/apps/api/src/articles/articles.service.ts deleted file mode 100644 index 7ecb6444e..000000000 --- a/apps-archived/news/apps/api/src/articles/articles.service.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { Injectable, Inject, NotFoundException } from '@nestjs/common'; -import { DATABASE_CONNECTION } from '../database/database.module'; -import { Database, articles, Article, eq, and, desc } from '@manacore/news-database'; - -@Injectable() -export class ArticlesService { - constructor(@Inject(DATABASE_CONNECTION) private database: Database) {} - - // Get AI-generated articles (feed, summary, in_depth) - async getAIArticles(options: { - type?: 'feed' | 'summary' | 'in_depth'; - categoryId?: string; - limit?: number; - offset?: number; - }): Promise { - const { type, categoryId, limit = 20, offset = 0 } = options; - - const conditions = [eq(articles.sourceOrigin, 'ai')]; - - if (type) { - conditions.push(eq(articles.type, type)); - } - - if (categoryId) { - conditions.push(eq(articles.categoryId, categoryId)); - } - - return this.database - .select() - .from(articles) - .where(and(...conditions)) - .orderBy(desc(articles.publishedAt)) - .limit(limit) - .offset(offset); - } - - // Get user-saved articles - async getSavedArticles(userId: string, includeArchived = false): Promise { - const conditions = [eq(articles.sourceOrigin, 'user_saved'), eq(articles.userId, userId)]; - - if (!includeArchived) { - conditions.push(eq(articles.isArchived, false)); - } - - return this.database - .select() - .from(articles) - .where(and(...conditions)) - .orderBy(desc(articles.createdAt)); - } - - // Get single article by ID - async getArticleById(articleId: string): Promise
{ - const [article] = await this.database - .select() - .from(articles) - .where(eq(articles.id, articleId)) - .limit(1); - - return article || null; - } - - // Create a saved article - async createSavedArticle(data: { - userId: string; - title: string; - content: string; - parsedContent: string; - originalUrl: string; - author?: string; - imageUrl?: string; - }): Promise
{ - const wordCount = data.content.split(/\s+/).length; - const readingTimeMinutes = Math.ceil(wordCount / 200); - - const [article] = await this.database - .insert(articles) - .values({ - type: 'saved', - sourceOrigin: 'user_saved', - userId: data.userId, - title: data.title, - content: data.content, - parsedContent: data.parsedContent, - originalUrl: data.originalUrl, - author: data.author, - imageUrl: data.imageUrl, - wordCount, - readingTimeMinutes, - isArchived: false, - }) - .returning(); - - return article; - } - - // Archive an article - async archiveArticle(articleId: string, userId: string): Promise { - const result = await this.database - .update(articles) - .set({ isArchived: true, updatedAt: new Date() }) - .where(and(eq(articles.id, articleId), eq(articles.userId, userId))) - .returning(); - - if (result.length === 0) { - throw new NotFoundException('Article not found'); - } - } - - // Unarchive an article - async unarchiveArticle(articleId: string, userId: string): Promise { - const result = await this.database - .update(articles) - .set({ isArchived: false, updatedAt: new Date() }) - .where(and(eq(articles.id, articleId), eq(articles.userId, userId))) - .returning(); - - if (result.length === 0) { - throw new NotFoundException('Article not found'); - } - } - - // Delete an article - async deleteArticle(articleId: string, userId: string): Promise { - const result = await this.database - .delete(articles) - .where(and(eq(articles.id, articleId), eq(articles.userId, userId))) - .returning(); - - if (result.length === 0) { - throw new NotFoundException('Article not found'); - } - } -} diff --git a/apps-archived/news/apps/api/src/auth/auth.controller.ts b/apps-archived/news/apps/api/src/auth/auth.controller.ts deleted file mode 100644 index 3b82b9c9e..000000000 --- a/apps-archived/news/apps/api/src/auth/auth.controller.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { Controller, Post, Get, Body, Headers, UnauthorizedException } from '@nestjs/common'; -import { AuthService } from './auth.service'; -import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator'; - -class SignUpDto { - @IsEmail() - email: string; - - @IsString() - @MinLength(6) - password: string; - - @IsOptional() - @IsString() - name?: string; -} - -class SignInDto { - @IsEmail() - email: string; - - @IsString() - password: string; -} - -@Controller('auth') -export class AuthController { - constructor(private authService: AuthService) {} - - @Post('signup') - async signUp(@Body() body: SignUpDto) { - const result = await this.authService.signUp(body.email, body.password, body.name); - return { - user: { - id: result.user.id, - email: result.user.email, - name: result.user.name, - }, - token: result.token, - }; - } - - @Post('signin') - async signIn(@Body() body: SignInDto) { - const result = await this.authService.signIn(body.email, body.password); - return { - user: { - id: result.user.id, - email: result.user.email, - name: result.user.name, - }, - token: result.token, - }; - } - - @Post('signout') - async signOut(@Headers('authorization') authHeader: string) { - if (!authHeader || !authHeader.startsWith('Bearer ')) { - throw new UnauthorizedException('No token provided'); - } - - const token = authHeader.substring(7); - await this.authService.signOut(token); - return { success: true }; - } - - @Get('session') - async getSession(@Headers('authorization') authHeader: string) { - if (!authHeader || !authHeader.startsWith('Bearer ')) { - throw new UnauthorizedException('No token provided'); - } - - const token = authHeader.substring(7); - const session = await this.authService.getSession(token); - - if (!session) { - throw new UnauthorizedException('Invalid or expired session'); - } - - return { - user: { - id: session.user.id, - email: session.user.email, - name: session.user.name, - }, - }; - } -} diff --git a/apps-archived/news/apps/api/src/auth/auth.module.ts b/apps-archived/news/apps/api/src/auth/auth.module.ts deleted file mode 100644 index 3bb2505a2..000000000 --- a/apps-archived/news/apps/api/src/auth/auth.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { AuthController } from './auth.controller'; -import { AuthService } from './auth.service'; - -@Module({ - controllers: [AuthController], - providers: [AuthService], - exports: [AuthService], -}) -export class AuthModule {} diff --git a/apps-archived/news/apps/api/src/auth/auth.service.ts b/apps-archived/news/apps/api/src/auth/auth.service.ts deleted file mode 100644 index 481321d4d..000000000 --- a/apps-archived/news/apps/api/src/auth/auth.service.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { Injectable, Inject, UnauthorizedException, ConflictException } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { DATABASE_CONNECTION } from '../database/database.module'; -import { Database, users, sessions, accounts, eq, and, User } from '@manacore/news-database'; -import * as crypto from 'crypto'; - -@Injectable() -export class AuthService { - constructor( - @Inject(DATABASE_CONNECTION) private database: Database, - private configService: ConfigService - ) {} - - private hashPassword(password: string): string { - const salt = crypto.randomBytes(16).toString('hex'); - const hash = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512').toString('hex'); - return `${salt}:${hash}`; - } - - private verifyPassword(password: string, storedHash: string): boolean { - const [salt, hash] = storedHash.split(':'); - const verifyHash = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512').toString('hex'); - return hash === verifyHash; - } - - private generateToken(): string { - return crypto.randomBytes(32).toString('hex'); - } - - async signUp( - email: string, - password: string, - name?: string - ): Promise<{ user: User; token: string }> { - // Check if user exists - const existingUser = await this.database - .select() - .from(users) - .where(eq(users.email, email.toLowerCase())) - .limit(1); - - if (existingUser.length > 0) { - throw new ConflictException('User already exists'); - } - - // Create user - const [user] = await this.database - .insert(users) - .values({ - email: email.toLowerCase(), - name: name || null, - }) - .returning(); - - // Create account with password - await this.database.insert(accounts).values({ - userId: user.id, - providerId: 'credential', - accountId: email.toLowerCase(), - password: this.hashPassword(password), - }); - - // Create session - const token = this.generateToken(); - const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days - - await this.database.insert(sessions).values({ - userId: user.id, - token, - expiresAt, - }); - - return { user, token }; - } - - async signIn(email: string, password: string): Promise<{ user: User; token: string }> { - // Find user - const [user] = await this.database - .select() - .from(users) - .where(eq(users.email, email.toLowerCase())) - .limit(1); - - if (!user) { - throw new UnauthorizedException('Invalid credentials'); - } - - // Find account - const [account] = await this.database - .select() - .from(accounts) - .where(and(eq(accounts.userId, user.id), eq(accounts.providerId, 'credential'))) - .limit(1); - - if (!account || !account.password) { - throw new UnauthorizedException('Invalid credentials'); - } - - // Verify password - if (!this.verifyPassword(password, account.password)) { - throw new UnauthorizedException('Invalid credentials'); - } - - // Create session - const token = this.generateToken(); - const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days - - await this.database.insert(sessions).values({ - userId: user.id, - token, - expiresAt, - }); - - return { user, token }; - } - - async signOut(token: string): Promise { - await this.database.delete(sessions).where(eq(sessions.token, token)); - } - - async validateSession(token: string): Promise<{ user: User; session: any } | null> { - const [session] = await this.database - .select() - .from(sessions) - .where(eq(sessions.token, token)) - .limit(1); - - if (!session || new Date(session.expiresAt) < new Date()) { - return null; - } - - const [user] = await this.database - .select() - .from(users) - .where(eq(users.id, session.userId)) - .limit(1); - - if (!user) { - return null; - } - - return { user, session }; - } - - async getSession(token: string): Promise<{ user: User } | null> { - const result = await this.validateSession(token); - if (!result) return null; - return { user: result.user }; - } -} diff --git a/apps-archived/news/apps/api/src/categories/categories.controller.ts b/apps-archived/news/apps/api/src/categories/categories.controller.ts deleted file mode 100644 index 043d3dfc7..000000000 --- a/apps-archived/news/apps/api/src/categories/categories.controller.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { CategoriesService } from './categories.service'; - -@Controller('categories') -export class CategoriesController { - constructor(private categoriesService: CategoriesService) {} - - @Get() - async getAllCategories() { - return this.categoriesService.getAllCategories(); - } -} diff --git a/apps-archived/news/apps/api/src/categories/categories.module.ts b/apps-archived/news/apps/api/src/categories/categories.module.ts deleted file mode 100644 index 8287f1da2..000000000 --- a/apps-archived/news/apps/api/src/categories/categories.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { CategoriesController } from './categories.controller'; -import { CategoriesService } from './categories.service'; - -@Module({ - controllers: [CategoriesController], - providers: [CategoriesService], - exports: [CategoriesService], -}) -export class CategoriesModule {} diff --git a/apps-archived/news/apps/api/src/categories/categories.service.ts b/apps-archived/news/apps/api/src/categories/categories.service.ts deleted file mode 100644 index 74119422c..000000000 --- a/apps-archived/news/apps/api/src/categories/categories.service.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Injectable, Inject } from '@nestjs/common'; -import { DATABASE_CONNECTION } from '../database/database.module'; -import { Database, categories, Category, asc } from '@manacore/news-database'; - -@Injectable() -export class CategoriesService { - constructor(@Inject(DATABASE_CONNECTION) private database: Database) {} - - async getAllCategories(): Promise { - return this.database.select().from(categories).orderBy(asc(categories.priority)); - } - - async createCategory(data: { - name: string; - displayName: string; - description?: string; - icon?: string; - color?: string; - priority?: number; - }): Promise { - const [category] = await this.database.insert(categories).values(data).returning(); - - return category; - } -} diff --git a/apps-archived/news/apps/api/src/common/decorators/current-user.decorator.ts b/apps-archived/news/apps/api/src/common/decorators/current-user.decorator.ts deleted file mode 100644 index ba497ee52..000000000 --- a/apps-archived/news/apps/api/src/common/decorators/current-user.decorator.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createParamDecorator, ExecutionContext } from '@nestjs/common'; - -export const CurrentUser = createParamDecorator((data: unknown, ctx: ExecutionContext) => { - const request = ctx.switchToHttp().getRequest(); - return request.user; -}); diff --git a/apps-archived/news/apps/api/src/common/guards/auth.guard.ts b/apps-archived/news/apps/api/src/common/guards/auth.guard.ts deleted file mode 100644 index 5a67907d9..000000000 --- a/apps-archived/news/apps/api/src/common/guards/auth.guard.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; -import { AuthService } from '../../auth/auth.service'; - -@Injectable() -export class AuthGuard implements CanActivate { - constructor(private authService: AuthService) {} - - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const authHeader = request.headers.authorization; - - if (!authHeader || !authHeader.startsWith('Bearer ')) { - throw new UnauthorizedException('No token provided'); - } - - const token = authHeader.substring(7); - - try { - const session = await this.authService.validateSession(token); - if (!session) { - throw new UnauthorizedException('Invalid or expired session'); - } - - request.user = session.user; - request.session = session; - return true; - } catch { - throw new UnauthorizedException('Invalid token'); - } - } -} diff --git a/apps-archived/news/apps/api/src/content-extraction/content-extraction.controller.ts b/apps-archived/news/apps/api/src/content-extraction/content-extraction.controller.ts deleted file mode 100644 index b65e3f9b8..000000000 --- a/apps-archived/news/apps/api/src/content-extraction/content-extraction.controller.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Controller, Post, Body, UseGuards } from '@nestjs/common'; -import { ContentExtractionService } from './content-extraction.service'; -import { AuthGuard } from '../common/guards/auth.guard'; -import { CurrentUser } from '../common/decorators/current-user.decorator'; -import { User } from '@manacore/news-database'; -import { IsUrl } from 'class-validator'; - -class ExtractUrlDto { - @IsUrl() - url: string; -} - -@Controller('extract') -export class ContentExtractionController { - constructor(private contentExtractionService: ContentExtractionService) {} - - // Protected: Save article from URL - @Post('save') - @UseGuards(AuthGuard) - async saveFromUrl(@Body() body: ExtractUrlDto, @CurrentUser() user: User) { - const article = await this.contentExtractionService.saveArticleFromUrl(user.id, body.url); - - return { - success: true, - article: { - id: article.id, - title: article.title, - createdAt: article.createdAt, - }, - }; - } - - // Public: Preview URL extraction (without saving) - @Post('preview') - async previewUrl(@Body() body: ExtractUrlDto) { - const extracted = await this.contentExtractionService.extractFromUrl(body.url); - - return { - title: extracted.title, - excerpt: extracted.excerpt, - byline: extracted.byline, - siteName: extracted.siteName, - contentLength: extracted.content.length, - }; - } -} diff --git a/apps-archived/news/apps/api/src/content-extraction/content-extraction.module.ts b/apps-archived/news/apps/api/src/content-extraction/content-extraction.module.ts deleted file mode 100644 index ec2de3e68..000000000 --- a/apps-archived/news/apps/api/src/content-extraction/content-extraction.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ContentExtractionController } from './content-extraction.controller'; -import { ContentExtractionService } from './content-extraction.service'; -import { ArticlesModule } from '../articles/articles.module'; -import { AuthModule } from '../auth/auth.module'; - -@Module({ - imports: [ArticlesModule, AuthModule], - controllers: [ContentExtractionController], - providers: [ContentExtractionService], - exports: [ContentExtractionService], -}) -export class ContentExtractionModule {} diff --git a/apps-archived/news/apps/api/src/content-extraction/content-extraction.service.ts b/apps-archived/news/apps/api/src/content-extraction/content-extraction.service.ts deleted file mode 100644 index 6e4af1441..000000000 --- a/apps-archived/news/apps/api/src/content-extraction/content-extraction.service.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Injectable, BadRequestException } from '@nestjs/common'; -import { Readability } from '@mozilla/readability'; -import { JSDOM } from 'jsdom'; -import { ArticlesService } from '../articles/articles.service'; - -export interface ExtractedContent { - title: string; - content: string; - htmlContent: string; - excerpt?: string; - byline?: string; - siteName?: string; -} - -@Injectable() -export class ContentExtractionService { - constructor(private articlesService: ArticlesService) {} - - async extractFromUrl(url: string): Promise { - // Validate URL - try { - new URL(url); - } catch { - throw new BadRequestException('Invalid URL'); - } - - // Fetch the page - const response = await fetch(url, { - headers: { - 'User-Agent': - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', - 'Accept-Language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7', - }, - }); - - if (!response.ok) { - throw new BadRequestException( - `Failed to fetch URL: ${response.status} ${response.statusText}` - ); - } - - const html = await response.text(); - - // Parse with JSDOM - const dom = new JSDOM(html, { url }); - const reader = new Readability(dom.window.document); - const article = reader.parse(); - - if (!article) { - throw new BadRequestException('Could not extract article content from this page'); - } - - return { - title: article.title || 'Untitled', - content: article.textContent || '', - htmlContent: article.content || '', - excerpt: article.excerpt, - byline: article.byline, - siteName: article.siteName, - }; - } - - async saveArticleFromUrl(userId: string, url: string) { - const extracted = await this.extractFromUrl(url); - - return this.articlesService.createSavedArticle({ - userId, - title: extracted.title, - content: extracted.content, - parsedContent: extracted.htmlContent, - originalUrl: url, - author: extracted.byline, - }); - } -} diff --git a/apps-archived/news/apps/api/src/database/database.module.ts b/apps-archived/news/apps/api/src/database/database.module.ts deleted file mode 100644 index 08d397892..000000000 --- a/apps-archived/news/apps/api/src/database/database.module.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Module, Global } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { createDb } from '@manacore/news-database'; - -export const DATABASE_CONNECTION = 'DATABASE_CONNECTION'; - -@Global() -@Module({ - providers: [ - { - provide: DATABASE_CONNECTION, - useFactory: (configService: ConfigService) => { - const databaseUrl = - configService.get('DATABASE_URL') || - 'postgresql://news:news_dev_password@localhost:5434/news_hub'; - - console.log('Connecting to database:', databaseUrl.replace(/:[^:@]+@/, ':****@')); - - return createDb(databaseUrl); - }, - inject: [ConfigService], - }, - ], - exports: [DATABASE_CONNECTION], -}) -export class DatabaseModule {} diff --git a/apps-archived/news/apps/api/src/main.ts b/apps-archived/news/apps/api/src/main.ts deleted file mode 100644 index a6c4babd0..000000000 --- a/apps-archived/news/apps/api/src/main.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { NestFactory } from '@nestjs/core'; -import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify'; -import { ValidationPipe } from '@nestjs/common'; -import { AppModule } from './app.module'; - -async function bootstrap() { - const app = await NestFactory.create( - AppModule, - new FastifyAdapter({ logger: true }) - ); - - app.enableCors({ - origin: [ - 'http://localhost:8081', // Expo web - 'http://localhost:19006', // Expo web alt - 'http://localhost:3000', // API itself (for testing) - /^exp:\/\/.*/, // Expo Go - ], - credentials: true, - }); - - app.useGlobalPipes( - new ValidationPipe({ - whitelist: true, - transform: true, - transformOptions: { - enableImplicitConversion: true, - }, - }) - ); - - const port = process.env.API_PORT || 3000; - await app.listen(port, '0.0.0.0'); - console.log(`API running on http://localhost:${port}`); -} - -bootstrap(); diff --git a/apps-archived/news/apps/api/src/users/users.controller.ts b/apps-archived/news/apps/api/src/users/users.controller.ts deleted file mode 100644 index c3efd0322..000000000 --- a/apps-archived/news/apps/api/src/users/users.controller.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Controller, Get, Patch, Body, UseGuards } from '@nestjs/common'; -import { UsersService } from './users.service'; -import { AuthGuard } from '../common/guards/auth.guard'; -import { CurrentUser } from '../common/decorators/current-user.decorator'; -import { User } from '@manacore/news-database'; -import { IsOptional, IsString, IsArray, IsEnum, IsBoolean } from 'class-validator'; - -class UpdateUserDto { - @IsOptional() - @IsString() - name?: string; - - @IsOptional() - @IsArray() - preferredCategories?: string[]; - - @IsOptional() - @IsArray() - blockedSources?: string[]; - - @IsOptional() - @IsEnum(['slow', 'normal', 'fast']) - readingSpeed?: 'slow' | 'normal' | 'fast'; - - @IsOptional() - @IsString() - notificationSettings?: string; - - @IsOptional() - @IsBoolean() - onboardingCompleted?: boolean; -} - -@Controller('users') -export class UsersController { - constructor(private usersService: UsersService) {} - - @Get('me') - @UseGuards(AuthGuard) - async getCurrentUser(@CurrentUser() user: User) { - return this.usersService.getUserById(user.id); - } - - @Patch('me') - @UseGuards(AuthGuard) - async updateCurrentUser(@CurrentUser() user: User, @Body() body: UpdateUserDto) { - return this.usersService.updateUser(user.id, body); - } - - @Patch('me/onboarding') - @UseGuards(AuthGuard) - async completeOnboarding(@CurrentUser() user: User) { - await this.usersService.completeOnboarding(user.id); - return { success: true }; - } -} diff --git a/apps-archived/news/apps/api/src/users/users.module.ts b/apps-archived/news/apps/api/src/users/users.module.ts deleted file mode 100644 index 4d55427e4..000000000 --- a/apps-archived/news/apps/api/src/users/users.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Module } from '@nestjs/common'; -import { UsersController } from './users.controller'; -import { UsersService } from './users.service'; -import { AuthModule } from '../auth/auth.module'; - -@Module({ - imports: [AuthModule], - controllers: [UsersController], - providers: [UsersService], - exports: [UsersService], -}) -export class UsersModule {} diff --git a/apps-archived/news/apps/api/src/users/users.service.ts b/apps-archived/news/apps/api/src/users/users.service.ts deleted file mode 100644 index 16964540e..000000000 --- a/apps-archived/news/apps/api/src/users/users.service.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Injectable, Inject } from '@nestjs/common'; -import { DATABASE_CONNECTION } from '../database/database.module'; -import { Database, users, User, eq } from '@manacore/news-database'; - -@Injectable() -export class UsersService { - constructor(@Inject(DATABASE_CONNECTION) private database: Database) {} - - async getUserById(userId: string): Promise { - const [user] = await this.database.select().from(users).where(eq(users.id, userId)).limit(1); - - return user || null; - } - - async updateUser( - userId: string, - data: { - name?: string; - preferredCategories?: string[]; - blockedSources?: string[]; - readingSpeed?: 'slow' | 'normal' | 'fast'; - notificationSettings?: string; - onboardingCompleted?: boolean; - } - ): Promise { - const [user] = await this.database - .update(users) - .set({ - ...data, - updatedAt: new Date(), - }) - .where(eq(users.id, userId)) - .returning(); - - return user; - } - - async completeOnboarding(userId: string): Promise { - await this.database - .update(users) - .set({ - onboardingCompleted: true, - updatedAt: new Date(), - }) - .where(eq(users.id, userId)); - } -} diff --git a/apps-archived/news/apps/api/tsconfig.json b/apps-archived/news/apps/api/tsconfig.json deleted file mode 100644 index f02c2417e..000000000 --- a/apps-archived/news/apps/api/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "declaration": true, - "removeComments": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "target": "ES2022", - "sourceMap": true, - "outDir": "./dist", - "baseUrl": "./", - "incremental": true, - "skipLibCheck": true, - "strictNullChecks": true, - "noImplicitAny": true, - "strictBindCallApply": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, - "esModuleInterop": true, - "resolveJsonModule": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/apps-archived/news/apps/landing/tailwind.config.mjs b/apps-archived/news/apps/landing/tailwind.config.mjs deleted file mode 100644 index d7222e3f4..000000000 --- a/apps-archived/news/apps/landing/tailwind.config.mjs +++ /dev/null @@ -1,39 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - content: [ - './src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}', - '../../packages/shared-landing-ui/src/**/*.{astro,html,js,jsx,ts,tsx}' - ], - theme: { - extend: { - colors: { - // News Hub Purple/Indigo Theme - primary: { - DEFAULT: '#6366f1', - hover: '#818cf8', - glow: 'rgba(99, 102, 241, 0.3)' - }, - background: { - page: '#0f0f1a', - card: '#1a1a2e', - 'card-hover': '#252542' - }, - text: { - primary: '#f9fafb', - secondary: '#d1d5db', - muted: '#6b7280' - }, - border: { - DEFAULT: '#252542', - hover: '#3a3a5c' - } - }, - fontFamily: { - sans: ['Inter', 'system-ui', 'sans-serif'] - } - } - }, - plugins: [ - require('@tailwindcss/typography') - ] -}; diff --git a/apps-archived/news/apps/web/package.json b/apps-archived/news/apps/web/package.json deleted file mode 100644 index 22ca0daab..000000000 --- a/apps-archived/news/apps/web/package.json +++ /dev/null @@ -1,41 +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", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json" - }, - "devDependencies": { - "@sveltejs/adapter-auto": "^6.0.0", - "@sveltejs/kit": "^2.43.2", - "@sveltejs/vite-plugin-svelte": "^6.2.0", - "@tailwindcss/postcss": "^4.1.17", - "@tailwindcss/typography": "^0.5.19", - "autoprefixer": "^10.4.21", - "postcss": "^8.5.6", - "svelte": "^5.39.5", - "svelte-check": "^4.3.2", - "tailwindcss": "^4.1.17", - "typescript": "^5.9.3", - "vite": "^7.1.7" - }, - "dependencies": { - "@manacore/shared-auth-ui": "workspace:*", - "@manacore/shared-branding": "workspace:*", - "@manacore/shared-i18n": "workspace:*", - "@manacore/shared-icons": "workspace:*", - "@manacore/shared-tailwind": "workspace:*", - "@manacore/shared-theme": "workspace:*", - "@manacore/shared-theme-ui": "workspace:*", - "@manacore/shared-ui": "workspace:*", - "@manacore/shared-utils": "workspace:*", - "marked": "^17.0.0" - } -} diff --git a/apps-archived/news/apps/web/src/app.d.ts b/apps-archived/news/apps/web/src/app.d.ts deleted file mode 100644 index 01c1c7b85..000000000 --- a/apps-archived/news/apps/web/src/app.d.ts +++ /dev/null @@ -1,33 +0,0 @@ -// See https://svelte.dev/docs/kit/types#app.d.ts -// for information about these interfaces - -interface NewsUser { - id: string; - email: string; - name?: string; - createdAt: string; -} - -interface NewsSession { - token: string; - userId: string; - expiresAt: string; -} - -declare global { - namespace App { - // interface Error {} - interface Locals { - session: NewsSession | null; - user: NewsUser | null; - } - interface PageData { - session: NewsSession | null; - user: NewsUser | null; - } - // interface PageState {} - // interface Platform {} - } -} - -export {}; diff --git a/apps-archived/news/apps/web/src/lib/api/feedback.ts b/apps-archived/news/apps/web/src/lib/api/feedback.ts deleted file mode 100644 index 3d14b71b9..000000000 --- a/apps-archived/news/apps/web/src/lib/api/feedback.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Feedback Service Instance for News Web App - */ - -import { createFeedbackService } from '@manacore/shared-feedback-service'; -import { authStore } from '$lib/stores/auth.svelte'; -import { PUBLIC_MANA_CORE_AUTH_URL } from '$env/static/public'; - -const MANA_AUTH_URL = PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; - -export const feedbackService = createFeedbackService({ - apiUrl: MANA_AUTH_URL, - appId: 'news', - getAuthToken: async () => authStore.getAccessToken(), -}); diff --git a/apps-archived/news/apps/web/src/lib/services/api.ts b/apps-archived/news/apps/web/src/lib/services/api.ts deleted file mode 100644 index e58d53e32..000000000 --- a/apps-archived/news/apps/web/src/lib/services/api.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { env } from '$env/dynamic/public'; - -const API_URL = env.PUBLIC_NEWS_API_URL || 'http://localhost:3000'; - -interface ApiResponse { - data?: T; - error?: string; -} - -export async function apiRequest( - endpoint: string, - options: RequestInit = {}, - token?: string -): Promise> { - try { - const headers: Record = { - 'Content-Type': 'application/json', - ...(options.headers as Record), - }; - - if (token) { - headers['Authorization'] = `Bearer ${token}`; - } - - const response = await fetch(`${API_URL}${endpoint}`, { - ...options, - headers, - }); - - if (!response.ok) { - const errorText = await response.text(); - return { error: errorText || `HTTP ${response.status}` }; - } - - const data = await response.json(); - return { data }; - } catch (error) { - return { error: error instanceof Error ? error.message : 'Unknown error' }; - } -} - -// Auth endpoints -export const authApi = { - login: (email: string, password: string) => - apiRequest<{ token: string; user: App.Locals['user'] }>('/auth/login', { - method: 'POST', - body: JSON.stringify({ email, password }), - }), - - signup: (email: string, password: string, name?: string) => - apiRequest<{ token: string; user: App.Locals['user'] }>('/auth/signup', { - method: 'POST', - body: JSON.stringify({ email, password, name }), - }), - - logout: (token: string) => apiRequest('/auth/logout', { method: 'POST' }, token), - - me: (token: string) => apiRequest('/auth/me', {}, token), -}; - -// Articles endpoints -export const articlesApi = { - getArticles: ( - params?: { type?: string; categoryId?: string; limit?: number; offset?: number }, - token?: string - ) => { - const searchParams = new URLSearchParams(); - if (params?.type) searchParams.set('type', params.type); - if (params?.categoryId) searchParams.set('categoryId', params.categoryId); - if (params?.limit) searchParams.set('limit', params.limit.toString()); - if (params?.offset) searchParams.set('offset', params.offset.toString()); - const query = searchParams.toString(); - return apiRequest(`/articles${query ? `?${query}` : ''}`, {}, token); - }, - - getArticle: (id: string, token?: string) => apiRequest(`/articles/${id}`, {}, token), - - getSavedArticles: (token: string) => apiRequest('/articles/saved/list', {}, token), - - archiveArticle: (id: string, token: string) => - apiRequest(`/articles/${id}/archive`, { method: 'POST' }, token), - - unarchiveArticle: (id: string, token: string) => - apiRequest(`/articles/${id}/unarchive`, { method: 'POST' }, token), - - deleteArticle: (id: string, token: string) => - apiRequest(`/articles/${id}`, { method: 'DELETE' }, token), -}; - -// Categories endpoints -export const categoriesApi = { - getCategories: (token?: string) => apiRequest('/categories', {}, token), -}; diff --git a/apps-archived/news/apps/web/src/lib/stores/auth.svelte.ts b/apps-archived/news/apps/web/src/lib/stores/auth.svelte.ts deleted file mode 100644 index 3f4ec582d..000000000 --- a/apps-archived/news/apps/web/src/lib/stores/auth.svelte.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { authApi } from '$lib/services/api'; - -class AuthStore { - user = $state(null); - session = $state(null); - loading = $state(false); - error = $state(null); - - get isAuthenticated() { - return !!this.session && !!this.user; - } - - async login(email: string, password: string) { - this.loading = true; - this.error = null; - - const { data, error } = await authApi.login(email, password); - - if (error) { - this.error = error; - this.loading = false; - return false; - } - - if (data) { - this.session = { token: data.token, userId: data.user?.id ?? '', expiresAt: '' }; - this.user = data.user; - // Store token in cookie/localStorage - if (typeof window !== 'undefined') { - document.cookie = `news_session=${data.token}; path=/; max-age=604800`; // 7 days - } - } - - this.loading = false; - return true; - } - - async signup(email: string, password: string, name?: string) { - this.loading = true; - this.error = null; - - const { data, error } = await authApi.signup(email, password, name); - - if (error) { - this.error = error; - this.loading = false; - return false; - } - - if (data) { - this.session = { token: data.token, userId: data.user?.id ?? '', expiresAt: '' }; - this.user = data.user; - if (typeof window !== 'undefined') { - document.cookie = `news_session=${data.token}; path=/; max-age=604800`; - } - } - - this.loading = false; - return true; - } - - async logout() { - if (this.session?.token) { - await authApi.logout(this.session.token); - } - - this.session = null; - this.user = null; - - if (typeof window !== 'undefined') { - document.cookie = 'news_session=; path=/; max-age=0'; - } - } - - setSession(session: App.Locals['session'], user: App.Locals['user']) { - this.session = session; - this.user = user; - } -} - -export const authStore = new AuthStore(); diff --git a/apps-archived/news/apps/web/src/routes/(protected)/+layout.svelte b/apps-archived/news/apps/web/src/routes/(protected)/+layout.svelte deleted file mode 100644 index 57185e315..000000000 --- a/apps-archived/news/apps/web/src/routes/(protected)/+layout.svelte +++ /dev/null @@ -1,154 +0,0 @@ - - - - -{#if loading} -
-
-
-

Laden...

-
-
-{:else} -
- - {#snippet logo()} - 📰 - News - {/snippet} - - -
-
- {@render children()} -
-
-
-{/if} diff --git a/apps-archived/news/apps/web/src/routes/(protected)/apps/+page.svelte b/apps-archived/news/apps/web/src/routes/(protected)/apps/+page.svelte deleted file mode 100644 index 7bbd76a08..000000000 --- a/apps-archived/news/apps/web/src/routes/(protected)/apps/+page.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - - Alle Apps - News - - -
- -
- - diff --git a/apps-archived/news/apps/web/src/routes/(protected)/feed/+page.svelte b/apps-archived/news/apps/web/src/routes/(protected)/feed/+page.svelte deleted file mode 100644 index 44bc74b21..000000000 --- a/apps-archived/news/apps/web/src/routes/(protected)/feed/+page.svelte +++ /dev/null @@ -1,96 +0,0 @@ - - - - Feed - News Hub - - -
-
-

Feed

-

Aktuelle Nachrichten im Überblick

-
- - {#if loading} -
-
-
- {:else if error} -
- {error} -
- {:else if articles.length === 0} -
- - - -

Noch keine Artikel vorhanden

-

- Artikel werden automatisch generiert und erscheinen hier -

-
- {:else} - - {/if} -
diff --git a/apps-archived/news/apps/web/src/routes/(protected)/feedback/+page.svelte b/apps-archived/news/apps/web/src/routes/(protected)/feedback/+page.svelte deleted file mode 100644 index 8b6a649b6..000000000 --- a/apps-archived/news/apps/web/src/routes/(protected)/feedback/+page.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/apps-archived/news/apps/web/src/routes/(protected)/in-depth/+page.svelte b/apps-archived/news/apps/web/src/routes/(protected)/in-depth/+page.svelte deleted file mode 100644 index 008d895cc..000000000 --- a/apps-archived/news/apps/web/src/routes/(protected)/in-depth/+page.svelte +++ /dev/null @@ -1,73 +0,0 @@ - - - - In-Depth - News Hub - - -
-
-

In-Depth Artikel

-

Ausführliche Analysen zu wichtigen Themen

-
- - {#if loading} -
-
-
- {:else if error} -
- {error} -
- {:else if articles.length === 0} -
-

Noch keine In-Depth Artikel vorhanden

-
- {:else} - - {/if} -
diff --git a/apps-archived/news/apps/web/src/routes/(protected)/saved/+page.svelte b/apps-archived/news/apps/web/src/routes/(protected)/saved/+page.svelte deleted file mode 100644 index 5a9bfc024..000000000 --- a/apps-archived/news/apps/web/src/routes/(protected)/saved/+page.svelte +++ /dev/null @@ -1,89 +0,0 @@ - - - - Gespeicherte Artikel - News Hub - - -
-
-

Gespeicherte Artikel

-

Deine gespeicherten Artikel zum späteren Lesen

-
- - {#if loading} -
-
-
- {:else if error} -
- {error} -
- {:else if articles.length === 0} -
- - - -

Noch keine Artikel gespeichert

-

- Speichere Artikel mit der Browser-Extension oder aus dem Feed -

-
- {:else} - - {/if} -
diff --git a/apps-archived/news/apps/web/src/routes/(protected)/summaries/+page.svelte b/apps-archived/news/apps/web/src/routes/(protected)/summaries/+page.svelte deleted file mode 100644 index 70db04bab..000000000 --- a/apps-archived/news/apps/web/src/routes/(protected)/summaries/+page.svelte +++ /dev/null @@ -1,72 +0,0 @@ - - - - Zusammenfassungen - News Hub - - -
-
-

Tägliche Zusammenfassungen

-

- Die wichtigsten Nachrichten des Tages kompakt zusammengefasst -

-
- - {#if loading} -
-
-
- {:else if error} -
- {error} -
- {:else if articles.length === 0} -
-

Noch keine Zusammenfassungen vorhanden

-
- {:else} - - {/if} -
diff --git a/apps-archived/news/apps/web/src/routes/+layout.svelte b/apps-archived/news/apps/web/src/routes/+layout.svelte deleted file mode 100644 index f69f9f00d..000000000 --- a/apps-archived/news/apps/web/src/routes/+layout.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - -
- {@render children()} -
diff --git a/apps-archived/news/apps/web/src/routes/+page.svelte b/apps-archived/news/apps/web/src/routes/+page.svelte deleted file mode 100644 index 827ccd0cd..000000000 --- a/apps-archived/news/apps/web/src/routes/+page.svelte +++ /dev/null @@ -1,31 +0,0 @@ - - -
-
- - - -
-
diff --git a/apps-archived/news/apps/web/src/routes/auth/login/+page.svelte b/apps-archived/news/apps/web/src/routes/auth/login/+page.svelte deleted file mode 100644 index 0c656fe78..000000000 --- a/apps-archived/news/apps/web/src/routes/auth/login/+page.svelte +++ /dev/null @@ -1,86 +0,0 @@ - - - - Login - News Hub - - -
-
-
- - - -

News Hub

-

Anmelden

-
- -
- {#if authStore.error} -
- {authStore.error} -
- {/if} - -
- - -
- -
- - -
- - -
- -

- Noch kein Konto? - Registrieren -

-
-
diff --git a/apps-archived/news/apps/web/src/routes/auth/register/+page.svelte b/apps-archived/news/apps/web/src/routes/auth/register/+page.svelte deleted file mode 100644 index 3298dcff4..000000000 --- a/apps-archived/news/apps/web/src/routes/auth/register/+page.svelte +++ /dev/null @@ -1,99 +0,0 @@ - - - - Registrieren - News Hub - - -
-
-
- - - -

News Hub

-

Konto erstellen

-
- -
- {#if authStore.error} -
- {authStore.error} -
- {/if} - -
- - -
- -
- - -
- -
- - -
- - -
- -

- Bereits ein Konto? - Anmelden -

-
-
diff --git a/apps-archived/news/apps/web/vite.config.ts b/apps-archived/news/apps/web/vite.config.ts deleted file mode 100644 index 51bc7e814..000000000 --- a/apps-archived/news/apps/web/vite.config.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - plugins: [sveltekit()], - ssr: { - noExternal: [ - 'marked', - '@manacore/shared-theme', - '@manacore/shared-auth-ui', - '@manacore/shared-branding', - '@manacore/shared-ui', - '@manacore/shared-theme-ui', - ], - }, - optimizeDeps: { - exclude: [ - '@manacore/shared-theme', - '@manacore/shared-auth-ui', - '@manacore/shared-branding', - '@manacore/shared-ui', - '@manacore/shared-theme-ui', - ], - }, -}); diff --git a/apps-archived/news/docker/docker-compose.yml b/apps-archived/news/docker/docker-compose.yml deleted file mode 100644 index 5fcabe9c8..000000000 --- a/apps-archived/news/docker/docker-compose.yml +++ /dev/null @@ -1,36 +0,0 @@ - -services: - postgres: - image: postgres:16-alpine - container_name: news-hub-db - restart: unless-stopped - environment: - POSTGRES_USER: news - POSTGRES_PASSWORD: news_dev_password - POSTGRES_DB: news_hub - ports: - - "5434:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - - ./init.sql:/docker-entrypoint-initdb.d/init.sql - healthcheck: - test: ["CMD-SHELL", "pg_isready -U news -d news_hub"] - interval: 5s - timeout: 5s - retries: 5 - - pgadmin: - image: dpage/pgadmin4:latest - container_name: news-hub-pgadmin - restart: unless-stopped - environment: - PGADMIN_DEFAULT_EMAIL: admin@local.dev - PGADMIN_DEFAULT_PASSWORD: admin - ports: - - "5050:80" - depends_on: - postgres: - condition: service_healthy - -volumes: - postgres_data: diff --git a/apps-archived/news/docker/init.sql b/apps-archived/news/docker/init.sql deleted file mode 100644 index 68b4117ef..000000000 --- a/apps-archived/news/docker/init.sql +++ /dev/null @@ -1,6 +0,0 @@ --- Extensions -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -CREATE EXTENSION IF NOT EXISTS "pg_trgm"; - --- Grants -GRANT ALL PRIVILEGES ON DATABASE news_hub TO news; diff --git a/apps-archived/news/packages/browser-extension/README.md b/apps-archived/news/packages/browser-extension/README.md deleted file mode 100644 index ff4a7b482..000000000 --- a/apps-archived/news/packages/browser-extension/README.md +++ /dev/null @@ -1,133 +0,0 @@ -# Kokon Browser Extension - -Eine Chrome/Firefox Browser-Erweiterung für die Kokon Read-Later App. - -## Features - -- **Ein-Klick Speichern**: Speichere jeden Artikel mit einem Klick -- **Automatische Content-Extraktion**: Nutzt die gleiche Mozilla Readability Engine wie die App -- **Session-Synchronisation**: Automatische Anmeldeerkennung mit der Web-App -- **Elegantes Design**: Moderne, responsive Benutzeroberfläche -- **Fehlerbehandlung**: Intelligente Fehlerbehandlung und Benutzerführung - -## Installation (Development) - -### Chrome/Edge - -1. Öffne `chrome://extensions/` -2. Aktiviere "Entwicklermodus" (Developer mode) -3. Klicke "Ungepackte Erweiterung laden" (Load unpacked) -4. Wähle den `browser-extension` Ordner aus - -### Firefox - -1. Öffne `about:debugging` -2. Klicke "Dieses Firefox" (This Firefox) -3. Klicke "Temporäres Add-on laden" (Load Temporary Add-on) -4. Wähle die `manifest.json` Datei aus - -## Verwendung - -1. **Erste Einrichtung**: - - Installiere die Erweiterung - - Logge dich in der Kokon Web-App ein (wird automatisch geöffnet) - -2. **Artikel speichern**: - - Navigiere zu einem beliebigen Artikel im Web - - Klicke auf das Kokon-Symbol in der Browser-Toolbar - - Klicke "Save Article" - - Der Artikel wird automatisch verarbeitet und in deiner Kokon-Liste gespeichert - -## Technische Details - -### Architektur - -- **Manifest V3**: Moderne Chrome Extension API -- **Service Worker**: Background-Verarbeitung für Session-Management -- **Popup Interface**: Elegant gestaltetes Popup mit Echtzeit-Feedback -- **Chrome Storage API**: Synchronisation mit Web-App-Sessions - -### Sicherheit - -- **Minimale Berechtigungen**: Nur `activeTab` und `storage` -- **HTTPS Only**: Sichere Kommunikation mit Supabase -- **Token-basierte Auth**: Nutzt bestehende Supabase-Session -- **Domain-Validierung**: Verhindert Speichern von Browser-internen Seiten - -### Integration - -- Nutzt die gleiche `save-article` Edge Function wie die App -- Teilt sich die Session mit der Web-App über Chrome Storage -- Automatische Token-Erneuerung und Logout-Erkennung - -## Datei-Struktur - -``` -browser-extension/ -├── manifest.json # Extension-Konfiguration (Manifest V3) -├── popup.html # Popup-Interface HTML -├── popup.js # Popup-Logik und API-Calls -├── background.js # Service Worker für Background-Tasks -├── icons/ # Extension-Icons (TODO: Icons hinzufügen) -│ ├── icon-16.png -│ ├── icon-32.png -│ ├── icon-48.png -│ └── icon-128.png -└── README.md # Diese Datei -``` - -## TODO: Icons - -Die Extension benötigt noch Icons in verschiedenen Größen: - -- 16x16px (Toolbar) -- 32x32px (Extension-Management) -- 48x48px (Extension-Management) -- 128x128px (Chrome Web Store) - -Icons sollten das Kokon-Logo (🥥) oder ein ähnliches Design verwenden. - -## Chrome Web Store Deployment - -Für die Veröffentlichung im Chrome Web Store: - -1. **Icons hinzufügen** (siehe TODO oben) -2. **Version bumpen** in `manifest.json` -3. **Extension packen**: - ```bash - zip -r kokon-extension.zip browser-extension/ - ``` -4. **Chrome Developer Dashboard**: Upload auf [Chrome Web Store Developer Dashboard](https://chrome.google.com/webstore/devconsole) - -## Firefox Add-ons Deployment - -Für Mozilla Add-ons: - -1. **Firefox-spezifische Anpassungen** (falls nötig) -2. **Signierung** über [Mozilla Add-on Developer Hub](https://addons.mozilla.org/developers/) - -## Entwicklung - -### Testing - -1. Lade die Extension im Entwicklermodus -2. Öffne eine beliebige Webseite -3. Teste das Popup und die Save-Funktionalität -4. Überprüfe die Browser-Konsole für Fehler - -### Debugging - -- **Popup debuggen**: Rechtsklick auf Extension-Icon → "Inspect popup" -- **Background Script**: In `chrome://extensions/` → "Inspect views: background page" -- **Storage prüfen**: Chrome DevTools → Application → Storage → Extension - -## Kompatibilität - -- **Chrome**: Version 88+ (Manifest V3 Support) -- **Edge**: Version 88+ (Chromium-basiert) -- **Firefox**: Version 109+ (Manifest V3 Support) -- **Safari**: Benötigt Anpassungen für Safari Web Extensions - -## Lizenz - -Teil des Kokon-Projekts - siehe Haupt-Repository für Lizenzdetails. diff --git a/apps-archived/news/packages/browser-extension/background.js b/apps-archived/news/packages/browser-extension/background.js deleted file mode 100644 index e40d91351..000000000 --- a/apps-archived/news/packages/browser-extension/background.js +++ /dev/null @@ -1,64 +0,0 @@ -// Background service worker for Kokon Browser Extension - -// Installation handler -chrome.runtime.onInstalled.addListener((details) => { - if (details.reason === 'install') { - console.log('Kokon extension installed'); - - // Optionally open the web app on first install - chrome.tabs.create({ - url: 'http://localhost:8081', // Local Expo web development server - }); - } -}); - -// Handle extension icon click (this is mainly handled by the popup, but kept for completeness) -chrome.action.onClicked.addListener((tab) => { - // This won't fire if popup.html is defined in manifest, but keeping for fallback - console.log('Extension icon clicked for tab:', tab.url); -}); - -// Listen for messages from content scripts (if needed in the future) -chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - console.log('Background received message:', request); - - // Handle any background tasks here - if (request.action === 'saveArticle') { - // This could be used for context menu integration in the future - console.log('Save article request for:', request.url); - } - - return true; // Keep message channel open for async response -}); - -// Sync storage with web app (for session management) -chrome.storage.onChanged.addListener((changes, areaName) => { - if (areaName === 'local') { - console.log('Storage changed:', changes); - - // Monitor auth state changes - if (changes['supabase.auth.token']) { - const newToken = changes['supabase.auth.token'].newValue; - if (newToken) { - console.log('User logged in'); - // Could update badge or perform other actions - } else { - console.log('User logged out'); - } - } - } -}); - -// Handle context menu (optional future feature) -// chrome.contextMenus.create({ -// id: "saveToKokon", -// title: "Save to Kokon", -// contexts: ["page", "link"] -// }); - -// chrome.contextMenus.onClicked.addListener((info, tab) => { -// if (info.menuItemId === "saveToKokon") { -// const url = info.linkUrl || tab.url; -// // Handle saving the article -// } -// }); diff --git a/apps-archived/news/packages/browser-extension/content.js b/apps-archived/news/packages/browser-extension/content.js deleted file mode 100644 index 2c819975e..000000000 --- a/apps-archived/news/packages/browser-extension/content.js +++ /dev/null @@ -1,91 +0,0 @@ -// Content script to sync localStorage with Chrome storage -console.log('🥥 Kokon content script loaded on:', window.location.href); - -// Function to sync localStorage to Chrome storage -function syncToChrome(key, value) { - if (chrome && chrome.storage) { - chrome.storage.local - .set({ [key]: value }) - .then(() => { - console.log('Content script: Successfully synced to Chrome storage:', key); - }) - .catch((error) => { - console.error('Content script: Failed to sync to Chrome storage:', error); - }); - } -} - -// Function to sync removal from localStorage to Chrome storage -function removeFromChrome(key) { - if (chrome && chrome.storage) { - chrome.storage.local - .remove([key]) - .then(() => { - console.log('Content script: Successfully removed from Chrome storage:', key); - }) - .catch((error) => { - console.error('Content script: Failed to remove from Chrome storage:', error); - }); - } -} - -// Listen for localStorage changes and sync to Chrome storage -function setupStorageSync() { - console.log('🥥 Setting up storage sync...'); - - // The actual Supabase auth token key - const SUPABASE_AUTH_KEY = 'sb-hepsjdbvpkumaoabbycd-auth-token'; - - // Override localStorage.setItem to sync - const originalSetItem = localStorage.setItem; - localStorage.setItem = function (key, value) { - console.log('🥥 localStorage.setItem called:', key); - originalSetItem.call(this, key, value); - if (key === SUPABASE_AUTH_KEY) { - console.log('🥥 Detected supabase token change, syncing...'); - // Store with standardized key for extension - syncToChrome('supabase.auth.token', value); - } - }; - - // Override localStorage.removeItem to sync - const originalRemoveItem = localStorage.removeItem; - localStorage.removeItem = function (key) { - console.log('🥥 localStorage.removeItem called:', key); - originalRemoveItem.call(this, key); - if (key === SUPABASE_AUTH_KEY) { - console.log('🥥 Detected supabase token removal, syncing...'); - removeFromChrome('supabase.auth.token'); - } - }; - - // Check for existing token on page load - const existingToken = localStorage.getItem(SUPABASE_AUTH_KEY); - console.log('🥥 Checking for existing token:', existingToken ? 'Found' : 'Not found'); - if (existingToken) { - console.log('🥥 Found existing token, syncing...'); - syncToChrome('supabase.auth.token', existingToken); - } - - // Also check all localStorage keys - console.log('🥥 All localStorage keys:', Object.keys(localStorage)); -} - -// Set up the sync when the page loads -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', setupStorageSync); -} else { - setupStorageSync(); -} - -// Also listen for storage events (in case other tabs make changes) -window.addEventListener('storage', (e) => { - if (e.key === 'sb-hepsjdbvpkumaoabbycd-auth-token') { - console.log('🥥 Storage event detected for supabase token'); - if (e.newValue) { - syncToChrome('supabase.auth.token', e.newValue); - } else { - removeFromChrome('supabase.auth.token'); - } - } -}); diff --git a/apps-archived/news/packages/browser-extension/debug.html b/apps-archived/news/packages/browser-extension/debug.html deleted file mode 100644 index a058b9eaa..000000000 --- a/apps-archived/news/packages/browser-extension/debug.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - Debug Extension Storage - - - -

Kokon Extension Debug

- -
- - - -
- -
-

Chrome Storage Contents:

-
Loading...
-
- - - - \ No newline at end of file diff --git a/apps-archived/news/packages/browser-extension/debug.js b/apps-archived/news/packages/browser-extension/debug.js deleted file mode 100644 index 2f0c4031c..000000000 --- a/apps-archived/news/packages/browser-extension/debug.js +++ /dev/null @@ -1,51 +0,0 @@ -// Debug script for Extension Storage - -async function checkStorage() { - try { - const result = await chrome.storage.local.get(null); - document.getElementById('storageContent').textContent = JSON.stringify(result, null, 2); - console.log('Chrome Storage contents:', result); - } catch (error) { - document.getElementById('storageContent').textContent = 'Error: ' + error.message; - console.error('Error checking storage:', error); - } -} - -async function clearStorage() { - try { - await chrome.storage.local.clear(); - document.getElementById('storageContent').textContent = 'Storage cleared'; - console.log('Chrome Storage cleared'); - } catch (error) { - console.error('Error clearing storage:', error); - } -} - -async function setTestData() { - try { - const testSession = { - access_token: 'test-token', - expires_at: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now - refresh_token: 'test-refresh', - }; - - await chrome.storage.local.set({ - 'supabase.auth.token': JSON.stringify(testSession), - }); - - document.getElementById('storageContent').textContent = 'Test data set'; - console.log('Test data set in Chrome Storage'); - } catch (error) { - console.error('Error setting test data:', error); - } -} - -// Set up event listeners when DOM is loaded -document.addEventListener('DOMContentLoaded', () => { - document.getElementById('checkBtn').addEventListener('click', checkStorage); - document.getElementById('clearBtn').addEventListener('click', clearStorage); - document.getElementById('testBtn').addEventListener('click', setTestData); - - // Auto-check on load - checkStorage(); -}); diff --git a/apps-archived/news/packages/browser-extension/manifest.json b/apps-archived/news/packages/browser-extension/manifest.json deleted file mode 100644 index 686e5f6ee..000000000 --- a/apps-archived/news/packages/browser-extension/manifest.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "manifest_version": 3, - "name": "News Hub - Save Article", - "version": "1.0.0", - "description": "Save articles from any website to your News Hub library", - "permissions": ["activeTab", "storage"], - "host_permissions": ["http://localhost:3000/*"], - "action": { - "default_popup": "popup.html", - "default_title": "Save to News Hub" - }, - "background": { - "service_worker": "background.js" - }, - "content_scripts": [ - { - "matches": ["http://localhost:*/*"], - "js": ["content.js"], - "run_at": "document_start" - } - ], - "content_security_policy": { - "extension_pages": "script-src 'self'; object-src 'self'" - }, - "icons": { - "16": "icons/icon16.png", - "48": "icons/icon48.png", - "128": "icons/icon128.png" - } -} diff --git a/apps-archived/news/packages/browser-extension/popup.html b/apps-archived/news/packages/browser-extension/popup.html deleted file mode 100644 index 340df07b7..000000000 --- a/apps-archived/news/packages/browser-extension/popup.html +++ /dev/null @@ -1,165 +0,0 @@ - - - - - - News Hub - Save Article - - - -
- -
Save Article to Library
- - - -
-
Loading page info...
-
-
- - - -
-
- - - - diff --git a/apps-archived/news/packages/browser-extension/popup.js b/apps-archived/news/packages/browser-extension/popup.js deleted file mode 100644 index 408c266ad..000000000 --- a/apps-archived/news/packages/browser-extension/popup.js +++ /dev/null @@ -1,178 +0,0 @@ -// Browser Extension Popup Script for News Hub -document.addEventListener('DOMContentLoaded', async () => { - const pageTitle = document.getElementById('pageTitle'); - const pageUrl = document.getElementById('pageUrl'); - const saveButton = document.getElementById('saveButton'); - const buttonText = document.getElementById('buttonText'); - const status = document.getElementById('status'); - const loginNotice = document.getElementById('loginNotice'); - const loginLink = document.getElementById('loginLink'); - - // API Configuration - const API_URL = 'http://localhost:3000'; - const APP_URL = 'http://localhost:8081'; - - let currentTab = null; - let authToken = null; - - // Get current tab info - try { - const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); - currentTab = tab; - - pageTitle.textContent = tab.title || 'Untitled Page'; - pageUrl.textContent = tab.url; - } catch (error) { - console.error('Error getting tab info:', error); - pageTitle.textContent = 'Error loading page info'; - status.textContent = 'Failed to get page information'; - status.className = 'status error'; - return; - } - - // Check if user is logged in by looking for stored token in Chrome storage - try { - const result = await chrome.storage.local.get(['news_hub_auth_token']); - authToken = result['news_hub_auth_token']; - - console.log('Checking Chrome storage for token...', authToken ? 'Found' : 'Not found'); - - if (authToken) { - // Verify token is still valid by calling session endpoint - try { - const response = await fetch(`${API_URL}/auth/session`, { - headers: { - Authorization: `Bearer ${authToken}`, - }, - }); - - if (response.ok) { - saveButton.disabled = false; - loginNotice.style.display = 'none'; - // Auto-save article immediately - saveArticle(); - } else { - // Token is invalid - await chrome.storage.local.remove(['news_hub_auth_token']); - authToken = null; - showLoginNotice(); - } - } catch (error) { - console.error('Error verifying token:', error); - showLoginNotice(); - } - } else { - showLoginNotice(); - } - } catch (error) { - console.error('Error checking login status:', error); - showLoginNotice(); - } - - function showLoginNotice() { - loginNotice.style.display = 'block'; - saveButton.disabled = true; - status.textContent = 'Please log in to News Hub first'; - status.className = 'status error'; - } - - // Handle login link click - loginLink.addEventListener('click', (e) => { - e.preventDefault(); - chrome.tabs.create({ url: APP_URL }); - window.close(); - }); - - // Save article function - async function saveArticle() { - if (!currentTab || !authToken) { - status.textContent = 'Please log in first'; - status.className = 'status error'; - return; - } - - // Validate URL - const url = currentTab.url; - if ( - !url || - url.startsWith('chrome://') || - url.startsWith('chrome-extension://') || - url.startsWith('about:') - ) { - status.textContent = 'Cannot save this type of page'; - status.className = 'status error'; - return; - } - - // Show loading state - saveButton.disabled = true; - buttonText.innerHTML = 'Saving...'; - status.textContent = 'Saving article...'; - status.className = 'status'; - - try { - const response = await fetch(`${API_URL}/extract/save`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${authToken}`, - }, - body: JSON.stringify({ url: url }), - }); - - const result = await response.json(); - - if (response.ok && result.success) { - status.textContent = 'Article saved!'; - status.className = 'status success'; - - // Show success for a moment, then close - setTimeout(() => { - window.close(); - }, 1500); - } else { - throw new Error(result.message || 'Failed to save article'); - } - } catch (error) { - console.error('Error saving article:', error); - - let errorMessage = 'Failed to save article'; - if (error.message.includes('fetch') || error.message.includes('NetworkError')) { - errorMessage = 'Network error - is the API running?'; - } else if (error.message.includes('401') || error.message.includes('Unauthorized')) { - errorMessage = 'Session expired - please log in again'; - showLoginNotice(); - } else if (error.message) { - errorMessage = error.message; - } - - status.textContent = errorMessage; - status.className = 'status error'; - } finally { - // Reset button state - saveButton.disabled = authToken ? false : true; - buttonText.textContent = 'Try Again'; - } - } - - // Handle save button click (manual save if auto-save failed) - saveButton.addEventListener('click', saveArticle); - - // Listen for storage changes (if user logs in/out in another tab) - chrome.storage.onChanged.addListener((changes, areaName) => { - if (areaName === 'local' && changes['news_hub_auth_token']) { - const newValue = changes['news_hub_auth_token'].newValue; - - if (newValue) { - authToken = newValue; - saveButton.disabled = false; - loginNotice.style.display = 'none'; - status.textContent = ''; - status.className = 'status'; - } else { - authToken = null; - showLoginNotice(); - } - } - }); -}); diff --git a/apps-archived/uload/.dockerignore b/apps-archived/uload/.dockerignore deleted file mode 100644 index e742e486d..000000000 --- a/apps-archived/uload/.dockerignore +++ /dev/null @@ -1,32 +0,0 @@ -node_modules -npm-debug.log -.git -.gitignore -.svelte-kit -build -.env -.env.* -!.env.example -.vscode -.idea -*.md -!README.md -!DEPLOYMENT.md -.DS_Store -Thumbs.db -test-results -e2e -tests -*.test.* -*.spec.* -playwright.config.* -vitest.config.* -docker-compose.yml -Dockerfile -.dockerignore -backend -pb_hooks -pb_migrations -pocketbase -mcp-servers -*.sql.gz \ No newline at end of file diff --git a/apps-archived/uload/.env.example b/apps-archived/uload/.env.example deleted file mode 100644 index 2f7f0c241..000000000 --- a/apps-archived/uload/.env.example +++ /dev/null @@ -1,36 +0,0 @@ -# SvelteKit Configuration -PORT=3000 -ORIGIN=https://your-domain.com -NODE_ENV=production -PUBLIC_APP_URL=https://ulo.ad - -# Database (PostgreSQL) -# Development: Use local Docker container -DATABASE_URL=postgresql://uload:uload_dev_password_123@localhost:5432/uload_dev -# Production: Use your Coolify/Hetzner PostgreSQL container -# DATABASE_URL=postgresql://uload:your_password@uload-db-prod:5432/uload_prod - -# File Storage (Cloudflare R2) -R2_ACCOUNT_ID=your_cloudflare_account_id -R2_ACCESS_KEY_ID=your_r2_access_key -R2_SECRET_ACCESS_KEY=your_r2_secret_key -R2_BUCKET_AVATARS=uload-avatars -R2_BUCKET_QR=uload-qr-codes -R2_PUBLIC_URL=https://files.ulo.ad - -# Email (Resend) -RESEND_API_KEY=re_your_resend_api_key -RESEND_FROM_EMAIL=noreply@ulo.ad - -# Umami Analytics (optional) -PUBLIC_UMAMI_URL=https://your-umami-instance.com -PUBLIC_UMAMI_WEBSITE_ID=your-website-id - -# External Auth (to be implemented) -# AUTH_PROVIDER_CLIENT_ID= -# AUTH_PROVIDER_CLIENT_SECRET= - -# Coolify specific (if needed) -# These will be set automatically by Coolify -# COOLIFY_URL= -# COOLIFY_TOKEN= diff --git a/apps-archived/uload/.env.production.example b/apps-archived/uload/.env.production.example deleted file mode 100644 index 697f30661..000000000 --- a/apps-archived/uload/.env.production.example +++ /dev/null @@ -1,20 +0,0 @@ -# SvelteKit Configuration -NODE_ENV=production -PORT=3000 -ORIGIN=https://your-domain.com -PUBLIC_POCKETBASE_URL=https://your-domain.com/api - -# PocketBase Admin Credentials -# These will be used to create the admin on first startup -POCKETBASE_ADMIN_EMAIL=till.schneider@memoro.ai -POCKETBASE_ADMIN_PASSWORD=p0ck3tRA1N - -# Umami Analytics -# Replace with your actual Umami instance and website ID -PUBLIC_UMAMI_URL=https://your-umami-instance.com -PUBLIC_UMAMI_WEBSITE_ID=your-website-id - -# Optional: Additional Configuration -# BODY_SIZE_LIMIT=512kb -# PROTOCOL_HEADER=x-forwarded-proto -# HOST_HEADER=x-forwarded-host \ No newline at end of file diff --git a/apps-archived/uload/.env.stripe.example b/apps-archived/uload/.env.stripe.example deleted file mode 100644 index e3682dd3e..000000000 --- a/apps-archived/uload/.env.stripe.example +++ /dev/null @@ -1,17 +0,0 @@ -# Stripe Configuration -# Copy this to .env.local or add to your .env file - -# Stripe API Keys (get from https://dashboard.stripe.com/test/apikeys) -PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_YOUR_PUBLISHABLE_KEY_HERE -STRIPE_SECRET_KEY=sk_test_YOUR_SECRET_KEY_HERE - -# Stripe Product & Price IDs (will be created automatically by Claude) -STRIPE_PRODUCT_PRO=prod_xxx -STRIPE_PRICE_MONTHLY=price_xxx -STRIPE_PRICE_YEARLY=price_xxx - -# Stripe Webhook Secret (from webhook endpoint in dashboard) -STRIPE_WEBHOOK_SECRET=whsec_xxx - -# App URL for redirects -PUBLIC_APP_URL=http://localhost:5173 # Production: https://ulo.ad \ No newline at end of file diff --git a/apps-archived/uload/.gitignore b/apps-archived/uload/.gitignore deleted file mode 100644 index cdb4c4d46..000000000 --- a/apps-archived/uload/.gitignore +++ /dev/null @@ -1,43 +0,0 @@ -# Dependencies -node_modules - -# Test results -test-results - -# Build output -.output -.vercel -.netlify -.wrangler -.svelte-kit -build -dist - -# OS -.DS_Store -Thumbs.db - -# Environment files -.env -.env.* -!.env.example -!.env.*.example - -# Vite -vite.config.js.timestamp-* -vite.config.ts.timestamp-* - -# MCP Configuration with credentials -.mcp.json -.mcp.json-dev - -# PocketBase -backend/pocketbase -backend/pb_data/ -*.log - -# IDE -.idea -.vscode -*.swp -*.swo diff --git a/apps-archived/uload/CLAUDE.md b/apps-archived/uload/CLAUDE.md deleted file mode 100644 index 578aecd42..000000000 --- a/apps-archived/uload/CLAUDE.md +++ /dev/null @@ -1,132 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -uLoad is a URL shortener and link management platform built with SvelteKit and PocketBase. - -**Live:** https://ulo.ad - -## Project Structure - -``` -uload/ -├── apps/ -│ └── web/ # SvelteKit web application -│ ├── src/ # Source code -│ │ ├── routes/ # SvelteKit pages -│ │ └── lib/ # Components, services, utilities -│ ├── static/ # Static assets -│ └── e2e/ # End-to-end tests -├── backend/ # PocketBase configuration -│ ├── pb_migrations/ # Database migrations -│ └── pb_schema.json # Schema definition -├── docs/ # Documentation -├── scripts/ # Utility scripts -└── CLAUDE.md -``` - -## Commands - -All commands should be run from `uload/apps/web/`: - -### Development - -```bash -pnpm run dev # Start development server (http://localhost:5173) -pnpm run preview # Preview production build locally -``` - -### Build & Deploy - -```bash -pnpm run build # Create production build -``` - -### Code Quality - -```bash -pnpm run format # Auto-format code with Prettier -pnpm run lint # Run ESLint and Prettier checks -pnpm run check # Run Svelte type checking -``` - -### Testing - -```bash -pnpm run test # Run all tests (unit + e2e) -pnpm run test:unit # Run unit tests with Vitest -pnpm run test:e2e # Run end-to-end tests with Playwright -``` - -### Database - -```bash -pnpm run db:generate # Generate Drizzle migrations -pnpm run db:migrate # Run migrations -pnpm run db:push # Push schema changes -pnpm run db:studio # Open Drizzle Studio -``` - -## Technology Stack - -- **Framework**: SvelteKit v2.22 with Svelte 5.0 -- **Backend**: PocketBase (embedded SQLite) -- **Database**: PostgreSQL via Drizzle ORM + Redis for caching -- **Styling**: Tailwind CSS v4.0 -- **Testing**: Vitest + Playwright -- **Payments**: Stripe -- **Email**: Resend -- **Storage**: Cloudflare R2 - -## Key Patterns - -### Svelte 5 Runes Mode - -- **NEVER use `$:` reactive statements** - use `$derived` instead -- **NEVER use `let` for reactive values** - use `$state` for reactive state -- **For side effects** - use `$effect` instead of `$:` statements - -```typescript -// ✅ CORRECT - Svelte 5 runes -let headerModule = $derived(card.config.modules?.find((m) => m.type === 'header')); -let count = $state(0); - -$effect(() => { - console.log('Count changed:', count); -}); -``` - -### PocketBase Usage - -In server-side code (`+page.server.ts`, `+server.ts`): - -- **ALWAYS use `locals.pb`** from the request context -- The imported `pb` is for client-side only - -```typescript -// Server-side -export const load: PageServerLoad = async ({ locals }) => { - const items = await locals.pb.collection('items').getList(); -}; - -// Client-side -import { pb } from '$lib/pocketbase'; -``` - -## Environment Configuration - -Copy `.env.example` to `.env` and configure: - -- `DATABASE_URL` - PostgreSQL connection string -- `R2_*` - Cloudflare R2 storage credentials -- `RESEND_API_KEY` - Email service -- `STRIPE_*` - Payment processing (see `.env.stripe.example`) - -## Code Style - -- Tabs for indentation -- Single quotes for strings -- 100 character line width -- Prettier auto-sorts Tailwind classes diff --git a/apps-archived/uload/Dockerfile b/apps-archived/uload/Dockerfile deleted file mode 100644 index f013e41c9..000000000 --- a/apps-archived/uload/Dockerfile +++ /dev/null @@ -1,73 +0,0 @@ -# ============================================================================= -# uload Web Application Dockerfile -# Multi-stage build for production deployment with Coolify -# -# IMPORTANT: This Dockerfile must be built from the MONOREPO ROOT, not from uload/ -# docker build -f uload/Dockerfile -t uload-web . -# -# ============================================================================= - -# ----------------------------------------------------------------------------- -# Stage 1: Builder -# ----------------------------------------------------------------------------- -FROM node:20-alpine AS builder - -# Install pnpm -RUN corepack enable && corepack prepare pnpm@9.15.0 --activate - -WORKDIR /app - -# Copy workspace configuration -COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ - -# Copy the uload web app -COPY uload/apps/web/ ./uload/apps/web/ - -# Copy required shared packages -COPY packages/shared-auth-ui/ ./packages/shared-auth-ui/ -COPY packages/shared-branding/ ./packages/shared-branding/ - -# Install dependencies with flat structure for Docker compatibility -RUN pnpm install --filter @uload/web... --shamefully-hoist - -# Build the app -WORKDIR /app/uload/apps/web - -# Note: RESEND_API_KEY is needed at build time for SvelteKit prerendering -ENV RESEND_API_KEY=build_placeholder -RUN pnpm build - -# ----------------------------------------------------------------------------- -# Stage 2: Production Runner -# ----------------------------------------------------------------------------- -FROM node:20-alpine AS runner - -# Security: Run as non-root user -RUN addgroup --system --gid 1001 nodejs && \ - adduser --system --uid 1001 sveltekit - -WORKDIR /app - -# Copy built app from the correct path -COPY --from=builder --chown=sveltekit:nodejs /app/uload/apps/web/build ./build -COPY --from=builder --chown=sveltekit:nodejs /app/uload/apps/web/package.json ./ - -# Copy hoisted node_modules from root (contains all deps with flat structure) -COPY --from=builder --chown=sveltekit:nodejs /app/node_modules ./node_modules - -# Environment -ENV NODE_ENV=production -ENV PORT=3000 -ENV HOST=0.0.0.0 - -# Health check -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1 - -# Switch to non-root user -USER sveltekit - -EXPOSE 3000 - -# Start Node server -CMD ["node", "build"] diff --git a/apps-archived/uload/README.md b/apps-archived/uload/README.md deleted file mode 100644 index bde7ba971..000000000 --- a/apps-archived/uload/README.md +++ /dev/null @@ -1,151 +0,0 @@ -# uLoad - URL Shortener & Link Management - -A modern URL shortener and link management platform built with SvelteKit and PocketBase. - -## 🚀 Production - -**Live:** https://ulo.ad -**Admin:** https://ulo.ad/_/ - -## 🛠 Tech Stack - -- **Frontend:** SvelteKit 2.0 + Svelte 5 -- **Backend:** PocketBase (embedded) -- **Styling:** Tailwind CSS 4.0 -- **Deployment:** Docker + Coolify on Hetzner VPS -- **Database:** SQLite (via PocketBase) - -## 📦 Features - -- URL shortening with custom codes -- QR code generation -- Click analytics -- User profiles (e.g., ulo.ad/p/username) -- Link management dashboard -- Real-time statistics - -## 🏃 Development - -```bash -# Install dependencies -npm install --legacy-peer-deps - -# Start development server -npm run dev - -# Start with PocketBase backend -npm run dev:all - -# Run tests -npm run test - -# Type checking -npm run check -``` - -## 🐳 Docker Deployment - -```bash -# Build and run locally -docker-compose up --build - -# Access at: -# Frontend: http://localhost:3000 -# PocketBase: http://localhost:8090 -``` - -## 📝 Documentation - -- [Deployment Guide](./DEPLOYMENT.md) - Complete Docker Compose deployment instructions -- [Lessons Learned](./DEPLOYMENT_LESSONS_LEARNED.md) - Troubleshooting and insights -- [Domain Setup](./DOMAIN_SETUP_ULO_AD.md) - ulo.ad configuration -- [Coolify Setup](./COOLIFY_SETUP.md) - Detailed Coolify configuration - -## 🔧 Environment Variables - -```bash -NODE_ENV=production -PORT=3000 -ORIGIN=https://ulo.ad -PUBLIC_POCKETBASE_URL=https://ulo.ad/api -POCKETBASE_ADMIN_EMAIL=admin@example.com -POCKETBASE_ADMIN_PASSWORD=secure_password -``` - -See `.env.example` for all configuration options. - -## 📂 Project Structure - -``` -uload/ -├── src/ # SvelteKit application -│ ├── routes/ # Pages and API routes -│ ├── lib/ # Components and utilities -│ └── app.html # HTML template -├── backend/ # PocketBase configuration -│ ├── pb_schema.json # Database schema -│ └── init-pocketbase.sh # Setup script -├── build/ # Production build output -├── static/ # Static assets -├── Dockerfile # Multi-stage Docker build -├── docker-compose.yml # Local development -├── supervisord.conf # Process management -└── CLAUDE.md # AI assistant context -``` - -## 🚢 Deployment - -The application is deployed on Hetzner VPS using Coolify with automatic deployments on push to main branch. - -```bash -# Commit and push to deploy -git add . -git commit -m "Update" -git push origin main -# Coolify automatically deploys -``` - -### Manual Deployment Steps: - -1. Set DNS A record to `91.99.221.179` -2. Add domain in Coolify -3. Update environment variables -4. Enable SSL certificate -5. Deploy application - -## 📊 Monitoring - -- **Health Check:** https://ulo.ad/health -- **Admin Panel:** https://ulo.ad/_/ -- **Server:** Hetzner CX21 (2 vCPU, 4GB RAM) -- **Uptime:** 99.9% SLA - -## 🔐 Security - -- HTTPS enforced -- Environment-based configuration -- Secure admin authentication -- Rate limiting on API endpoints -- Regular security updates - -## 🤝 Contributing - -1. Fork the repository -2. Create your feature branch (`git checkout -b feature/amazing-feature`) -3. Commit your changes (`git commit -m 'Add amazing feature'`) -4. Push to the branch (`git push origin feature/amazing-feature`) -5. Open a Pull Request - -## 🐛 Troubleshooting - -Common issues and solutions are documented in [DEPLOYMENT_LESSONS_LEARNED.md](./DEPLOYMENT_LESSONS_LEARNED.md) - -For support, check: - -- Application logs in Coolify -- Health endpoint status -- PocketBase admin panel - -## 📄 License - -Private - Memoro AI © 2024 diff --git a/apps-archived/uload/apps/backend/.env.example b/apps-archived/uload/apps/backend/.env.example deleted file mode 100644 index baa0f2313..000000000 --- a/apps-archived/uload/apps/backend/.env.example +++ /dev/null @@ -1,22 +0,0 @@ -# Server -NODE_ENV=development -PORT=3003 - -# Database -DATABASE_URL=postgresql://postgres:postgres@localhost:5434/uload - -# Redis (for caching) -REDIS_HOST=localhost -REDIS_PORT=6379 -REDIS_PASSWORD= - -# Mana Core Auth -MANA_SERVICE_URL=https://mana-core-middleware-111768794939.europe-west3.run.app -APP_ID=your-uload-app-id -MANA_SERVICE_KEY= - -# Frontend URL (for CORS) -FRONTEND_URL=http://localhost:5173 - -# Short URL base (for generating short links) -SHORT_URL_BASE=https://ulo.ad diff --git a/apps-archived/uload/apps/backend/Dockerfile b/apps-archived/uload/apps/backend/Dockerfile deleted file mode 100644 index 73d79f1c4..000000000 --- a/apps-archived/uload/apps/backend/Dockerfile +++ /dev/null @@ -1,65 +0,0 @@ -# Build stage -FROM node:20-alpine AS builder - -# Install pnpm -RUN corepack enable && corepack prepare pnpm@9.15.0 --activate - -WORKDIR /app - -# Copy package files -COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ - -# Copy workspace packages -COPY packages/uload-database ./packages/uload-database - -# Copy backend source -COPY uload/apps/backend ./uload/apps/backend - -# Install dependencies -RUN pnpm install --frozen-lockfile - -# Build the database package first -WORKDIR /app/packages/uload-database -RUN pnpm build - -# Build the backend -WORKDIR /app/uload/apps/backend -RUN pnpm build - -# Production stage -FROM node:20-alpine AS production - -# Install dumb-init for proper signal handling -RUN apk add --no-cache dumb-init - -# Create non-root user -RUN addgroup --system --gid 1001 nodejs && \ - adduser --system --uid 1001 nestjs - -WORKDIR /app - -# Copy built artifacts -COPY --from=builder --chown=nestjs:nodejs /app/uload/apps/backend/dist ./dist -COPY --from=builder --chown=nestjs:nodejs /app/uload/apps/backend/package.json ./ -COPY --from=builder --chown=nestjs:nodejs /app/uload/apps/backend/node_modules ./node_modules - -# Copy database package (needed at runtime) -COPY --from=builder --chown=nestjs:nodejs /app/packages/uload-database/dist ./node_modules/@manacore/uload-database/dist -COPY --from=builder --chown=nestjs:nodejs /app/packages/uload-database/package.json ./node_modules/@manacore/uload-database/ - -USER nestjs - -# Expose port -EXPOSE 3003 - -# Health check -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:3003/health || exit 1 - -# Set environment -ENV NODE_ENV=production -ENV PORT=3003 - -# Start with dumb-init -ENTRYPOINT ["dumb-init", "--"] -CMD ["node", "dist/main"] diff --git a/apps-archived/uload/apps/backend/nest-cli.json b/apps-archived/uload/apps/backend/nest-cli.json deleted file mode 100644 index 95538fb90..000000000 --- a/apps-archived/uload/apps/backend/nest-cli.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/nest-cli", - "collection": "@nestjs/schematics", - "sourceRoot": "src", - "compilerOptions": { - "deleteOutDir": true - } -} diff --git a/apps-archived/uload/apps/backend/package.json b/apps-archived/uload/apps/backend/package.json deleted file mode 100644 index db10ccc65..000000000 --- a/apps-archived/uload/apps/backend/package.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "name": "@uload/backend", - "version": "0.0.1", - "description": "ULOAD URL Shortener Backend", - "private": true, - "license": "UNLICENSED", - "scripts": { - "build": "nest build", - "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", - "start": "nest start", - "start:dev": "nest start --watch", - "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "jest", - "test:watch": "jest --watch", - "test:cov": "jest --coverage", - "test:e2e": "jest --config ./test/jest-e2e.json", - "type-check": "tsc --noEmit" - }, - "dependencies": { - "@manacore/uload-database": "workspace:*", - "@nestjs/axios": "^4.0.1", - "@nestjs/common": "^11.0.1", - "@nestjs/config": "^4.0.2", - "@nestjs/core": "^11.0.1", - "@nestjs/platform-express": "^11.0.1", - "@nestjs/terminus": "^11.0.0", - "axios": "^1.7.2", - "class-transformer": "^0.5.1", - "class-validator": "^0.14.2", - "ioredis": "^5.4.1", - "joi": "^18.0.1", - "nanoid": "^5.0.7", - "nestjs-cls": "^6.0.1", - "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1", - "ua-parser-js": "^2.0.0" - }, - "devDependencies": { - "@nestjs/cli": "^11.0.0", - "@nestjs/schematics": "^11.0.0", - "@nestjs/testing": "^11.0.1", - "@types/express": "^5.0.0", - "@types/jest": "^30.0.0", - "@types/node": "^22.10.7", - "@types/supertest": "^6.0.2", - "@types/ua-parser-js": "^0.7.39", - "jest": "^30.0.0", - "prettier": "^3.4.2", - "source-map-support": "^0.5.21", - "supertest": "^7.0.0", - "ts-jest": "^29.2.5", - "ts-loader": "^9.5.2", - "ts-node": "^10.9.2", - "tsconfig-paths": "^4.2.0", - "typescript": "^5.9.3" - }, - "jest": { - "moduleFileExtensions": [ - "js", - "json", - "ts" - ], - "rootDir": "src", - "testRegex": ".*\\.spec\\.ts$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - }, - "collectCoverageFrom": [ - "**/*.(t|j)s" - ], - "coverageDirectory": "../coverage", - "testEnvironment": "node" - } -} diff --git a/apps-archived/uload/apps/backend/src/app.module.ts b/apps-archived/uload/apps/backend/src/app.module.ts deleted file mode 100644 index dcd4f6fbc..000000000 --- a/apps-archived/uload/apps/backend/src/app.module.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { ClsModule } from 'nestjs-cls'; -import { TerminusModule } from '@nestjs/terminus'; -import { HttpModule } from '@nestjs/axios'; -import { ManaCoreModule } from '@mana-core/nestjs-integration'; - -import { validationSchema } from './config/validation.schema'; -import { DatabaseModule } from './database/database.module'; -import { LinkRepository } from './database/repositories/link.repository'; -import { ClickRepository } from './database/repositories/click.repository'; - -import { HealthController } from './controllers/health.controller'; -import { RedirectController } from './controllers/redirect.controller'; -import { LinksController } from './controllers/links.controller'; -import { AnalyticsController } from './controllers/analytics.controller'; - -import { LinksService } from './services/links.service'; -import { RedirectService } from './services/redirect.service'; -import { AnalyticsService } from './services/analytics.service'; - -@Module({ - imports: [ - // Context-Local Storage for request-scoped data - ClsModule.forRoot({ - global: true, - middleware: { mount: true, generateId: true }, - }), - - // Configuration - ConfigModule.forRoot({ - isGlobal: true, - validationSchema, - validationOptions: { - allowUnknown: true, - abortEarly: false, - }, - ignoreEnvFile: process.env.NODE_ENV === 'production', - }), - - // Mana Core Authentication - ManaCoreModule.forRootAsync({ - imports: [ConfigModule], - useFactory: (configService: ConfigService) => ({ - manaServiceUrl: configService.get('MANA_SERVICE_URL')!, - appId: configService.get('APP_ID')!, - serviceKey: configService.get('MANA_SERVICE_KEY', ''), - debug: configService.get('NODE_ENV') === 'development', - }), - inject: [ConfigService], - }) as any, - - // Health checks - TerminusModule, - HttpModule, - - // Database - DatabaseModule, - ], - controllers: [HealthController, RedirectController, LinksController, AnalyticsController], - providers: [ - // Repositories - LinkRepository, - ClickRepository, - // Services - LinksService, - RedirectService, - AnalyticsService, - ], -}) -export class AppModule implements NestModule { - configure(consumer: MiddlewareConsumer) { - // Add custom middleware here if needed - } -} diff --git a/apps-archived/uload/apps/backend/src/config/validation.schema.ts b/apps-archived/uload/apps/backend/src/config/validation.schema.ts deleted file mode 100644 index 9f16db20e..000000000 --- a/apps-archived/uload/apps/backend/src/config/validation.schema.ts +++ /dev/null @@ -1,26 +0,0 @@ -import * as Joi from 'joi'; - -export const validationSchema = Joi.object({ - // Server - NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'), - PORT: Joi.number().default(3003), - - // Database - DATABASE_URL: Joi.string().uri().required(), - - // Redis - REDIS_HOST: Joi.string().default('localhost'), - REDIS_PORT: Joi.number().default(6379), - REDIS_PASSWORD: Joi.string().allow('').optional(), - - // Mana Core Auth - MANA_SERVICE_URL: Joi.string().uri().required(), - APP_ID: Joi.string().uuid().required(), - MANA_SERVICE_KEY: Joi.string().allow('').optional(), - - // Frontend - FRONTEND_URL: Joi.string().uri().optional(), - - // Short URL - SHORT_URL_BASE: Joi.string().uri().default('https://ulo.ad'), -}); diff --git a/apps-archived/uload/apps/backend/src/controllers/analytics.controller.ts b/apps-archived/uload/apps/backend/src/controllers/analytics.controller.ts deleted file mode 100644 index 49b7ea1eb..000000000 --- a/apps-archived/uload/apps/backend/src/controllers/analytics.controller.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { - Controller, - Get, - Param, - Query, - UseGuards, - NotFoundException, - ForbiddenException, -} from '@nestjs/common'; -import { AuthGuard, CurrentUser } from '@mana-core/nestjs-integration'; -import { AnalyticsService } from '../services/analytics.service'; -import { LinksService } from '../services/links.service'; - -@Controller('api/analytics') -@UseGuards(AuthGuard) -export class AnalyticsController { - constructor( - private readonly analyticsService: AnalyticsService, - private readonly linksService: LinksService - ) {} - - @Get('links/:linkId') - async getLinkAnalytics( - @CurrentUser() user: any, - @Param('linkId') linkId: string, - @Query('from') fromDate?: string, - @Query('to') toDate?: string - ) { - const userId = user.sub; - - // Verify user owns the link - const link = await this.linksService.getLinkById(linkId, userId); - if (!link) { - throw new NotFoundException('Link not found'); - } - - const stats = await this.analyticsService.getStats( - linkId, - fromDate ? new Date(fromDate) : undefined, - toDate ? new Date(toDate) : undefined - ); - - return { - success: true, - data: { - linkId, - shortCode: link.shortCode, - stats, - }, - }; - } - - @Get('links/:linkId/clicks') - async getLinkClicks( - @CurrentUser() user: any, - @Param('linkId') linkId: string, - @Query('limit') limit: number = 100 - ) { - const userId = user.sub; - - // Verify user owns the link - const link = await this.linksService.getLinkById(linkId, userId); - if (!link) { - throw new NotFoundException('Link not found'); - } - - const { clicks, total } = await this.analyticsService.getRecentClicks(linkId, limit); - - return { - success: true, - data: { - linkId, - clicks: clicks.map((click) => ({ - ...click, - ipHash: undefined, // Don't expose IP hash - })), - total, - }, - }; - } - - @Get('overview') - async getOverview(@CurrentUser() user: any) { - const userId = user.sub; - const totalLinks = await this.linksService.getLinkCount(userId); - - return { - success: true, - data: { - totalLinks, - // Add more overview stats as needed - }, - }; - } -} diff --git a/apps-archived/uload/apps/backend/src/controllers/health.controller.ts b/apps-archived/uload/apps/backend/src/controllers/health.controller.ts deleted file mode 100644 index 1c0d5aa30..000000000 --- a/apps-archived/uload/apps/backend/src/controllers/health.controller.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { HealthCheckService, HealthCheck, HealthCheckResult } from '@nestjs/terminus'; - -@Controller('health') -export class HealthController { - constructor(private health: HealthCheckService) {} - - @Get() - @HealthCheck() - check(): Promise { - return this.health.check([]); - } - - @Get('ready') - ready() { - return { - status: 'ready', - timestamp: new Date().toISOString(), - }; - } - - @Get('live') - live() { - return { - status: 'live', - timestamp: new Date().toISOString(), - uptime: process.uptime(), - environment: process.env.NODE_ENV || 'development', - }; - } -} diff --git a/apps-archived/uload/apps/backend/src/controllers/links.controller.ts b/apps-archived/uload/apps/backend/src/controllers/links.controller.ts deleted file mode 100644 index 643dc59ff..000000000 --- a/apps-archived/uload/apps/backend/src/controllers/links.controller.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { - Controller, - Get, - Post, - Patch, - Delete, - Body, - Param, - Query, - UseGuards, - NotFoundException, -} from '@nestjs/common'; -import { AuthGuard, CurrentUser } from '@mana-core/nestjs-integration'; -import { LinksService } from '../services/links.service'; -import type { CreateLinkDto, UpdateLinkDto } from '../services/links.service'; - -@Controller('api/links') -@UseGuards(AuthGuard) -export class LinksController { - constructor(private readonly linksService: LinksService) {} - - @Get() - async getLinks( - @CurrentUser() user: any, - @Query('page') page: number = 1, - @Query('limit') limit: number = 20, - @Query('search') search?: string, - @Query('isActive') isActive?: boolean - ) { - const userId = user.sub; - const { items, total } = await this.linksService.getLinks(userId, { - page, - limit, - search, - isActive, - }); - - return { - success: true, - data: { - links: items.map((link) => ({ - ...link, - shortUrl: this.linksService.getShortUrl(link.shortCode), - hasPassword: !!link.password, - password: undefined, // Never send password to client - })), - pagination: { - page, - limit, - total, - totalPages: Math.ceil(total / limit), - hasMore: page * limit < total, - }, - }, - }; - } - - @Get(':id') - async getLink(@CurrentUser() user: any, @Param('id') id: string) { - const userId = user.sub; - const link = await this.linksService.getLinkById(id, userId); - - if (!link) { - throw new NotFoundException('Link not found'); - } - - return { - success: true, - data: { - ...link, - shortUrl: this.linksService.getShortUrl(link.shortCode), - hasPassword: !!link.password, - password: undefined, - }, - }; - } - - @Post() - async createLink(@CurrentUser() user: any, @Body() dto: CreateLinkDto) { - const userId = user.sub; - const link = await this.linksService.createLink(userId, dto); - - return { - success: true, - data: { - ...link, - shortUrl: this.linksService.getShortUrl(link.shortCode), - hasPassword: !!link.password, - password: undefined, - }, - }; - } - - @Patch(':id') - async updateLink(@CurrentUser() user: any, @Param('id') id: string, @Body() dto: UpdateLinkDto) { - const userId = user.sub; - const link = await this.linksService.updateLink(id, userId, dto); - - if (!link) { - throw new NotFoundException('Link not found'); - } - - return { - success: true, - data: { - ...link, - shortUrl: this.linksService.getShortUrl(link.shortCode), - hasPassword: !!link.password, - password: undefined, - }, - }; - } - - @Delete(':id') - async deleteLink(@CurrentUser() user: any, @Param('id') id: string) { - const userId = user.sub; - const deleted = await this.linksService.deleteLink(id, userId); - - if (!deleted) { - throw new NotFoundException('Link not found'); - } - - return { - success: true, - message: 'Link deleted successfully', - }; - } -} diff --git a/apps-archived/uload/apps/backend/src/controllers/redirect.controller.ts b/apps-archived/uload/apps/backend/src/controllers/redirect.controller.ts deleted file mode 100644 index dac6dbf85..000000000 --- a/apps-archived/uload/apps/backend/src/controllers/redirect.controller.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Controller, Get, Post, Param, Body, Req, Res, HttpStatus, Query } from '@nestjs/common'; -import { Response, Request } from 'express'; -import { RedirectService } from '../services/redirect.service'; -import { AnalyticsService } from '../services/analytics.service'; - -@Controller() -export class RedirectController { - constructor( - private readonly redirectService: RedirectService, - private readonly analyticsService: AnalyticsService - ) {} - - @Get(':code') - async redirect( - @Param('code') code: string, - @Query('utm_source') utmSource: string, - @Query('utm_medium') utmMedium: string, - @Query('utm_campaign') utmCampaign: string, - @Req() request: Request, - @Res() response: Response - ) { - // Skip for API and health routes - if (code === 'v1' || code === 'health') { - return response.status(HttpStatus.NOT_FOUND).json({ - success: false, - error: 'not_found', - }); - } - - const result = await this.redirectService.getRedirect(code); - - if (!result.success) { - switch (result.error) { - case 'not_found': - return response.status(HttpStatus.NOT_FOUND).json({ - success: false, - error: 'Link not found', - }); - - case 'expired': - return response.status(HttpStatus.GONE).json({ - success: false, - error: 'This link has expired', - }); - - case 'inactive': - return response.status(HttpStatus.GONE).json({ - success: false, - error: 'This link is no longer active', - }); - - case 'max_clicks': - return response.status(HttpStatus.GONE).json({ - success: false, - error: 'This link has reached its maximum clicks', - }); - - case 'password_required': - return response.status(HttpStatus.OK).json({ - success: false, - passwordRequired: true, - linkId: result.linkId, - }); - } - } - - // Record click asynchronously (don't wait) - this.analyticsService - .recordClick(result.linkId!, { - userAgent: request.headers['user-agent'] || '', - referer: request.headers['referer'] || '', - ip: request.ip, - utmSource, - utmMedium, - utmCampaign, - }) - .catch((err) => console.error('Failed to record click:', err)); - - // Perform redirect - return response.redirect(302, result.targetUrl!); - } - - @Post(':code/unlock') - async unlockLink( - @Param('code') code: string, - @Body('password') password: string, - @Res() response: Response - ) { - const result = await this.redirectService.verifyPassword(code, password); - - if (!result.success) { - return response.status(HttpStatus.UNAUTHORIZED).json({ - success: false, - error: 'Invalid password', - }); - } - - return response.json({ - success: true, - targetUrl: result.targetUrl, - }); - } -} diff --git a/apps-archived/uload/apps/backend/src/database/database.module.ts b/apps-archived/uload/apps/backend/src/database/database.module.ts deleted file mode 100644 index 35e7eb4a5..000000000 --- a/apps-archived/uload/apps/backend/src/database/database.module.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Module, Global, OnModuleDestroy, Logger } from '@nestjs/common'; -import { getDb, closeDb } from '@manacore/uload-database'; -import type { Database } from '@manacore/uload-database'; - -export const DATABASE_TOKEN = 'DATABASE'; - -@Global() -@Module({ - providers: [ - { - provide: DATABASE_TOKEN, - useFactory: () => { - const logger = new Logger('DatabaseModule'); - logger.log('Initializing database connection'); - return getDb(); - }, - }, - ], - exports: [DATABASE_TOKEN], -}) -export class DatabaseModule implements OnModuleDestroy { - private readonly logger = new Logger(DatabaseModule.name); - - async onModuleDestroy() { - this.logger.log('Closing database connection'); - await closeDb(); - } -} - -export type { Database }; diff --git a/apps-archived/uload/apps/backend/src/database/repositories/click.repository.ts b/apps-archived/uload/apps/backend/src/database/repositories/click.repository.ts deleted file mode 100644 index 7f9cedd1f..000000000 --- a/apps-archived/uload/apps/backend/src/database/repositories/click.repository.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { Injectable, Inject, Logger } from '@nestjs/common'; -import { DATABASE_TOKEN } from '../database.module'; -import type { Database } from '../database.module'; -import { clicks, eq, desc, sql, and, gte, lte } from '@manacore/uload-database'; -import type { Click, NewClick } from '@manacore/uload-database'; - -export interface ClickStats { - totalClicks: number; - uniqueVisitors: number; - topCountries: { country: string; count: number }[]; - topBrowsers: { browser: string; count: number }[]; - topDevices: { deviceType: string; count: number }[]; - clicksByDay: { date: string; count: number }[]; -} - -@Injectable() -export class ClickRepository { - private readonly logger = new Logger(ClickRepository.name); - - constructor(@Inject(DATABASE_TOKEN) private readonly db: Database) {} - - async create(data: NewClick): Promise { - const result = await this.db.insert(clicks).values(data).returning(); - return result[0]; - } - - async findByLinkId( - linkId: string, - options: { limit?: number; offset?: number } = {} - ): Promise { - const { limit = 100, offset = 0 } = options; - return this.db - .select() - .from(clicks) - .where(eq(clicks.linkId, linkId)) - .orderBy(desc(clicks.clickedAt)) - .limit(limit) - .offset(offset); - } - - async countByLinkId(linkId: string): Promise { - const result = await this.db - .select({ count: sql`count(*)::int` }) - .from(clicks) - .where(eq(clicks.linkId, linkId)); - return result[0]?.count || 0; - } - - async getStats(linkId: string, fromDate?: Date, toDate?: Date): Promise { - const conditions = [eq(clicks.linkId, linkId)]; - - if (fromDate) { - conditions.push(gte(clicks.clickedAt, fromDate)); - } - if (toDate) { - conditions.push(lte(clicks.clickedAt, toDate)); - } - - const whereClause = and(...conditions); - - // Total clicks - const totalResult = await this.db - .select({ count: sql`count(*)::int` }) - .from(clicks) - .where(whereClause); - - // Unique visitors (by IP hash) - const uniqueResult = await this.db - .select({ count: sql`count(distinct ${clicks.ipHash})::int` }) - .from(clicks) - .where(whereClause); - - // Top countries - const countriesResult = await this.db - .select({ - country: clicks.country, - count: sql`count(*)::int`, - }) - .from(clicks) - .where(whereClause) - .groupBy(clicks.country) - .orderBy(sql`count(*) desc`) - .limit(10); - - // Top browsers - const browsersResult = await this.db - .select({ - browser: clicks.browser, - count: sql`count(*)::int`, - }) - .from(clicks) - .where(whereClause) - .groupBy(clicks.browser) - .orderBy(sql`count(*) desc`) - .limit(10); - - // Top devices - const devicesResult = await this.db - .select({ - deviceType: clicks.deviceType, - count: sql`count(*)::int`, - }) - .from(clicks) - .where(whereClause) - .groupBy(clicks.deviceType) - .orderBy(sql`count(*) desc`) - .limit(10); - - // Clicks by day (last 30 days) - const clicksByDayResult = await this.db - .select({ - date: sql`date_trunc('day', ${clicks.clickedAt})::date::text`, - count: sql`count(*)::int`, - }) - .from(clicks) - .where(whereClause) - .groupBy(sql`date_trunc('day', ${clicks.clickedAt})`) - .orderBy(sql`date_trunc('day', ${clicks.clickedAt})`) - .limit(30); - - return { - totalClicks: totalResult[0]?.count || 0, - uniqueVisitors: uniqueResult[0]?.count || 0, - topCountries: countriesResult.map((r) => ({ - country: r.country || 'Unknown', - count: r.count, - })), - topBrowsers: browsersResult.map((r) => ({ - browser: r.browser || 'Unknown', - count: r.count, - })), - topDevices: devicesResult.map((r) => ({ - deviceType: r.deviceType || 'Unknown', - count: r.count, - })), - clicksByDay: clicksByDayResult.map((r) => ({ - date: r.date, - count: r.count, - })), - }; - } - - async deleteByLinkId(linkId: string): Promise { - const result = await this.db - .delete(clicks) - .where(eq(clicks.linkId, linkId)) - .returning({ id: clicks.id }); - return result.length; - } -} diff --git a/apps-archived/uload/apps/backend/src/database/repositories/index.ts b/apps-archived/uload/apps/backend/src/database/repositories/index.ts deleted file mode 100644 index 119d02c0d..000000000 --- a/apps-archived/uload/apps/backend/src/database/repositories/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { LinkRepository, type ListLinksOptions } from './link.repository'; -export { ClickRepository, type ClickStats } from './click.repository'; diff --git a/apps-archived/uload/apps/backend/src/database/repositories/link.repository.ts b/apps-archived/uload/apps/backend/src/database/repositories/link.repository.ts deleted file mode 100644 index ed31208ed..000000000 --- a/apps-archived/uload/apps/backend/src/database/repositories/link.repository.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { Injectable, Inject, Logger } from '@nestjs/common'; -import { DATABASE_TOKEN } from '../database.module'; -import type { Database } from '../database.module'; -import { links, eq, and, desc, sql, or, ilike } from '@manacore/uload-database'; -import type { Link, NewLink } from '@manacore/uload-database'; - -export interface ListLinksOptions { - page?: number; - limit?: number; - search?: string; - isActive?: boolean; -} - -@Injectable() -export class LinkRepository { - private readonly logger = new Logger(LinkRepository.name); - - constructor(@Inject(DATABASE_TOKEN) private readonly db: Database) {} - - async findByShortCode(shortCode: string): Promise { - const result = await this.db - .select() - .from(links) - .where(eq(links.shortCode, shortCode)) - .limit(1); - return result[0] || null; - } - - async findById(id: string): Promise { - const result = await this.db.select().from(links).where(eq(links.id, id)).limit(1); - return result[0] || null; - } - - async findByIdAndUserId(id: string, userId: string): Promise { - const result = await this.db - .select() - .from(links) - .where(and(eq(links.id, id), eq(links.userId, userId))) - .limit(1); - return result[0] || null; - } - - async findByUserId( - userId: string, - options: ListLinksOptions = {} - ): Promise<{ items: Link[]; total: number }> { - const { page = 1, limit = 20, search, isActive } = options; - const offset = (page - 1) * limit; - - const conditions = [eq(links.userId, userId)]; - - if (search) { - conditions.push( - or( - ilike(links.title, `%${search}%`), - ilike(links.originalUrl, `%${search}%`), - ilike(links.shortCode, `%${search}%`) - )! - ); - } - - if (isActive !== undefined) { - conditions.push(eq(links.isActive, isActive)); - } - - const [countResult, items] = await Promise.all([ - this.db - .select({ count: sql`count(*)::int` }) - .from(links) - .where(and(...conditions)), - this.db - .select() - .from(links) - .where(and(...conditions)) - .orderBy(desc(links.createdAt)) - .limit(limit) - .offset(offset), - ]); - - return { - items, - total: countResult[0]?.count || 0, - }; - } - - async create(data: NewLink): Promise { - this.logger.debug(`Creating link: ${data.shortCode}`); - const result = await this.db.insert(links).values(data).returning(); - return result[0]; - } - - async update( - id: string, - userId: string, - data: Partial> - ): Promise { - const result = await this.db - .update(links) - .set({ ...data, updatedAt: new Date() }) - .where(and(eq(links.id, id), eq(links.userId, userId))) - .returning(); - return result[0] || null; - } - - async delete(id: string, userId: string): Promise { - const result = await this.db - .delete(links) - .where(and(eq(links.id, id), eq(links.userId, userId))) - .returning({ id: links.id }); - return result.length > 0; - } - - async incrementClickCount(id: string): Promise { - await this.db - .update(links) - .set({ clickCount: sql`${links.clickCount} + 1` }) - .where(eq(links.id, id)); - } - - async isShortCodeAvailable(shortCode: string): Promise { - const result = await this.db - .select({ id: links.id }) - .from(links) - .where(eq(links.shortCode, shortCode)) - .limit(1); - return result.length === 0; - } - - async countByUserId(userId: string): Promise { - const result = await this.db - .select({ count: sql`count(*)::int` }) - .from(links) - .where(eq(links.userId, userId)); - return result[0]?.count || 0; - } -} diff --git a/apps-archived/uload/apps/backend/src/main.ts b/apps-archived/uload/apps/backend/src/main.ts deleted file mode 100644 index 9495e5e5d..000000000 --- a/apps-archived/uload/apps/backend/src/main.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { NestFactory } from '@nestjs/core'; -import { ValidationPipe, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { AppModule } from './app.module'; - -async function bootstrap() { - const logger = new Logger('Bootstrap'); - - const app = await NestFactory.create(AppModule, { - logger: ['error', 'warn', 'log', 'debug', 'verbose'], - }); - - const configService = app.get(ConfigService); - - // CORS configuration - app.enableCors({ - origin: configService.get('FRONTEND_URL') || true, - credentials: true, - methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization'], - }); - - // Global validation pipe - app.useGlobalPipes( - new ValidationPipe({ - whitelist: true, - forbidNonWhitelisted: true, - transform: true, - transformOptions: { - enableImplicitConversion: true, - }, - }) - ); - - // Global prefix for API routes (except health and redirect) - app.setGlobalPrefix('v1', { - exclude: ['health', 'health/(.*)', ':code'], - }); - - const port = configService.get('PORT') || 3003; - - await app.listen(port); - logger.log(`ULOAD Backend running on port ${port}`); - logger.log(`Health check: http://localhost:${port}/health`); -} - -bootstrap(); diff --git a/apps-archived/uload/apps/backend/src/services/analytics.service.ts b/apps-archived/uload/apps/backend/src/services/analytics.service.ts deleted file mode 100644 index 52284623d..000000000 --- a/apps-archived/uload/apps/backend/src/services/analytics.service.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import * as UAParser from 'ua-parser-js'; -import { ClickRepository } from '../database/repositories'; -import type { ClickStats } from '../database/repositories'; -import { RedirectService } from './redirect.service'; -import type { NewClick } from '@manacore/uload-database'; - -export interface RecordClickData { - userAgent: string; - referer?: string; - ip?: string; - utmSource?: string; - utmMedium?: string; - utmCampaign?: string; -} - -@Injectable() -export class AnalyticsService { - private readonly logger = new Logger(AnalyticsService.name); - - constructor( - private readonly clickRepository: ClickRepository, - private readonly redirectService: RedirectService - ) {} - - async recordClick(linkId: string, data: RecordClickData): Promise { - try { - // Parse user agent - const parser = new UAParser.UAParser(data.userAgent); - const browser = parser.getBrowser(); - const os = parser.getOS(); - const device = parser.getDevice(); - - // Hash IP for privacy - const ipHash = data.ip ? this.hashIp(data.ip) : null; - - // Determine device type - let deviceType = 'desktop'; - if (device.type === 'mobile') { - deviceType = 'mobile'; - } else if (device.type === 'tablet') { - deviceType = 'tablet'; - } - - const clickData: NewClick = { - linkId, - ipHash, - userAgent: data.userAgent, - referer: data.referer, - browser: browser.name || 'Unknown', - deviceType, - os: os.name || 'Unknown', - // TODO: Geo lookup from IP - country: null, - city: null, - utmSource: data.utmSource, - utmMedium: data.utmMedium, - utmCampaign: data.utmCampaign, - }; - - await this.clickRepository.create(clickData); - - // Increment click count on the link - await this.redirectService.incrementClickCount(linkId); - - this.logger.debug(`Recorded click for link ${linkId}`); - } catch (error) { - this.logger.error(`Failed to record click for link ${linkId}:`, error); - // Don't throw - click recording should not block redirect - } - } - - async getStats(linkId: string, fromDate?: Date, toDate?: Date): Promise { - return this.clickRepository.getStats(linkId, fromDate, toDate); - } - - async getRecentClicks( - linkId: string, - limit: number = 100 - ): Promise<{ clicks: any[]; total: number }> { - const [clicks, total] = await Promise.all([ - this.clickRepository.findByLinkId(linkId, { limit }), - this.clickRepository.countByLinkId(linkId), - ]); - - return { clicks, total }; - } - - private hashIp(ip: string): string { - // Simple hash for privacy - in production use a proper hash function - let hash = 0; - for (let i = 0; i < ip.length; i++) { - const char = ip.charCodeAt(i); - hash = (hash << 5) - hash + char; - hash = hash & hash; // Convert to 32bit integer - } - return hash.toString(16); - } -} diff --git a/apps-archived/uload/apps/backend/src/services/links.service.ts b/apps-archived/uload/apps/backend/src/services/links.service.ts deleted file mode 100644 index 35a3b4754..000000000 --- a/apps-archived/uload/apps/backend/src/services/links.service.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { Injectable, Logger, BadRequestException } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { nanoid } from 'nanoid'; -import { LinkRepository } from '../database/repositories'; -import type { ListLinksOptions } from '../database/repositories'; -import type { Link, NewLink } from '@manacore/uload-database'; - -export interface CreateLinkDto { - originalUrl: string; - customCode?: string; - title?: string; - description?: string; - password?: string; - maxClicks?: number; - expiresAt?: Date; - tags?: string[]; - utmSource?: string; - utmMedium?: string; - utmCampaign?: string; - workspaceId?: string; -} - -export interface UpdateLinkDto { - title?: string; - description?: string; - password?: string; - maxClicks?: number; - expiresAt?: Date; - isActive?: boolean; - tags?: string[]; - utmSource?: string; - utmMedium?: string; - utmCampaign?: string; -} - -@Injectable() -export class LinksService { - private readonly logger = new Logger(LinksService.name); - private readonly shortUrlBase: string; - - constructor( - private readonly linkRepository: LinkRepository, - private readonly configService: ConfigService - ) { - this.shortUrlBase = this.configService.get('SHORT_URL_BASE', 'https://ulo.ad'); - } - - async createLink(userId: string, dto: CreateLinkDto): Promise { - // Generate or validate short code - let shortCode = dto.customCode; - - if (shortCode) { - // Validate custom code format - if (!/^[a-zA-Z0-9_-]+$/.test(shortCode)) { - throw new BadRequestException( - 'Custom code can only contain letters, numbers, hyphens and underscores' - ); - } - - // Check if custom code is available - const isAvailable = await this.linkRepository.isShortCodeAvailable(shortCode); - if (!isAvailable) { - throw new BadRequestException('This custom code is already taken'); - } - } else { - // Generate random short code - shortCode = nanoid(7); - - // Make sure it's unique (very unlikely to collide, but check anyway) - let attempts = 0; - while (!(await this.linkRepository.isShortCodeAvailable(shortCode)) && attempts < 5) { - shortCode = nanoid(7); - attempts++; - } - } - - const newLink: NewLink = { - shortCode, - customCode: dto.customCode, - originalUrl: dto.originalUrl, - title: dto.title, - description: dto.description, - userId, - password: dto.password, // TODO: Hash password if provided - maxClicks: dto.maxClicks, - expiresAt: dto.expiresAt, - tags: dto.tags, - utmSource: dto.utmSource, - utmMedium: dto.utmMedium, - utmCampaign: dto.utmCampaign, - workspaceId: dto.workspaceId, - }; - - const link = await this.linkRepository.create(newLink); - this.logger.log(`Created link ${link.shortCode} for user ${userId}`); - - return link; - } - - async updateLink(id: string, userId: string, dto: UpdateLinkDto): Promise { - const link = await this.linkRepository.update(id, userId, dto); - - if (link) { - this.logger.log(`Updated link ${link.shortCode} for user ${userId}`); - } - - return link; - } - - async deleteLink(id: string, userId: string): Promise { - const deleted = await this.linkRepository.delete(id, userId); - - if (deleted) { - this.logger.log(`Deleted link ${id} for user ${userId}`); - } - - return deleted; - } - - async getLinkById(id: string, userId: string): Promise { - return this.linkRepository.findByIdAndUserId(id, userId); - } - - async getLinks( - userId: string, - options: ListLinksOptions - ): Promise<{ items: Link[]; total: number }> { - return this.linkRepository.findByUserId(userId, options); - } - - async getLinkCount(userId: string): Promise { - return this.linkRepository.countByUserId(userId); - } - - getShortUrl(shortCode: string): string { - return `${this.shortUrlBase}/${shortCode}`; - } -} diff --git a/apps-archived/uload/apps/backend/src/services/redirect.service.ts b/apps-archived/uload/apps/backend/src/services/redirect.service.ts deleted file mode 100644 index 0343b8a4e..000000000 --- a/apps-archived/uload/apps/backend/src/services/redirect.service.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { LinkRepository } from '../database/repositories'; -import type { Link } from '@manacore/uload-database'; - -export interface RedirectResult { - success: boolean; - targetUrl?: string; - linkId?: string; - error?: 'not_found' | 'expired' | 'inactive' | 'max_clicks' | 'password_required'; -} - -@Injectable() -export class RedirectService { - private readonly logger = new Logger(RedirectService.name); - - constructor(private readonly linkRepository: LinkRepository) {} - - async getRedirect(shortCode: string): Promise { - const link = await this.linkRepository.findByShortCode(shortCode); - - if (!link) { - return { success: false, error: 'not_found' }; - } - - // Check if link is active - if (!link.isActive) { - return { success: false, error: 'inactive', linkId: link.id }; - } - - // Check if link has expired - if (link.expiresAt && new Date(link.expiresAt) < new Date()) { - return { success: false, error: 'expired', linkId: link.id }; - } - - // Check max clicks - if (link.maxClicks && (link.clickCount ?? 0) >= link.maxClicks) { - return { success: false, error: 'max_clicks', linkId: link.id }; - } - - // Check if password protected - if (link.password) { - return { success: false, error: 'password_required', linkId: link.id }; - } - - return { - success: true, - targetUrl: link.originalUrl, - linkId: link.id, - }; - } - - async verifyPassword(shortCode: string, password: string): Promise { - const link = await this.linkRepository.findByShortCode(shortCode); - - if (!link) { - return { success: false, error: 'not_found' }; - } - - // TODO: Compare hashed passwords - if (link.password !== password) { - return { success: false, error: 'password_required', linkId: link.id }; - } - - return { - success: true, - targetUrl: link.originalUrl, - linkId: link.id, - }; - } - - async incrementClickCount(linkId: string): Promise { - await this.linkRepository.incrementClickCount(linkId); - } -} diff --git a/apps-archived/uload/apps/backend/tsconfig.json b/apps-archived/uload/apps/backend/tsconfig.json deleted file mode 100644 index 38c2b55d7..000000000 --- a/apps-archived/uload/apps/backend/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "declaration": true, - "removeComments": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "target": "ES2021", - "sourceMap": true, - "outDir": "./dist", - "baseUrl": "./", - "incremental": true, - "skipLibCheck": true, - "strictNullChecks": true, - "noImplicitAny": true, - "strictBindCallApply": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, - "esModuleInterop": true, - "resolveJsonModule": true - } -} diff --git a/apps-archived/uload/apps/landing/astro.config.mjs b/apps-archived/uload/apps/landing/astro.config.mjs deleted file mode 100644 index c517fdcfa..000000000 --- a/apps-archived/uload/apps/landing/astro.config.mjs +++ /dev/null @@ -1,20 +0,0 @@ -import { defineConfig } from 'astro/config'; -import tailwind from '@astrojs/tailwind'; -import mdx from '@astrojs/mdx'; -import sitemap from '@astrojs/sitemap'; - -export default defineConfig({ - site: 'https://ulo.ad', - integrations: [ - tailwind(), - mdx(), - sitemap() - ], - i18n: { - defaultLocale: 'de', - locales: ['de', 'en'], - routing: { - prefixDefaultLocale: false - } - } -}); diff --git a/apps-archived/uload/apps/landing/package.json b/apps-archived/uload/apps/landing/package.json deleted file mode 100644 index 88cfab93c..000000000 --- a/apps-archived/uload/apps/landing/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "@uload/landing", - "type": "module", - "version": "1.0.0", - "scripts": { - "dev": "astro dev", - "build": "astro build", - "preview": "astro preview", - "astro": "astro", - "check": "astro check" - }, - "dependencies": { - "@astrojs/check": "^0.9.4", - "@astrojs/mdx": "^4.0.8", - "@astrojs/sitemap": "^3.2.1", - "@astrojs/tailwind": "^6.0.2", - "@manacore/shared-landing-ui": "workspace:*", - "astro": "^5.1.1", - "tailwindcss": "^3.4.17" - }, - "devDependencies": { - "@types/node": "^22.10.2", - "typescript": "^5.7.2" - } -} diff --git a/apps-archived/uload/apps/landing/src/components/Footer.astro b/apps-archived/uload/apps/landing/src/components/Footer.astro deleted file mode 100644 index adbcb23be..000000000 --- a/apps-archived/uload/apps/landing/src/components/Footer.astro +++ /dev/null @@ -1,114 +0,0 @@ ---- -const currentYear = new Date().getFullYear(); - -const footerLinks = { - produkt: [ - { href: '/features', label: 'Features' }, - { href: '/#pricing', label: 'Preise' }, - { href: '/blog', label: 'Blog' }, - ], - unternehmen: [{ href: '/about', label: 'Über uns' }], - rechtliches: [ - { href: '/datenschutz', label: 'Datenschutz' }, - { href: '/impressum', label: 'Impressum' }, - { href: '/agb', label: 'AGB' }, - { href: '/sicherheit', label: 'Sicherheit' }, - ], -}; - -const appUrl = 'https://app.ulo.ad'; ---- - -
-
-
- -
- -
- u -
- uLoad -
-

- Der intelligente URL-Shortener für Profis. Verkürzen Sie Links, erstellen Sie QR-Codes und - analysieren Sie Klicks. -

-
- - -
-

Produkt

- -
- - -
-

Unternehmen

- -
- - -
-

Rechtliches

- -
-
- - -
-

- © {currentYear} uLoad. Alle Rechte vorbehalten. -

- -
-
-
diff --git a/apps-archived/uload/apps/landing/src/components/HeroSection.astro b/apps-archived/uload/apps/landing/src/components/HeroSection.astro deleted file mode 100644 index b57d1ad8e..000000000 --- a/apps-archived/uload/apps/landing/src/components/HeroSection.astro +++ /dev/null @@ -1,195 +0,0 @@ ---- -const appUrl = 'https://app.ulo.ad'; ---- - -
- -
-
-
-
-
-
- -
-
- -
- - - - - DSGVO-konform - - - - - - Blitzschnell - - - - - - 100% Sicher - -
- - -

- More than links. - - Your digital identity. - -

- -

- Der einzige Link-Shortener mit integriertem Profile-Builder. Erstelle kurze Links, - beeindruckende Profilkarten und manage alles im Team. -

- - - - - -
- -

- Keine Anmeldung erforderlich • Kostenlos • QR-Code inklusive -

-
-
- - -
- -
-
- - - -
-

Smart Links

-

Kurze URLs mit Tracking, Ablaufdatum und Passwortschutz

- - Mehr erfahren → - -
- - -
-
- - - -
-

Profile Cards

-

Beeindruckende Profilseiten mit Drag & Drop Builder

- - Templates ansehen → - -
- - -
-
- - - -
-

Team Workspace

-

Gemeinsam Links verwalten mit granularen Berechtigungen

- - Für Teams → - -
-
-
-
diff --git a/apps-archived/uload/apps/landing/src/components/Navigation.astro b/apps-archived/uload/apps/landing/src/components/Navigation.astro deleted file mode 100644 index 7fb1f5f16..000000000 --- a/apps-archived/uload/apps/landing/src/components/Navigation.astro +++ /dev/null @@ -1,86 +0,0 @@ ---- -const navLinks = [ - { href: '/features', label: 'Features' }, - { href: '/blog', label: 'Blog' }, - { href: '/about', label: 'Über uns' }, -]; - -const appUrl = 'https://app.ulo.ad'; ---- - -
- -
- - diff --git a/apps-archived/uload/apps/landing/src/content/blog/link-tracking-guide.md b/apps-archived/uload/apps/landing/src/content/blog/link-tracking-guide.md deleted file mode 100644 index a4134ca62..000000000 --- a/apps-archived/uload/apps/landing/src/content/blog/link-tracking-guide.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: Der ultimative Link-Tracking Guide für 2024 -description: Erfahren Sie, wie Sie mit modernem Link-Tracking Ihre Marketing-Performance messbar verbessern und dabei DSGVO-konform bleiben. -pubDate: 2024-01-20 -author: Till Schneider -tags: [tracking, analytics, dsgvo, marketing] ---- - -Link-Tracking ist der Schlüssel zu datengetriebenem Marketing. In diesem umfassenden Guide zeigen wir Ihnen, wie Sie Ihre Links professionell tracken, dabei datenschutzkonform bleiben und Ihre Conversion-Rate signifikant steigern. - -## Was ist Link-Tracking? - -Link-Tracking ermöglicht es Ihnen, das Verhalten Ihrer Nutzer zu verstehen: - -- Woher kommen Ihre Besucher? -- Welche Kampagnen funktionieren? -- Wie hoch ist Ihre Conversion-Rate? -- Welche Inhalte performen am besten? - -## Die wichtigsten Metriken - -### 1. Click-Through-Rate (CTR) - -Die CTR zeigt, wie viele Personen Ihren Link gesehen und geklickt haben. Eine gute CTR liegt je nach Kanal zwischen 2-5%. - -### 2. Conversion Rate - -Der Prozentsatz der Klicks, die zu einer gewünschten Aktion führen. - -### 3. Bounce Rate - -Wie viele Nutzer verlassen Ihre Seite sofort wieder? - -### 4. Geographic Distribution - -Verstehen Sie, aus welchen Ländern und Regionen Ihre Besucher kommen. - -## UTM-Parameter richtig einsetzen - -UTM-Parameter sind der Standard für Campaign-Tracking: - -``` -https://ulo.ad/angebot -?utm_source=newsletter -&utm_medium=email -&utm_campaign=winter-sale -``` - -### Die 5 UTM-Parameter - -1. **utm_source**: Woher kommt der Traffic? -2. **utm_medium**: Welches Medium? -3. **utm_campaign**: Welche Kampagne? -4. **utm_content**: Welcher spezifische Link? -5. **utm_term**: Welches Keyword? - -## DSGVO-konformes Tracking - -### Was ist erlaubt? - -✅ **Anonymisierte Daten** - -- Gerätetyp -- Browser -- Ungefährer Standort -- Referrer - -### Was braucht Zustimmung? - -❌ **Personenbezogene Daten** - -- Vollständige IP-Adressen -- Device Fingerprinting -- Cross-Site Tracking - -## Best Practices für Link-Tracking - -### 1. Konsistente Namenskonvention - -Entwickeln Sie ein einheitliches Schema für Ihre Kampagnen. - -### 2. Dokumentation führen - -Erstellen Sie eine Tracking-Tabelle für alle Kampagnen. - -### 3. Regelmäßige Bereinigung - -Löschen Sie alte, inaktive Links regelmäßig. - -## Fazit - -Professionelles Link-Tracking ist kein Nice-to-have, sondern ein Must-have für erfolgreiches digitales Marketing. Mit den richtigen Tools und Prozessen können Sie Ihre Marketing-Performance signifikant steigern. diff --git a/apps-archived/uload/apps/landing/src/content/blog/psychologie-kurzer-urls.md b/apps-archived/uload/apps/landing/src/content/blog/psychologie-kurzer-urls.md deleted file mode 100644 index e05de2c8d..000000000 --- a/apps-archived/uload/apps/landing/src/content/blog/psychologie-kurzer-urls.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: Die Psychologie kurzer URLs - Warum unser Gehirn sie liebt -description: 42% weniger Klicks bei langen URLs – diese erstaunliche Zahl zeigt, wie stark die Länge eines Links unsere Entscheidung beeinflusst. Erfahren Sie die Wissenschaft dahinter. -pubDate: 2024-01-15 -author: Till Schneider -tags: [urls, psychology, conversion, marketing] ---- - -**42% weniger Klicks bei langen URLs** – diese erstaunliche Zahl zeigt, wie stark die Länge eines Links unsere Entscheidung beeinflusst, darauf zu klicken oder nicht. In diesem umfassenden Artikel tauchen wir tief in die Psychologie hinter kurzen URLs ein und zeigen Ihnen, wie Sie dieses Wissen für Ihren digitalen Erfolg nutzen können. - -## Das Problem mit langen URLs: Wenn Links Misstrauen erzeugen - -Stellen Sie sich vor: Fast die Hälfte Ihrer potenziellen Besucher klickt nicht auf Ihren Link – nur weil er zu lang ist. Was auf den ersten Blick wie eine technische Kleinigkeit erscheint, ist in Wahrheit ein psychologisches Phänomen mit enormen Auswirkungen auf Ihre Online-Performance. - -### Die Spam-Alarm-Reaktion unseres Gehirns - -Aktuelle Studien zeigen eindeutig: URLs, die länger als 100 Zeichen sind, lösen automatisch Misstrauen aus. Unser Gehirn hat über Jahre hinweg gelernt, dass lange, unleserliche Links mit unzähligen Parametern oft zu zweifelhaften Inhalten führen. - -Vergleichen Sie diese beiden URLs: - -**Lange URL (schlecht):** - -``` -https://example.com/product?id=12345&utm_source=newsletter&utm_medium=email&utm_campaign=summer2024 -``` - -**Kurze URL (gut):** - -``` -https://ulo.ad/summer-sale -``` - -### Mobile Nutzer: Die vergessene Mehrheit - -In einer Welt, in der über 60% des Web-Traffics von mobilen Geräten kommt, sind lange URLs ein noch größeres Problem. Mobile Nutzer scrollen definitiv nicht horizontal, um einen Link vollständig zu sehen. - -## Die Wissenschaft dahinter: Cognitive Load Theory - -Die Cognitive Load Theory erklärt, warum kurze URLs so effektiv sind. Unser Gehirn ist darauf programmiert, Energie zu sparen. Bei der Verarbeitung von Informationen sucht es immer nach dem Weg des geringsten Widerstands. - -## Die vier Säulen des Link-Vertrauens - -1. **Erkennbare Domain (60% Wichtigkeit)** - Menschen wollen wissen, wo sie landen werden -2. **Keine kryptischen Zeichen (25% Wichtigkeit)** - Zufällige Zahlen-Buchstaben-Kombinationen schrecken ab -3. **Optimale Länge (10% Wichtigkeit)** - Die magische Grenze liegt bei etwa 50 Zeichen -4. **HTTPS-Verschlüsselung (5% Wichtigkeit)** - Ein Hygienefaktor - -## Praktische Optimierungsstrategien - -### 1. Sprechende URLs verwenden - -❌ **Schlecht:** `ulo.ad/p47829` -✅ **Gut:** `ulo.ad/sommer-sale` - -### 2. Die 50-Zeichen-Regel - -Halten Sie Ihre URLs unter 50 Zeichen. Das ist: - -- Kurz genug für Twitter/X -- Lesbar auf Mobilgeräten -- Merkbar für Nutzer - -### 3. A/B-Testing ist Ihr Freund - -Testen Sie verschiedene URL-Varianten und messen Sie die Performance. - -## Fazit: Die Macht der Kürze - -Die Psychologie kurzer URLs ist keine Raketenwissenschaft, aber ihre Auswirkungen sind enorm. In einer Welt, in der Aufmerksamkeit die wertvollste Währung ist, können kurze, vertrauenswürdige Links den Unterschied zwischen Erfolg und Misserfolg ausmachen. - -### Die wichtigsten Takeaways - -1. **42% weniger Klicks** bei URLs über 100 Zeichen -2. **Cognitive Load Theory**: Unser Gehirn liebt Einfachheit -3. **50 Zeichen** ist die magische Grenze -4. **Sprechende URLs** performen 39% besser diff --git a/apps-archived/uload/apps/landing/src/content/config.ts b/apps-archived/uload/apps/landing/src/content/config.ts deleted file mode 100644 index 858ec45ce..000000000 --- a/apps-archived/uload/apps/landing/src/content/config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { defineCollection, z } from 'astro:content'; - -const blogCollection = defineCollection({ - type: 'content', - schema: z.object({ - title: z.string(), - description: z.string(), - pubDate: z.date(), - author: z.string().optional(), - image: z.string().optional(), - tags: z.array(z.string()).optional(), - }), -}); - -export const collections = { - blog: blogCollection, -}; diff --git a/apps-archived/uload/apps/landing/src/env.d.ts b/apps-archived/uload/apps/landing/src/env.d.ts deleted file mode 100644 index acef35f17..000000000 --- a/apps-archived/uload/apps/landing/src/env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// diff --git a/apps-archived/uload/apps/landing/src/layouts/BaseLayout.astro b/apps-archived/uload/apps/landing/src/layouts/BaseLayout.astro deleted file mode 100644 index 081344dfb..000000000 --- a/apps-archived/uload/apps/landing/src/layouts/BaseLayout.astro +++ /dev/null @@ -1,59 +0,0 @@ ---- -import '../styles/global.css'; -import Navigation from '../components/Navigation.astro'; -import Footer from '../components/Footer.astro'; - -interface Props { - title: string; - description?: string; - ogImage?: string; -} - -const { - title, - description = 'uLoad - Der intelligente URL-Shortener für Profis. Verkürzen Sie Links, erstellen Sie QR-Codes und analysieren Sie Klicks.', - ogImage = '/og-image.png', -} = Astro.props; -const canonicalURL = new URL(Astro.url.pathname, Astro.site); ---- - - - - - - - - - - {title} | uLoad - - - - - - - - - - - - - - - - - - - - - - -
- -
-