Skill: queue-engine

## When to use this skill

7 stars

Best use case

Skill: queue-engine is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

## When to use this skill

Teams using Skill: queue-engine should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/queue-engine/SKILL.md --create-dirs "https://raw.githubusercontent.com/heldernoid/agentic-build-templates/main/projects/healthcare-wellness/clinic-queue-manager/skills/queue-engine/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/queue-engine/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How Skill: queue-engine Compares

Feature / AgentSkill: queue-engineStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

## When to use this skill

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Skill: queue-engine

## When to use this skill
Use when implementing the queue sorting, status transition logic, WebSocket broadcast system, wait time calculation, or Chart.js components for the clinic-queue-manager frontend. Covers the algorithmic and component patterns that are not obvious from the API spec alone.

## Queue ordering algorithm

The queue is always sorted: emergency first, then urgent, then normal. Within each priority tier, entries are sorted by `registered_at` ascending (FIFO).

SQL:
```sql
SELECT q.*, r.name AS room_name, p.name AS provider_name
FROM queue_entries q
LEFT JOIN rooms r ON q.room_id = r.id
LEFT JOIN providers p ON q.provider_id = p.id
WHERE q.queue_date = ?
  AND q.status IN ('waiting', 'called', 'with_provider')
ORDER BY q.priority DESC, q.registered_at ASC;
```

To get only the next patient to call (status=waiting):
```sql
SELECT * FROM queue_entries
WHERE queue_date = ? AND status = 'waiting'
ORDER BY priority DESC, registered_at ASC
LIMIT 1;
```

## Wait time calculation

Wait time in minutes is computed at query time (not stored) as:
```sql
-- For waiting/called entries
CAST((unixepoch('now') - unixepoch(registered_at)) / 60 AS INTEGER) AS wait_minutes

-- For done entries (time from registered to when provider saw them)
CAST((unixepoch(seen_at) - unixepoch(registered_at)) / 60 AS INTEGER) AS wait_minutes
```

In TypeScript:
```typescript
function waitMinutes(registeredAt: string, seenAt: string | null): number {
  const end = seenAt ? new Date(seenAt) : new Date();
  return Math.floor((end.getTime() - new Date(registeredAt).getTime()) / 60_000);
}
```

## Consultation time calculation

```typescript
function consultMinutes(seenAt: string | null, doneAt: string | null): number | null {
  if (!seenAt || !doneAt) return null;
  return Math.floor((new Date(doneAt).getTime() - new Date(seenAt).getTime()) / 60_000);
}
```

## Status transition validation

```typescript
const VALID_TRANSITIONS: Record<string, string[]> = {
  waiting:       ['called', 'no_show'],
  called:        ['with_provider', 'no_show', 'waiting'],  // 'waiting' = recall
  with_provider: ['done', 'no_show'],
  done:          [],
  no_show:       ['waiting'],  // re-admit
};

function assertValidTransition(from: string, to: string): void {
  const allowed = VALID_TRANSITIONS[from] ?? [];
  if (!allowed.includes(to)) {
    throw new StatusTransitionError(`Invalid status transition: ${from} -> ${to}`);
  }
}
```

Timestamp updates per transition:
```typescript
const TIMESTAMP_FIELD: Record<string, string | null> = {
  called:        'called_at',
  with_provider: 'seen_at',
  done:          'done_at',
  no_show:       'done_at',
  waiting:       null,  // on recall, clear called_at
};
```

On recall (`called -> waiting`), clear `called_at`:
```sql
UPDATE queue_entries
SET status = 'waiting', called_at = NULL
WHERE id = ?;
```

On re-admit (`no_show -> waiting`), create a new entry:
```typescript
// Do NOT update the no_show entry - it is a terminal record
// Instead, insert a new entry with the same fields but a new ticket number
const newTicket = await assignNextTicket(db, queueDate);
db.prepare(`
  INSERT INTO queue_entries (ticket_number, queue_date, patient_label, priority, room_id, provider_id, notes, status)
  SELECT ?, queue_date, patient_label, ?, room_id, provider_id, notes, 'waiting'
  FROM queue_entries WHERE id = ?
`).run(newTicket, originalPriority, originalId);
```

## Ticket number assignment

```typescript
function assignNextTicket(db: Database, queueDate: string): string {
  const row = db.prepare(`
    SELECT MAX(CAST(ticket_number AS INTEGER)) AS max_num
    FROM queue_entries
    WHERE queue_date = ?
  `).get(queueDate) as { max_num: number | null };
  const next = (row.max_num ?? 0) + 1;
  return String(next).padStart(3, '0');
}
```

## WebSocket broadcast

The WebSocket server shares the HTTP server via the `upgrade` event:

```typescript
import { WebSocketServer, WebSocket } from 'ws';
import type { Server } from 'node:http';

const clients = new Set<WebSocket>();

export function attachWss(server: Server): void {
  const wss = new WebSocketServer({ noServer: true });

  server.on('upgrade', (req, socket, head) => {
    if (req.url === '/ws') {
      wss.handleUpgrade(req, socket, head, (ws) => {
        wss.emit('connection', ws, req);
      });
    } else {
      socket.destroy();
    }
  });

  wss.on('connection', (ws) => {
    clients.add(ws);
    ws.on('message', (raw) => {
      try {
        const msg = JSON.parse(raw.toString()) as { type: string };
        if (msg.type === 'subscribe') {
          // Send current queue state to this new subscriber
          const state = buildQueueState();
          ws.send(JSON.stringify({ type: 'queue_state', ...state }));
        }
      } catch { /* ignore malformed */ }
    });
    ws.on('close', () => clients.delete(ws));
  });
}

export type WsMessage =
  | { type: 'queue_state'; entries: QueueEntry[]; stats: QueueStats }
  | { type: 'queue_add'; entry: QueueEntry }
  | { type: 'queue_update'; entry: QueueEntry }
  | { type: 'queue_remove'; id: number }
  | { type: 'stats_update'; stats: QueueStats };

export function broadcast(msg: WsMessage): void {
  const payload = JSON.stringify(msg);
  for (const ws of clients) {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(payload);
    }
  }
}
```

Every mutating API handler calls `broadcast()` after committing the DB change.

## Stats cron (every 30 seconds)

```typescript
import cron from 'node-cron';
import { broadcast } from './ws.js';
import { getTodayStats } from './db/stats.js';

cron.schedule('*/30 * * * * *', () => {
  const stats = getTodayStats();
  broadcast({ type: 'stats_update', stats });
});
```

## WaitTimer React component

The wait timer ticks in real-time on the client. It turns amber at the `urgentThreshold` and red at the `alertThreshold` (both from settings, defaulting to 15 and 30 minutes).

```typescript
interface WaitTimerProps {
  registeredAt: string;     // ISO string
  urgentThreshold: number;  // minutes, default 15
  alertThreshold: number;   // minutes, default 30
}

function WaitTimer({ registeredAt, urgentThreshold, alertThreshold }: WaitTimerProps) {
  const [minutes, setMinutes] = React.useState(0);

  React.useEffect(() => {
    const tick = () => {
      const diff = Math.floor((Date.now() - new Date(registeredAt).getTime()) / 60_000);
      setMinutes(diff);
    };
    tick();
    const id = setInterval(tick, 60_000);
    return () => clearInterval(id);
  }, [registeredAt]);

  const color =
    minutes >= alertThreshold  ? '#dc2626' :
    minutes >= urgentThreshold ? '#d97706' :
    'inherit';

  return (
    <span style={{ fontFamily: 'var(--font-mono)', fontSize: '13px', fontWeight: 600, color }}>
      {minutes} min
    </span>
  );
}
```

## QueueCard React component

```typescript
interface QueueCardProps {
  entry: QueueEntry;
  onCall: (id: number) => void;
  onSeen: (id: number) => void;
  onDone: (id: number) => void;
  onNoShow: (id: number) => void;
  urgentThreshold: number;
  alertThreshold: number;
}

const PRIORITY_BAR_COLOR: Record<number, string> = {
  0: 'var(--border)',
  1: '#d97706',
  2: '#dc2626',
};

const STATUS_BG: Record<string, string> = {
  waiting:       '#e0f2fe',
  called:        '#fef9c3',
  with_provider: '#dcfce7',
  done:          '#f3f4f6',
  no_show:       '#fee2e2',
};

function QueueCard({ entry, onCall, onSeen, onDone, onNoShow, urgentThreshold, alertThreshold }: QueueCardProps) {
  return (
    <div style={{ display: 'flex', background: '#fff', border: '1px solid var(--border)', borderRadius: 8, overflow: 'hidden', boxShadow: 'var(--shadow-sm)' }}>
      <div style={{ width: 6, background: PRIORITY_BAR_COLOR[entry.priority], flexShrink: 0 }} />
      <div style={{ flex: 1, padding: '12px 16px' }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 6 }}>
          <span style={{ fontFamily: 'var(--font-mono)', fontSize: 22, fontWeight: 700 }}>#{entry.ticket_number}</span>
          <StatusBadge status={entry.status} />
        </div>
        {entry.status === 'waiting' && (
          <WaitTimer registeredAt={entry.registered_at} urgentThreshold={urgentThreshold} alertThreshold={alertThreshold} />
        )}
        <div style={{ marginTop: 8, display: 'flex', gap: 6 }}>
          {entry.status === 'waiting' && <button onClick={() => onCall(entry.id)}>Call</button>}
          {entry.status === 'called' && <button onClick={() => onSeen(entry.id)}>Seen</button>}
          {entry.status === 'with_provider' && <button onClick={() => onDone(entry.id)}>Done</button>}
          {(entry.status === 'waiting' || entry.status === 'called') && (
            <button onClick={() => onNoShow(entry.id)}>No Show</button>
          )}
        </div>
      </div>
    </div>
  );
}
```

## Chart.js - Hourly Registrations bar chart

```typescript
import { Chart, BarController, BarElement, CategoryScale, LinearScale, Tooltip } from 'chart.js';
Chart.register(BarController, BarElement, CategoryScale, LinearScale, Tooltip);

interface HourlyDatum { hour: number; count: number; }

function buildHourlyConfig(data: HourlyDatum[]): ChartConfiguration<'bar'> {
  return {
    type: 'bar',
    data: {
      labels: data.map(d => `${d.hour}am`),
      datasets: [{
        data: data.map(d => d.count),
        backgroundColor: 'rgba(8, 145, 178, 0.7)',
        borderRadius: 3,
        borderSkipped: false,
      }],
    },
    options: {
      responsive: true,
      plugins: { legend: { display: false }, tooltip: { callbacks: {
        label: (ctx) => `${ctx.parsed.y} registrations`,
      }}},
      scales: {
        x: { grid: { display: false } },
        y: { beginAtZero: true, ticks: { stepSize: 1 } },
      },
    },
  };
}
```

## Display board WebSocket client

The public display board (no sidebar, dark background) connects and keeps the queue in sync:

```typescript
function useDisplayQueue() {
  const [entries, setEntries] = React.useState<QueueEntry[]>([]);
  const [stats, setStats] = React.useState<QueueStats | null>(null);

  React.useEffect(() => {
    const ws = new WebSocket('ws://localhost:3854/ws');

    ws.onopen = () => ws.send(JSON.stringify({ type: 'subscribe' }));

    ws.onmessage = (ev) => {
      const msg = JSON.parse(ev.data as string) as WsMessage;
      switch (msg.type) {
        case 'queue_state':
          setEntries(msg.entries);
          setStats(msg.stats);
          break;
        case 'queue_add':
          setEntries(prev => [...prev, msg.entry]);
          break;
        case 'queue_update':
          setEntries(prev => prev.map(e => e.id === msg.entry.id ? msg.entry : e));
          break;
        case 'queue_remove':
          setEntries(prev => prev.filter(e => e.id !== msg.id));
          break;
        case 'stats_update':
          setStats(msg.stats);
          break;
      }
    };

    ws.onclose = () => {
      // Fallback: poll REST API every 60 seconds if settings.display_refresh_seconds is configured
      setTimeout(() => window.location.reload(), 5000);
    };

    return () => ws.close();
  }, []);

  // The display board shows: called + with_provider entries in "Now Serving"
  // and the next N waiting entries in the "Waiting" list
  const nowServing = entries.filter(e => e.status === 'called' || e.status === 'with_provider');
  const waiting = entries.filter(e => e.status === 'waiting');

  return { nowServing, waiting, stats };
}
```

## Typed API client

```typescript
const API_BASE = 'http://localhost:3854';

async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
  const res = await fetch(`${API_BASE}${path}`, {
    headers: { 'Content-Type': 'application/json' },
    ...init,
  });
  if (!res.ok) {
    const err = await res.json().catch(() => ({})) as { error?: string };
    throw new Error(err.error ?? `HTTP ${res.status}`);
  }
  return res.json() as Promise<T>;
}

export const api = {
  queue: {
    list:       (params?: string) => apiFetch<{ entries: QueueEntry[] }>(`/api/queue${params ? '?' + params : ''}`),
    register:   (body: RegisterBody) => apiFetch<QueueEntry>('/api/queue', { method: 'POST', body: JSON.stringify(body) }),
    next:       () => apiFetch<{ entry: QueueEntry | null }>('/api/queue/next'),
    get:        (id: number) => apiFetch<QueueEntry>(`/api/queue/${id}`),
    update:     (id: number, body: Partial<RegisterBody>) => apiFetch<QueueEntry>(`/api/queue/${id}`, { method: 'PATCH', body: JSON.stringify(body) }),
    transition: (id: number, status: QueueStatus) => apiFetch<QueueEntry>(`/api/queue/${id}/status`, { method: 'POST', body: JSON.stringify({ status }) }),
    delete:     (id: number) => apiFetch<void>(`/api/queue/${id}`, { method: 'DELETE' }),
  },
  rooms: {
    list:   () => apiFetch<{ rooms: Room[] }>('/api/rooms'),
    create: (body: { name: string }) => apiFetch<Room>('/api/rooms', { method: 'POST', body: JSON.stringify(body) }),
    update: (id: number, body: Partial<Room>) => apiFetch<Room>(`/api/rooms/${id}`, { method: 'PATCH', body: JSON.stringify(body) }),
    delete: (id: number) => apiFetch<void>(`/api/rooms/${id}`, { method: 'DELETE' }),
  },
  providers: {
    list:   () => apiFetch<{ providers: Provider[] }>('/api/providers'),
    create: (body: CreateProviderBody) => apiFetch<Provider>('/api/providers', { method: 'POST', body: JSON.stringify(body) }),
    update: (id: number, body: Partial<Provider>) => apiFetch<Provider>(`/api/providers/${id}`, { method: 'PATCH', body: JSON.stringify(body) }),
    delete: (id: number) => apiFetch<void>(`/api/providers/${id}`, { method: 'DELETE' }),
  },
  stats: {
    today:   () => apiFetch<TodayStats>('/api/stats/today'),
    history: (from?: string, to?: string) => apiFetch<HistoryStats>(`/api/stats/history${from ? `?from=${from}&to=${to}` : ''}`),
  },
  settings: {
    get:    () => apiFetch<Settings>('/api/settings'),
    update: (body: Partial<Settings>) => apiFetch<Settings>('/api/settings', { method: 'PATCH', body: JSON.stringify(body) }),
  },
};
```

## StatusBadge component

```typescript
const STATUS_CONFIG = {
  waiting:       { label: 'Waiting',       bg: '#e0f2fe', color: '#0369a1' },
  called:        { label: 'Called',        bg: '#fef9c3', color: '#854d0e' },
  with_provider: { label: 'With Provider', bg: '#dcfce7', color: '#15803d' },
  done:          { label: 'Done',          bg: '#f3f4f6', color: '#57534e' },
  no_show:       { label: 'No Show',       bg: '#fee2e2', color: '#dc2626' },
} as const;

type QueueStatus = keyof typeof STATUS_CONFIG;

function StatusBadge({ status }: { status: QueueStatus }) {
  const { label, bg, color } = STATUS_CONFIG[status];
  return (
    <span style={{ background: bg, color, fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 8 }}>
      {label}
    </span>
  );
}
```

## PriorityBadge component

```typescript
const PRIORITY_CONFIG = {
  0: { label: 'Normal',    bg: 'var(--bg-subtle)', color: 'var(--text-secondary)' },
  1: { label: 'Urgent',    bg: '#fef3c7',          color: '#d97706' },
  2: { label: 'Emergency', bg: '#fee2e2',          color: '#dc2626' },
} as const;

function PriorityBadge({ priority }: { priority: 0 | 1 | 2 }) {
  const { label, bg, color } = PRIORITY_CONFIG[priority];
  return (
    <span style={{ background: bg, color, fontSize: 11, fontWeight: 700, padding: '1px 6px', borderRadius: 4 }}>
      {label.toUpperCase()}
    </span>
  );
}
```

## Zod schemas (server-side validation)

```typescript
import { z } from 'zod';

export const RegisterSchema = z.object({
  patient_label: z.string().min(1).max(120),
  priority:      z.union([z.literal(0), z.literal(1), z.literal(2)]).default(0),
  room_id:       z.number().int().positive().nullable().optional(),
  provider_id:   z.number().int().positive().nullable().optional(),
  notes:         z.string().max(500).nullable().optional(),
});

export const StatusTransitionSchema = z.object({
  status: z.enum(['waiting', 'called', 'with_provider', 'done', 'no_show']),
});

export const CreateProviderSchema = z.object({
  name: z.string().min(1).max(120),
  role: z.enum(['doctor', 'nurse', 'admin']),
});

export const SettingsPatchSchema = z.object({
  clinic_name:               z.string().max(120).optional(),
  default_priority:          z.union([z.literal(0), z.literal(1), z.literal(2)]).optional(),
  ticket_reset:              z.enum(['daily', 'weekly', 'never']).optional(),
  show_est_wait:             z.union([z.literal(0), z.literal(1)]).optional(),
  show_called_section:       z.union([z.literal(0), z.literal(1)]).optional(),
  display_refresh_seconds:   z.number().int().min(10).max(300).optional(),
  urgent_wait_threshold_min: z.number().int().min(1).max(60).optional(),
  alert_wait_threshold_min:  z.number().int().min(1).max(120).optional(),
  log_level:                 z.enum(['debug', 'info', 'warn', 'error']).optional(),
}).strict();
```

## Avg wait and consult SQL (for stats)

```sql
-- avg_wait_minutes for today (only entries that reached seen_at)
SELECT ROUND(AVG(
  CAST((unixepoch(seen_at) - unixepoch(registered_at)) / 60 AS REAL)
), 0) AS avg_wait_minutes
FROM queue_entries
WHERE queue_date = ? AND seen_at IS NOT NULL;

-- avg_consult_minutes for today
SELECT ROUND(AVG(
  CAST((unixepoch(done_at) - unixepoch(seen_at)) / 60 AS REAL)
), 0) AS avg_consult_minutes
FROM queue_entries
WHERE queue_date = ? AND seen_at IS NOT NULL AND done_at IS NOT NULL;
```

Related Skills

Skill: clinic-queue-manager API

7
from heldernoid/agentic-build-templates

## When to use this skill

reminder-engine

7
from heldernoid/agentic-build-templates

Server-side cron scheduler that polls a reminders table and delivers appointment reminders via WebSocket to the browser, which then shows Web Notifications API alerts. Use when building or debugging reminder delivery in appointment or scheduling applications.

sql-tutorial-engine

7
from heldernoid/agentic-build-templates

No description provided.

flashcard-engine

7
from heldernoid/agentic-build-templates

Spaced repetition flashcard system with SM-2 scheduling, Markdown support, and Anki import. Use when you need to study or manage flashcard decks, grade cards, check review schedules, or import/export card data. Triggers include "flashcards", "spaced repetition", "study cards", "Anki import", "SM-2", "review schedule".

docker-engine

7
from heldernoid/agentic-build-templates

Interact with the Docker Engine API via dockerode in Node.js/TypeScript -- listing containers, streaming logs, collecting stats, and running Compose operations as subprocesses.

prompt-engineering

7
from heldernoid/agentic-build-templates

No description provided.

Skill: Uptime Monitoring

7
from heldernoid/agentic-build-templates

## Overview

Skill: Status Page

7
from heldernoid/agentic-build-templates

## Overview

Skill: unit-conversion

7
from heldernoid/agentic-build-templates

## Overview

Skill: recipe-scaler

7
from heldernoid/agentic-build-templates

## Overview

reading-list

7
from heldernoid/agentic-build-templates

Operate the reading-list API to save, manage, tag, search, and export articles.

email-digest

7
from heldernoid/agentic-build-templates

Configure, test, and troubleshoot the reading-list daily email digest delivered via nodemailer.