fix(matrix-stats-bot): adapt to Umami v2 API response format

The Umami API returns stats in a different format than expected:
- Before: { pageviews: { value, change } }
- After: { pageviews: number, comparison: { pageviews: number } }

Transform the raw API response to the expected format and calculate
percentage change from comparison values.

Also update URL_SCHEMA.md with complete list of all mana.how services.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-02-14 11:03:03 +01:00
parent 5e01c833ce
commit 70e45ed82e
3 changed files with 118 additions and 37 deletions

View file

@ -13,25 +13,68 @@ This document defines the URL schema for all mana.how subdomains.
## Complete URL Mapping ## Complete URL Mapping
### Core Apps ### Core Apps (Production)
| App | Landing Page | Web App | API | | App | Web App | API | Status |
|-----|--------------|---------|-----| |-----|---------|-----|--------|
| **Calendar** | calendars.mana.how | calendar.mana.how | calendar-api.mana.how | | **Calendar** | calendar.mana.how | api.mana.how/calendar | Active |
| **Clock** | clocks.mana.how | clock.mana.how | clock-api.mana.how | | **Clock** | clock.mana.how | api.mana.how/clock | Active |
| **Todo** | todos.mana.how | todo.mana.how | todo-api.mana.how | | **Todo** | todo.mana.how | api.mana.how/todo | Active |
| **Contacts** | contacts.mana.how | contact.mana.how | contact-api.mana.how | | **Contacts** | contacts.mana.how | api.mana.how/contacts | Active |
| **Chat** | chats.mana.how | chat.mana.how | chat-api.mana.how | | **Chat** | chat.mana.how | api.mana.how/chat | Active |
| **Picture** | pictures.mana.how | picture.mana.how | picture-api.mana.how | | **Storage** | storage.mana.how | api.mana.how/storage | Active |
| **Zitare** | zitares.mana.how | zitare.mana.how | zitare-api.mana.how | | **Zitare** | zitare.mana.how | api.mana.how/zitare | Active |
| **NutriPhi** | nutriphi.mana.how | api.mana.how/nutriphi | Active |
| **Presi** | presi.mana.how | api.mana.how/presi | Active |
| **SkillTree** | skilltree.mana.how | api.mana.how/skilltree | Active |
| **Photos** | photos.mana.how | api.mana.how/photos | Active |
### Platform ### Platform Services
| Service | URL | | Service | URL | Description |
|---------|-----| |---------|-----|-------------|
| **Main Dashboard** | mana.how | | **Main Dashboard** | mana.how | Main landing/dashboard |
| **Dashboard App** | app.mana.how | | **Auth Service** | auth.mana.how | Central authentication (mana-core-auth) |
| **Auth Service** | auth.mana.how | | **API Gateway** | api.mana.how | Unified API gateway |
| **Media Service** | media.mana.how | Image/video processing |
| **LLM Service** | llm.mana.how | LLM abstraction layer |
| **LLM Playground** | playground.mana.how | LLM testing interface |
| **Link Shortener** | link.mana.how | URL shortener (uload) |
| **File Storage** | files.mana.how | MinIO/S3 file access |
### Matrix/Communication
| Service | URL | Description |
|---------|-----|-------------|
| **Matrix Server** | matrix.mana.how | Synapse homeserver |
| **Element Web** | element.mana.how | Matrix web client |
| **N8N** | n.mana.how | Workflow automation |
### Monitoring & Admin
| Service | URL | Description |
|---------|-----|-------------|
| **Grafana** | grafana.mana.how | Metrics dashboards |
| **Umami** | (internal :8010) | Web analytics |
### Umami Tracking (Analytics)
For web analytics, the following apps are tracked in Umami:
| Umami Website ID | Display Name | Domain |
|------------------|--------------|--------|
| `manacore-webapp` | Dashboard | mana.how |
| `chat-webapp` | Chat | chat.mana.how |
| `todo-webapp` | Todo | todo.mana.how |
| `calendar-webapp` | Calendar | calendar.mana.how |
| `clock-webapp` | Clock | clock.mana.how |
| `contacts-webapp` | Contacts | contacts.mana.how |
| `storage-webapp` | Storage | storage.mana.how |
| `zitare-webapp` | Zitare | zitare.mana.how |
| `nutriphi-webapp` | NutriPhi | nutriphi.mana.how |
| `presi-webapp` | Presi | presi.mana.how |
| `skilltree-webapp` | SkillTree | skilltree.mana.how |
| `photos-webapp` | Photos | photos.mana.how |
--- ---

View file

@ -155,17 +155,11 @@ Sag "hilfe" fur alle Befehle!`;
await this.client.setTyping(roomId, true, 60000); await this.client.setTyping(roomId, true, 60000);
try { try {
// Download audio from Matrix // Download audio from Matrix using authenticated API
const mxcUrl = event.content.url!; const mxcUrl = event.content.url!;
const httpUrl = this.client.mxcToHttp(mxcUrl); this.logger.log(`Downloading audio from ${mxcUrl}`);
this.logger.log(`Downloading audio from ${httpUrl}`);
const response = await fetch(httpUrl); const buffer = await this.downloadMedia(mxcUrl);
if (!response.ok) {
throw new Error(`Failed to download audio: ${response.status}`);
}
const buffer = Buffer.from(await response.arrayBuffer());
// Transcribe audio // Transcribe audio
const transcription = await this.transcriptionService.transcribe(buffer); const transcription = await this.transcriptionService.transcribe(buffer);
@ -709,17 +703,11 @@ Sag "hilfe" fur alle Befehle!`;
} }
private async downloadMatrixImage(mxcUrl: string): Promise<string> { private async downloadMatrixImage(mxcUrl: string): Promise<string> {
const httpUrl = this.client.mxcToHttp(mxcUrl); this.logger.log(`Downloading image from ${mxcUrl}`);
this.logger.log(`Downloading image from ${httpUrl}`);
const response = await fetch(httpUrl); // Use the authenticated download method from BaseMatrixService
if (!response.ok) { const buffer = await this.downloadMedia(mxcUrl);
throw new Error(`Failed to download image: ${response.status}`); return buffer.toString('base64');
}
const buffer = await response.arrayBuffer();
const base64 = Buffer.from(buffer).toString('base64');
return base64;
} }
private markdownToHtmlLocal(markdown: string): string { private markdownToHtmlLocal(markdown: string): string {

View file

@ -9,6 +9,22 @@ interface UmamiStats {
totaltime: { value: number; change: number }; totaltime: { value: number; change: number };
} }
// Raw API response format from Umami
interface UmamiStatsRaw {
pageviews: number;
visitors: number;
visits: number;
bounces: number;
totaltime: number;
comparison: {
pageviews: number;
visitors: number;
visits: number;
bounces: number;
totaltime: number;
};
}
interface UmamiRealtime { interface UmamiRealtime {
pageviews: number; pageviews: number;
visitors: number; visitors: number;
@ -119,9 +135,40 @@ export class UmamiService implements OnModuleInit {
} }
async getStats(websiteId: string, startAt: number, endAt: number): Promise<UmamiStats | null> { async getStats(websiteId: string, startAt: number, endAt: number): Promise<UmamiStats | null> {
return this.request<UmamiStats>( const raw = await this.request<UmamiStatsRaw>(
`/api/websites/${websiteId}/stats?startAt=${startAt}&endAt=${endAt}` `/api/websites/${websiteId}/stats?startAt=${startAt}&endAt=${endAt}`
); );
if (!raw) return null;
// Transform raw API response to expected format
const calcChange = (current: number, previous: number): number => {
if (previous === 0) return current > 0 ? 100 : 0;
return Math.round(((current - previous) / previous) * 100);
};
return {
pageviews: {
value: raw.pageviews,
change: calcChange(raw.pageviews, raw.comparison?.pageviews ?? 0),
},
visitors: {
value: raw.visitors,
change: calcChange(raw.visitors, raw.comparison?.visitors ?? 0),
},
visits: {
value: raw.visits,
change: calcChange(raw.visits, raw.comparison?.visits ?? 0),
},
bounces: {
value: raw.bounces,
change: calcChange(raw.bounces, raw.comparison?.bounces ?? 0),
},
totaltime: {
value: raw.totaltime,
change: calcChange(raw.totaltime, raw.comparison?.totaltime ?? 0),
},
};
} }
async getRealtime(websiteId: string): Promise<UmamiRealtime | null> { async getRealtime(websiteId: string): Promise<UmamiRealtime | null> {
@ -133,7 +180,10 @@ export class UmamiService implements OnModuleInit {
startAt: number, startAt: number,
endAt: number, endAt: number,
unit: 'hour' | 'day' | 'month' = 'day' unit: 'hour' | 'day' | 'month' = 'day'
): Promise<{ pageviews: { x: string; y: number }[]; sessions: { x: string; y: number }[] } | null> { ): Promise<{
pageviews: { x: string; y: number }[];
sessions: { x: string; y: number }[];
} | null> {
return this.request( return this.request(
`/api/websites/${websiteId}/pageviews?startAt=${startAt}&endAt=${endAt}&unit=${unit}` `/api/websites/${websiteId}/pageviews?startAt=${startAt}&endAt=${endAt}&unit=${unit}`
); );