hazo_admin 0.7.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGE_LOG.md +60 -0
  2. package/README.md +65 -4
  3. package/SETUP_CHECKLIST.md +24 -0
  4. package/dist/api/index.d.ts +6 -1
  5. package/dist/api/index.d.ts.map +1 -1
  6. package/dist/api/index.js +166 -2
  7. package/dist/components/issues_panel/board_columns.d.ts +17 -0
  8. package/dist/components/issues_panel/board_columns.d.ts.map +1 -0
  9. package/dist/components/issues_panel/board_columns.js +37 -0
  10. package/dist/components/issues_panel/card_assignee_control.d.ts +17 -0
  11. package/dist/components/issues_panel/card_assignee_control.d.ts.map +1 -0
  12. package/dist/components/issues_panel/card_assignee_control.js +51 -0
  13. package/dist/components/issues_panel/card_type_control.d.ts +18 -0
  14. package/dist/components/issues_panel/card_type_control.d.ts.map +1 -0
  15. package/dist/components/issues_panel/card_type_control.js +42 -0
  16. package/dist/components/issues_panel/facet_sidebar.d.ts +25 -0
  17. package/dist/components/issues_panel/facet_sidebar.d.ts.map +1 -0
  18. package/dist/components/issues_panel/facet_sidebar.js +72 -0
  19. package/dist/components/issues_panel/facet_topbar.d.ts +20 -0
  20. package/dist/components/issues_panel/facet_topbar.d.ts.map +1 -0
  21. package/dist/components/issues_panel/facet_topbar.js +42 -0
  22. package/dist/components/issues_panel/filter.d.ts +12 -0
  23. package/dist/components/issues_panel/filter.d.ts.map +1 -0
  24. package/dist/components/issues_panel/filter.js +41 -0
  25. package/dist/components/issues_panel/index.d.ts.map +1 -1
  26. package/dist/components/issues_panel/index.js +145 -43
  27. package/dist/components/issues_panel/manage_types_dialog.d.ts +28 -0
  28. package/dist/components/issues_panel/manage_types_dialog.d.ts.map +1 -0
  29. package/dist/components/issues_panel/manage_types_dialog.js +84 -0
  30. package/dist/components/issues_panel/ui_helpers.d.ts +21 -0
  31. package/dist/components/issues_panel/ui_helpers.d.ts.map +1 -0
  32. package/dist/components/issues_panel/ui_helpers.js +136 -0
  33. package/dist/components/issues_panel/use_issue_types.d.ts +30 -0
  34. package/dist/components/issues_panel/use_issue_types.d.ts.map +1 -0
  35. package/dist/components/issues_panel/use_issue_types.js +81 -0
  36. package/dist/index.d.ts +2 -2
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -1
  39. package/dist/index.ui.d.ts +2 -0
  40. package/dist/index.ui.d.ts.map +1 -1
  41. package/dist/index.ui.js +1 -0
  42. package/dist/issues/archive_handler.d.ts +1 -1
  43. package/dist/issues/archive_handler.js +2 -2
  44. package/dist/issues/index.d.ts +6 -0
  45. package/dist/issues/index.d.ts.map +1 -1
  46. package/dist/issues/index.js +4 -0
  47. package/dist/issues/notify_handler.d.ts +26 -0
  48. package/dist/issues/notify_handler.d.ts.map +1 -0
  49. package/dist/issues/notify_handler.js +97 -0
  50. package/dist/issues/raise.d.ts +44 -0
  51. package/dist/issues/raise.d.ts.map +1 -0
  52. package/dist/issues/raise.js +77 -0
  53. package/dist/issues/recipients.d.ts +20 -0
  54. package/dist/issues/recipients.d.ts.map +1 -0
  55. package/dist/issues/recipients.js +61 -0
  56. package/dist/issues/registry.client.d.ts +1 -0
  57. package/dist/issues/registry.client.d.ts.map +1 -1
  58. package/dist/issues/registry.client.js +4 -1
  59. package/dist/issues/registry.d.ts +3 -3
  60. package/dist/issues/registry.d.ts.map +1 -1
  61. package/dist/issues/store.d.ts +10 -5
  62. package/dist/issues/store.d.ts.map +1 -1
  63. package/dist/issues/store.js +58 -28
  64. package/dist/issues/type_catalog.d.ts +48 -0
  65. package/dist/issues/type_catalog.d.ts.map +1 -0
  66. package/dist/issues/type_catalog.js +185 -0
  67. package/package.json +5 -5
@@ -0,0 +1,84 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from 'react';
4
+ import { LIGHT_THEME_VARS } from './ui_helpers.js';
5
+ function draftFromType(t) {
6
+ return {
7
+ label: t.label,
8
+ color: t.color ?? '',
9
+ description: t.description ?? '',
10
+ sort_order: String(t.sort_order ?? 0),
11
+ };
12
+ }
13
+ const EMPTY_NEW_DRAFT = { type_key: '', label: '', color: '', description: '', sort_order: '' };
14
+ export function ManageTypesDialog({ open, onOpenChange, catalog, onCreate, onUpdate, onDelete, ui }) {
15
+ const [drafts, setDrafts] = useState({});
16
+ const [newDraft, setNewDraft] = useState(EMPTY_NEW_DRAFT);
17
+ const [savingKey, setSavingKey] = useState(null);
18
+ const [creating, setCreating] = useState(false);
19
+ if (!ui?.HazoUiDialog)
20
+ return null;
21
+ const { HazoUiDialog, Input, Button } = ui;
22
+ const draftFor = (t) => drafts[t.type_key] ?? draftFromType(t);
23
+ const setDraftField = (type_key, base, field, val) => {
24
+ setDrafts((prev) => ({ ...prev, [type_key]: { ...base, [field]: val } }));
25
+ };
26
+ const saveRow = async (t) => {
27
+ const draft = draftFor(t);
28
+ setSavingKey(t.type_key);
29
+ try {
30
+ await onUpdate(t.type_key, {
31
+ label: draft.label,
32
+ color: draft.color === '' ? null : draft.color,
33
+ description: draft.description === '' ? null : draft.description,
34
+ sort_order: draft.sort_order === '' ? undefined : Number(draft.sort_order),
35
+ });
36
+ setDrafts((prev) => {
37
+ const next = { ...prev };
38
+ delete next[t.type_key];
39
+ return next;
40
+ });
41
+ }
42
+ finally {
43
+ setSavingKey(null);
44
+ }
45
+ };
46
+ const deleteRow = async (t) => {
47
+ setSavingKey(t.type_key);
48
+ try {
49
+ await onDelete(t.type_key);
50
+ }
51
+ finally {
52
+ setSavingKey(null);
53
+ }
54
+ };
55
+ const addType = async () => {
56
+ if (!newDraft.type_key.trim() || !newDraft.label.trim())
57
+ return;
58
+ setCreating(true);
59
+ try {
60
+ await onCreate({
61
+ type_key: newDraft.type_key.trim(),
62
+ label: newDraft.label.trim(),
63
+ color: newDraft.color === '' ? null : newDraft.color,
64
+ description: newDraft.description === '' ? null : newDraft.description,
65
+ sort_order: newDraft.sort_order === '' ? undefined : Number(newDraft.sort_order),
66
+ });
67
+ setNewDraft(EMPTY_NEW_DRAFT);
68
+ }
69
+ finally {
70
+ setCreating(false);
71
+ }
72
+ };
73
+ function renderInput(value, onChangeVal, placeholder, extraClassName = '') {
74
+ return Input ? (_jsx(Input, { value: value, placeholder: placeholder, onChange: (e) => onChangeVal(e.target.value), className: `h-8 text-sm ${extraClassName}` })) : (_jsx("input", { type: "text", value: value, placeholder: placeholder, onChange: (e) => onChangeVal(e.target.value), className: `h-8 text-sm border rounded px-2 ${extraClassName}` }));
75
+ }
76
+ function renderButton(label, onClick, disabled, variant) {
77
+ return Button ? (_jsx(Button, { size: "sm", variant: variant, onClick: onClick, disabled: disabled, children: label })) : (_jsx("button", { type: "button", onClick: onClick, disabled: disabled, className: `text-xs px-2 py-1 rounded ${variant === 'destructive' ? 'bg-red-50 text-red-600 hover:bg-red-100' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'} disabled:opacity-50`, children: label }));
78
+ }
79
+ return (_jsx(HazoUiDialog, { open: open, onOpenChange: onOpenChange, title: "Manage issue types", showActionButton: false, showCancelButton: false, contentClassName: "bg-white text-slate-900", bodyBackgroundColor: "#ffffff", children: _jsxs("div", { className: "space-y-4", style: LIGHT_THEME_VARS, children: [_jsxs("div", { className: "space-y-3 max-h-80 overflow-y-auto pr-1", children: [catalog.length === 0 && _jsx("div", { className: "text-sm text-gray-400", children: "No issue types yet." }), catalog.map((t) => {
80
+ const draft = draftFor(t);
81
+ const saving = savingKey === t.type_key;
82
+ return (_jsxs("div", { className: "border rounded p-2 space-y-2", children: [_jsx("div", { className: "text-xs text-gray-400", children: t.type_key }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [renderInput(draft.label, (v) => setDraftField(t.type_key, draft, 'label', v), 'Label'), renderInput(draft.color, (v) => setDraftField(t.type_key, draft, 'color', v), 'Color (#hex)'), renderInput(draft.description, (v) => setDraftField(t.type_key, draft, 'description', v), 'Description', 'col-span-2'), renderInput(draft.sort_order, (v) => setDraftField(t.type_key, draft, 'sort_order', v), 'Sort order')] }), _jsxs("div", { className: "flex gap-2 justify-end", children: [renderButton('Save', () => saveRow(t), saving), renderButton('Delete', () => deleteRow(t), saving, 'destructive')] })] }, t.type_key));
83
+ })] }), _jsxs("div", { className: "border-t pt-3 space-y-2", children: [_jsx("div", { className: "text-xs font-semibold uppercase text-gray-400", children: "Add type" }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [renderInput(newDraft.type_key, (v) => setNewDraft((prev) => ({ ...prev, type_key: v })), 'type_key'), renderInput(newDraft.label, (v) => setNewDraft((prev) => ({ ...prev, label: v })), 'Label'), renderInput(newDraft.color, (v) => setNewDraft((prev) => ({ ...prev, color: v })), 'Color (#hex)'), renderInput(newDraft.sort_order, (v) => setNewDraft((prev) => ({ ...prev, sort_order: v })), 'Sort order'), renderInput(newDraft.description, (v) => setNewDraft((prev) => ({ ...prev, description: v })), 'Description', 'col-span-2')] }), _jsx("div", { className: "flex justify-end", children: renderButton('Add', addType, creating || !newDraft.type_key.trim() || !newDraft.label.trim()) })] })] }) }));
84
+ }
@@ -0,0 +1,21 @@
1
+ /** Minimal class-name joiner (clsx-lite) — no external dep. */
2
+ export declare function cx(...parts: Array<string | false | null | undefined>): string;
3
+ export interface SeverityMeta {
4
+ label: string;
5
+ /** Solid dot / rail colour. */
6
+ dot: string;
7
+ /** Badge foreground + background (light + dark). */
8
+ badge: string;
9
+ /** Left rail accent border colour. */
10
+ rail: string;
11
+ /** Sort weight, high → low. */
12
+ weight: number;
13
+ }
14
+ export declare function severityMeta(severity: string | null | undefined): SeverityMeta;
15
+ /** Compact relative time: "just now", "5m", "3h", "2d", else a short date. */
16
+ export declare function relativeTime(iso: string | null | undefined): string;
17
+ /** Up-to-two-letter initials from a display label or id. */
18
+ export declare function initials(label: string | null | undefined): string;
19
+ export declare function avatarTint(key: string | null | undefined): string;
20
+ export declare const LIGHT_THEME_VARS: Record<string, string>;
21
+ //# sourceMappingURL=ui_helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui_helpers.d.ts","sourceRoot":"","sources":["../../../src/components/issues_panel/ui_helpers.ts"],"names":[],"mappings":"AAMA,+DAA+D;AAC/D,wBAAgB,EAAE,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,MAAM,GAAG,KAAK,GAAG,IAAI,GAAG,SAAS,CAAC,GAAG,MAAM,CAE7E;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,+BAA+B;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,MAAM,EAAE,MAAM,CAAC;CAChB;AAyCD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,YAAY,CAE9E;AAED,8EAA8E;AAC9E,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAenE;AAED,4DAA4D;AAC5D,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAOjE;AAcD,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAKjE;AAOD,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA4BnD,CAAC"}
@@ -0,0 +1,136 @@
1
+ 'use client';
2
+ // Shared presentational helpers for the Issues panel. Client-safe, no deps.
3
+ // Keeps the visual language (severity colours, relative time, avatars) in one
4
+ // place so cards, the facet rail, and the top bar stay consistent.
5
+ /** Minimal class-name joiner (clsx-lite) — no external dep. */
6
+ export function cx(...parts) {
7
+ return parts.filter(Boolean).join(' ');
8
+ }
9
+ const SEVERITY = {
10
+ critical: {
11
+ label: 'Critical',
12
+ dot: 'bg-rose-500',
13
+ badge: 'bg-rose-50 text-rose-700 ring-1 ring-inset ring-rose-100',
14
+ rail: 'before:bg-rose-500',
15
+ weight: 4,
16
+ },
17
+ high: {
18
+ label: 'High',
19
+ dot: 'bg-amber-500',
20
+ badge: 'bg-amber-50 text-amber-700 ring-1 ring-inset ring-amber-100',
21
+ rail: 'before:bg-amber-400',
22
+ weight: 3,
23
+ },
24
+ medium: {
25
+ label: 'Medium',
26
+ dot: 'bg-indigo-500',
27
+ badge: 'bg-indigo-50 text-indigo-700 ring-1 ring-inset ring-indigo-100',
28
+ rail: 'before:bg-indigo-400',
29
+ weight: 2,
30
+ },
31
+ low: {
32
+ label: 'Low',
33
+ dot: 'bg-slate-400',
34
+ badge: 'bg-slate-100 text-slate-600 ring-1 ring-inset ring-slate-200',
35
+ rail: 'before:bg-slate-300',
36
+ weight: 1,
37
+ },
38
+ };
39
+ const SEVERITY_FALLBACK = {
40
+ label: 'Unknown',
41
+ dot: 'bg-slate-300',
42
+ badge: 'bg-slate-100 text-slate-500 ring-1 ring-inset ring-slate-200',
43
+ rail: 'before:bg-slate-200',
44
+ weight: 0,
45
+ };
46
+ export function severityMeta(severity) {
47
+ return SEVERITY[(severity ?? '').toLowerCase()] ?? SEVERITY_FALLBACK;
48
+ }
49
+ /** Compact relative time: "just now", "5m", "3h", "2d", else a short date. */
50
+ export function relativeTime(iso) {
51
+ if (!iso)
52
+ return '';
53
+ const then = Date.parse(iso);
54
+ if (Number.isNaN(then))
55
+ return '';
56
+ const secs = Math.round((Date.now() - then) / 1000);
57
+ if (secs < 45)
58
+ return 'just now';
59
+ const mins = Math.round(secs / 60);
60
+ if (mins < 60)
61
+ return `${mins}m`;
62
+ const hrs = Math.round(mins / 60);
63
+ if (hrs < 24)
64
+ return `${hrs}h`;
65
+ const days = Math.round(hrs / 24);
66
+ if (days < 7)
67
+ return `${days}d`;
68
+ const wks = Math.round(days / 7);
69
+ if (wks < 5)
70
+ return `${wks}w`;
71
+ return new Date(then).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
72
+ }
73
+ /** Up-to-two-letter initials from a display label or id. */
74
+ export function initials(label) {
75
+ const s = (label ?? '').trim();
76
+ if (!s)
77
+ return '?';
78
+ const parts = s.split(/[\s@._-]+/).filter(Boolean);
79
+ if (parts.length === 0)
80
+ return s.slice(0, 2).toUpperCase();
81
+ if (parts.length === 1)
82
+ return parts[0].slice(0, 2).toUpperCase();
83
+ return (parts[0][0] + parts[1][0]).toUpperCase();
84
+ }
85
+ /** Deterministic avatar tint from a string key — solid, kinstripe-style chips. */
86
+ const AVATAR_TINTS = [
87
+ 'bg-indigo-500 text-white',
88
+ 'bg-emerald-500 text-white',
89
+ 'bg-violet-500 text-white',
90
+ 'bg-amber-500 text-white',
91
+ 'bg-rose-500 text-white',
92
+ 'bg-cyan-500 text-white',
93
+ 'bg-sky-500 text-white',
94
+ 'bg-fuchsia-500 text-white',
95
+ ];
96
+ export function avatarTint(key) {
97
+ const s = key ?? '';
98
+ let hash = 0;
99
+ for (let i = 0; i < s.length; i++)
100
+ hash = (hash * 31 + s.charCodeAt(i)) | 0;
101
+ return AVATAR_TINTS[Math.abs(hash) % AVATAR_TINTS.length];
102
+ }
103
+ // Pinned LIGHT theme tokens (HSL triplets, hazo_ui/shadcn contract). Applied as
104
+ // inline CSS custom properties on the panel root so the token-driven Kanban and
105
+ // any hazo_ui Select/Input render light + on-brand (indigo primary) regardless of
106
+ // the host app's OS-level dark mode. `primary` is nudged to indigo for the
107
+ // kinstripe look; everything else is hazo_ui's canonical light palette.
108
+ export const LIGHT_THEME_VARS = {
109
+ '--background': '0 0% 100%',
110
+ '--foreground': '222.2 47% 11%',
111
+ '--card': '0 0% 100%',
112
+ '--card-foreground': '222.2 47% 11%',
113
+ '--popover': '0 0% 100%',
114
+ '--popover-foreground': '222.2 47% 11%',
115
+ '--primary': '243 75% 59%', // indigo-600
116
+ '--primary-foreground': '210 40% 98%',
117
+ '--secondary': '210 40% 96.1%',
118
+ '--secondary-foreground': '222.2 47.4% 11.2%',
119
+ '--muted': '220 20% 96.5%',
120
+ '--muted-foreground': '215 16% 47%',
121
+ '--accent': '243 75% 96%', // indigo-tinted hover
122
+ '--accent-foreground': '243 75% 30%',
123
+ '--destructive': '0 84.2% 60.2%',
124
+ '--destructive-foreground': '210 40% 98%',
125
+ '--border': '214 32% 91%',
126
+ '--input': '214 32% 91%',
127
+ '--ring': '243 75% 59%',
128
+ '--radius': '0.65rem',
129
+ '--hazo-kanban-priority-p0': '0 84% 60%',
130
+ '--hazo-kanban-priority-p1': '38 92% 50%',
131
+ '--hazo-kanban-priority-p2': '243 75% 59%',
132
+ '--hazo-kanban-priority-p3': '220 9% 64%',
133
+ '--hazo-kanban-card-bg': '0 0% 100%',
134
+ '--hazo-kanban-card-border': '214 32% 91%',
135
+ '--hazo-kanban-column-gap': '0.85rem',
136
+ };
@@ -0,0 +1,30 @@
1
+ export interface CatalogType {
2
+ type_key: string;
3
+ label: string;
4
+ color: string | null;
5
+ description: string | null;
6
+ scope_id: string | null;
7
+ sort_order: number;
8
+ }
9
+ export interface UseIssueTypesResult {
10
+ types: CatalogType[];
11
+ loading: boolean;
12
+ error: string | null;
13
+ refetch: () => Promise<void>;
14
+ createType: (input: {
15
+ type_key: string;
16
+ label: string;
17
+ color?: string | null;
18
+ description?: string | null;
19
+ sort_order?: number;
20
+ }) => Promise<void>;
21
+ updateType: (type_key: string, patch: {
22
+ label?: string;
23
+ color?: string | null;
24
+ description?: string | null;
25
+ sort_order?: number;
26
+ }) => Promise<void>;
27
+ deleteType: (type_key: string) => Promise<void>;
28
+ }
29
+ export declare function useIssueTypes(basePath: string): UseIssueTypesResult;
30
+ //# sourceMappingURL=use_issue_types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use_issue_types.d.ts","sourceRoot":"","sources":["../../../src/components/issues_panel/use_issue_types.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,UAAU,EAAE,CAAC,KAAK,EAAE;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpB,UAAU,EAAE,CACV,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,KAC/F,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACjD;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,mBAAmB,CAiGnE"}
@@ -0,0 +1,81 @@
1
+ 'use client';
2
+ import { useState, useEffect, useCallback } from 'react';
3
+ export function useIssueTypes(basePath) {
4
+ const [types, setTypes] = useState([]);
5
+ const [loading, setLoading] = useState(false);
6
+ const [error, setError] = useState(null);
7
+ const refetch = useCallback(async () => {
8
+ setLoading(true);
9
+ setError(null);
10
+ try {
11
+ const res = await fetch(`${basePath}/issue-types`);
12
+ const json = await res.json();
13
+ if (!res.ok) {
14
+ setError(String(json?.error ?? `Failed to fetch issue types (${res.status})`));
15
+ return;
16
+ }
17
+ const rows = Array.isArray(json) ? json : (json.issueTypes ?? json.data ?? []);
18
+ setTypes(rows);
19
+ }
20
+ catch (err) {
21
+ setError(String(err));
22
+ }
23
+ finally {
24
+ setLoading(false);
25
+ }
26
+ }, [basePath]);
27
+ useEffect(() => {
28
+ refetch();
29
+ }, [refetch]);
30
+ const createType = useCallback(async (input) => {
31
+ try {
32
+ const res = await fetch(`${basePath}/issue-types`, {
33
+ method: 'POST',
34
+ headers: { 'Content-Type': 'application/json' },
35
+ body: JSON.stringify(input),
36
+ });
37
+ if (!res.ok) {
38
+ const json = await res.json().catch(() => null);
39
+ setError(String(json?.error ?? `Failed to create issue type (${res.status})`));
40
+ return;
41
+ }
42
+ await refetch();
43
+ }
44
+ catch (err) {
45
+ setError(String(err));
46
+ }
47
+ }, [basePath, refetch]);
48
+ const updateType = useCallback(async (type_key, patch) => {
49
+ try {
50
+ const res = await fetch(`${basePath}/issue-types/${type_key}`, {
51
+ method: 'PATCH',
52
+ headers: { 'Content-Type': 'application/json' },
53
+ body: JSON.stringify(patch),
54
+ });
55
+ if (!res.ok) {
56
+ const json = await res.json().catch(() => null);
57
+ setError(String(json?.error ?? `Failed to update issue type (${res.status})`));
58
+ return;
59
+ }
60
+ await refetch();
61
+ }
62
+ catch (err) {
63
+ setError(String(err));
64
+ }
65
+ }, [basePath, refetch]);
66
+ const deleteType = useCallback(async (type_key) => {
67
+ try {
68
+ const res = await fetch(`${basePath}/issue-types/${type_key}`, { method: 'DELETE' });
69
+ if (!res.ok) {
70
+ const json = await res.json().catch(() => null);
71
+ setError(String(json?.error ?? `Failed to delete issue type (${res.status})`));
72
+ return;
73
+ }
74
+ await refetch();
75
+ }
76
+ catch (err) {
77
+ setError(String(err));
78
+ }
79
+ }, [basePath, refetch]);
80
+ return { types, loading, error, refetch, createType, updateType, deleteType };
81
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { adminGate, withAdminGate } from './lib/admin_gate.server.js';
2
2
  export type { AdminGateResult } from './lib/admin_gate.server.js';
3
3
  export { HAZO_ADMIN_PERMISSIONS } from './index.client.js';
4
- export { createIssueStore, createIssueStoreFromConnect, registerIssueType, getIssueType, listIssueTypes, loadIssueRoutingConfig, getRecipientStrategy, issueArchiveJobHandler, registerIssueArchiveJob, ADMIN_ISSUE_ARCHIVE_JOB_TYPE, } from './issues/index.js';
5
- export type { IssueRecord, IssueStore, CreateIssueInput, IssueTypeDef, IssueActionDef, IssueActionCtx, IssueActionResult, RecipientResult, NotifyPayload, IssueRoutingConfig, RecipientStrategy, } from './issues/index.js';
4
+ export { createIssueStore, createIssueStoreFromConnect, createIssueTypeCatalogStore, createIssueTypeCatalogStoreFromConnect, registerIssueType, getIssueType, listIssueTypes, loadIssueRoutingConfig, getRecipientStrategy, issueArchiveJobHandler, registerIssueArchiveJob, ADMIN_ISSUE_ARCHIVE_JOB_TYPE, raiseIssue, issueNotifyJobHandler, registerIssueNotifyJob, ADMIN_ISSUE_NOTIFY_JOB_TYPE, resolveGlobalAdmins, resolveScopeAdmins, GLOBAL_ADMIN_PERMISSION, SCOPE_ADMIN_PERMISSION, GLOBAL_ISSUE_SCOPE_ID, } from './issues/index.js';
5
+ export type { IssueRecord, IssueStore, CreateIssueInput, IssueTypeCatalogRecord, IssueTypeCatalogStore, IssueTypeDef, IssueActionDef, IssueActionCtx, IssueActionResult, RecipientResult, NotifyPayload, IssueRoutingConfig, RecipientStrategy, RaiseIssueInput, RaiseIssueOptions, IssueSeverity, } from './issues/index.js';
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AACtE,YAAY,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,OAAO,EACL,gBAAgB,EAChB,2BAA2B,EAC3B,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,sBAAsB,EACtB,uBAAuB,EACvB,4BAA4B,GAC7B,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,WAAW,EACX,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AACtE,YAAY,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,OAAO,EACL,gBAAgB,EAChB,2BAA2B,EAC3B,2BAA2B,EAC3B,sCAAsC,EACtC,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,sBAAsB,EACtB,uBAAuB,EACvB,4BAA4B,EAC5B,UAAU,EACV,qBAAqB,EACrB,sBAAsB,EACtB,2BAA2B,EAC3B,mBAAmB,EACnB,kBAAkB,EAClB,uBAAuB,EACvB,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,WAAW,EACX,UAAU,EACV,gBAAgB,EAChB,sBAAsB,EACtB,qBAAqB,EACrB,YAAY,EACZ,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EACjB,aAAa,GACd,MAAM,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  export { adminGate, withAdminGate } from './lib/admin_gate.server.js';
2
2
  export { HAZO_ADMIN_PERMISSIONS } from './index.client.js';
3
- export { createIssueStore, createIssueStoreFromConnect, registerIssueType, getIssueType, listIssueTypes, loadIssueRoutingConfig, getRecipientStrategy, issueArchiveJobHandler, registerIssueArchiveJob, ADMIN_ISSUE_ARCHIVE_JOB_TYPE, } from './issues/index.js';
3
+ export { createIssueStore, createIssueStoreFromConnect, createIssueTypeCatalogStore, createIssueTypeCatalogStoreFromConnect, registerIssueType, getIssueType, listIssueTypes, loadIssueRoutingConfig, getRecipientStrategy, issueArchiveJobHandler, registerIssueArchiveJob, ADMIN_ISSUE_ARCHIVE_JOB_TYPE, raiseIssue, issueNotifyJobHandler, registerIssueNotifyJob, ADMIN_ISSUE_NOTIFY_JOB_TYPE, resolveGlobalAdmins, resolveScopeAdmins, GLOBAL_ADMIN_PERMISSION, SCOPE_ADMIN_PERMISSION, GLOBAL_ISSUE_SCOPE_ID, } from './issues/index.js';
@@ -16,4 +16,6 @@ export type { AdminManifest, AdminNavSection, AdminNavItem } from './components/
16
16
  export { resolveNav } from './components/admin_nav.js';
17
17
  export type { AdminKindDef } from './components/admin_kinds.js';
18
18
  export { ADMIN_KINDS, getKindDef, getDefaultPermission, getDefaultIcon } from './components/admin_kinds.js';
19
+ export { IssuesPanel } from './components/issues_panel/index.js';
20
+ export type { IssuesPanelProps } from './components/issues_panel/index.js';
19
21
  //# sourceMappingURL=index.ui.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.ui.d.ts","sourceRoot":"","sources":["../src/index.ui.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC3D,YAAY,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrD,YAAY,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,YAAY,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,YAAY,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,YAAY,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AACxE,YAAY,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC3D,YAAY,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAC9F,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,YAAY,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC"}
1
+ {"version":3,"file":"index.ui.d.ts","sourceRoot":"","sources":["../src/index.ui.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC3D,YAAY,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrD,YAAY,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,YAAY,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,YAAY,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,YAAY,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AACxE,YAAY,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC3D,YAAY,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAC9F,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,YAAY,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC5G,OAAO,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AACjE,YAAY,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC"}
package/dist/index.ui.js CHANGED
@@ -8,3 +8,4 @@ export { EnvMigrationPanel } from './components/env_migration_panel.js';
8
8
  export { HealthPanel } from './components/health_panel.js';
9
9
  export { resolveNav } from './components/admin_nav.js';
10
10
  export { ADMIN_KINDS, getKindDef, getDefaultPermission, getDefaultIcon } from './components/admin_kinds.js';
11
+ export { IssuesPanel } from './components/issues_panel/index.js';
@@ -3,7 +3,7 @@ export declare const ADMIN_ISSUE_ARCHIVE_JOB_TYPE: "admin.issue_archive";
3
3
  /**
4
4
  * Job handler for admin.issue_archive jobs.
5
5
  *
6
- * Payload: { cutoffMonths?: number } — defaults to 3 months.
6
+ * Payload: { cutoffMonths?: number } — defaults to 1 month.
7
7
  *
8
8
  * Moves all closed issues whose resolved_at is older than cutoffMonths months
9
9
  * to status=archived.
@@ -4,7 +4,7 @@ export const ADMIN_ISSUE_ARCHIVE_JOB_TYPE = 'admin.issue_archive';
4
4
  /**
5
5
  * Job handler for admin.issue_archive jobs.
6
6
  *
7
- * Payload: { cutoffMonths?: number } — defaults to 3 months.
7
+ * Payload: { cutoffMonths?: number } — defaults to 1 month.
8
8
  *
9
9
  * Moves all closed issues whose resolved_at is older than cutoffMonths months
10
10
  * to status=archived.
@@ -17,7 +17,7 @@ export async function issueArchiveJobHandler(job, ctx, opts) {
17
17
  : raw && typeof raw === 'object'
18
18
  ? raw
19
19
  : {};
20
- const cutoffMonths = typeof payload.cutoffMonths === 'number' ? payload.cutoffMonths : 3;
20
+ const cutoffMonths = typeof payload.cutoffMonths === 'number' ? payload.cutoffMonths : 1;
21
21
  const cutoffDate = new Date();
22
22
  cutoffDate.setMonth(cutoffDate.getMonth() - cutoffMonths);
23
23
  ctx.log(`[admin.issue_archive] archiving issues closed before ${cutoffDate.toISOString()}`);
@@ -1,8 +1,14 @@
1
1
  export { createIssueStore, createIssueStoreFromConnect } from './store.js';
2
2
  export type { IssueRecord, IssueStore, CreateIssueInput } from './store.js';
3
+ export { createIssueTypeCatalogStore, createIssueTypeCatalogStoreFromConnect, } from './type_catalog.js';
4
+ export type { IssueTypeCatalogRecord, IssueTypeCatalogStore } from './type_catalog.js';
3
5
  export { registerIssueType, getIssueType, listIssueTypes } from './registry.js';
4
6
  export type { IssueTypeDef, IssueActionDef, IssueActionCtx, IssueActionResult, RecipientResult, NotifyPayload, } from './registry.js';
5
7
  export { loadIssueRoutingConfig, getRecipientStrategy } from './routing.js';
6
8
  export type { IssueRoutingConfig, RecipientStrategy } from './routing.js';
7
9
  export { issueArchiveJobHandler, registerIssueArchiveJob, ADMIN_ISSUE_ARCHIVE_JOB_TYPE, } from './archive_handler.js';
10
+ export { raiseIssue } from './raise.js';
11
+ export type { RaiseIssueInput, RaiseIssueOptions, IssueSeverity } from './raise.js';
12
+ export { issueNotifyJobHandler, registerIssueNotifyJob, ADMIN_ISSUE_NOTIFY_JOB_TYPE, } from './notify_handler.js';
13
+ export { resolveGlobalAdmins, resolveScopeAdmins, GLOBAL_ADMIN_PERMISSION, SCOPE_ADMIN_PERMISSION, GLOBAL_ISSUE_SCOPE_ID, } from './recipients.js';
8
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/issues/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AAC3E,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC5E,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAChF,YAAY,EACV,YAAY,EACZ,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,aAAa,GACd,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAC5E,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAC1E,OAAO,EACL,sBAAsB,EACtB,uBAAuB,EACvB,4BAA4B,GAC7B,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/issues/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AAC3E,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC5E,OAAO,EACL,2BAA2B,EAC3B,sCAAsC,GACvC,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AACvF,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAChF,YAAY,EACV,YAAY,EACZ,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,aAAa,GACd,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAC5E,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAC1E,OAAO,EACL,sBAAsB,EACtB,uBAAuB,EACvB,4BAA4B,GAC7B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,YAAY,EAAE,eAAe,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACpF,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,2BAA2B,GAC5B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,uBAAuB,EACvB,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,iBAAiB,CAAC"}
@@ -1,4 +1,8 @@
1
1
  export { createIssueStore, createIssueStoreFromConnect } from './store.js';
2
+ export { createIssueTypeCatalogStore, createIssueTypeCatalogStoreFromConnect, } from './type_catalog.js';
2
3
  export { registerIssueType, getIssueType, listIssueTypes } from './registry.js';
3
4
  export { loadIssueRoutingConfig, getRecipientStrategy } from './routing.js';
4
5
  export { issueArchiveJobHandler, registerIssueArchiveJob, ADMIN_ISSUE_ARCHIVE_JOB_TYPE, } from './archive_handler.js';
6
+ export { raiseIssue } from './raise.js';
7
+ export { issueNotifyJobHandler, registerIssueNotifyJob, ADMIN_ISSUE_NOTIFY_JOB_TYPE, } from './notify_handler.js';
8
+ export { resolveGlobalAdmins, resolveScopeAdmins, GLOBAL_ADMIN_PERMISSION, SCOPE_ADMIN_PERMISSION, GLOBAL_ISSUE_SCOPE_ID, } from './recipients.js';
@@ -0,0 +1,26 @@
1
+ import 'server-only';
2
+ export declare const ADMIN_ISSUE_NOTIFY_JOB_TYPE: "hazo_admin.issue.notify";
3
+ /**
4
+ * Job handler for hazo_admin.issue.notify jobs.
5
+ * Payload: { issueId: string }. Loads the issue, resolves recipients (typed-def
6
+ * hook first, then global/scope admin fallback), and best-effort dispatches a
7
+ * "new issue raised" notification via hazo_notify. All external calls are
8
+ * best-effort — a missing hazo_notify or empty recipient set is a clean no-op.
9
+ */
10
+ export declare function issueNotifyJobHandler(job: {
11
+ id: string;
12
+ payload: unknown;
13
+ }, ctx: {
14
+ log: (...args: any[]) => void;
15
+ }, opts: {
16
+ getHazoConnect: () => Promise<any> | any;
17
+ }): Promise<{
18
+ notified: number;
19
+ }>;
20
+ /** Register the hazo_admin.issue.notify job type with a hazo_jobs worker. */
21
+ export declare function registerIssueNotifyJob(worker: {
22
+ register: (type: string, handler: (job: any, ctx: any) => Promise<unknown>) => void;
23
+ }, opts: {
24
+ getHazoConnect: () => Promise<any> | any;
25
+ }): void;
26
+ //# sourceMappingURL=notify_handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notify_handler.d.ts","sourceRoot":"","sources":["../../src/issues/notify_handler.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAKrB,eAAO,MAAM,2BAA2B,EAAG,yBAAkC,CAAC;AAE9E;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,GAAG,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,EACrC,GAAG,EAAE;IAAE,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;CAAE,EACtC,IAAI,EAAE;IAAE,cAAc,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAA;CAAE,GACjD,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAwF/B;AAED,6EAA6E;AAC7E,wBAAgB,sBAAsB,CACpC,MAAM,EAAE;IACN,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;CACrF,EACD,IAAI,EAAE;IAAE,cAAc,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAA;CAAE,GACjD,IAAI,CAMN"}
@@ -0,0 +1,97 @@
1
+ import 'server-only';
2
+ import { createIssueStore } from './store.js';
3
+ import { getIssueType } from './registry.js';
4
+ import { resolveGlobalAdmins, resolveScopeAdmins, GLOBAL_ISSUE_SCOPE_ID } from './recipients.js';
5
+ export const ADMIN_ISSUE_NOTIFY_JOB_TYPE = 'hazo_admin.issue.notify';
6
+ /**
7
+ * Job handler for hazo_admin.issue.notify jobs.
8
+ * Payload: { issueId: string }. Loads the issue, resolves recipients (typed-def
9
+ * hook first, then global/scope admin fallback), and best-effort dispatches a
10
+ * "new issue raised" notification via hazo_notify. All external calls are
11
+ * best-effort — a missing hazo_notify or empty recipient set is a clean no-op.
12
+ */
13
+ export async function issueNotifyJobHandler(job, ctx, opts) {
14
+ ctx.log(`[${ADMIN_ISSUE_NOTIFY_JOB_TYPE}] starting job ${job.id}`);
15
+ const raw = job.payload;
16
+ const payload = typeof raw === 'string'
17
+ ? JSON.parse(raw)
18
+ : raw && typeof raw === 'object'
19
+ ? raw
20
+ : {};
21
+ const issueId = typeof payload.issueId === 'string' ? payload.issueId : undefined;
22
+ if (!issueId) {
23
+ ctx.log(`[${ADMIN_ISSUE_NOTIFY_JOB_TYPE}] no issueId in payload — skipping`);
24
+ return { notified: 0 };
25
+ }
26
+ const connect = await opts.getHazoConnect();
27
+ const store = createIssueStore(connect);
28
+ const issue = await store.getIssue(issueId);
29
+ if (!issue) {
30
+ ctx.log(`[${ADMIN_ISSUE_NOTIFY_JOB_TYPE}] issue ${issueId} not found — skipping`);
31
+ return { notified: 0 };
32
+ }
33
+ // 1) Typed-def recipient hook, if the type supplies one.
34
+ let recipients = [];
35
+ const typeDef = getIssueType(issue.type);
36
+ if (typeDef?.resolveRecipients) {
37
+ try {
38
+ const r = await typeDef.resolveRecipients(issue, {
39
+ getHazoConnect: opts.getHazoConnect,
40
+ actorUserId: '',
41
+ actorPermissions: [],
42
+ });
43
+ recipients = Array.isArray(r?.user_ids) ? r.user_ids : [];
44
+ }
45
+ catch {
46
+ recipients = [];
47
+ }
48
+ }
49
+ // 2) Fallback: global admins (no scope) or scope admins (scoped).
50
+ const isGlobal = !issue.scope_id || issue.scope_id === GLOBAL_ISSUE_SCOPE_ID;
51
+ if (recipients.length === 0) {
52
+ recipients = isGlobal
53
+ ? await resolveGlobalAdmins(connect)
54
+ : await resolveScopeAdmins(connect, issue.scope_id);
55
+ }
56
+ if (recipients.length === 0) {
57
+ ctx.log(`[${ADMIN_ISSUE_NOTIFY_JOB_TYPE}] no recipients for issue ${issueId} — skipping`);
58
+ return { notified: 0 };
59
+ }
60
+ const notifyMod = (await import('hazo_notify/dispatcher').catch(() => null));
61
+ if (!notifyMod?.dispatch) {
62
+ ctx.log(`[${ADMIN_ISSUE_NOTIFY_JOB_TYPE}] hazo_notify absent — skipping dispatch`);
63
+ return { notified: 0 };
64
+ }
65
+ const in_app_text = `New ${issue.severity} issue: ${issue.title}`;
66
+ try {
67
+ await notifyMod.dispatch({
68
+ event_type: 'hazo_admin.issue.raised',
69
+ subject_id: issue.id,
70
+ scope_id: issue.scope_id,
71
+ recipient_user_ids: recipients,
72
+ in_app_text,
73
+ deep_link: '/admin/issues',
74
+ surfaces: { in_app: true, banner: false },
75
+ channels: { in_app: true, email: true },
76
+ channel_payloads: {
77
+ in_app: { text: in_app_text },
78
+ email: { subject: `New issue: ${issue.title}`, body: issue.summary },
79
+ },
80
+ payload: {
81
+ issue_id: issue.id,
82
+ type: issue.type,
83
+ severity: issue.severity,
84
+ source: issue.source,
85
+ },
86
+ });
87
+ }
88
+ catch (err) {
89
+ ctx.log(`[${ADMIN_ISSUE_NOTIFY_JOB_TYPE}] dispatch failed (best-effort): ${err?.message}`);
90
+ }
91
+ ctx.log(`[${ADMIN_ISSUE_NOTIFY_JOB_TYPE}] notified ${recipients.length} recipient(s) for issue ${issueId}`);
92
+ return { notified: recipients.length };
93
+ }
94
+ /** Register the hazo_admin.issue.notify job type with a hazo_jobs worker. */
95
+ export function registerIssueNotifyJob(worker, opts) {
96
+ worker.register(ADMIN_ISSUE_NOTIFY_JOB_TYPE, (job, ctx) => issueNotifyJobHandler(job, ctx, opts));
97
+ }