hazo_admin 0.3.1 → 0.5.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.
- package/CHANGE_LOG.md +26 -0
- package/README.md +115 -2
- package/SETUP_CHECKLIST.md +4 -1
- package/config/hazo_admin_config.ini.sample +11 -0
- package/dist/api/index.d.ts +4 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +174 -0
- package/dist/components/admin_app.d.ts.map +1 -1
- package/dist/components/admin_app.js +3 -0
- package/dist/components/admin_kinds.d.ts.map +1 -1
- package/dist/components/admin_kinds.js +9 -1
- package/dist/components/admin_nav.d.ts +2 -2
- package/dist/components/admin_nav.d.ts.map +1 -1
- package/dist/components/admin_nav.js +12 -0
- package/dist/components/issues_panel/index.d.ts +6 -0
- package/dist/components/issues_panel/index.d.ts.map +1 -0
- package/dist/components/issues_panel/index.js +130 -0
- package/dist/index.client.d.ts +4 -0
- package/dist/index.client.d.ts.map +1 -1
- package/dist/index.client.js +4 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/issues/archive_handler.d.ts +29 -0
- package/dist/issues/archive_handler.d.ts.map +1 -0
- package/dist/issues/archive_handler.js +35 -0
- package/dist/issues/index.d.ts +8 -0
- package/dist/issues/index.d.ts.map +1 -0
- package/dist/issues/index.js +4 -0
- package/dist/issues/registry.client.d.ts +32 -0
- package/dist/issues/registry.client.d.ts.map +1 -0
- package/dist/issues/registry.client.js +15 -0
- package/dist/issues/registry.d.ts +45 -0
- package/dist/issues/registry.d.ts.map +1 -0
- package/dist/issues/registry.js +11 -0
- package/dist/issues/routing.d.ts +17 -0
- package/dist/issues/routing.d.ts.map +1 -0
- package/dist/issues/routing.js +43 -0
- package/dist/issues/store.d.ts +61 -0
- package/dist/issues/store.d.ts.map +1 -0
- package/dist/issues/store.js +227 -0
- package/package.json +15 -10
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState, useCallback } from 'react';
|
|
4
|
+
import { getIssueCardRenderer, DefaultIssueCard } from '../../issues/registry.client.js';
|
|
5
|
+
const ACTIVE_COLUMNS = [
|
|
6
|
+
{ key: 'new', title: 'New' },
|
|
7
|
+
{ key: 'wip', title: 'WIP' },
|
|
8
|
+
{ key: 'closed', title: 'Closed' },
|
|
9
|
+
];
|
|
10
|
+
const ARCHIVED_COLUMNS = [
|
|
11
|
+
{ key: 'archived', title: 'Archived' },
|
|
12
|
+
];
|
|
13
|
+
function toKanbanItem(issue) {
|
|
14
|
+
return { ...issue, columnKey: issue.status };
|
|
15
|
+
}
|
|
16
|
+
export function IssuesPanel({ basePath = '/api/admin', currentUserId }) {
|
|
17
|
+
const [Kanban, setKanban] = useState(null);
|
|
18
|
+
const [KanbanFilter, setKanbanFilter] = useState(null);
|
|
19
|
+
const [applyFilter, setApplyFilter] = useState(null);
|
|
20
|
+
const [notInstalled, setNotInstalled] = useState(false);
|
|
21
|
+
const [uiLoading, setUiLoading] = useState(true);
|
|
22
|
+
const [tab, setTab] = useState('active');
|
|
23
|
+
const [activeIssues, setActiveIssues] = useState([]);
|
|
24
|
+
const [archivedIssues, setArchivedIssues] = useState([]);
|
|
25
|
+
const [dataLoading, setDataLoading] = useState(false);
|
|
26
|
+
const [dataError, setDataError] = useState(null);
|
|
27
|
+
const [filter, setFilter] = useState({ search: '', categories: [], priority: null });
|
|
28
|
+
// Probe hazo_ui for Kanban components
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
import('hazo_ui').catch(() => null).then((pkg) => {
|
|
31
|
+
if (!pkg || !pkg.HazoUiKanban) {
|
|
32
|
+
setNotInstalled(true);
|
|
33
|
+
setUiLoading(false);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
setKanban(() => pkg.HazoUiKanban);
|
|
37
|
+
setKanbanFilter(() => pkg.HazoUiKanbanFilter ?? null);
|
|
38
|
+
setApplyFilter(() => pkg.applyKanbanFilter ?? null);
|
|
39
|
+
setUiLoading(false);
|
|
40
|
+
});
|
|
41
|
+
}, []);
|
|
42
|
+
// Fetch active issues on mount
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
setDataLoading(true);
|
|
45
|
+
setDataError(null);
|
|
46
|
+
fetch(`${basePath}/issues?status=new,wip,closed`)
|
|
47
|
+
.then((r) => r.json())
|
|
48
|
+
.then((json) => {
|
|
49
|
+
const rows = Array.isArray(json) ? json : (json.data ?? []);
|
|
50
|
+
setActiveIssues(rows);
|
|
51
|
+
})
|
|
52
|
+
.catch((err) => setDataError(String(err)))
|
|
53
|
+
.finally(() => setDataLoading(false));
|
|
54
|
+
}, [basePath]);
|
|
55
|
+
// Fetch archived issues when tab switches to archived
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (tab !== 'archived')
|
|
58
|
+
return;
|
|
59
|
+
if (archivedIssues.length > 0)
|
|
60
|
+
return; // already loaded
|
|
61
|
+
setDataLoading(true);
|
|
62
|
+
setDataError(null);
|
|
63
|
+
fetch(`${basePath}/issues?status=archived`)
|
|
64
|
+
.then((r) => r.json())
|
|
65
|
+
.then((json) => {
|
|
66
|
+
const rows = Array.isArray(json) ? json : (json.data ?? []);
|
|
67
|
+
setArchivedIssues(rows);
|
|
68
|
+
})
|
|
69
|
+
.catch((err) => setDataError(String(err)))
|
|
70
|
+
.finally(() => setDataLoading(false));
|
|
71
|
+
}, [tab, basePath, archivedIssues.length]);
|
|
72
|
+
const handleMove = useCallback(async (event) => {
|
|
73
|
+
try {
|
|
74
|
+
const res = await fetch(`${basePath}/issues/${event.itemId}/transition`, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: { 'Content-Type': 'application/json' },
|
|
77
|
+
body: JSON.stringify({ status: event.toColumn, actorUserId: currentUserId }),
|
|
78
|
+
});
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
event.revert();
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
// Update local state
|
|
84
|
+
setActiveIssues((prev) => prev.map((iss) => iss.id === event.itemId ? { ...iss, status: event.toColumn, columnKey: event.toColumn } : iss));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
event.revert();
|
|
89
|
+
}
|
|
90
|
+
}, [basePath, currentUserId]);
|
|
91
|
+
const handleAction = useCallback(async (issueId, actionKey, params) => {
|
|
92
|
+
await fetch(`${basePath}/issues/${issueId}/resolve`, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: { 'Content-Type': 'application/json' },
|
|
95
|
+
body: JSON.stringify({ actionKey, params }),
|
|
96
|
+
});
|
|
97
|
+
}, [basePath]);
|
|
98
|
+
if (uiLoading) {
|
|
99
|
+
return _jsx("div", { className: "p-6 text-sm text-gray-400", children: "Loading\u2026" });
|
|
100
|
+
}
|
|
101
|
+
if (notInstalled) {
|
|
102
|
+
return (_jsxs("div", { className: "p-6 max-w-xl", children: [_jsx("h2", { className: "text-base font-semibold text-gray-800 mb-2", children: "Issues" }), _jsxs("p", { className: "text-sm text-gray-500", children: [_jsx("code", { className: "text-xs bg-gray-100 px-1 rounded", children: "hazo_ui" }), " Kanban components are not available. Ensure ", _jsx("code", { className: "text-xs bg-gray-100 px-1 rounded", children: "hazo_ui" }), " is installed and exports", ' ', _jsx("code", { className: "text-xs bg-gray-100 px-1 rounded", children: "HazoUiKanban" }), "."] })] }));
|
|
103
|
+
}
|
|
104
|
+
const HazoUiKanban = Kanban;
|
|
105
|
+
const HazoUiKanbanFilter = KanbanFilter;
|
|
106
|
+
const currentIssues = tab === 'active' ? activeIssues : archivedIssues;
|
|
107
|
+
const kanbanItems = currentIssues.map(toKanbanItem);
|
|
108
|
+
// Apply filter for active tab
|
|
109
|
+
const filteredItems = (tab === 'active' && applyFilter)
|
|
110
|
+
? applyFilter(kanbanItems, filter)
|
|
111
|
+
: kanbanItems;
|
|
112
|
+
// Unique types for category filter
|
|
113
|
+
const uniqueTypes = Array.from(new Set(activeIssues.map((i) => i.type))).sort();
|
|
114
|
+
const columns = tab === 'active' ? ACTIVE_COLUMNS : ARCHIVED_COLUMNS;
|
|
115
|
+
function renderCard(item) {
|
|
116
|
+
const Renderer = getIssueCardRenderer(item.type);
|
|
117
|
+
const onAction = async (actionKey, params) => {
|
|
118
|
+
await handleAction(item.id, actionKey, params);
|
|
119
|
+
};
|
|
120
|
+
if (Renderer) {
|
|
121
|
+
return _jsx(Renderer, { issue: item, basePath: basePath, onAction: onAction });
|
|
122
|
+
}
|
|
123
|
+
return _jsx(DefaultIssueCard, { issue: item, basePath: basePath, onAction: onAction });
|
|
124
|
+
}
|
|
125
|
+
return (_jsxs("div", { className: "flex flex-col h-full", children: [_jsxs("div", { className: "p-4 border-b flex items-center gap-4", children: [_jsx("h2", { className: "text-base font-semibold text-gray-800", children: "Issues" }), _jsxs("div", { className: "flex gap-2 ml-auto", children: [_jsx("button", { onClick: () => setTab('active'), className: `px-3 py-1 rounded text-sm font-medium transition-colors ${tab === 'active'
|
|
126
|
+
? 'bg-blue-100 text-blue-700'
|
|
127
|
+
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'}`, children: "Active" }), _jsx("button", { onClick: () => setTab('archived'), className: `px-3 py-1 rounded text-sm font-medium transition-colors ${tab === 'archived'
|
|
128
|
+
? 'bg-blue-100 text-blue-700'
|
|
129
|
+
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'}`, children: "Archived" })] })] }), tab === 'active' && HazoUiKanbanFilter && (_jsx("div", { className: "px-4 py-2 border-b", children: _jsx(HazoUiKanbanFilter, { search: true, searchPlaceholder: "Search issues\u2026", categories: uniqueTypes, value: filter, onChange: setFilter }) })), dataError && (_jsxs("div", { className: "px-4 py-2 text-sm text-red-600 bg-red-50 border-b", children: ["Failed to load issues: ", dataError] })), dataLoading && (_jsx("div", { className: "px-4 py-2 text-sm text-gray-400", children: "Loading issues\u2026" })), _jsx("div", { className: "flex-1 overflow-auto p-4", children: HazoUiKanban && (_jsx(HazoUiKanban, { columns: columns, items: filteredItems, renderCard: renderCard, onMove: tab === 'active' ? handleMove : undefined, disableEdit: tab === 'archived' })) })] }));
|
|
130
|
+
}
|
package/dist/index.client.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { registerIssueCardRenderer, getIssueCardRenderer, listIssueCardRenderers, DefaultIssueCard, } from './issues/registry.client.js';
|
|
2
|
+
export type { IssueCardData, IssueCardRenderer, IssueCardRendererProps, } from './issues/registry.client.js';
|
|
1
3
|
export declare const HAZO_ADMIN_PERMISSIONS: {
|
|
2
4
|
readonly ADMIN_SYSTEM: "admin_system";
|
|
3
5
|
readonly ENV_MIGRATE: "env.migrate";
|
|
@@ -10,6 +12,8 @@ export declare const HAZO_ADMIN_PERMISSIONS: {
|
|
|
10
12
|
readonly FEEDBACK_REVIEW: "admin.feedback.review";
|
|
11
13
|
readonly METRICS_VIEW: "metrics.view";
|
|
12
14
|
readonly METRICS_MANAGE: "metrics.manage";
|
|
15
|
+
readonly ADMIN_ISSUE_TRIAGE: "admin_issue_triage";
|
|
16
|
+
readonly ADMIN_USER_SCOPE_ASSIGNMENT: "admin_user_scope_assignment";
|
|
13
17
|
};
|
|
14
18
|
export type HazoAdminPermissionKey = keyof typeof HAZO_ADMIN_PERMISSIONS;
|
|
15
19
|
export type HazoAdminPermission = (typeof HAZO_ADMIN_PERMISSIONS)[HazoAdminPermissionKey];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.client.d.ts","sourceRoot":"","sources":["../src/index.client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.client.d.ts","sourceRoot":"","sources":["../src/index.client.ts"],"names":[],"mappings":"AACA,OAAO,EACL,yBAAyB,EACzB,oBAAoB,EACpB,sBAAsB,EACtB,gBAAgB,GACjB,MAAM,6BAA6B,CAAC;AACrC,YAAY,EACV,aAAa,EACb,iBAAiB,EACjB,sBAAsB,GACvB,MAAM,6BAA6B,CAAC;AAErC,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;CAczB,CAAC;AAEX,MAAM,MAAM,sBAAsB,GAAG,MAAM,OAAO,sBAAsB,CAAC;AACzE,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,sBAAsB,CAAC,CAAC"}
|
package/dist/index.client.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Client-side issue card renderer registry
|
|
2
|
+
export { registerIssueCardRenderer, getIssueCardRenderer, listIssueCardRenderers, DefaultIssueCard, } from './issues/registry.client.js';
|
|
1
3
|
export const HAZO_ADMIN_PERMISSIONS = {
|
|
2
4
|
ADMIN_SYSTEM: 'admin_system',
|
|
3
5
|
ENV_MIGRATE: 'env.migrate',
|
|
@@ -10,4 +12,6 @@ export const HAZO_ADMIN_PERMISSIONS = {
|
|
|
10
12
|
FEEDBACK_REVIEW: 'admin.feedback.review',
|
|
11
13
|
METRICS_VIEW: 'metrics.view',
|
|
12
14
|
METRICS_MANAGE: 'metrics.manage',
|
|
15
|
+
ADMIN_ISSUE_TRIAGE: 'admin_issue_triage',
|
|
16
|
+
ADMIN_USER_SCOPE_ASSIGNMENT: 'admin_user_scope_assignment',
|
|
13
17
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +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, 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
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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"}
|
|
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,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"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +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, registerIssueType, getIssueType, listIssueTypes, loadIssueRoutingConfig, getRecipientStrategy, issueArchiveJobHandler, registerIssueArchiveJob, ADMIN_ISSUE_ARCHIVE_JOB_TYPE, } from './issues/index.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
export declare const ADMIN_ISSUE_ARCHIVE_JOB_TYPE: "admin.issue_archive";
|
|
3
|
+
/**
|
|
4
|
+
* Job handler for admin.issue_archive jobs.
|
|
5
|
+
*
|
|
6
|
+
* Payload: { cutoffMonths?: number } — defaults to 3 months.
|
|
7
|
+
*
|
|
8
|
+
* Moves all closed issues whose resolved_at is older than cutoffMonths months
|
|
9
|
+
* to status=archived.
|
|
10
|
+
*/
|
|
11
|
+
export declare function issueArchiveJobHandler(job: {
|
|
12
|
+
id: string;
|
|
13
|
+
payload: unknown;
|
|
14
|
+
}, ctx: {
|
|
15
|
+
log: (...args: any[]) => void;
|
|
16
|
+
}, opts: {
|
|
17
|
+
getHazoConnect: () => Promise<any> | any;
|
|
18
|
+
}): Promise<{
|
|
19
|
+
archivedCount: number;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* Register the admin.issue_archive job type with a hazo_jobs worker.
|
|
23
|
+
*/
|
|
24
|
+
export declare function registerIssueArchiveJob(worker: {
|
|
25
|
+
register: (type: string, handler: (job: any, ctx: any) => Promise<unknown>) => void;
|
|
26
|
+
}, opts: {
|
|
27
|
+
getHazoConnect: () => Promise<any> | any;
|
|
28
|
+
}): void;
|
|
29
|
+
//# sourceMappingURL=archive_handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive_handler.d.ts","sourceRoot":"","sources":["../../src/issues/archive_handler.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAGrB,eAAO,MAAM,4BAA4B,EAAG,qBAA8B,CAAC;AAE3E;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAC1C,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,aAAa,EAAE,MAAM,CAAA;CAAE,CAAC,CA0BpC;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,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,35 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
import { createIssueStore } from './store.js';
|
|
3
|
+
export const ADMIN_ISSUE_ARCHIVE_JOB_TYPE = 'admin.issue_archive';
|
|
4
|
+
/**
|
|
5
|
+
* Job handler for admin.issue_archive jobs.
|
|
6
|
+
*
|
|
7
|
+
* Payload: { cutoffMonths?: number } — defaults to 3 months.
|
|
8
|
+
*
|
|
9
|
+
* Moves all closed issues whose resolved_at is older than cutoffMonths months
|
|
10
|
+
* to status=archived.
|
|
11
|
+
*/
|
|
12
|
+
export async function issueArchiveJobHandler(job, ctx, opts) {
|
|
13
|
+
ctx.log(`[admin.issue_archive] starting job ${job.id}`);
|
|
14
|
+
const raw = job.payload;
|
|
15
|
+
const payload = typeof raw === 'string'
|
|
16
|
+
? JSON.parse(raw)
|
|
17
|
+
: raw && typeof raw === 'object'
|
|
18
|
+
? raw
|
|
19
|
+
: {};
|
|
20
|
+
const cutoffMonths = typeof payload.cutoffMonths === 'number' ? payload.cutoffMonths : 3;
|
|
21
|
+
const cutoffDate = new Date();
|
|
22
|
+
cutoffDate.setMonth(cutoffDate.getMonth() - cutoffMonths);
|
|
23
|
+
ctx.log(`[admin.issue_archive] archiving issues closed before ${cutoffDate.toISOString()}`);
|
|
24
|
+
const connect = await opts.getHazoConnect();
|
|
25
|
+
const store = createIssueStore(connect);
|
|
26
|
+
const archivedCount = await store.archiveClosedOlderThan(cutoffDate);
|
|
27
|
+
ctx.log(`[admin.issue_archive] archived ${archivedCount} issue(s)`);
|
|
28
|
+
return { archivedCount };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Register the admin.issue_archive job type with a hazo_jobs worker.
|
|
32
|
+
*/
|
|
33
|
+
export function registerIssueArchiveJob(worker, opts) {
|
|
34
|
+
worker.register(ADMIN_ISSUE_ARCHIVE_JOB_TYPE, (job, ctx) => issueArchiveJobHandler(job, ctx, opts));
|
|
35
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { createIssueStore } from './store.js';
|
|
2
|
+
export type { IssueRecord, IssueStore, CreateIssueInput } from './store.js';
|
|
3
|
+
export { registerIssueType, getIssueType, listIssueTypes } from './registry.js';
|
|
4
|
+
export type { IssueTypeDef, IssueActionDef, IssueActionCtx, IssueActionResult, RecipientResult, NotifyPayload, } from './registry.js';
|
|
5
|
+
export { loadIssueRoutingConfig, getRecipientStrategy } from './routing.js';
|
|
6
|
+
export type { IssueRoutingConfig, RecipientStrategy } from './routing.js';
|
|
7
|
+
export { issueArchiveJobHandler, registerIssueArchiveJob, ADMIN_ISSUE_ARCHIVE_JOB_TYPE, } from './archive_handler.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/issues/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC9C,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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createIssueStore } from './store.js';
|
|
2
|
+
export { registerIssueType, getIssueType, listIssueTypes } from './registry.js';
|
|
3
|
+
export { loadIssueRoutingConfig, getRecipientStrategy } from './routing.js';
|
|
4
|
+
export { issueArchiveJobHandler, registerIssueArchiveJob, ADMIN_ISSUE_ARCHIVE_JOB_TYPE, } from './archive_handler.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
export interface IssueCardData {
|
|
3
|
+
id: string;
|
|
4
|
+
type: string;
|
|
5
|
+
status: string;
|
|
6
|
+
subject_user_id: string;
|
|
7
|
+
assigned_to: string | null;
|
|
8
|
+
title: string;
|
|
9
|
+
summary: string;
|
|
10
|
+
payload: Record<string, unknown>;
|
|
11
|
+
occurrence_count: number;
|
|
12
|
+
first_seen_at: string;
|
|
13
|
+
last_seen_at: string;
|
|
14
|
+
resolution: string | null;
|
|
15
|
+
resolution_reason: string | null;
|
|
16
|
+
resolved_by: string | null;
|
|
17
|
+
resolved_at: string | null;
|
|
18
|
+
}
|
|
19
|
+
export interface IssueCardRendererProps {
|
|
20
|
+
issue: IssueCardData;
|
|
21
|
+
basePath: string;
|
|
22
|
+
onAction?: (actionKey: string, params: Record<string, unknown>) => Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export type IssueCardRenderer = React.ComponentType<IssueCardRendererProps>;
|
|
25
|
+
export declare function registerIssueCardRenderer(typeKey: string, renderer: IssueCardRenderer): void;
|
|
26
|
+
export declare function getIssueCardRenderer(typeKey: string): IssueCardRenderer | undefined;
|
|
27
|
+
export declare function listIssueCardRenderers(): Array<{
|
|
28
|
+
typeKey: string;
|
|
29
|
+
renderer: IssueCardRenderer;
|
|
30
|
+
}>;
|
|
31
|
+
export declare function DefaultIssueCard({ issue }: IssueCardRendererProps): React.JSX.Element;
|
|
32
|
+
//# sourceMappingURL=registry.client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.client.d.ts","sourceRoot":"","sources":["../../src/issues/registry.client.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,aAAa,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAClF;AAED,MAAM,MAAM,iBAAiB,GAAG,KAAK,CAAC,aAAa,CAAC,sBAAsB,CAAC,CAAC;AAI5E,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAE5F;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAEnF;AAED,wBAAgB,sBAAsB,IAAI,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,iBAAiB,CAAA;CAAE,CAAC,CAEhG;AAED,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,EAAE,sBAAsB,qBAWjE"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
const registry = new Map();
|
|
4
|
+
export function registerIssueCardRenderer(typeKey, renderer) {
|
|
5
|
+
registry.set(typeKey, renderer);
|
|
6
|
+
}
|
|
7
|
+
export function getIssueCardRenderer(typeKey) {
|
|
8
|
+
return registry.get(typeKey);
|
|
9
|
+
}
|
|
10
|
+
export function listIssueCardRenderers() {
|
|
11
|
+
return Array.from(registry.entries()).map(([typeKey, renderer]) => ({ typeKey, renderer }));
|
|
12
|
+
}
|
|
13
|
+
export function DefaultIssueCard({ issue }) {
|
|
14
|
+
return (_jsxs("div", { className: "text-sm space-y-1", children: [_jsx("div", { className: "font-medium text-gray-800", children: issue.title }), _jsx("div", { className: "text-gray-500 text-xs", children: issue.summary }), issue.occurrence_count > 1 && (_jsxs("div", { className: "text-xs text-amber-600", children: ["Occurred ", issue.occurrence_count, "\u00D7"] })), _jsx("div", { className: "text-xs text-gray-400", children: issue.type })] }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
import type { IssueRecord } from './store.js';
|
|
3
|
+
export type { IssueRecord };
|
|
4
|
+
export interface IssueActionCtx {
|
|
5
|
+
/** Injected by the API handler — provides access to hazo services. */
|
|
6
|
+
getHazoConnect: () => Promise<any> | any;
|
|
7
|
+
actorUserId: string;
|
|
8
|
+
actorPermissions: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface IssueActionResult {
|
|
11
|
+
success: boolean;
|
|
12
|
+
message?: string;
|
|
13
|
+
data?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
export interface RecipientResult {
|
|
16
|
+
user_ids: string[];
|
|
17
|
+
scope_id: string;
|
|
18
|
+
}
|
|
19
|
+
export interface NotifyPayload {
|
|
20
|
+
subject: string;
|
|
21
|
+
body: string;
|
|
22
|
+
deep_link?: string;
|
|
23
|
+
payload?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
export interface IssueActionDef {
|
|
26
|
+
key: string;
|
|
27
|
+
label: string;
|
|
28
|
+
requiredPermission: string;
|
|
29
|
+
run(issue: IssueRecord, params: Record<string, unknown>, ctx: IssueActionCtx): Promise<IssueActionResult>;
|
|
30
|
+
}
|
|
31
|
+
export interface IssueTypeDef {
|
|
32
|
+
typeKey: string;
|
|
33
|
+
label: string;
|
|
34
|
+
buildDescriptor(payload: Record<string, unknown>): {
|
|
35
|
+
title: string;
|
|
36
|
+
summary: string;
|
|
37
|
+
};
|
|
38
|
+
resolveRecipients(issue: IssueRecord, ctx: IssueActionCtx): Promise<RecipientResult>;
|
|
39
|
+
actions: IssueActionDef[];
|
|
40
|
+
buildResolutionNotice(issue: IssueRecord, actionKey: string, result: IssueActionResult): NotifyPayload;
|
|
41
|
+
}
|
|
42
|
+
export declare function registerIssueType(def: IssueTypeDef): void;
|
|
43
|
+
export declare function getIssueType(typeKey: string): IssueTypeDef | undefined;
|
|
44
|
+
export declare function listIssueTypes(): IssueTypeDef[];
|
|
45
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/issues/registry.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,YAAY,EAAE,WAAW,EAAE,CAAC;AAE5B,MAAM,WAAW,cAAc;IAC7B,sEAAsE;IACtE,cAAc,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,kBAAkB,EAAE,MAAM,CAAC;IAC3B,GAAG,CACD,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,iBAAiB,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACtF,iBAAiB,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IACrF,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,qBAAqB,CACnB,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,iBAAiB,GACxB,aAAa,CAAC;CAClB;AAID,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAEzD;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAEtE;AAED,wBAAgB,cAAc,IAAI,YAAY,EAAE,CAE/C"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
const registry = new Map();
|
|
3
|
+
export function registerIssueType(def) {
|
|
4
|
+
registry.set(def.typeKey, def);
|
|
5
|
+
}
|
|
6
|
+
export function getIssueType(typeKey) {
|
|
7
|
+
return registry.get(typeKey);
|
|
8
|
+
}
|
|
9
|
+
export function listIssueTypes() {
|
|
10
|
+
return Array.from(registry.values());
|
|
11
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
export type RecipientStrategy = 'nearest_scope_admin';
|
|
3
|
+
export interface IssueRoutingConfig {
|
|
4
|
+
[typeKey: string]: RecipientStrategy;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Loads routing config from hazo_admin_config.ini [issue_routing] section.
|
|
8
|
+
* Falls back to { auth_permission: 'nearest_scope_admin' } if hazo_config is not installed
|
|
9
|
+
* or the section is absent.
|
|
10
|
+
*/
|
|
11
|
+
export declare function loadIssueRoutingConfig(): IssueRoutingConfig;
|
|
12
|
+
/**
|
|
13
|
+
* Given an issue type key, look up the routing strategy from the loaded config.
|
|
14
|
+
* Returns null if no strategy is configured for this type.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getRecipientStrategy(typeKey: string): RecipientStrategy | null;
|
|
17
|
+
//# sourceMappingURL=routing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/issues/routing.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAErB,MAAM,MAAM,iBAAiB,GAAG,qBAAqB,CAAC;AAEtD,MAAM,WAAW,kBAAkB;IACjC,CAAC,OAAO,EAAE,MAAM,GAAG,iBAAiB,CAAC;CACtC;AAQD;;;;GAIG;AACH,wBAAgB,sBAAsB,IAAI,kBAAkB,CA2B3D;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CAG9E"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
const DEFAULT_ROUTING = {
|
|
3
|
+
auth_permission: 'nearest_scope_admin',
|
|
4
|
+
};
|
|
5
|
+
let _cachedConfig = null;
|
|
6
|
+
/**
|
|
7
|
+
* Loads routing config from hazo_admin_config.ini [issue_routing] section.
|
|
8
|
+
* Falls back to { auth_permission: 'nearest_scope_admin' } if hazo_config is not installed
|
|
9
|
+
* or the section is absent.
|
|
10
|
+
*/
|
|
11
|
+
export function loadIssueRoutingConfig() {
|
|
12
|
+
if (_cachedConfig !== null)
|
|
13
|
+
return _cachedConfig;
|
|
14
|
+
try {
|
|
15
|
+
// Lazy import — hazo_config is an optional peer dep.
|
|
16
|
+
// Use require-style dynamic import via a variable to avoid static analysis bundling it.
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
18
|
+
const hazoConfigMod = require('hazo_config');
|
|
19
|
+
const config = new hazoConfigMod.HazoConfig('hazo_admin_config.ini');
|
|
20
|
+
const result = { ...DEFAULT_ROUTING };
|
|
21
|
+
// hazo_config doesn't expose a section-dump API, so we read known keys.
|
|
22
|
+
// Consumers can extend by overriding loadIssueRoutingConfig entirely.
|
|
23
|
+
const authPerm = config.get('issue_routing', 'auth_permission');
|
|
24
|
+
if (authPerm && (authPerm === 'nearest_scope_admin')) {
|
|
25
|
+
result.auth_permission = authPerm;
|
|
26
|
+
}
|
|
27
|
+
_cachedConfig = result;
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// hazo_config not installed or config file not found — use defaults.
|
|
32
|
+
_cachedConfig = { ...DEFAULT_ROUTING };
|
|
33
|
+
return _cachedConfig;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Given an issue type key, look up the routing strategy from the loaded config.
|
|
38
|
+
* Returns null if no strategy is configured for this type.
|
|
39
|
+
*/
|
|
40
|
+
export function getRecipientStrategy(typeKey) {
|
|
41
|
+
const config = loadIssueRoutingConfig();
|
|
42
|
+
return config[typeKey] ?? null;
|
|
43
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
export interface IssueRecord {
|
|
3
|
+
id: string;
|
|
4
|
+
scope_id: string;
|
|
5
|
+
type: string;
|
|
6
|
+
status: 'new' | 'wip' | 'closed' | 'archived';
|
|
7
|
+
subject_user_id: string;
|
|
8
|
+
assigned_to: string | null;
|
|
9
|
+
recipient_scope_id: string | null;
|
|
10
|
+
title: string;
|
|
11
|
+
summary: string;
|
|
12
|
+
payload: Record<string, unknown>;
|
|
13
|
+
dedupe_key: string;
|
|
14
|
+
occurrence_count: number;
|
|
15
|
+
first_seen_at: string;
|
|
16
|
+
last_seen_at: string;
|
|
17
|
+
resolution: 'granted' | 'denied' | null;
|
|
18
|
+
resolution_role_id: string | null;
|
|
19
|
+
resolution_reason: string | null;
|
|
20
|
+
resolved_by: string | null;
|
|
21
|
+
resolved_at: string | null;
|
|
22
|
+
created_at: string;
|
|
23
|
+
updated_at: string;
|
|
24
|
+
}
|
|
25
|
+
export interface CreateIssueInput {
|
|
26
|
+
scope_id: string;
|
|
27
|
+
type: string;
|
|
28
|
+
subject_user_id: string;
|
|
29
|
+
recipient_scope_id?: string;
|
|
30
|
+
title: string;
|
|
31
|
+
summary: string;
|
|
32
|
+
payload: Record<string, unknown>;
|
|
33
|
+
dedupe_key: string;
|
|
34
|
+
}
|
|
35
|
+
export interface IssueStore {
|
|
36
|
+
createOrBumpIssue(input: CreateIssueInput): Promise<{
|
|
37
|
+
issue: IssueRecord;
|
|
38
|
+
isNew: boolean;
|
|
39
|
+
}>;
|
|
40
|
+
listIssues(opts: {
|
|
41
|
+
adminScopeIds?: string[];
|
|
42
|
+
isGlobalAdmin?: boolean;
|
|
43
|
+
status?: IssueRecord['status'] | IssueRecord['status'][];
|
|
44
|
+
assignedTo?: string | null;
|
|
45
|
+
type?: string;
|
|
46
|
+
limit?: number;
|
|
47
|
+
offset?: number;
|
|
48
|
+
}): Promise<IssueRecord[]>;
|
|
49
|
+
getIssue(id: string): Promise<IssueRecord | null>;
|
|
50
|
+
transitionStatus(id: string, status: 'wip' | 'closed', actorUserId: string): Promise<IssueRecord>;
|
|
51
|
+
setAssignee(id: string, userId: string | null): Promise<IssueRecord>;
|
|
52
|
+
resolveIssue(id: string, opts: {
|
|
53
|
+
resolution: 'granted' | 'denied';
|
|
54
|
+
role_id?: string;
|
|
55
|
+
reason?: string;
|
|
56
|
+
resolved_by: string;
|
|
57
|
+
}): Promise<IssueRecord>;
|
|
58
|
+
archiveClosedOlderThan(cutoffDate: Date): Promise<number>;
|
|
59
|
+
}
|
|
60
|
+
export declare function createIssueStore(adapter: any): IssueStore;
|
|
61
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/issues/store.ts"],"names":[],"mappings":"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"}
|