feat: integrate uload and picture, unify package naming

- Add uload project with apps/web structure
  - Reorganize from flat to monorepo structure
  - Remove PocketBase binary and local data
  - Update to pnpm and @uload/web namespace

- Add picture project to monorepo
  - Remove embedded git repository

- Unify all package names to @{project}/{app} schema:
  - @maerchenzauber/* (was @storyteller/*)
  - @manacore/* (was manacore-*, manacore)
  - @manadeck/* (was web, backend, manadeck)
  - @memoro/* (was memoro-web, landing, memoro)
  - @picture/* (already unified)
  - @uload/web

- Add convenient dev scripts for all apps:
  - pnpm dev:{project}:web
  - pnpm dev:{project}:landing
  - pnpm dev:{project}:mobile
  - pnpm dev:{project}:backend

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-11-25 04:00:36 +01:00
parent c6c4c5a552
commit c712a2504a
1031 changed files with 189301 additions and 290 deletions

View file

@ -0,0 +1,44 @@
#!/bin/bash
# ULoad Database Optimization Script
# Applies performance optimizations to the SQLite database
echo "🚀 Applying database optimizations..."
# Backup current database
echo "📦 Creating backup..."
cp backend/pb_data/data.db backend/pb_data/data.db.backup.$(date +%Y%m%d_%H%M%S)
# Apply optimizations to local database
echo "⚡ Applying optimizations to local database..."
sqlite3 backend/pb_data/data.db < scripts/optimize-database.sql
# Check if production database optimization is needed
if [ "$1" = "--production" ]; then
echo "🌐 Production mode detected"
echo "⚠️ Manual production database optimization required"
echo " Run this SQL script on your production PocketBase:"
echo " cat scripts/optimize-database.sql"
fi
echo "✅ Database optimizations applied successfully!"
echo "📊 Database size after optimization:"
ls -lh backend/pb_data/data.db
echo ""
echo "🔍 Performance improvements applied:"
echo " • WAL mode enabled for better concurrency"
echo " • Cache size optimized to 8MB"
echo " • Memory-mapped I/O enabled"
echo " • Missing indexes created for:"
echo " - Links by user and active status"
echo " - Analytics by link and date"
echo " - Composite indexes for dashboard queries"
echo " • Statistics updated with ANALYZE"
echo ""
echo "🎯 Expected performance improvements:"
echo " • 50-80% faster link lookups"
echo " • 60-90% faster analytics queries"
echo " • Better concurrent access performance"
echo " • Faster dashboard loading"

View file

@ -0,0 +1,46 @@
#!/bin/bash
echo "🔍 Checking Redis Cache in Production"
echo "======================================"
echo ""
# Your production domain
DOMAIN="https://ulo.ad"
# Test link (create one if needed)
TEST_CODE="test-redis-$(date +%s)"
TEST_URL="https://example.com/redis-test"
echo "1. Creating a test link via API..."
# You would need to create a test link first via your admin panel or API
echo ""
echo "2. Testing redirect performance..."
echo ""
# First request (should be cache MISS)
echo "First request (expected: CACHE MISS):"
time curl -I -s -o /dev/null -w "HTTP Status: %{http_code}\nTime: %{time_total}s\nRedirect: %{redirect_url}\n" "$DOMAIN/$TEST_CODE"
echo ""
echo "Waiting 1 second..."
sleep 1
# Second request (should be cache HIT)
echo ""
echo "Second request (expected: CACHE HIT):"
time curl -I -s -o /dev/null -w "HTTP Status: %{http_code}\nTime: %{time_total}s\nRedirect: %{redirect_url}\n" "$DOMAIN/$TEST_CODE"
echo ""
echo "Third request (should be cache HIT):"
time curl -I -s -o /dev/null -w "HTTP Status: %{http_code}\nTime: %{time_total}s\nRedirect: %{redirect_url}\n" "$DOMAIN/$TEST_CODE"
echo ""
echo "======================================"
echo "✅ If the 2nd and 3rd requests are faster, cache is working!"
echo ""
echo "Tips for verification:"
echo "- Check your server logs for 'Cache HIT!' messages"
echo "- First visit should show 'Cache MISS'"
echo "- Subsequent visits should show 'Cache HIT!'"
echo "- Cache TTL: 5 min (normal) or 24h (popular links)"

View file

@ -0,0 +1,12 @@
#!/bin/bash
echo "Creating PocketBase admin account..."
cd backend
# Use expect or echo to provide input
echo -e "till.schneider@memoro.ai\np0ck3t-RA1N\np0ck3t-RA1N" | ./pocketbase superuser create
echo "Admin account created!"
echo "Email: till.schneider@memoro.ai"
echo "Password: p0ck3t-RA1N"

View file

@ -0,0 +1,425 @@
#!/usr/bin/env node
/**
* Creates PocketBase collections via API
* Run: node scripts/create-collections.mjs
*/
import PocketBase from 'pocketbase';
const pb = new PocketBase('http://localhost:8090');
// Admin credentials
const ADMIN_EMAIL = 'till.schneider@memoro.ai';
const ADMIN_PASSWORD = 'p0ck3t-RAJ';
async function createCollections() {
console.log('🔧 Creating PocketBase Collections...\n');
try {
// 1. Authenticate as admin
console.log('🔐 Authenticating as admin...');
await pb.admins.authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD);
console.log('✅ Authenticated\n');
// 2. Create Links Collection
console.log('📦 Creating Links collection...');
try {
await pb.collections.create({
name: 'links',
type: 'base',
fields: [
{
name: 'short_code',
type: 'text',
required: true,
options: {
min: 3,
max: 50,
pattern: '^[a-zA-Z0-9_/-]+$'
}
},
{
name: 'custom_code',
type: 'text',
required: false
},
{
name: 'original_url',
type: 'url',
required: true
},
{
name: 'title',
type: 'text',
required: false,
options: {
max: 200
}
},
{
name: 'description',
type: 'text',
required: false,
options: {
max: 500
}
},
{
name: 'user_id',
type: 'relation',
required: false,
options: {
collectionId: '_pb_users_auth_',
cascadeDelete: true,
maxSelect: 1
}
},
{
name: 'is_active',
type: 'bool',
required: false
},
{
name: 'password',
type: 'text',
required: false
},
{
name: 'max_clicks',
type: 'number',
required: false,
options: {
min: 0
}
},
{
name: 'expires_at',
type: 'date',
required: false
},
{
name: 'click_count',
type: 'number',
required: false,
options: {
default: 0
}
},
{
name: 'qr_code',
type: 'file',
required: false,
options: {
maxSelect: 1,
maxSize: 5242880
}
},
{
name: 'tags',
type: 'json',
required: false
},
{
name: 'utm_source',
type: 'text',
required: false
},
{
name: 'utm_medium',
type: 'text',
required: false
},
{
name: 'utm_campaign',
type: 'text',
required: false
}
],
indexes: [
'CREATE UNIQUE INDEX idx_short_code ON links (short_code)'
],
listRule: '',
viewRule: '',
createRule: '@request.auth.id != ""',
updateRule: '@request.auth.id = user_id',
deleteRule: '@request.auth.id = user_id'
});
console.log('✅ Links collection created');
} catch (e) {
if (e.response?.message?.includes('already exists')) {
console.log('⚠️ Links collection already exists');
} else {
throw e;
}
}
// 3. Create Clicks Collection
console.log('📦 Creating Clicks collection...');
try {
await pb.collections.create({
name: 'clicks',
type: 'base',
fields: [
{
name: 'link_id',
type: 'relation',
required: true,
options: {
collectionId: 'links',
cascadeDelete: true,
maxSelect: 1
}
},
{
name: 'ip_hash',
type: 'text',
required: false
},
{
name: 'user_agent',
type: 'text',
required: false
},
{
name: 'referer',
type: 'text',
required: false
},
{
name: 'browser',
type: 'text',
required: false
},
{
name: 'device_type',
type: 'text',
required: false
},
{
name: 'os',
type: 'text',
required: false
},
{
name: 'country',
type: 'text',
required: false
},
{
name: 'city',
type: 'text',
required: false
},
{
name: 'clicked_at',
type: 'date',
required: false
}
],
listRule: '',
viewRule: '',
createRule: '',
updateRule: null,
deleteRule: '@request.auth.id = link_id.user_id'
});
console.log('✅ Clicks collection created');
} catch (e) {
if (e.response?.message?.includes('already exists')) {
console.log('⚠️ Clicks collection already exists');
} else {
throw e;
}
}
// 4. Create Accounts Collection
console.log('📦 Creating Accounts collection...');
try {
await pb.collections.create({
name: 'accounts',
type: 'base',
fields: [
{
name: 'name',
type: 'text',
required: true
},
{
name: 'owner',
type: 'relation',
required: true,
options: {
collectionId: '_pb_users_auth_',
cascadeDelete: true,
maxSelect: 1
}
},
{
name: 'members',
type: 'relation',
required: false,
options: {
collectionId: '_pb_users_auth_',
cascadeDelete: false,
maxSelect: null
}
},
{
name: 'isActive',
type: 'bool',
required: false,
options: {
default: true
}
},
{
name: 'planType',
type: 'select',
required: false,
options: {
values: ['free', 'team', 'enterprise']
}
},
{
name: 'settings',
type: 'json',
required: false
}
],
listRule: '@request.auth.id = owner || @request.auth.id ?~ members',
viewRule: '@request.auth.id = owner || @request.auth.id ?~ members',
createRule: '@request.auth.id != ""',
updateRule: '@request.auth.id = owner',
deleteRule: '@request.auth.id = owner'
});
console.log('✅ Accounts collection created');
} catch (e) {
if (e.response?.message?.includes('already exists')) {
console.log('⚠️ Accounts collection already exists');
} else {
throw e;
}
}
// 5. Update Users Collection
console.log('📦 Updating Users collection...');
try {
const usersCollection = await pb.collections.getOne('_pb_users_auth_');
// Add custom fields to users
const updatedFields = [
...usersCollection.fields,
{
name: 'bio',
type: 'text',
required: false
},
{
name: 'website',
type: 'url',
required: false
},
{
name: 'location',
type: 'text',
required: false
},
{
name: 'github',
type: 'text',
required: false
},
{
name: 'twitter',
type: 'text',
required: false
},
{
name: 'linkedin',
type: 'text',
required: false
},
{
name: 'instagram',
type: 'text',
required: false
},
{
name: 'publicProfile',
type: 'bool',
required: false,
options: {
default: false
}
},
{
name: 'showClickStats',
type: 'bool',
required: false,
options: {
default: true
}
},
{
name: 'isPremium',
type: 'bool',
required: false,
options: {
default: false
}
},
{
name: 'stripeCustomerId',
type: 'text',
required: false
},
{
name: 'stripeSubscriptionId',
type: 'text',
required: false
},
{
name: 'subscriptionStatus',
type: 'text',
required: false
},
{
name: 'planType',
type: 'select',
required: false,
options: {
values: ['free', 'monthly', 'yearly', 'lifetime']
}
}
];
// Filter out duplicates
const fieldNames = new Set();
const uniqueFields = updatedFields.filter(field => {
if (fieldNames.has(field.name)) {
return false;
}
fieldNames.add(field.name);
return true;
});
await pb.collections.update('_pb_users_auth_', {
...usersCollection,
fields: uniqueFields
});
console.log('✅ Users collection updated');
} catch (e) {
console.log('⚠️ Could not update Users collection:', e.message);
}
console.log('\n✅ All collections created successfully!');
console.log('\n📝 Next step: Run the seed script');
console.log(' node scripts/seed-local-db.js');
} catch (error) {
console.error('❌ Error:', error);
process.exit(1);
}
}
createCollections();

View file

@ -0,0 +1,320 @@
#!/usr/bin/env node
/**
* Script to create default templates in the unified cards collection
* Run this with: node scripts/create-default-templates.js
*/
const PocketBase = require('pocketbase').default;
const pb = new PocketBase('http://127.0.0.1:8090');
async function createDefaultTemplates() {
try {
// Admin authentication
await pb.admins.authWithPassword(
process.env.POCKETBASE_ADMIN_EMAIL || 'admin@example.com',
process.env.POCKETBASE_ADMIN_PASSWORD || 'admin123456'
);
console.log('✅ Authenticated as admin');
// Default template configurations
const templates = [
{
type: 'template',
visibility: 'public',
is_featured: true,
allow_duplication: true,
category: 'personal',
variant: 'default',
config: {
mode: 'beginner',
modules: [
{
id: 'header-1',
type: 'header',
props: {
title: 'John Doe',
subtitle: 'Software Developer',
avatar: '/api/files/_pb_users_auth_/placeholder/avatar.jpg'
},
order: 0
},
{
id: 'content-1',
type: 'content',
props: {
text: 'Passionate about creating amazing digital experiences and building innovative solutions.'
},
order: 1
},
{
id: 'links-1',
type: 'links',
props: {
links: [
{ label: 'GitHub', href: 'https://github.com', icon: '🔗' },
{ label: 'LinkedIn', href: 'https://linkedin.com', icon: '💼' },
{ label: 'Portfolio', href: 'https://example.com', icon: '🌐' }
],
style: 'button',
layout: 'vertical'
},
order: 2
}
],
layout: {
padding: '1.5rem',
gap: '1rem',
maxWidth: '400px'
},
animations: {
hover: true,
entrance: 'fade'
}
},
metadata: {
name: 'Simple Profile',
description: 'A clean and simple profile card perfect for personal branding',
version: '1.0.0'
},
constraints: {
aspectRatio: '16/9'
},
tags: ['profile', 'minimal', 'clean', 'personal'],
usage_count: 0,
likes_count: 0
},
{
type: 'template',
visibility: 'public',
is_featured: true,
allow_duplication: true,
category: 'creative',
variant: 'glass',
config: {
mode: 'beginner',
modules: [
{
id: 'header-1',
type: 'header',
props: {
title: 'Creative Portfolio',
subtitle: 'Design • Photography • Art',
avatar: '/api/files/_pb_users_auth_/placeholder/creative-avatar.jpg'
},
order: 0
},
{
id: 'gallery-1',
type: 'gallery',
props: {
images: [
'/api/files/_pb_users_auth_/placeholder/work1.jpg',
'/api/files/_pb_users_auth_/placeholder/work2.jpg',
'/api/files/_pb_users_auth_/placeholder/work3.jpg'
],
layout: 'grid',
columns: 3
},
order: 1
},
{
id: 'content-1',
type: 'content',
props: {
text: 'Bringing ideas to life through visual storytelling and innovative design solutions.'
},
order: 2
},
{
id: 'links-1',
type: 'links',
props: {
links: [
{ label: 'Behance', href: 'https://behance.net', icon: '🎨' },
{ label: 'Instagram', href: 'https://instagram.com', icon: '📸' },
{ label: 'Dribbble', href: 'https://dribbble.com', icon: '🏀' }
],
style: 'minimal',
layout: 'horizontal'
},
order: 3
}
],
layout: {
padding: '1.5rem',
gap: '1.2rem',
maxWidth: '450px'
},
animations: {
hover: true,
entrance: 'slide'
}
},
metadata: {
name: 'Creative Portfolio',
description: 'Showcase your creative work with this visually appealing portfolio card',
version: '1.0.0'
},
constraints: {
aspectRatio: '4/3'
},
tags: ['creative', 'portfolio', 'art', 'design', 'gallery'],
usage_count: 0,
likes_count: 0
},
{
type: 'template',
visibility: 'public',
is_featured: false,
allow_duplication: true,
category: 'minimal',
variant: 'minimal',
config: {
mode: 'beginner',
modules: [
{
id: 'header-1',
type: 'header',
props: {
title: 'Jane Smith',
subtitle: 'Designer & Developer'
},
order: 0
},
{
id: 'links-1',
type: 'links',
props: {
links: [
{ label: 'Website', href: 'https://janesmith.dev', icon: '🌐' },
{ label: 'Email', href: 'mailto:jane@example.com', icon: '✉️' }
],
style: 'text',
layout: 'vertical'
},
order: 1
}
],
layout: {
padding: '1rem',
gap: '0.8rem',
maxWidth: '300px'
}
},
metadata: {
name: 'Minimal Card',
description: 'Clean and minimal design focusing on essential information only',
version: '1.0.0'
},
constraints: {
aspectRatio: '1/1'
},
tags: ['minimal', 'simple', 'clean', 'text'],
usage_count: 0,
likes_count: 0
},
{
type: 'template',
visibility: 'public',
is_featured: false,
allow_duplication: true,
category: 'social',
variant: 'hero',
config: {
mode: 'beginner',
modules: [
{
id: 'header-1',
type: 'header',
props: {
title: '@socialinfluencer',
subtitle: 'Content Creator & Influencer',
avatar: '/api/files/_pb_users_auth_/placeholder/influencer-avatar.jpg',
verified: true
},
order: 0
},
{
id: 'stats-1',
type: 'stats',
props: {
stats: [
{ label: 'Followers', value: '10.2K', icon: '👥' },
{ label: 'Posts', value: '450', icon: '📱' },
{ label: 'Engagement', value: '5.8%', icon: '💝' }
],
layout: 'compact'
},
order: 1
},
{
id: 'content-1',
type: 'content',
props: {
text: 'Sharing daily inspiration, lifestyle tips, and behind-the-scenes moments ✨'
},
order: 2
},
{
id: 'links-1',
type: 'links',
props: {
links: [
{ label: 'Instagram', href: 'https://instagram.com', icon: '📸' },
{ label: 'TikTok', href: 'https://tiktok.com', icon: '🎵' },
{ label: 'YouTube', href: 'https://youtube.com', icon: '🎥' },
{ label: 'Patreon', href: 'https://patreon.com', icon: '💖' }
],
style: 'button',
layout: 'grid',
columns: 2
},
order: 3
}
],
layout: {
padding: '1.5rem',
gap: '1rem',
maxWidth: '400px'
},
animations: {
hover: true,
entrance: 'bounce'
}
},
metadata: {
name: 'Social Media Hub',
description:
'Perfect for influencers and content creators to showcase their social presence',
version: '1.0.0'
},
constraints: {
aspectRatio: '9/16'
},
tags: ['social', 'influencer', 'content', 'creator', 'instagram'],
usage_count: 0,
likes_count: 0
}
];
// Create templates
for (const template of templates) {
try {
const result = await pb.collection('cards').create(template);
console.log(`✅ Created template: ${template.metadata.name} (ID: ${result.id})`);
} catch (error) {
console.error(`❌ Failed to create template: ${template.metadata.name}`, error);
}
}
console.log('🎉 Successfully created default templates!');
} catch (error) {
console.error('❌ Error creating templates:', error);
}
}
// Run the script
createDefaultTemplates();

View file

@ -0,0 +1,389 @@
#!/usr/bin/env node
/**
* Script to create the unified cards collection in PocketBase
* Run this with: node scripts/create-unified-cards-collection.js
*/
const PocketBase = require('pocketbase');
const pb = new PocketBase('http://127.0.0.1:8090');
async function createUnifiedCardsCollection() {
try {
// Admin authentication
await pb.admins.authWithPassword(
process.env.POCKETBASE_ADMIN_EMAIL || 'admin@example.com',
process.env.POCKETBASE_ADMIN_PASSWORD || 'admin123456'
);
console.log('✅ Authenticated as admin');
// Define the unified cards collection schema
const collection = {
name: 'cards',
type: 'base',
schema: [
// Core relationships
{
name: 'user_id',
type: 'relation',
required: false, // null for system templates
options: {
collectionId: '_pb_users_auth_',
cascadeDelete: true,
minSelect: null,
maxSelect: 1,
displayFields: ['email', 'username']
}
},
// Card type and origin
{
name: 'type',
type: 'select',
required: true,
options: {
values: ['user', 'template', 'system'],
maxSelect: 1
}
},
{
name: 'source',
type: 'select',
required: false,
options: {
values: ['created', 'duplicated', 'imported', 'migrated'],
maxSelect: 1
}
},
{
name: 'template_id',
type: 'relation',
required: false,
options: {
collectionId: 'cards', // Self-reference
cascadeDelete: false,
minSelect: null,
maxSelect: 1,
displayFields: ['metadata']
}
},
// Card configuration (unified structure)
{
name: 'config',
type: 'json',
required: true,
options: {
maxSize: 1000000 // 1MB max
}
},
{
name: 'metadata',
type: 'json',
required: false,
options: {
maxSize: 100000 // 100KB max
}
},
{
name: 'constraints',
type: 'json',
required: false,
options: {
maxSize: 10000 // 10KB max
}
},
// Organization
{
name: 'page',
type: 'text',
required: false,
options: {
min: 0,
max: 100,
pattern: ''
}
},
{
name: 'position',
type: 'number',
required: false,
options: {
min: 0,
max: 9999,
noDecimal: true
}
},
{
name: 'folder_id',
type: 'relation',
required: false,
options: {
collectionId: 'folders',
cascadeDelete: false,
minSelect: null,
maxSelect: 1,
displayFields: ['name']
}
},
// Visibility and sharing
{
name: 'visibility',
type: 'select',
required: true,
options: {
values: ['private', 'public', 'unlisted'],
maxSelect: 1
}
},
{
name: 'is_featured',
type: 'bool',
required: false
},
{
name: 'allow_duplication',
type: 'bool',
required: false
},
// Statistics
{
name: 'usage_count',
type: 'number',
required: false,
options: {
min: 0,
max: 999999,
noDecimal: true
}
},
{
name: 'likes_count',
type: 'number',
required: false,
options: {
min: 0,
max: 999999,
noDecimal: true
}
},
// Search and categorization
{
name: 'tags',
type: 'json',
required: false,
options: {
maxSize: 10000
}
},
{
name: 'category',
type: 'select',
required: false,
options: {
values: ['personal', 'creative', 'minimal', 'social', 'portfolio', 'other'],
maxSelect: 1
}
},
// Variant for styling
{
name: 'variant',
type: 'select',
required: false,
options: {
values: ['default', 'compact', 'hero', 'minimal', 'glass', 'gradient'],
maxSelect: 1
}
}
],
// Indexes for performance
indexes: [
'CREATE INDEX idx_cards_user_id ON cards (user_id)',
'CREATE INDEX idx_cards_type ON cards (type)',
'CREATE INDEX idx_cards_page ON cards (page)',
'CREATE INDEX idx_cards_visibility ON cards (visibility)',
'CREATE INDEX idx_cards_template_id ON cards (template_id)',
'CREATE INDEX idx_cards_position ON cards (position)'
],
// API Rules
listRule:
'@request.auth.id = user_id || visibility = "public" || (visibility = "unlisted" && @request.query.id != "")',
viewRule: '@request.auth.id = user_id || visibility != "private"',
createRule: '@request.auth.id != ""',
updateRule:
'@request.auth.id = user_id || (@request.auth.id != "" && type = "system" && @request.auth.admin = true)',
deleteRule: '@request.auth.id = user_id && type != "system"',
options: {}
};
// Create the collection
const result = await pb.collections.create(collection);
console.log('✅ Created unified cards collection:', result.name);
// Create some default system templates
await createDefaultTemplates(pb);
console.log('🎉 Successfully created unified cards collection!');
} catch (error) {
console.error('❌ Error creating collection:', error);
// If collection already exists, try to update it
if (error.response?.code === 400) {
console.log('Collection might already exist. Trying to update...');
try {
const existing = await pb.collections.getOne('cards');
await pb.collections.update(existing.id, collection);
console.log('✅ Updated existing cards collection');
} catch (updateError) {
console.error('❌ Failed to update:', updateError);
}
}
}
}
async function createDefaultTemplates(pb) {
const templates = [
{
type: 'template',
visibility: 'public',
is_featured: true,
allow_duplication: true,
category: 'personal',
variant: 'default',
config: {
mode: 'beginner',
modules: [
{
id: 'header-1',
type: 'header',
props: {
title: 'John Doe',
subtitle: 'Software Developer'
},
order: 0
},
{
id: 'content-1',
type: 'content',
props: {
text: 'Passionate about creating amazing digital experiences.'
},
order: 1
},
{
id: 'links-1',
type: 'links',
props: {
links: [
{ label: 'GitHub', href: 'https://github.com', icon: '🔗' },
{ label: 'LinkedIn', href: 'https://linkedin.com', icon: '💼' }
],
style: 'button'
},
order: 2
}
]
},
metadata: {
name: 'Simple Profile',
description: 'A clean and simple profile card',
version: '1.0.0',
tags: ['profile', 'minimal', 'clean']
},
constraints: {
aspectRatio: '16/9'
},
usage_count: 0,
likes_count: 0
},
{
type: 'template',
visibility: 'public',
is_featured: true,
allow_duplication: true,
category: 'personal',
variant: 'gradient',
config: {
mode: 'advanced',
template: `
<div class="professional-card">
<h1>{{name}}</h1>
<p class="tagline">{{tagline}}</p>
<div class="contact">
<a href="mailto:{{email}}">{{email}}</a>
<a href="tel:{{phone}}">{{phone}}</a>
</div>
</div>
`,
css: `
.professional-card {
padding: 2rem;
text-align: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.tagline {
font-size: 1.2rem;
margin: 1rem 0;
opacity: 0.9;
}
.contact {
display: flex;
justify-content: center;
gap: 2rem;
margin-top: 2rem;
}
.contact a {
color: white;
text-decoration: none;
}
`,
variables: [
{ name: 'name', type: 'text', label: 'Your Name' },
{ name: 'tagline', type: 'text', label: 'Tagline' },
{ name: 'email', type: 'text', label: 'Email' },
{ name: 'phone', type: 'text', label: 'Phone' }
],
values: {
name: 'Your Name',
tagline: 'Your tagline here',
email: 'contact@example.com',
phone: '+1 234 567 890'
}
},
metadata: {
name: 'Professional Card',
description: 'Professional contact card template',
version: '1.0.0',
tags: ['professional', 'contact']
},
constraints: {
aspectRatio: '16/9'
},
usage_count: 0,
likes_count: 0
}
];
for (const template of templates) {
try {
await pb.collection('cards').create(template);
console.log(`✅ Created template: ${template.metadata.name}`);
} catch (error) {
console.error(`❌ Failed to create template: ${template.metadata.name}`, error);
}
}
}
// Run the script
createUnifiedCardsCollection();

View file

@ -0,0 +1,106 @@
import PocketBase from 'pocketbase';
const pb = new PocketBase('https://pb.ulo.ad');
console.log('Testing authentication...\n');
// Test 1: Try to create a new test user
async function testRegistration() {
const testEmail = `test_${Date.now()}@example.com`;
const testPassword = 'TestPassword123!';
console.log('1. Testing registration with:', testEmail);
try {
// Generate a random ID for PocketBase
const randomId = Math.random().toString(36).substring(2, 17);
const userData = {
id: randomId,
email: testEmail,
password: testPassword,
passwordConfirm: testPassword,
emailVisibility: true
};
const newUser = await pb.collection('users').create(userData);
console.log('✅ Registration successful! User ID:', newUser.id);
// Try to login with the new user
console.log('\n2. Testing login with newly created user...');
const authData = await pb.collection('users').authWithPassword(testEmail, testPassword);
console.log('✅ Login successful! Token:', authData.token.substring(0, 20) + '...');
return { email: testEmail, password: testPassword };
} catch (err) {
console.error('❌ Registration failed:', err.response || err);
return null;
}
}
// Test 2: Try existing user
async function testExistingUser() {
console.log('\n3. Testing with existing user: tills95@gmail.com');
const passwords = ['dev123456', 'password', '12345678', 'admin123'];
for (const password of passwords) {
try {
console.log(` Trying password: ${password}`);
const authData = await pb.collection('users').authWithPassword('tills95@gmail.com', password);
console.log('✅ Login successful with password:', password);
return true;
} catch (err) {
console.log(` ❌ Failed with: ${password}`);
}
}
return false;
}
// Test 3: Check collection rules
async function checkCollectionRules() {
console.log('\n4. Checking collection configuration...');
try {
// This will fail without admin auth, but we can see the error
const response = await fetch('https://pb.ulo.ad/api/collections/users', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
if (response.status === 403) {
console.log(' ⚠️ Collection info requires admin auth (expected)');
} else {
const data = await response.json();
console.log(' Collection info:', data);
}
} catch (err) {
console.log(' Error checking collection:', err.message);
}
}
// Run all tests
async function runTests() {
console.log('🔍 Starting authentication debug...\n');
console.log('PocketBase URL: https://pb.ulo.ad');
console.log('=====================================\n');
// Test registration and login
const newUser = await testRegistration();
// Test existing user
await testExistingUser();
// Check collection
await checkCollectionRules();
if (newUser) {
console.log('\n✅ Test user created successfully!');
console.log('Email:', newUser.email);
console.log('Password:', newUser.password);
console.log('\nYou can use these credentials to test login in the app.');
}
console.log('\n=====================================');
console.log('Debug complete!\n');
}
runTests().catch(console.error);

View file

@ -0,0 +1,41 @@
import fs from 'fs';
// Read the markdown file
const content = fs.readFileSync('docs/mail/email-templates-bilingual.md', 'utf8');
// Extract templates
const templates = {};
// Find verification template
const verificationMatch = content.match(/## 1\. E-Mail-Verifizierung.*?```html\n([\s\S]*?)```/);
if (verificationMatch) {
templates.verification = verificationMatch[1].trim();
}
// Find password reset template
const passwordMatch = content.match(/## 2\. Passwort-Reset.*?```html\n([\s\S]*?)```/);
if (passwordMatch) {
templates.passwordReset = passwordMatch[1].trim();
}
// Find email change template
const emailChangeMatch = content.match(/## 3\. E-Mail-Änderung.*?```html\n([\s\S]*?)```/);
if (emailChangeMatch) {
templates.emailChange = emailChangeMatch[1].trim();
}
// Find OTP template
const otpMatch = content.match(/## 4\. OTP.*?```html\n([\s\S]*?)```/);
if (otpMatch) {
templates.otp = otpMatch[1].trim();
}
// Find login alert template
const loginAlertMatch = content.match(/## 5\. Login-Alert.*?```html\n([\s\S]*?)```/);
if (loginAlertMatch) {
templates.loginAlert = loginAlertMatch[1].trim();
}
// Save to JSON file
fs.writeFileSync('email-templates.json', JSON.stringify(templates, null, 2));
console.log('Templates extracted to email-templates.json');

View file

@ -0,0 +1,23 @@
#!/bin/bash
# Fix field names to match PocketBase schema
echo "Fixing field names in source files..."
# Fix user -> user_id in filters and create statements
find src -type f \( -name "*.ts" -o -name "*.svelte" \) -exec sed -i '' \
-e 's/filter: `user="/filter: `user_id="/g' \
-e 's/filter: `link="/filter: `link_id="/g' \
-e 's/filter: `folder="/filter: `folder_id="/g' \
-e 's/\&\& user="/\&\& user_id="/g' \
-e 's/\&\& link="/\&\& link_id="/g' \
-e 's/\&\& folder="/\&\& folder_id="/g' {} \;
# Fix field names in create/update operations
find src -type f \( -name "*.ts" -o -name "*.svelte" \) -exec sed -i '' \
-e 's/^\([[:space:]]*\)user: /\1user_id: /g' \
-e 's/^\([[:space:]]*\)link: /\1link_id: /g' \
-e 's/^\([[:space:]]*\)folder: /\1folder_id: /g' {} \;
echo "Done! Field names have been updated."
echo "Please restart your dev server to apply changes."

25
uload/scripts/fix-imports.sh Executable file
View file

@ -0,0 +1,25 @@
#!/bin/bash
# Fix all problematic card imports
FILES=$(find src -name "*.svelte" -type f -exec grep -l "from '\$lib/components/cards/\(BaseCard\|ThemeProvider\|SafeHTMLCard\|CardListV2\|Card\)\.svelte'" {} \; 2>/dev/null)
for file in $FILES; do
echo "Fixing imports in: $file"
# Replace imports
sed -i '' "s|import.*from '\$lib/components/cards/Card\.svelte'.*|import CardRenderer from '\$lib/components/cards/CardRenderer.svelte';|g" "$file"
sed -i '' "s|import.*from '\$lib/components/cards/BaseCard\.svelte'.*||g" "$file"
sed -i '' "s|import.*from '\$lib/components/cards/ThemeProvider\.svelte'.*||g" "$file"
sed -i '' "s|import.*from '\$lib/components/cards/SafeHTMLCard\.svelte'.*||g" "$file"
sed -i '' "s|import.*from '\$lib/components/cards/CardListV2\.svelte'.*||g" "$file"
# Replace component usage (common patterns)
sed -i '' "s|<Card |<CardRenderer |g" "$file"
sed -i '' "s|<BaseCard |<CardRenderer |g" "$file"
sed -i '' "s|<SafeHTMLCard |<CardRenderer |g" "$file"
# Fix type imports
sed -i '' "s|UnifiedCard|Card|g" "$file"
done
echo "Import fixes completed!"

View file

@ -0,0 +1,86 @@
#!/bin/bash
# Script to update the links collection in PocketBase
# This adds missing fields to improve the structure
echo "Updating links collection structure..."
# PocketBase URL
PB_URL="https://pb.ulo.ad"
# Note: This script outlines the manual steps needed
# Since we can't modify the collection directly through the API due to references,
# these changes need to be made in the PocketBase Admin UI
cat << EOF
===========================================
MANUAL STEPS FOR POCKETBASE ADMIN UI
===========================================
Please log into PocketBase Admin at: ${PB_URL}/_/
1. Navigate to Collections > links
2. Add the following NEW fields:
a) use_username (Bool)
- Required: No
- Default: false
b) click_count (Number)
- Required: No
- Default: 0
- Note: Will be calculated from clicks collection
c) last_clicked_at (Date)
- Required: No
d) utm_source (Text)
- Required: No
- Max length: 255
e) utm_medium (Text)
- Required: No
- Max length: 255
f) utm_campaign (Text)
- Required: No
- Max length: 255
3. Rename field (if possible):
- "password""password_hash"
- Note: If renaming breaks references, keep as "password"
4. Update field settings:
- max_clicks: Set "Only integers" to true, Min value: 0
- click_count: Set "Only integers" to true, Min value: 0
5. Add Indexes (under "Indexes" tab):
- short_code (unique)
- user_id
- is_active
- created_by
6. Save the collection
===========================================
CODE CHANGES NEEDED AFTER DB UPDATE:
===========================================
After updating the database, update these files:
1. /src/lib/pocketbase/types.ts
- Add new fields to Link interface
2. /src/routes/(app)/my/links/+page.server.ts
- Handle use_username field in create action
- Remove references to folder_id
3. /src/routes/[code]/+page.server.ts
- Update click tracking to increment click_count
- Set last_clicked_at on each click
EOF
echo "Instructions printed. Please follow the manual steps above."

View file

@ -0,0 +1,65 @@
#!/usr/bin/env node
// Script to generate PWA icons from logo
// Since we don't have image processing libs, we'll create placeholder SVGs
const fs = require('fs');
const path = require('path');
// SVG Logo content (simple U for uLoad)
const createSvgIcon = (size) => `
<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
<rect width="${size}" height="${size}" fill="#3B82F6" rx="${size * 0.15}"/>
<text x="50%" y="55%" font-family="system-ui, -apple-system, sans-serif" font-size="${size * 0.5}px" font-weight="bold" fill="white" text-anchor="middle" dominant-baseline="middle">U</text>
</svg>
`;
// Icon sizes needed for PWA
const sizes = [72, 96, 128, 144, 152, 192, 384, 512];
// Ensure directory exists
const iconsDir = path.join(__dirname, '..', 'static', 'icons');
if (!fs.existsSync(iconsDir)) {
fs.mkdirSync(iconsDir, { recursive: true });
}
// Generate SVG icons
sizes.forEach(size => {
const filename = `icon-${size}x${size}.svg`;
const filepath = path.join(iconsDir, filename);
const content = createSvgIcon(size);
fs.writeFileSync(filepath, content.trim());
console.log(`Generated ${filename}`);
});
// Also create apple-touch-icon
const appleIcon = createSvgIcon(180);
fs.writeFileSync(path.join(iconsDir, 'apple-touch-icon.svg'), appleIcon.trim());
console.log('Generated apple-touch-icon.svg');
// Create maskable icon (with safe area padding)
const createMaskableIcon = (size) => {
const safeArea = size * 0.8; // 80% safe area
const padding = (size - safeArea) / 2;
return `
<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
<rect width="${size}" height="${size}" fill="#3B82F6"/>
<rect x="${padding}" y="${padding}" width="${safeArea}" height="${safeArea}" fill="#3B82F6" rx="${safeArea * 0.15}"/>
<text x="50%" y="55%" font-family="system-ui, -apple-system, sans-serif" font-size="${safeArea * 0.5}px" font-weight="bold" fill="white" text-anchor="middle" dominant-baseline="middle">U</text>
</svg>
`;
};
// Generate maskable icons
[192, 512].forEach(size => {
const filename = `icon-maskable-${size}x${size}.svg`;
const filepath = path.join(iconsDir, filename);
const content = createMaskableIcon(size);
fs.writeFileSync(filepath, content.trim());
console.log(`Generated ${filename}`);
});
console.log('\n✅ All PWA icons generated successfully!');

View file

@ -0,0 +1,72 @@
#!/usr/bin/env node
// Script to generate PWA icons from logo
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// SVG Logo content (simple U for uLoad)
const createSvgIcon = (size) => `
<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
<rect width="${size}" height="${size}" fill="#3B82F6" rx="${size * 0.15}"/>
<text x="50%" y="55%" font-family="system-ui, -apple-system, sans-serif" font-size="${size * 0.5}px" font-weight="bold" fill="white" text-anchor="middle" dominant-baseline="middle">U</text>
</svg>
`;
// Icon sizes needed for PWA
const sizes = [72, 96, 128, 144, 152, 192, 384, 512];
// Ensure directory exists
const iconsDir = path.join(__dirname, '..', 'static', 'icons');
if (!fs.existsSync(iconsDir)) {
fs.mkdirSync(iconsDir, { recursive: true });
}
// Generate SVG icons - but also need PNG for manifest
sizes.forEach(size => {
const filename = `icon-${size}x${size}.svg`;
const filepath = path.join(iconsDir, filename);
const content = createSvgIcon(size);
fs.writeFileSync(filepath, content.trim());
console.log(`Generated ${filename}`);
// Also create PNG placeholder (we'll use SVG as PNG alternative)
const pngFilename = `icon-${size}x${size}.png`;
const pngFilepath = path.join(iconsDir, pngFilename);
// Create a symlink or copy the SVG as PNG won't work, so we'll update manifest instead
});
// Also create apple-touch-icon
const appleIcon = createSvgIcon(180);
fs.writeFileSync(path.join(iconsDir, 'apple-touch-icon.svg'), appleIcon.trim());
console.log('Generated apple-touch-icon.svg');
// Create maskable icon (with safe area padding)
const createMaskableIcon = (size) => {
const safeArea = size * 0.8; // 80% safe area
const padding = (size - safeArea) / 2;
return `
<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
<rect width="${size}" height="${size}" fill="#3B82F6"/>
<rect x="${padding}" y="${padding}" width="${safeArea}" height="${safeArea}" fill="#3B82F6" rx="${safeArea * 0.15}"/>
<text x="50%" y="55%" font-family="system-ui, -apple-system, sans-serif" font-size="${safeArea * 0.5}px" font-weight="bold" fill="white" text-anchor="middle" dominant-baseline="middle">U</text>
</svg>
`;
};
// Generate maskable icons
[192, 512].forEach(size => {
const filename = `icon-maskable-${size}x${size}.svg`;
const filepath = path.join(iconsDir, filename);
const content = createMaskableIcon(size);
fs.writeFileSync(filepath, content.trim());
console.log(`Generated ${filename}`);
});
console.log('\n✅ All PWA icons generated successfully!');

View file

@ -0,0 +1,63 @@
#!/bin/bash
# Migration script for PocketBase links collection
# This script documents the manual steps needed to replace the old links collection
echo "🔄 Links Collection Migration Guide"
echo "=================================="
cat << EOF
⚠️ IMPORTANT: Database Migration Required
The new 'links_improved' collection has been created with all the enhanced fields:
✅ created/updated timestamps (automatic)
✅ use_username (bool)
✅ click_count (number)
✅ last_clicked_at (date)
✅ utm_source, utm_medium, utm_campaign (text)
MANUAL STEPS REQUIRED IN POCKETBASE ADMIN:
1. BACKUP FIRST (!)
- Go to: https://pb.ulo.ad/_/
- Create a backup before proceeding
2. UPDATE REFERENCES:
Because the old 'links' collection has references (clicks, linktags),
we need to update the application code first:
a) Update all references in the app from 'links' to 'links_improved'
b) Test with the new collection
c) When everything works, delete the old collection
3. COLLECTION RENAMING:
After updating the code:
- Delete the old 'links' collection
- Rename 'links_improved' to 'links'
ALTERNATIVE APPROACH (RECOMMENDED):
Instead of renaming, update the application code to use 'links_improved'
and keep it as the new name.
EOF
echo ""
echo "📋 Migration Status:"
echo "✅ New collection 'links_improved' created"
echo "✅ Test data migrated successfully"
echo "✅ All new fields working correctly"
echo "⏳ Manual steps required (see above)"
echo ""
echo "🔍 Test the new collection:"
echo "Collection ID: pbc_394542459"
echo "Test records created: 2"
echo ""
# Show the test data
echo "📊 Sample data from new collection:"
echo "=================================="
echo "Test Link 1: TEST001 - https://example.com (with UTM tracking)"
echo "Test Link 2: JxBSn7 - https://ulo.ad/my/links (migrated)"
echo ""
echo "Both records have automatic timestamps ✅"

View file

@ -0,0 +1,79 @@
#!/usr/bin/env node
// Migration script to convert use_username links to username-prefixed short_codes
// Run this script to update existing links in the database
import PocketBase from 'pocketbase';
const pb = new PocketBase('http://localhost:8090');
async function migrate() {
try {
console.log('Starting migration...');
console.log('This script will update links with use_username=true to use the new format');
// Get all links with use_username=true
const links = await pb.collection('links').getFullList({
filter: 'use_username = true',
expand: 'user_id'
});
console.log(`Found ${links.length} links to migrate`);
let successCount = 0;
let errorCount = 0;
let skippedCount = 0;
for (const link of links) {
try {
// Get the username from the expanded user
const username = link.expand?.user_id?.username;
if (!username) {
console.error(`No username found for link ${link.id} (user: ${link.user_id})`);
errorCount++;
continue;
}
// Check if already migrated (contains slash)
if (link.short_code.includes('/')) {
console.log(`Link ${link.id} already migrated: ${link.short_code}`);
skippedCount++;
continue;
}
// Create new short_code with username prefix
const newShortCode = `${username}/${link.short_code}`;
console.log(`Updating link ${link.id}: ${link.short_code} -> ${newShortCode}`);
// Update the link
await pb.collection('links').update(link.id, {
short_code: newShortCode
});
successCount++;
} catch (error) {
console.error(`Error migrating link ${link.id}:`, error.message);
errorCount++;
}
}
console.log('\n=== Migration Summary ===');
console.log(`✅ Successfully migrated: ${successCount} links`);
console.log(`⏭️ Skipped (already migrated): ${skippedCount} links`);
console.log(`❌ Errors: ${errorCount} links`);
if (successCount > 0) {
console.log('\n⚠ Important: The use_username field should be removed from the collection schema');
console.log('You can do this manually in PocketBase Admin UI');
}
} catch (error) {
console.error('Migration failed:', error);
process.exit(1);
}
}
// Run migration
migrate();

View file

@ -0,0 +1,77 @@
#!/usr/bin/env node
// Migration script to convert use_username links to username-prefixed short_codes
// This script:
// 1. Finds all links with use_username=true
// 2. Updates their short_code to include the username prefix
// 3. Removes the use_username field (done via PocketBase admin)
import PocketBase from 'pocketbase';
const pb = new PocketBase('http://localhost:8090');
async function migrate() {
try {
// Authenticate as admin (update with your credentials)
await pb.admins.authWithPassword('admin@example.com', 'your-password');
console.log('Fetching all links with use_username=true...');
// Get all links with use_username=true
const links = await pb.collection('links').getFullList({
filter: 'use_username = true',
expand: 'user_id'
});
console.log(`Found ${links.length} links to migrate`);
let successCount = 0;
let errorCount = 0;
for (const link of links) {
try {
// Get the username from the expanded user
const username = link.expand?.user_id?.username;
if (!username) {
console.error(`No username found for link ${link.id} (user: ${link.user_id})`);
errorCount++;
continue;
}
// Create new short_code with username prefix
const newShortCode = `${username}/${link.short_code}`;
console.log(`Updating link ${link.id}: ${link.short_code} -> ${newShortCode}`);
// Update the link
await pb.collection('links').update(link.id, {
short_code: newShortCode
});
successCount++;
} catch (error) {
console.error(`Error migrating link ${link.id}:`, error);
errorCount++;
}
}
console.log('\nMigration complete!');
console.log(`✅ Successfully migrated: ${successCount} links`);
console.log(`❌ Errors: ${errorCount} links`);
if (successCount > 0) {
console.log('\n⚠ Next steps:');
console.log('1. Remove the use_username field from the links collection in PocketBase admin');
console.log('2. Test that all migrated links still work');
console.log('3. Deploy the updated application code');
}
} catch (error) {
console.error('Migration failed:', error);
process.exit(1);
}
}
// Run migration
migrate();

View file

@ -0,0 +1,157 @@
#!/usr/bin/env node
/**
* Migration script to convert from shared_access to workspace system
* Run this after deploying the new workspace collections
*/
import PocketBase from 'pocketbase';
import dotenv from 'dotenv';
// Load environment variables
dotenv.config({ path: '.env.production' });
const pb = new PocketBase(process.env.PUBLIC_POCKETBASE_URL || 'https://pb.ulo.ad');
async function migrate() {
console.log('Starting migration to workspace system...');
try {
// Authenticate as admin
const adminEmail = process.env.ADMIN_EMAIL;
const adminPassword = process.env.ADMIN_PASSWORD;
if (!adminEmail || !adminPassword) {
console.error('Please set ADMIN_EMAIL and ADMIN_PASSWORD in .env.production');
process.exit(1);
}
await pb.admins.authWithPassword(adminEmail, adminPassword);
console.log('✓ Authenticated as admin');
// Get all users
const users = await pb.collection('users').getFullList();
console.log(`Found ${users.length} users to migrate`);
// Create personal workspace for each user
for (const user of users) {
try {
// Check if personal workspace already exists
const existingWorkspaces = await pb.collection('workspaces').getList(1, 1, {
filter: `owner="${user.id}" && type="personal"`
});
if (existingWorkspaces.items.length === 0) {
// Create personal workspace
const workspace = await pb.collection('workspaces').create({
name: `${user.name || user.email}'s Workspace`,
owner: user.id,
type: 'personal',
subscription_status: user.subscription_status || 'free',
description: 'Personal workspace'
});
console.log(`✓ Created personal workspace for ${user.email}`);
// Add user as owner in workspace_members
await pb.collection('workspace_members').create({
workspace: workspace.id,
user: user.id,
role: 'owner',
invitation_status: 'accepted',
accepted_at: new Date().toISOString()
});
} else {
console.log(`- Personal workspace already exists for ${user.email}`);
}
} catch (error) {
console.error(`✗ Error creating workspace for ${user.email}:`, error.message);
}
}
// Migrate shared_access to workspace_members
console.log('\nMigrating shared access...');
try {
const sharedAccess = await pb.collection('shared_access').getFullList({
expand: 'owner,user'
});
console.log(`Found ${sharedAccess.length} shared access records`);
for (const access of sharedAccess) {
try {
// Find or create team workspace for the owner
let teamWorkspace;
const existingTeamWorkspaces = await pb.collection('workspaces').getList(1, 1, {
filter: `owner="${access.owner}" && type="team"`
});
if (existingTeamWorkspaces.items.length === 0) {
// Create team workspace
const ownerData = access.expand?.owner;
teamWorkspace = await pb.collection('workspaces').create({
name: `${ownerData?.name || ownerData?.email}'s Team`,
owner: access.owner,
type: 'team',
description: 'Team workspace for collaboration'
});
console.log(`✓ Created team workspace for owner ${access.owner}`);
// Add owner as owner in workspace_members
await pb.collection('workspace_members').create({
workspace: teamWorkspace.id,
user: access.owner,
role: 'owner',
invitation_status: 'accepted',
accepted_at: new Date().toISOString()
});
} else {
teamWorkspace = existingTeamWorkspaces.items[0];
}
// Check if member already exists
const existingMembers = await pb.collection('workspace_members').getList(1, 1, {
filter: `workspace="${teamWorkspace.id}" && user="${access.user}"`
});
if (existingMembers.items.length === 0) {
// Add team member
await pb.collection('workspace_members').create({
workspace: teamWorkspace.id,
user: access.user,
role: access.permissions?.manage_team ? 'admin' : 'member',
permissions: access.permissions,
invitation_status: access.invitation_status,
invitation_token: access.invitation_token,
invited_at: access.invited_at,
accepted_at: access.accepted_at
});
console.log(`✓ Migrated team member ${access.user} to workspace ${teamWorkspace.id}`);
} else {
console.log(`- Team member ${access.user} already exists in workspace`);
}
} catch (error) {
console.error(`✗ Error migrating shared access ${access.id}:`, error.message);
}
}
} catch (error) {
console.error('✗ Error fetching shared access:', error.message);
}
console.log('\n✅ Migration completed successfully!');
console.log('\nNext steps:');
console.log('1. Test the new workspace system');
console.log('2. Update any links/cards to reference workspace instead of owner');
console.log('3. Once verified, you can remove the old shared_access collection');
} catch (error) {
console.error('✗ Migration failed:', error);
process.exit(1);
}
}
// Run migration
migrate().catch(console.error);

View file

@ -0,0 +1,69 @@
-- ULoad Database Performance Optimizations
-- Diese SQL-Befehle optimieren die SQLite-Datenbank für bessere Performance
-- 1. Aktiviere WAL-Modus für bessere Concurrency
PRAGMA journal_mode=WAL;
-- 2. Optimiere Cache-Größe (8MB)
PRAGMA cache_size=8000;
-- 3. Synchronisation optimieren für bessere Performance
PRAGMA synchronous=NORMAL;
-- 4. Memory-mapped I/O aktivieren (256MB)
PRAGMA mmap_size=268435456;
-- 5. Auto-Vacuum optimieren
PRAGMA auto_vacuum=INCREMENTAL;
-- 6. Temp Store in Memory
PRAGMA temp_store=MEMORY;
-- 7. Analyze Statistiken aktualisieren
ANALYZE;
-- 8. Erstelle fehlende Indizes für bessere Performance
-- Links Collection Indizes
CREATE INDEX IF NOT EXISTS idx_links_user ON links(user);
CREATE INDEX IF NOT EXISTS idx_links_short_code ON links(short_code);
CREATE INDEX IF NOT EXISTS idx_links_active ON links(active);
CREATE INDEX IF NOT EXISTS idx_links_created ON links(created);
CREATE INDEX IF NOT EXISTS idx_links_user_active ON links(user, active);
CREATE INDEX IF NOT EXISTS idx_links_user_created ON links(user, created DESC);
-- Analytics Collection Indizes
CREATE INDEX IF NOT EXISTS idx_analytics_link ON analytics(link);
CREATE INDEX IF NOT EXISTS idx_analytics_created ON analytics(created);
CREATE INDEX IF NOT EXISTS idx_analytics_link_created ON analytics(link, created DESC);
CREATE INDEX IF NOT EXISTS idx_analytics_country ON analytics(country);
CREATE INDEX IF NOT EXISTS idx_analytics_device ON analytics(device);
-- Users Collection Indizes (falls vorhanden)
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_created ON users(created);
-- Tags Collection Indizes (falls vorhanden)
CREATE INDEX IF NOT EXISTS idx_tags_user_id ON tags(user_id);
CREATE INDEX IF NOT EXISTS idx_tags_slug ON tags(slug);
CREATE INDEX IF NOT EXISTS idx_tags_is_public ON tags(is_public);
-- Link Tags Junction Table Indizes
CREATE INDEX IF NOT EXISTS idx_linktags_link_id ON linktags(link_id);
CREATE INDEX IF NOT EXISTS idx_linktags_tag_id ON linktags(tag_id);
CREATE INDEX IF NOT EXISTS idx_linktags_composite ON linktags(link_id, tag_id);
-- Clicks/Analytics Performance Indizes für häufige Queries
CREATE INDEX IF NOT EXISTS idx_analytics_link_country ON analytics(link, country);
CREATE INDEX IF NOT EXISTS idx_analytics_link_device ON analytics(link, device);
-- Composite Index für Dashboard Queries
CREATE INDEX IF NOT EXISTS idx_links_user_active_created ON links(user, active, created DESC);
-- Vacuum und Reindex für sofortige Verbesserung
VACUUM;
REINDEX;
-- Statistiken neu berechnen
ANALYZE;

243
uload/scripts/seed-local-db.js Executable file
View file

@ -0,0 +1,243 @@
#!/usr/bin/env node
/**
* Seed Script for Local PocketBase Development
*
* This script creates test data for local development.
*
* Usage:
* 1. Make sure PocketBase is running locally (http://localhost:8090)
* 2. Run: node scripts/seed-local-db.js
*/
import PocketBase from 'pocketbase';
import { randomBytes } from 'crypto';
// Configuration
const POCKETBASE_URL = process.env.PUBLIC_POCKETBASE_URL || 'http://localhost:8090';
const ADMIN_EMAIL = process.env.POCKETBASE_ADMIN_EMAIL || 'admin@localhost';
const ADMIN_PASSWORD = process.env.POCKETBASE_ADMIN_PASSWORD || 'admin123456';
console.log('🌱 Seeding Local PocketBase Database');
console.log('=====================================\n');
console.log(`📍 PocketBase URL: ${POCKETBASE_URL}`);
const pb = new PocketBase(POCKETBASE_URL);
pb.autoCancellation(false);
// Test data
const testUsers = [
{
email: 'test@localhost',
password: 'test123456',
passwordConfirm: 'test123456',
username: 'testuser',
name: 'Test User',
emailVisibility: true
},
{
email: 'demo@localhost',
password: 'demo123456',
passwordConfirm: 'demo123456',
username: 'demouser',
name: 'Demo User',
emailVisibility: true
}
];
const testLinks = [
{
short_code: 'test1',
original_url: 'https://example.com',
title: 'Test Link 1',
description: 'This is a test link for development',
is_active: true,
click_limit: null,
expires_at: null,
password: null
},
{
short_code: 'test2',
original_url: 'https://google.com',
title: 'Google Test',
description: 'Link to Google for testing',
is_active: true,
click_limit: 100,
expires_at: null,
password: null
},
{
short_code: 'protected',
original_url: 'https://github.com',
title: 'Protected Link',
description: 'Password protected link',
is_active: true,
click_limit: null,
expires_at: null,
password: 'secret123'
},
{
short_code: 'expired',
original_url: 'https://stackoverflow.com',
title: 'Expired Link',
description: 'This link has expired',
is_active: true,
click_limit: null,
expires_at: new Date(Date.now() - 86400000).toISOString(), // Yesterday
password: null
}
];
async function seedDatabase() {
try {
// Step 1: Try to authenticate as admin
console.log('🔐 Authenticating as admin...');
try {
await pb.admins.authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD);
console.log('✅ Admin authenticated\n');
} catch (error) {
console.log('⚠️ Admin auth failed. You may need to:');
console.log(' 1. Create admin account at http://localhost:8090/_/');
console.log(' 2. Update POCKETBASE_ADMIN_EMAIL and POCKETBASE_ADMIN_PASSWORD\n');
// Try to continue without admin auth (some operations might fail)
}
// Step 2: Create test users
console.log('👥 Creating test users...');
const createdUsers = [];
for (const userData of testUsers) {
try {
const user = await pb.collection('users').create(userData);
createdUsers.push(user);
console.log(` ✅ Created user: ${userData.email}`);
} catch (error) {
if (error.response?.data?.email?.message?.includes('already exists')) {
console.log(` ⚠️ User ${userData.email} already exists`);
// Try to get existing user
try {
const users = await pb.collection('users').getList(1, 1, {
filter: `email = "${userData.email}"`
});
if (users.items.length > 0) {
createdUsers.push(users.items[0]);
}
} catch (e) {
console.log(` ❌ Could not fetch existing user: ${userData.email}`);
}
} else {
console.log(` ❌ Failed to create user ${userData.email}:`, error.message);
}
}
}
console.log('');
// Step 3: Create test links
console.log('🔗 Creating test links...');
// Use the first created user as the owner
const ownerId = createdUsers[0]?.id;
for (const linkData of testLinks) {
try {
// Add owner if we have one
if (ownerId) {
linkData.user_id = ownerId;
}
// Generate a random custom code if needed
if (!linkData.custom_code) {
linkData.custom_code = linkData.short_code;
}
const link = await pb.collection('links').create(linkData);
console.log(` ✅ Created link: ${linkData.short_code} -> ${linkData.original_url}`);
} catch (error) {
if (error.response?.data?.short_code?.message?.includes('already exists')) {
console.log(` ⚠️ Link ${linkData.short_code} already exists`);
} else {
console.log(` ❌ Failed to create link ${linkData.short_code}:`, error.message);
}
}
}
console.log('');
// Step 4: Create some test clicks
console.log('📊 Creating test click data...');
try {
// Get one of the links we created
const links = await pb.collection('links').getList(1, 1, {
filter: 'short_code = "test1"'
});
if (links.items.length > 0) {
const link = links.items[0];
// Create some fake clicks
const clickData = [
{
link_id: link.id,
ip_hash: '127.0.0.1',
user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
browser: 'Chrome',
device_type: 'Desktop',
os: 'macOS',
country: 'Germany',
city: 'Munich',
clicked_at: new Date().toISOString()
},
{
link_id: link.id,
ip_hash: '192.168.1.1',
user_agent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0)',
browser: 'Safari',
device_type: 'Mobile',
os: 'iOS',
country: 'USA',
city: 'New York',
clicked_at: new Date(Date.now() - 3600000).toISOString() // 1 hour ago
}
];
for (const click of clickData) {
try {
await pb.collection('clicks').create(click);
console.log(` ✅ Created test click from ${click.country}`);
} catch (error) {
console.log(` ❌ Failed to create click:`, error.message);
}
}
}
} catch (error) {
console.log(` ⚠️ Could not create click data:`, error.message);
}
console.log('');
// Summary
console.log('=====================================');
console.log('🎉 Database seeding complete!\n');
console.log('📝 Test Accounts:');
console.log(' Email: test@localhost');
console.log(' Password: test123456\n');
console.log('🔗 Test Links:');
console.log(' http://localhost:5173/test1 - Normal link');
console.log(' http://localhost:5173/test2 - Link with click limit');
console.log(' http://localhost:5173/protected - Password: secret123');
console.log(' http://localhost:5173/expired - Expired link\n');
console.log('👉 Next: Open http://localhost:5173 and test the app!');
} catch (error) {
console.error('❌ Seeding failed:', error);
process.exit(1);
}
}
// Run the seeding
seedDatabase().then(() => {
process.exit(0);
}).catch(error => {
console.error('Fatal error:', error);
process.exit(1);
});

View file

@ -0,0 +1,59 @@
import PocketBase from 'pocketbase';
const pb = new PocketBase('https://pb.ulo.ad');
async function createTestUser() {
const email = 'test@example.com';
const password = 'test123456';
const randomId = Math.random().toString(36).substring(2, 17);
console.log('Creating test user...');
console.log('Email:', email);
console.log('Password:', password);
try {
// First check if user already exists
const existing = await pb.collection('users').getList(1, 1, {
filter: `email = "${email}"`
});
if (existing.items.length > 0) {
console.log('✅ Test user already exists!');
console.log('ID:', existing.items[0].id);
// Try to login
try {
await pb.collection('users').authWithPassword(email, password);
console.log('✅ Login successful with existing user!');
} catch (err) {
console.log('⚠️ User exists but password might be different');
}
return;
}
} catch (err) {
// User doesn't exist, continue to create
}
try {
const userData = {
id: randomId,
email: email,
password: password,
passwordConfirm: password,
emailVisibility: true,
username: 'testuser'
};
const newUser = await pb.collection('users').create(userData);
console.log('✅ Test user created successfully!');
console.log('ID:', newUser.id);
// Verify login works
const authData = await pb.collection('users').authWithPassword(email, password);
console.log('✅ Login verified! Token:', authData.token.substring(0, 20) + '...');
} catch (err) {
console.error('❌ Failed to create test user:', err.response || err);
}
}
createTestUser().catch(console.error);

View file

@ -0,0 +1,54 @@
import PocketBase from 'pocketbase';
const pb = new PocketBase('http://localhost:8090');
// Test email - ersetze mit deiner E-Mail
const testEmail = 'test@example.com'; // HIER DEINE EMAIL EINGEBEN!
async function testEmailFunctions() {
console.log('🔧 Testing PocketBase Email Functions...\n');
try {
// 1. Test Password Reset
console.log('1⃣ Testing Password Reset Email...');
try {
await pb.collection('users').requestPasswordReset(testEmail);
console.log('✅ Password reset email request sent successfully');
console.log(' Check your inbox for the password reset email\n');
} catch (error) {
console.error('❌ Password reset failed:', error.message);
console.log(' Error details:', error.response?.data || error);
}
// 2. Test Verification Email (needs existing unverified user)
console.log('2⃣ Testing Verification Email...');
try {
await pb.collection('users').requestVerification(testEmail);
console.log('✅ Verification email request sent successfully');
console.log(' Check your inbox for the verification email\n');
} catch (error) {
console.error('❌ Verification email failed:', error.message);
console.log(' Error details:', error.response?.data || error);
}
// 3. Check PocketBase health
console.log('3⃣ Checking PocketBase health...');
try {
const health = await pb.health.check();
console.log('✅ PocketBase is healthy:', health);
} catch (error) {
console.error('❌ PocketBase health check failed:', error.message);
}
} catch (error) {
console.error('❌ General error:', error);
}
console.log('\n📌 Important checks:');
console.log('1. Is PocketBase running? (http://localhost:8090)');
console.log('2. Are SMTP settings configured in PocketBase?');
console.log('3. Is the Application URL set in PocketBase settings?');
console.log('4. Check PocketBase logs: Admin → Logs');
}
// Run tests
testEmailFunctions();

30
uload/scripts/test-env.js Normal file
View file

@ -0,0 +1,30 @@
// Test which PocketBase URL is being used
console.log('Testing Environment Variables:');
console.log('----------------------------');
console.log('NODE_ENV:', process.env.NODE_ENV);
console.log('PUBLIC_POCKETBASE_URL:', process.env.PUBLIC_POCKETBASE_URL);
// Check if .env file exists
const fs = require('fs');
const path = require('path');
const envFiles = ['.env', '.env.development', '.env.production'];
envFiles.forEach(file => {
const filePath = path.join(__dirname, file);
if (fs.existsSync(filePath)) {
console.log(`\n${file} exists`);
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split('\n');
lines.forEach(line => {
if (line.includes('POCKETBASE_URL')) {
console.log(` -> ${line}`);
}
});
}
});
console.log('\nNOTE: SvelteKit/Vite loads environment variables differently.');
console.log('The app should use:');
console.log('- Development: http://localhost:8090 (from .env or fallback)');
console.log('- Production: https://pb.ulo.ad (from .env.production)');

View file

@ -0,0 +1,34 @@
// Test which PocketBase URL is being used
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
console.log('Testing Environment Variables:');
console.log('----------------------------');
console.log('NODE_ENV:', process.env.NODE_ENV);
console.log('PUBLIC_POCKETBASE_URL:', process.env.PUBLIC_POCKETBASE_URL);
const envFiles = ['.env', '.env.development', '.env.production'];
envFiles.forEach(file => {
const filePath = join(__dirname, file);
if (existsSync(filePath)) {
console.log(`\n${file} exists`);
const content = readFileSync(filePath, 'utf8');
const lines = content.split('\n');
lines.forEach(line => {
if (line.includes('POCKETBASE_URL')) {
console.log(` -> ${line}`);
}
});
}
});
console.log('\nNOTE: SvelteKit/Vite loads environment variables differently.');
console.log('The app should use:');
console.log('- Development: http://localhost:8090 (from .env or fallback)');
console.log('- Production: https://pb.ulo.ad (from .env.production)');

View file

@ -0,0 +1,48 @@
#!/usr/bin/env node
import Redis from 'ioredis';
console.log('🔍 Testing Local Redis Connection\n');
console.log('==================================\n');
// Local Redis configuration
const redis = new Redis({
host: 'localhost',
port: 6379,
// No password for local Redis
});
async function testConnection() {
try {
// Test ping
const pong = await redis.ping();
console.log('✅ Redis is running locally!');
console.log(` Response: ${pong}`);
// Test set/get
await redis.set('test:local', 'Hello from local Redis!');
const value = await redis.get('test:local');
console.log(` Test value: ${value}`);
// Clean up
await redis.del('test:local');
console.log('\n✅ All tests passed! Local Redis is working.');
console.log('\n📝 Next steps:');
console.log(' 1. Run "npm run dev" to start the app');
console.log(' 2. Visit a short link');
console.log(' 3. Check console for "Redis: Connected successfully"');
console.log(' 4. Refresh the link - should be faster (cache hit)');
} catch (error) {
console.error('❌ Redis connection failed:', error.message);
console.log('\nTroubleshooting:');
console.log(' 1. Check if Redis is running: brew services list');
console.log(' 2. Start Redis: brew services start redis');
console.log(' 3. Test connection: redis-cli ping');
} finally {
redis.disconnect();
}
}
testConnection();

View file

@ -0,0 +1,35 @@
import PocketBase from 'pocketbase';
const pb = new PocketBase('http://localhost:8090');
console.log('Testing PocketBase connection...');
try {
// Test health endpoint
const health = await fetch('http://localhost:8090/api/health');
const healthData = await health.json();
console.log('Health check:', healthData);
// Try to create a test user
const testUser = await pb.collection('users').create({
email: `test${Date.now()}@example.com`,
password: 'testpassword123',
passwordConfirm: 'testpassword123',
username: `testuser${Date.now()}`,
name: 'Test User',
emailVisibility: true
});
console.log('✅ User created successfully:', testUser.id);
console.log('Username:', testUser.username);
console.log('Email:', testUser.email);
// Clean up - delete test user
await pb.collection('users').delete(testUser.id);
console.log('✅ Test user deleted');
} catch (error) {
console.error('❌ Error:', error.message);
if (error.data) {
console.error('Error details:', JSON.stringify(error.data, null, 2));
}
}

View file

@ -0,0 +1,40 @@
import PocketBase from 'pocketbase';
const pb = new PocketBase('https://pb.ulo.ad');
async function test() {
try {
console.log('Testing connection to PocketBase Production...');
// Test 1: Get all users
const users = await pb.collection('users').getList(1, 10);
console.log('Found users:', users.items.length);
users.items.forEach((u) => console.log(` - username: "${u.username}" (${u.email})`));
// Test 2: Find specific user
try {
const user = await pb.collection('users').getFirstListItem('username="memoro"');
console.log('\nFound specific user:', user.username, user.id);
// Test 3: Get folders for user
const folders = await pb.collection('folders').getList(1, 50, {
filter: `user_id="${user.id}" && is_public=true`
});
console.log('Public folders:', folders.items.length);
// Test 4: Get links for user
const links = await pb.collection('links').getList(1, 100, {
filter: `user_id="${user.id}" && is_active=true`
});
console.log('Active links:', links.items.length);
} catch (err) {
console.log('\nCould not find user "memoro"');
console.log('Error:', err.message);
}
} catch (err) {
console.error('Error:', err.message);
console.error('Full error:', err);
}
}
test();

35
uload/scripts/test-pb.js Normal file
View file

@ -0,0 +1,35 @@
import PocketBase from 'pocketbase';
const pb = new PocketBase('http://127.0.0.1:8090');
async function test() {
try {
console.log('Testing connection to PocketBase...');
// Test 1: Get all users
const users = await pb.collection('users').getList(1, 10);
console.log('Found users:', users.items.length);
users.items.forEach((u) => console.log(` - username: "${u.username}" (${u.email})`));
// Test 2: Find specific user
const user = await pb.collection('users').getFirstListItem('username="memoro"');
console.log('\nFound specific user:', user.username, user.id);
// Test 3: Get folders for user
const folders = await pb.collection('folders').getList(1, 50, {
filter: `user_id="${user.id}" && is_public=true`
});
console.log('Public folders:', folders.items.length);
// Test 4: Get links for user
const links = await pb.collection('links').getList(1, 100, {
filter: `user_id="${user.id}" && is_active=true`
});
console.log('Active links:', links.items.length);
} catch (err) {
console.error('Error:', err.message);
console.error('Full error:', err);
}
}
test();

View file

@ -0,0 +1,71 @@
import PocketBase from 'pocketbase';
const POCKETBASE_URL = process.env.PUBLIC_POCKETBASE_URL || 'http://localhost:8090';
console.log('Testing PocketBase connection...');
console.log('URL:', POCKETBASE_URL);
const pb = new PocketBase(POCKETBASE_URL);
async function testConnection() {
try {
// Test 1: Check health endpoint
const health = await pb.health.check();
console.log('✓ Health check passed:', health);
// Test 2: List collections
const collections = await pb.collections.getList();
console.log(
'✓ Collections found:',
collections.items.map((c) => c.name)
);
// Test 3: Check users collection
const usersCollection = await pb.collections.getOne('users');
console.log('✓ Users collection schema:', {
name: usersCollection.name,
fields: usersCollection.schema.map((f) => ({
name: f.name,
type: f.type,
required: f.required
}))
});
// Test 4: Try to list users (might fail due to permissions)
try {
const users = await pb.collection('users').getList(1, 1);
console.log('✓ Can list users:', users.totalItems, 'total users');
} catch (e) {
console.log('⚠ Cannot list users (probably permission issue):', e.message);
}
// Test 5: Test registration endpoint
console.log('\nTesting registration capability...');
const testEmail = `test${Date.now()}@example.com`;
try {
const result = await pb.collection('users').create({
email: testEmail,
password: 'Test123456!',
passwordConfirm: 'Test123456!',
username: `testuser${Date.now()}`
});
console.log('✓ Registration test successful, user created:', result.id);
// Clean up test user
await pb.collection('users').delete(result.id);
console.log('✓ Test user cleaned up');
} catch (e) {
console.error('✗ Registration failed:', e.response || e.message);
if (e.response?.data) {
console.error('Error details:', JSON.stringify(e.response.data, null, 2));
}
}
} catch (error) {
console.error('✗ Connection failed:', error.message);
if (error.response) {
console.error('Response:', error.response);
}
process.exit(1);
}
}
testConnection();

View file

@ -0,0 +1,138 @@
import PocketBase from 'pocketbase';
// Produktions-PocketBase URL
const PROD_POCKETBASE_URL = 'http://pocketbase-xs0ccokk8s0goko4w40gwc0w.91.99.221.179.sslip.io';
console.log('Testing PRODUCTION PocketBase connection...');
console.log('URL:', PROD_POCKETBASE_URL);
console.log('----------------------------------------\n');
const pb = new PocketBase(PROD_POCKETBASE_URL);
async function testConnection() {
try {
// Test 1: Check health endpoint
console.log('1. Testing health endpoint...');
try {
const response = await fetch(`${PROD_POCKETBASE_URL}/api/health`);
const health = await response.json();
console.log('✓ Health check:', health);
} catch (e) {
console.log('✗ Health check failed:', e.message);
}
// Test 2: List collections (using MCP)
console.log('\n2. Testing collections access...');
try {
const collections = await pb.collections.getList();
console.log(
'✓ Collections found:',
collections.items.map((c) => c.name)
);
// Check for users collection specifically
const hasUsers = collections.items.some((c) => c.name === 'users');
if (hasUsers) {
console.log('✓ Users collection exists');
} else {
console.log('✗ Users collection NOT found!');
}
} catch (e) {
console.log('✗ Cannot list collections:', e.message);
}
// Test 3: Check users collection schema
console.log('\n3. Checking users collection schema...');
try {
const usersCollection = await pb.collections.getOne('users');
console.log('✓ Users collection fields:');
usersCollection.schema.forEach((field) => {
console.log(
` - ${field.name}: ${field.type} ${field.required ? '(required)' : '(optional)'}`
);
});
// Check authentication settings
console.log('\n✓ Authentication settings:');
console.log(
` - Password auth enabled: ${usersCollection.options?.allowEmailAuth || false}`
);
console.log(` - OAuth2 enabled: ${usersCollection.options?.allowOAuth2Auth || false}`);
} catch (e) {
console.log('✗ Cannot get users collection:', e.message);
}
// Test 4: Check API rules
console.log('\n4. Checking API rules for users collection...');
try {
const usersCollection = await pb.collections.getOne('users');
console.log('API Rules:');
console.log(` - List rule: ${usersCollection.listRule || 'none'}`);
console.log(` - View rule: ${usersCollection.viewRule || 'none'}`);
console.log(` - Create rule: ${usersCollection.createRule || 'none'}`);
console.log(` - Update rule: ${usersCollection.updateRule || 'none'}`);
console.log(` - Delete rule: ${usersCollection.deleteRule || 'none'}`);
} catch (e) {
console.log('✗ Cannot check API rules:', e.message);
}
// Test 5: Test registration endpoint
console.log('\n5. Testing registration endpoint...');
const testEmail = `test${Date.now()}@example.com`;
const testUsername = `testuser${Date.now()}`;
try {
console.log(` Attempting to register: ${testEmail}`);
const result = await pb.collection('users').create({
email: testEmail,
password: 'Test123456!',
passwordConfirm: 'Test123456!',
username: testUsername
});
console.log('✓ Registration successful! User ID:', result.id);
// Try to delete test user
try {
await pb.collection('users').delete(result.id);
console.log('✓ Test user cleaned up');
} catch (e) {
console.log('⚠ Could not clean up test user:', e.message);
}
} catch (e) {
console.error('✗ Registration failed!');
console.error(' Error:', e.message);
if (e.response?.data) {
console.error(' Details:', JSON.stringify(e.response.data, null, 2));
}
if (e.data) {
console.error(' Data:', JSON.stringify(e.data, null, 2));
}
}
// Test 6: Check CORS settings
console.log('\n6. Checking CORS...');
try {
const response = await fetch(`${PROD_POCKETBASE_URL}/api/collections`, {
method: 'GET',
headers: {
Origin: 'https://your-frontend-domain.com'
}
});
console.log(
'✓ CORS headers present:',
response.headers.get('access-control-allow-origin') || 'not set'
);
} catch (e) {
console.log('✗ CORS check failed:', e.message);
}
} catch (error) {
console.error('\n✗ Connection test failed:', error.message);
if (error.response) {
console.error('Response:', error.response);
}
process.exit(1);
}
}
// Run the connection test
testConnection();

View file

@ -0,0 +1,232 @@
#!/usr/bin/env node
import 'dotenv/config';
import { redis, cache, ensureRedisConnection } from './src/lib/server/redis.js';
import { linkCache } from './src/lib/server/linkCache.js';
console.log('🔍 Testing Redis Cache for Link Redirections\n');
console.log('==========================================\n');
async function testRedisConnection() {
console.log('1. Testing Redis Connection...');
const connected = await ensureRedisConnection();
if (connected) {
console.log('✅ Redis connected successfully');
console.log(` Host: ${process.env.REDIS_HOST || 'ycsoowwsc84s0s8gc8oooosk'}`);
console.log(` Port: ${process.env.REDIS_PORT || '6379'}\n`);
return true;
} else {
console.log('❌ Redis connection failed\n');
return false;
}
}
async function testBasicCacheOperations() {
console.log('2. Testing Basic Cache Operations...');
// Test set and get
const testKey = 'test:key';
const testValue = { message: 'Hello Redis', timestamp: Date.now() };
await cache.set(testKey, testValue, 60);
console.log(' Set test value in cache');
const retrieved = await cache.get(testKey);
console.log(' Retrieved value:', retrieved);
if (retrieved && retrieved.message === testValue.message) {
console.log('✅ Basic cache operations working\n');
// Clean up
await cache.del(testKey);
return true;
} else {
console.log('❌ Basic cache operations failed\n');
return false;
}
}
async function testLinkCaching() {
console.log('3. Testing Link Cache Functions...');
const testShortCode = 'test123';
const testUrl = 'https://example.com/test';
// Cache a redirect
await linkCache.cacheRedirect(testShortCode, testUrl);
console.log(` Cached redirect: ${testShortCode} -> ${testUrl}`);
// Try to retrieve it
const cachedUrl = await linkCache.getRedirectUrl(testShortCode);
console.log(` Retrieved URL: ${cachedUrl}`);
if (cachedUrl === testUrl) {
console.log('✅ Link caching working correctly\n');
// Clean up
await linkCache.invalidate(testShortCode);
return true;
} else {
console.log('❌ Link caching failed\n');
return false;
}
}
async function checkCachedLinks() {
console.log('4. Checking Currently Cached Links...');
try {
// Get all keys matching redirect pattern
const keys = await redis.keys('redirect:*');
console.log(` Found ${keys.length} cached redirects`);
if (keys.length > 0) {
console.log('\n Sample cached links:');
for (const key of keys.slice(0, 5)) {
const url = await redis.get(key);
const ttl = await redis.ttl(key);
const shortCode = key.replace('redirect:', '');
console.log(` - ${shortCode}: ${url} (TTL: ${ttl}s)`);
}
}
// Check trending links
const trending = await linkCache.getTrendingLinks(5);
if (trending.length > 0) {
console.log('\n Trending links:');
trending.forEach((code, i) => {
console.log(` ${i + 1}. ${code}`);
});
}
console.log('✅ Cache inspection complete\n');
return true;
} catch (error) {
console.log('❌ Failed to inspect cache:', error.message, '\n');
return false;
}
}
async function performanceTest() {
console.log('5. Performance Test (Cache vs No Cache)...');
const testCode = 'perf-test';
const testUrl = 'https://example.com/performance';
// Test without cache (simulate)
const dbStart = Date.now();
await new Promise(resolve => setTimeout(resolve, 50)); // Simulate DB latency
const dbTime = Date.now() - dbStart;
console.log(` Database fetch simulation: ${dbTime}ms`);
// Cache the URL
await linkCache.cacheRedirect(testCode, testUrl);
// Test with cache
const cacheStart = Date.now();
await linkCache.getRedirectUrl(testCode);
const cacheTime = Date.now() - cacheStart;
console.log(` Cache fetch: ${cacheTime}ms`);
const improvement = Math.round((dbTime - cacheTime) / dbTime * 100);
console.log(` 🚀 Performance improvement: ${improvement}%`);
if (cacheTime < dbTime) {
console.log('✅ Cache is faster than database\n');
} else {
console.log('⚠️ Cache performance needs investigation\n');
}
// Clean up
await linkCache.invalidate(testCode);
return true;
}
async function monitorLiveTraffic() {
console.log('6. Monitoring Live Traffic (10 seconds)...');
console.log(' Open your app and click some links to see cache activity\n');
// Subscribe to Redis monitor for 10 seconds
const monitor = await redis.monitor();
let commandCount = 0;
let cacheHits = 0;
let cacheSets = 0;
monitor.on('monitor', (time, args) => {
const command = args[0];
if (command === 'get' && args[1]?.includes('redirect:')) {
cacheHits++;
console.log(` 🎯 Cache GET: ${args[1]}`);
} else if (command === 'setex' && args[1]?.includes('redirect:')) {
cacheSets++;
console.log(` 💾 Cache SET: ${args[1]} (TTL: ${args[2]}s)`);
}
commandCount++;
});
// Stop monitoring after 10 seconds
await new Promise(resolve => setTimeout(resolve, 10000));
monitor.disconnect();
console.log(`\n Monitoring complete:`);
console.log(` - Total Redis commands: ${commandCount}`);
console.log(` - Cache GETs: ${cacheHits}`);
console.log(` - Cache SETs: ${cacheSets}`);
console.log('✅ Live monitoring complete\n');
return true;
}
// Main test runner
async function runAllTests() {
console.log('Starting Redis Cache Tests...\n');
const tests = [
testRedisConnection,
testBasicCacheOperations,
testLinkCaching,
checkCachedLinks,
performanceTest
];
let passed = 0;
let failed = 0;
for (const test of tests) {
try {
const result = await test();
if (result) passed++;
else failed++;
} catch (error) {
console.log(`❌ Test failed with error: ${error.message}\n`);
failed++;
}
}
console.log('==========================================');
console.log(`Test Results: ${passed} passed, ${failed} failed`);
// Optional: Run live monitoring
console.log('\nWould you like to monitor live traffic? (Ctrl+C to skip)');
console.log('Starting in 3 seconds...\n');
await new Promise(resolve => setTimeout(resolve, 3000));
try {
await monitorLiveTraffic();
} catch (error) {
console.log('Monitoring cancelled');
}
// Close Redis connection
redis.disconnect();
process.exit(failed > 0 ? 1 : 0);
}
// Run tests
runAllTests().catch(error => {
console.error('Test suite failed:', error);
redis.disconnect();
process.exit(1);
});

View file

@ -0,0 +1,268 @@
#!/usr/bin/env node
import Redis from 'ioredis';
console.log('🔍 Testing Redis Cache for Link Redirections\n');
console.log('==========================================\n');
// Redis Configuration (same as in your app)
const redisConfig = {
host: process.env.REDIS_HOST || 'ycsoowwsc84s0s8gc8oooosk',
port: parseInt(process.env.REDIS_PORT || '6379'),
username: process.env.REDIS_USERNAME || 'default',
password: process.env.REDIS_PASSWORD,
retryDelayOnFailover: 100,
maxRetriesPerRequest: 3
};
const redis = new Redis(redisConfig);
// Helper functions
const cache = {
async get(key) {
const value = await redis.get(key);
return value ? JSON.parse(value) : null;
},
async set(key, value, ttlSeconds = 3600) {
await redis.setex(key, ttlSeconds, JSON.stringify(value));
},
async del(key) {
await redis.del(key);
}
};
async function testRedisConnection() {
console.log('1. Testing Redis Connection...');
try {
await redis.ping();
console.log('✅ Redis connected successfully');
console.log(` Host: ${redisConfig.host}`);
console.log(` Port: ${redisConfig.port}\n`);
return true;
} catch (error) {
console.log('❌ Redis connection failed:', error.message);
console.log(' Make sure Redis is running and credentials are correct\n');
return false;
}
}
async function testBasicCacheOperations() {
console.log('2. Testing Basic Cache Operations...');
const testKey = 'test:key';
const testValue = { message: 'Hello Redis', timestamp: Date.now() };
try {
await cache.set(testKey, testValue, 60);
console.log(' ✓ Set test value in cache');
const retrieved = await cache.get(testKey);
console.log(' ✓ Retrieved value:', retrieved);
if (retrieved && retrieved.message === testValue.message) {
console.log('✅ Basic cache operations working\n');
await cache.del(testKey);
return true;
} else {
console.log('❌ Value mismatch in cache\n');
return false;
}
} catch (error) {
console.log('❌ Cache operation failed:', error.message, '\n');
return false;
}
}
async function testLinkCaching() {
console.log('3. Testing Link Redirect Cache...');
const testShortCode = 'test123';
const testUrl = 'https://example.com/test';
const cacheKey = `redirect:${testShortCode}`;
try {
// Cache a redirect (directly as string, not JSON)
await redis.setex(cacheKey, 300, testUrl);
console.log(` ✓ Cached redirect: ${testShortCode} -> ${testUrl}`);
// Retrieve it
const cachedUrl = await redis.get(cacheKey);
console.log(` ✓ Retrieved URL: ${cachedUrl}`);
if (cachedUrl === testUrl) {
console.log('✅ Link caching working correctly\n');
await redis.del(cacheKey);
return true;
} else {
console.log('❌ Link cache retrieval failed\n');
return false;
}
} catch (error) {
console.log('❌ Link caching failed:', error.message, '\n');
return false;
}
}
async function checkCachedLinks() {
console.log('4. Checking Currently Cached Links...');
try {
// Get all redirect keys
const keys = await redis.keys('redirect:*');
console.log(` Found ${keys.length} cached redirects`);
if (keys.length > 0) {
console.log('\n Sample cached links (max 5):');
for (const key of keys.slice(0, 5)) {
const url = await redis.get(key);
const ttl = await redis.ttl(key);
const shortCode = key.replace('redirect:', '');
console.log(` - ${shortCode}: ${url?.substring(0, 50)}... (TTL: ${ttl}s)`);
}
} else {
console.log(' No cached redirects found (this is normal if cache is cold)');
}
// Check trending links
const trending = await redis.zrevrange('trending:links', 0, 4);
if (trending.length > 0) {
console.log('\n Trending links:');
trending.forEach((code, i) => {
console.log(` ${i + 1}. ${code}`);
});
} else {
console.log(' No trending data yet');
}
console.log('✅ Cache inspection complete\n');
return true;
} catch (error) {
console.log('❌ Failed to inspect cache:', error.message, '\n');
return false;
}
}
async function performanceTest() {
console.log('5. Performance Test (Cache vs Simulated DB)...');
const testCode = 'perf-test';
const testUrl = 'https://example.com/performance';
const cacheKey = `redirect:${testCode}`;
try {
// Simulate database fetch
const dbStart = Date.now();
await new Promise(resolve => setTimeout(resolve, 50));
const dbTime = Date.now() - dbStart;
console.log(` Database fetch simulation: ${dbTime}ms`);
// Cache the URL
await redis.setex(cacheKey, 300, testUrl);
// Test cache fetch
const cacheStart = Date.now();
await redis.get(cacheKey);
const cacheTime = Date.now() - cacheStart;
console.log(` Cache fetch: ${cacheTime}ms`);
const improvement = Math.round((dbTime - cacheTime) / dbTime * 100);
console.log(` 🚀 Performance improvement: ~${improvement}%`);
if (cacheTime < dbTime) {
console.log('✅ Cache is significantly faster\n');
} else {
console.log('⚠️ Cache performance unexpected\n');
}
await redis.del(cacheKey);
return true;
} catch (error) {
console.log('❌ Performance test failed:', error.message, '\n');
return false;
}
}
async function testRealLink() {
console.log('6. Testing with Real Application Flow...');
console.log(' Instructions:');
console.log(' 1. Start your app: npm run dev');
console.log(' 2. Visit a short link in your browser');
console.log(' 3. Check the console output for cache HIT/MISS messages');
console.log(' 4. Refresh the same link - should show "Cache HIT!"\n');
// Check if any real links are cached
const realKeys = await redis.keys('redirect:*');
if (realKeys.length > 0) {
console.log(' Currently cached real links:');
for (const key of realKeys.slice(0, 3)) {
const ttl = await redis.ttl(key);
console.log(` - ${key} (expires in ${ttl}s)`);
}
}
console.log('✅ Ready for real-world testing\n');
return true;
}
// Main test runner
async function runAllTests() {
const tests = [
testRedisConnection,
testBasicCacheOperations,
testLinkCaching,
checkCachedLinks,
performanceTest,
testRealLink
];
let passed = 0;
let failed = 0;
for (const test of tests) {
try {
const result = await test();
if (result) passed++;
else failed++;
} catch (error) {
console.log(`❌ Test crashed: ${error.message}\n`);
failed++;
}
}
console.log('==========================================');
console.log(`\n📊 Test Results: ${passed} passed, ${failed} failed`);
if (passed === tests.length) {
console.log('🎉 All tests passed! Redis cache is working correctly.');
} else if (failed === tests.length) {
console.log('⚠️ All tests failed. Check your Redis configuration.');
} else {
console.log('⚠️ Some tests failed. Review the output above.');
}
console.log('\n💡 Tips for verifying cache in production:');
console.log(' - Check server logs for "Cache HIT!" messages');
console.log(' - First visit: "Cache MISS" + redirect');
console.log(' - Second visit: "Cache HIT!" + faster redirect');
console.log(' - Cache TTL: 5 min (unpopular) or 24h (popular links)');
redis.disconnect();
process.exit(failed > 0 ? 1 : 0);
}
// Handle errors
redis.on('error', (err) => {
console.error('Redis connection error:', err.message);
console.error('Make sure Redis is running and accessible');
process.exit(1);
});
// Run tests
console.log('Connecting to Redis...\n');
runAllTests().catch(error => {
console.error('Test suite failed:', error);
redis.disconnect();
process.exit(1);
});

View file

@ -0,0 +1,64 @@
import PocketBase from 'pocketbase';
const pb = new PocketBase('http://localhost:8090');
// Generiere eine zufällige Test-E-Mail
const timestamp = Date.now();
const testEmail = `test${timestamp}@example.com`;
const testPassword = 'TestPassword123!';
async function testRegistration() {
console.log('🔧 Testing Registration and Verification Email...\n');
console.log(`📧 Test email: ${testEmail}`);
console.log(`🔑 Test password: ${testPassword}\n`);
try {
// 1. Erstelle einen neuen User
console.log('1⃣ Creating new user...');
const newUser = await pb.collection('users').create({
email: testEmail,
password: testPassword,
passwordConfirm: testPassword,
username: `user${timestamp}`,
emailVisibility: true
});
console.log('✅ User created successfully');
console.log(' User ID:', newUser.id);
console.log(' Verified:', newUser.verified);
console.log('');
// PocketBase sollte automatisch eine Verification-E-Mail senden
// wenn SMTP konfiguriert ist
// 2. Warte kurz
console.log('⏳ Waiting 2 seconds...\n');
await new Promise((resolve) => setTimeout(resolve, 2000));
// 3. Versuche manuell eine Verification-E-Mail zu senden
console.log('2⃣ Manually requesting verification email...');
try {
await pb.collection('users').requestVerification(testEmail);
console.log('✅ Manual verification email request sent');
console.log(' Check if you received 1 or 2 emails');
} catch (error) {
console.error('❌ Manual verification failed:', error.message);
}
} catch (error) {
console.error('❌ Registration failed:', error);
if (error.response) {
console.error(' Response:', error.response.data);
}
}
console.log('\n📌 Check your email logs/inbox for:');
console.log(` - Email to: ${testEmail}`);
console.log(' - Should receive verification email(s)');
console.log('\n📌 Also check:');
console.log(' - PocketBase Admin → Logs');
console.log(' - PocketBase Admin → Settings → Mail settings');
console.log(' - Application URL is set correctly');
}
// Run test
testRegistration();

View file

@ -0,0 +1,76 @@
import PocketBase from 'pocketbase';
// Test verschiedene URL-Varianten
const urls = [
'http://pocketbase-xs0ccokk8s0goko4w40gwc0w.91.99.221.179.sslip.io', // ohne trailing slash
'http://pocketbase-xs0ccokk8s0goko4w40gwc0w.91.99.221.179.sslip.io/' // mit trailing slash
];
console.log('Testing different URL configurations...\n');
async function testUrl(url) {
console.log(`Testing: ${url}`);
console.log('-'.repeat(60));
try {
const pb = new PocketBase(url);
// Test 1: Health check
const healthUrl = `${url}/api/health`;
const healthResponse = await fetch(healthUrl);
console.log(`✓ Health check status: ${healthResponse.status}`);
// Test 2: Registration
const testEmail = `test${Date.now()}@example.com`;
const testUsername = `user${Date.now()}`;
const userData = {
email: testEmail,
password: 'TestPass123!',
passwordConfirm: 'TestPass123!',
username: testUsername,
name: 'Test User',
emailVisibility: true
};
console.log(` Attempting registration with: ${testEmail}`);
try {
const result = await pb.collection('users').create(userData);
console.log(`✓ Registration successful! User ID: ${result.id}`);
// Clean up
try {
await pb.collection('users').delete(result.id);
console.log('✓ Test user cleaned up');
} catch (e) {
console.log('⚠ Could not clean up test user');
}
} catch (err) {
console.error(`✗ Registration failed: ${err.message}`);
if (err.response?.data) {
console.error(' Error details:', JSON.stringify(err.response.data, null, 2));
}
}
console.log('✓ URL configuration works!\n');
return true;
} catch (error) {
console.error(`✗ URL configuration failed: ${error.message}\n`);
return false;
}
}
// Test all URLs
async function testAll() {
for (const url of urls) {
const success = await testUrl(url);
if (success) {
console.log(`\n🎉 WORKING URL: ${url}`);
console.log('Use this exact URL in your environment variables (without quotes)');
break;
}
}
}
testAll();