Add comprehensive platform administration panel

- Database: Add login_history, ai_recommendation_log tables; is_platform_owner
  column on users; subscription fields on organizations (payment_date,
  confirmation_number, renewal_date)
- Backend: New AdminAnalyticsService with platform metrics, tenant detail, and
  health score calculations (0-100 based on activity, budget, transactions,
  members, AI usage)
- Backend: Login/org-switch now records to login_history; AI recommendations
  logged to ai_recommendation_log; platform owner protected from superadmin toggle
- Frontend: 4-tab admin panel (Dashboard, Organizations, Users, Tenant Health)
  with tenant detail drawer, subscription management, health scoring visualization
- Platform owner account (admin@hoaledgeriq.com) auto-redirects to admin panel
- Seed data includes platform owner account and sample login history

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 08:51:39 -05:00
parent 0bd30a0eb8
commit a32d4cc179
20 changed files with 3183 additions and 317 deletions

View File

@@ -166,8 +166,9 @@ export class InvestmentPlanningService {
* 4. Call the AI API
* 5. Parse and return structured recommendations
*/
async getAIRecommendations(): Promise<AIResponse> {
async getAIRecommendations(userId?: string, orgId?: string): Promise<AIResponse> {
this.debug('getAIRecommendations', 'Starting AI recommendation flow');
const startTime = Date.now();
const [snapshot, cdRates, monthlyForecast] = await Promise.all([
this.getFinancialSnapshot(),
@@ -188,6 +189,7 @@ export class InvestmentPlanningService {
const messages = this.buildPromptMessages(snapshot, cdRates, monthlyForecast);
const aiResponse = await this.callAI(messages);
const elapsed = Date.now() - startTime;
this.debug('final_response', {
recommendation_count: aiResponse.recommendations.length,
@@ -195,9 +197,33 @@ export class InvestmentPlanningService {
risk_notes_count: aiResponse.risk_notes?.length || 0,
});
// Log AI usage to shared.ai_recommendation_log (fire-and-forget)
this.logAIUsage(userId, orgId, aiResponse, elapsed).catch(() => {});
return aiResponse;
}
private async logAIUsage(userId: string | undefined, orgId: string | undefined, response: AIResponse, elapsed: number) {
try {
const schema = this.tenant.getSchema();
await this.dataSource.query(
`INSERT INTO shared.ai_recommendation_log
(tenant_schema, organization_id, user_id, recommendation_count, response_time_ms, status)
VALUES ($1, $2, $3, $4, $5, $6)`,
[
schema || null,
orgId || null,
userId || null,
response.recommendations?.length || 0,
elapsed,
response.recommendations?.length > 0 ? 'success' : 'empty',
],
);
} catch (err) {
// Non-critical — don't let logging failure break recommendations
}
}
// ── Private: Tenant-Scoped Data Queries ──
private async getAccountBalances(): Promise<AccountBalance[]> {