File size: 3,156 Bytes
1f5ea39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import { createRequire } from 'module';
import semver from 'semver';
import { db } from '../db/database.js';
import { SYSTEM_NOTICES } from './registry.js';
import { evaluate } from './conditions.js';
import type { SystemNotice, SystemNoticeDTO } from './types.js';

function getCurrentAppVersion(): string {
  const fromEnv = semver.valid(process.env.APP_VERSION ?? '');
  if (fromEnv) return fromEnv;
  try {
    const pkg = require('../../package.json') as { version?: string };
    return semver.valid(pkg.version ?? '') ?? '0.0.0';
  } catch {
    return '0.0.0';
  }
}

export function isNoticeVersionActive(n: SystemNotice, currentAppVersion: string): boolean {
  const appVersion = semver.coerce(currentAppVersion)?.version ?? '0.0.0';
  if (n.minVersion !== undefined) {
    const min = semver.valid(n.minVersion);
    if (!min) { console.warn(`[systemNotices] "${n.id}" invalid minVersion "${n.minVersion}" — skipping`); return false; }
    if (semver.lt(appVersion, min)) return false;
  }
  if (n.maxVersion !== undefined) {
    const max = semver.valid(n.maxVersion);
    if (!max) { console.warn(`[systemNotices] "${n.id}" invalid maxVersion "${n.maxVersion}" — skipping`); return false; }
    if (semver.gte(appVersion, max)) return false;
  }
  return true;
}

function severityWeight(s: string): number {
  return s === 'critical' ? 2 : s === 'warn' ? 1 : 0;
}

export function getActiveNoticesFor(userId: number): SystemNoticeDTO[] {
  const user = db.prepare(
    'SELECT login_count, first_seen_version, role FROM users WHERE id = ?'
  ).get(userId) as { login_count: number; first_seen_version: string; role: string } | undefined;

  if (!user) return [];

  const { count: tripCount } = db.prepare(
    'SELECT COUNT(*) AS count FROM trips WHERE user_id = ?'
  ).get(userId) as { count: number };

  const dismissedIds = new Set<string>(
    (db.prepare('SELECT notice_id FROM user_notice_dismissals WHERE user_id = ?')
      .all(userId) as Array<{ notice_id: string }>)
      .map(r => r.notice_id)
  );

  const now = new Date();
  const currentAppVersion = getCurrentAppVersion();
  const ctx = { user: { ...user, noTrips: tripCount }, currentAppVersion, now };

  return SYSTEM_NOTICES
    .filter(n => {
      if (dismissedIds.has(n.id)) return false;
      if (!isNoticeVersionActive(n, currentAppVersion)) return false;
      return evaluate(n, ctx);
    })
    .sort((a, b) => {
      const pw = (b.priority ?? 0) - (a.priority ?? 0);
      if (pw !== 0) return pw;
      const sw = severityWeight(b.severity) - severityWeight(a.severity);
      if (sw !== 0) return sw;
      return new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime();
    })
    .map(({ conditions: _c, publishedAt: _p, minVersion: _mn, maxVersion: _mx, priority: _pr, ...dto }) => dto);
}

export function dismissNotice(userId: number, noticeId: string): boolean {
  const exists = SYSTEM_NOTICES.some(n => n.id === noticeId);
  if (!exists) return false;
  db.prepare(`
    INSERT OR IGNORE INTO user_notice_dismissals (user_id, notice_id, dismissed_at)
    VALUES (?, ?, ?)
  `).run(userId, noticeId, Date.now());
  return true;
}