feat: auth system, scrip scheduler, UI widgets, and new API routes

- Add session-based authentication (login page, middleware, auth context)
- Add cron-like scrip scheduler for time-based conditions
- Add layout builder, scrip wizard, searchable select components
- Add trend chart widget for dashboards
- Add notifications, attachments, queue-permissions API routes
- Add seed-users script
- Update schema with 10 new migrations (0008-0017)
- Apply redesign: Linear-inspired dark theme, conversation-centric UI
- Gitignore runtime data directory

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Gjermund Høsøien Wiggen
2026-06-15 20:42:17 +02:00
parent 1d4dc38d06
commit 70f0924d4b
59 changed files with 21795 additions and 321 deletions

View File

@@ -5,13 +5,17 @@ export interface LifecycleDefinition {
inactive: string[];
};
transitions: Record<string, string[]>;
transition_rights?: Record<string, string>; // "from→to" → rightName
}
export interface ValidationResult {
valid: boolean;
error?: string;
requiredRight?: string; // Named right required for this transition, if any
}
const FALLBACK_RIGHT = 'ticket.modify';
export class LifecycleValidator {
validateTransition(
lifecycleDef: LifecycleDefinition,
@@ -35,13 +39,15 @@ export class LifecycleValidator {
const allowedTransitions = this.getAllowedTransitions(lifecycleDef, fromStatus);
if (allowedTransitions.includes(toStatus)) {
return { valid: true };
const right = this.getRequiredRight(lifecycleDef, fromStatus, toStatus);
return { valid: true, requiredRight: right ?? FALLBACK_RIGHT };
}
// Also handle wildcard "*" -> any transition
const wildcardTransitions = this.getAllowedTransitions(lifecycleDef, '*');
if (wildcardTransitions.includes(toStatus)) {
return { valid: true };
const right = this.getRequiredRight(lifecycleDef, fromStatus, toStatus);
return { valid: true, requiredRight: right ?? FALLBACK_RIGHT };
}
return {
@@ -50,6 +56,37 @@ export class LifecycleValidator {
};
}
/**
* Get the required right for a transition using RT's 4-level priority:
* 1. exact "from→to"
* 2. wildcard from "*→to"
* 3. wildcard to "from→*"
* 4. full wildcard "*→*"
* 5. fallback: ticket.modify
*/
getRequiredRight(
lifecycleDef: LifecycleDefinition,
fromStatus: string,
toStatus: string,
): string | null {
const rights = lifecycleDef.transition_rights ?? {};
// Priority 1: exact match
if (rights[`${fromStatus}${toStatus}`]) return rights[`${fromStatus}${toStatus}`];
// Priority 2: wildcard from
if (rights[`*→${toStatus}`]) return rights[`*→${toStatus}`];
// Priority 3: wildcard to
if (rights[`${fromStatus}→*`]) return rights[`${fromStatus}→*`];
// Priority 4: full wildcard
if (rights['*→*']) return rights['*→*'];
// Priority 5: fallback
return null;
}
isResolvedStatus(lifecycleDef: LifecycleDefinition, status: string): boolean {
return lifecycleDef.statuses.inactive.includes(status);
}
@@ -58,16 +95,12 @@ export class LifecycleValidator {
lifecycleDef: LifecycleDefinition,
fromStatus: string,
): string[] {
// Direct transition
if (lifecycleDef.transitions[fromStatus]) {
return lifecycleDef.transitions[fromStatus]!;
}
// Wildcard transitions
if (lifecycleDef.transitions['*']) {
return lifecycleDef.transitions['*']!;
}
return [];
}
}