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,6 +5,7 @@ import type { LifecycleDefinition } from '../lifecycle/validator.ts';
export interface ConditionEvaluateContext {
lifecycleDef?: LifecycleDefinition;
customFields?: Record<string, string>; // key → value map of CF values
}
export interface ConditionConfig {
@@ -16,6 +17,7 @@ export interface ConditionConfig {
old_value?: unknown;
new_value?: unknown;
value?: unknown;
link_type?: unknown;
}
export interface ConditionEvaluator {
@@ -82,11 +84,52 @@ export class OnCustomFieldChange implements ConditionEvaluator {
}
}
export class OnLinkCreate implements ConditionEvaluator {
evaluate(_ticket: Ticket, transactions: Transaction[], _context?: ConditionEvaluateContext, config?: ConditionConfig): boolean {
return transactions.some((tx) => {
if (tx.transaction_type !== 'LinkCreate') return false;
if (config?.link_type) {
const linkType = tx.field;
if (!matchesStatusFilter(linkType, config.link_type)) return false;
}
return true;
});
}
}
export class OnOverdue implements ConditionEvaluator {
evaluate(_ticket: Ticket, _transactions: Transaction[], context?: ConditionEvaluateContext, config?: ConditionConfig): boolean {
const fieldKey = config?.field_key ?? config?.field_id ?? config?.field;
if (!fieldKey) return false;
const cfValue = context?.customFields?.[String(fieldKey)];
if (!cfValue) return false;
// Parse the date value
const dueDate = new Date(cfValue);
if (isNaN(dueDate.getTime())) return false;
// Check if overdue (past due date)
if (new Date() <= dueDate) return false;
// Check that ticket is still active (not in inactive state)
const lifecycleDef = context?.lifecycleDef;
if (lifecycleDef) {
const inactiveStates = lifecycleDef.statuses.inactive;
if (inactiveStates.includes(_ticket.status)) return false;
}
return true;
}
}
const conditionRegistry: Record<string, ConditionEvaluator> = {
OnCreate: new OnCreate(),
OnStatusChange: new OnStatusChange(),
OnResolve: new OnResolve(),
OnCustomFieldChange: new OnCustomFieldChange(),
OnLinkCreate: new OnLinkCreate(),
OnOverdue: new OnOverdue(),
};
export function getConditionEvaluator(type: string): ConditionEvaluator | null {