FASE 1: Notifications UI - Add NotificationBell, NotificationDrawer, NotificationItem components - Integrate notification bell in DashboardLayout header - Real-time unread count with polling FASE 2: AI Integration Backend - Add AI module with OpenRouter client - Endpoints: POST /ai/chat, GET /ai/models, GET/PATCH /ai/config - GET /ai/usage, GET /ai/usage/current, GET /ai/health - Database: schema ai with configs and usage tables - Token tracking and cost calculation FASE 3: Settings Page Refactor - Restructure with tabs navigation - GeneralSettings: profile, organization, appearance - NotificationSettings: channels and categories toggles - SecuritySettings: password change, 2FA placeholder, sessions Files created: 25+ Endpoints added: 7 Story Points completed: 21 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
91 lines
2.1 KiB
TypeScript
91 lines
2.1 KiB
TypeScript
import {
|
|
Entity,
|
|
PrimaryGeneratedColumn,
|
|
Column,
|
|
CreateDateColumn,
|
|
} from 'typeorm';
|
|
import { AIProvider } from './ai-config.entity';
|
|
|
|
export enum AIModelType {
|
|
CHAT = 'chat',
|
|
COMPLETION = 'completion',
|
|
EMBEDDING = 'embedding',
|
|
IMAGE = 'image',
|
|
}
|
|
|
|
export enum UsageStatus {
|
|
PENDING = 'pending',
|
|
COMPLETED = 'completed',
|
|
FAILED = 'failed',
|
|
CANCELLED = 'cancelled',
|
|
}
|
|
|
|
@Entity({ name: 'usage', schema: 'ai' })
|
|
export class AIUsage {
|
|
@PrimaryGeneratedColumn('uuid')
|
|
id: string;
|
|
|
|
@Column({ type: 'uuid' })
|
|
tenant_id: string;
|
|
|
|
@Column({ type: 'uuid' })
|
|
user_id: string;
|
|
|
|
@Column({ type: 'enum', enum: AIProvider })
|
|
provider: AIProvider;
|
|
|
|
@Column({ type: 'varchar', length: 100 })
|
|
model: string;
|
|
|
|
@Column({ type: 'enum', enum: AIModelType, default: AIModelType.CHAT })
|
|
model_type: AIModelType;
|
|
|
|
@Column({ type: 'enum', enum: UsageStatus, default: UsageStatus.PENDING })
|
|
status: UsageStatus;
|
|
|
|
@Column({ type: 'int', default: 0 })
|
|
input_tokens: number;
|
|
|
|
@Column({ type: 'int', default: 0 })
|
|
output_tokens: number;
|
|
|
|
// total_tokens is computed in DB, but we can add it for convenience
|
|
get total_tokens(): number {
|
|
return this.input_tokens + this.output_tokens;
|
|
}
|
|
|
|
@Column({ type: 'numeric', precision: 12, scale: 6, default: 0 })
|
|
cost_input: number;
|
|
|
|
@Column({ type: 'numeric', precision: 12, scale: 6, default: 0 })
|
|
cost_output: number;
|
|
|
|
get cost_total(): number {
|
|
return Number(this.cost_input) + Number(this.cost_output);
|
|
}
|
|
|
|
@Column({ type: 'int', nullable: true })
|
|
latency_ms: number;
|
|
|
|
@Column({ type: 'timestamptz', default: () => 'NOW()' })
|
|
started_at: Date;
|
|
|
|
@Column({ type: 'timestamptz', nullable: true })
|
|
completed_at: Date;
|
|
|
|
@Column({ type: 'varchar', length: 100, nullable: true })
|
|
request_id: string;
|
|
|
|
@Column({ type: 'varchar', length: 50, nullable: true })
|
|
endpoint: string;
|
|
|
|
@Column({ type: 'text', nullable: true })
|
|
error_message: string;
|
|
|
|
@Column({ type: 'jsonb', default: {} })
|
|
metadata: Record<string, any>;
|
|
|
|
@CreateDateColumn({ type: 'timestamptz' })
|
|
created_at: Date;
|
|
}
|