feat(matrix): add Stats Bot and Project Doc Bot services

Complete GDPR-compliant bot suite for Matrix:

matrix-stats-bot (port 3312):
- Analytics reports from Umami
- Commands: !stats, !today, !week, !realtime, !users
- Scheduled daily/weekly reports to Matrix room

matrix-project-doc-bot (port 3313):
- Project documentation with photos, voice, text
- Voice transcription via OpenAI Whisper
- Blog generation with 5 styles (casual, technical, tutorial, social, story)
- Commands: !new, !projects, !switch, !status, !generate, !export
- Uses PostgreSQL + S3 (MinIO) for storage

Changes:
- docker-compose.macmini.yml: Added both Matrix bots
- health-check.sh: Added health checks for both bots

Environment variables required:
- MATRIX_STATS_BOT_TOKEN, MATRIX_PROJECT_DOC_BOT_TOKEN
- OPENAI_API_KEY (for Project Doc Bot)

https://claude.ai/code/session_01E3r5aFW3YLAhEJfsL2ryhv
This commit is contained in:
Claude 2026-01-28 00:44:28 +00:00
parent aabe328b51
commit 7c5e9e3c49
No known key found for this signature in database
46 changed files with 2215 additions and 0 deletions

View file

@ -0,0 +1,33 @@
import { Module, Global, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
export const DATABASE_CONNECTION = 'DATABASE_CONNECTION';
@Global()
@Module({
providers: [
{
provide: DATABASE_CONNECTION,
useFactory: (configService: ConfigService) => {
const logger = new Logger('Database');
const url = configService.get<string>('database.url');
if (!url) {
logger.error('DATABASE_URL is required');
throw new Error('DATABASE_URL is required');
}
const client = postgres(url);
logger.log('Database connected');
return drizzle(client, { schema });
},
inject: [ConfigService],
},
],
exports: [DATABASE_CONNECTION],
})
export class DatabaseModule {}

View file

@ -0,0 +1,33 @@
import { pgTable, text, timestamp, uuid, integer } from 'drizzle-orm/pg-core';
export const projects = pgTable('projects', {
id: uuid('id').primaryKey().defaultRandom(),
matrixUserId: text('matrix_user_id').notNull(),
name: text('name').notNull(),
status: text('status').notNull().default('active'), // active, archived
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
});
export const projectItems = pgTable('project_items', {
id: uuid('id').primaryKey().defaultRandom(),
projectId: uuid('project_id')
.notNull()
.references(() => projects.id, { onDelete: 'cascade' }),
type: text('type').notNull(), // photo, voice, text
content: text('content'), // text content or transcription
mediaUrl: text('media_url'), // S3 URL for media
mediaMxcUrl: text('media_mxc_url'), // Matrix MXC URL
duration: integer('duration'), // Voice duration in seconds
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const generations = pgTable('generations', {
id: uuid('id').primaryKey().defaultRandom(),
projectId: uuid('project_id')
.notNull()
.references(() => projects.id, { onDelete: 'cascade' }),
style: text('style').notNull(),
content: text('content').notNull(),
createdAt: timestamp('created_at').notNull().defaultNow(),
});