feat: Authenticated Shell: Header, Quick Tabs and Change Password

This commit is contained in:
OpenVelo Agent
2026-07-21 22:26:44 +00:00
parent e5720c66dc
commit 1539a904c1
35 changed files with 1978 additions and 89 deletions
+3
View File
@@ -14,6 +14,9 @@ export const ERROR_CODES = {
TOKEN_REVOKED: 'TOKEN_REVOKED',
THEME_INVALID: 'THEME_INVALID',
REGISTRATIONS_DISABLED: 'REGISTRATIONS_DISABLED',
INVALID_OLD_PASSWORD: 'INVALID_OLD_PASSWORD',
PASSWORD_POLICY: 'PASSWORD_POLICY',
PASSWORDS_DO_NOT_MATCH: 'PASSWORDS_DO_NOT_MATCH',
INTERNAL: 'INTERNAL',
} as const;
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { SettingsService } from '../../modules/settings/settings.module';
export type EventStatusName = 'Stopped' | 'Running';
export type EventState = 'countdown' | 'running' | 'stopped' | 'unconfigured';
export interface EventStatus {
status: EventStatusName;
@@ -11,38 +12,94 @@ export interface EventStatus {
countdownMs: number;
}
export interface EventStatePayload {
state: EventState;
serverNowUtc: string;
eventStartUtc: string | null;
eventEndUtc: string | null;
secondsToStart: number | null;
secondsToEnd: number | null;
}
@Injectable()
export class EventStatusService {
constructor(private readonly settings: SettingsService) {}
async getStatus(now: Date = new Date()): Promise<EventStatus> {
const startStr = await this.settings.get('eventStartUtc');
const endStr = await this.settings.get('eventEndUtc');
const start = new Date(startStr);
const end = new Date(endStr);
const nowMs = now.getTime();
const startMs = start.getTime();
const endMs = end.getTime();
const s = await this.getState(now);
let status: EventStatusName = 'Stopped';
let countdownMs = 0;
if (nowMs < startMs) {
if (s.state === 'countdown' && s.secondsToStart != null) {
status = 'Stopped';
countdownMs = startMs - nowMs;
} else if (nowMs <= endMs) {
countdownMs = s.secondsToStart * 1000;
} else if (s.state === 'running' && s.secondsToEnd != null) {
status = 'Running';
countdownMs = endMs - nowMs;
countdownMs = s.secondsToEnd * 1000;
} else if (s.state === 'stopped') {
status = 'Stopped';
countdownMs = 0;
} else {
status = 'Stopped';
countdownMs = 0;
}
return {
status,
startUtc: start.toISOString(),
endUtc: end.toISOString(),
serverNowUtc: now.toISOString(),
startUtc: s.eventStartUtc ?? '',
endUtc: s.eventEndUtc ?? '',
serverNowUtc: s.serverNowUtc,
countdownMs,
};
}
}
async getState(now: Date = new Date()): Promise<EventStatePayload> {
const startStr = await this.settings.get('eventStartUtc');
const endStr = await this.settings.get('eventEndUtc');
if (!startStr || !endStr) {
return {
state: 'unconfigured',
serverNowUtc: now.toISOString(),
eventStartUtc: startStr ?? null,
eventEndUtc: endStr ?? null,
secondsToStart: null,
secondsToEnd: null,
};
}
const start = new Date(startStr);
const end = new Date(endStr);
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
return {
state: 'unconfigured',
serverNowUtc: now.toISOString(),
eventStartUtc: startStr,
eventEndUtc: endStr,
secondsToStart: null,
secondsToEnd: null,
};
}
const nowMs = now.getTime();
const startMs = start.getTime();
const endMs = end.getTime();
let state: EventState;
let secondsToStart: number | null = null;
let secondsToEnd: number | null = null;
if (nowMs < startMs) {
state = 'countdown';
secondsToStart = Math.max(0, Math.floor((startMs - nowMs) / 1000));
} else if (nowMs <= endMs) {
state = 'running';
secondsToEnd = Math.max(0, Math.floor((endMs - nowMs) / 1000));
} else {
state = 'stopped';
}
return {
state,
serverNowUtc: now.toISOString(),
eventStartUtc: start.toISOString(),
eventEndUtc: end.toISOString(),
secondsToStart,
secondsToEnd,
};
}
}