chore: archive inactive projects to apps-archived/

Move inactive projects out of active workspace:
- bauntown (community website)
- maerchenzauber (AI story generation)
- memoro (voice memo app)
- news (news aggregation)
- nutriphi (nutrition tracking)
- reader (reading app)
- uload (URL shortener)
- wisekeep (AI wisdom extraction)

Update CLAUDE.md documentation:
- Add presi to active projects
- Document archived projects section
- Update workspace configuration

Archived apps can be re-activated by moving back to apps/

🤖 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-29 07:03:59 +01:00
parent b97149ac12
commit 61d181fbc2
3148 changed files with 437 additions and 46640 deletions

View file

@ -0,0 +1,95 @@
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
},
};
}
}

View file

@ -0,0 +1,31 @@
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<HealthCheckResult> {
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',
};
}
}

View file

@ -0,0 +1,127 @@
import {
Controller,
Get,
Post,
Patch,
Delete,
Body,
Param,
Query,
UseGuards,
NotFoundException,
} from '@nestjs/common';
import { AuthGuard, CurrentUser } from '@mana-core/nestjs-integration';
import { LinksService, type CreateLinkDto, type 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',
};
}
}

View file

@ -0,0 +1,103 @@
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,
});
}
}