hazo_admin 0.7.1 → 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 +4 -4
@@ -0,0 +1,44 @@
1
+ import 'server-only';
2
+ import type { IssueRecord } from './store.js';
3
+ export type IssueSeverity = 'low' | 'medium' | 'high' | 'critical';
4
+ export interface RaiseIssueInput {
5
+ /** Registered IssueTypeDef.typeKey. Required — raiseIssue throws if unknown. */
6
+ type: string;
7
+ /** Scope id. Present ⇒ scoped (local admin). Omitted/null ⇒ global (super admin). */
8
+ scope_id?: string | null;
9
+ severity?: IssueSeverity;
10
+ /** Raising package/app id, e.g. 'hazo_files'. */
11
+ source?: string;
12
+ subject_user_id?: string | null;
13
+ recipient_scope_id?: string;
14
+ /** Overrides the typed-def descriptor when supplied. */
15
+ title?: string;
16
+ summary?: string;
17
+ payload?: Record<string, unknown>;
18
+ /** Auto-derived from source/type/title when omitted. */
19
+ dedupe_key?: string;
20
+ }
21
+ export interface RaiseIssueOptions {
22
+ /**
23
+ * Best-effort notify enqueue override. When omitted, raiseIssue lazily loads
24
+ * hazo_jobs/server and submits an ADMIN_ISSUE_NOTIFY_JOB_TYPE job itself.
25
+ * Tests and test-apps inject this to observe/enqueue deterministically.
26
+ */
27
+ enqueueNotify?: (payload: {
28
+ issueId: string;
29
+ }) => Promise<void> | void;
30
+ /** SQL dialect for the fallback hazo_jobs client. */
31
+ dialect?: 'pg' | 'sqlite';
32
+ }
33
+ /**
34
+ * The single documented ingest protocol for raising an issue into the admin
35
+ * Kanban. Trusted-by-construction (server-side caller already holds a connect
36
+ * adapter). Validates the type is registered, derives title/summary/dedupe_key,
37
+ * persists via the store, and — only when the issue is newly created — best-effort
38
+ * enqueues the notify job.
39
+ */
40
+ export declare function raiseIssue(connect: any, input: RaiseIssueInput, opts?: RaiseIssueOptions): Promise<{
41
+ issue: IssueRecord;
42
+ isNew: boolean;
43
+ }>;
44
+ //# sourceMappingURL=raise.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"raise.d.ts","sourceRoot":"","sources":["../../src/issues/raise.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAGrB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAO9C,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;AAEnE,MAAM,WAAW,eAAe;IAC9B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,qFAAqF;IACrF,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACvE,qDAAqD;IACrD,OAAO,CAAC,EAAE,IAAI,GAAG,QAAQ,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,GAAG,EACZ,KAAK,EAAE,eAAe,EACtB,IAAI,GAAE,iBAAsB,GAC3B,OAAO,CAAC;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC,CAmEjD"}
@@ -0,0 +1,77 @@
1
+ import 'server-only';
2
+ import { createLogger } from 'hazo_core';
3
+ import { createIssueStoreFromConnect } from './store.js';
4
+ import { getIssueType } from './registry.js';
5
+ import { GLOBAL_ISSUE_SCOPE_ID } from './recipients.js';
6
+ import { ADMIN_ISSUE_NOTIFY_JOB_TYPE } from './notify_handler.js';
7
+ const log = createLogger('hazo_admin:issues:raise');
8
+ /**
9
+ * The single documented ingest protocol for raising an issue into the admin
10
+ * Kanban. Trusted-by-construction (server-side caller already holds a connect
11
+ * adapter). Validates the type is registered, derives title/summary/dedupe_key,
12
+ * persists via the store, and — only when the issue is newly created — best-effort
13
+ * enqueues the notify job.
14
+ */
15
+ export async function raiseIssue(connect, input, opts = {}) {
16
+ const typeDef = getIssueType(input.type);
17
+ if (!typeDef) {
18
+ throw new Error(`hazo_admin:raiseIssue — unknown issue type '${input.type}'. Register it via registerIssueType() first.`);
19
+ }
20
+ const payload = input.payload ?? {};
21
+ let title = input.title;
22
+ let summary = input.summary;
23
+ if ((!title || !summary) && typeof typeDef.buildDescriptor === 'function') {
24
+ try {
25
+ const d = typeDef.buildDescriptor(payload);
26
+ title = title ?? d.title;
27
+ summary = summary ?? d.summary;
28
+ }
29
+ catch (err) {
30
+ log.debug('raiseIssue: buildDescriptor threw', { type: input.type, error: err?.message });
31
+ }
32
+ }
33
+ title = title ?? input.type;
34
+ summary = summary ?? '';
35
+ const source = input.source ?? null;
36
+ const scopeId = input.scope_id ?? GLOBAL_ISSUE_SCOPE_ID;
37
+ const dedupe_key = input.dedupe_key ?? `${source ?? 'unknown'}:${input.type}:${title}`;
38
+ const store = await createIssueStoreFromConnect(connect);
39
+ const result = await store.createOrBumpIssue({
40
+ scope_id: scopeId,
41
+ type: input.type,
42
+ severity: input.severity ?? 'medium',
43
+ source: source ?? undefined,
44
+ subject_user_id: input.subject_user_id ?? null,
45
+ recipient_scope_id: input.recipient_scope_id,
46
+ title,
47
+ summary,
48
+ payload,
49
+ dedupe_key,
50
+ });
51
+ // Only a brand-new issue notifies — dedupe bumps do not re-notify.
52
+ if (result.isNew) {
53
+ try {
54
+ if (opts.enqueueNotify) {
55
+ await opts.enqueueNotify({ issueId: result.issue.id });
56
+ }
57
+ else {
58
+ const jobsMod = (await import('hazo_jobs/server').catch(() => null));
59
+ if (jobsMod?.createJobsClient && typeof connect?.raw === 'function') {
60
+ const client = jobsMod.createJobsClient({
61
+ connect: { adapter: connect },
62
+ dialect: opts.dialect ?? 'pg',
63
+ });
64
+ await client.submit({
65
+ description: `Notify admins of raised issue ${result.issue.id}`,
66
+ type: ADMIN_ISSUE_NOTIFY_JOB_TYPE,
67
+ payload: { issueId: result.issue.id },
68
+ });
69
+ }
70
+ }
71
+ }
72
+ catch (err) {
73
+ log.debug('raiseIssue: notify enqueue skipped (best-effort)', { error: err?.message });
74
+ }
75
+ }
76
+ return result;
77
+ }
@@ -0,0 +1,20 @@
1
+ import 'server-only';
2
+ /** Permission that identifies a super/global admin (matches hazo_auth). */
3
+ export declare const GLOBAL_ADMIN_PERMISSION = "hazo_org_global_admin";
4
+ /** Permission that identifies a scope-local admin able to triage issues. */
5
+ export declare const SCOPE_ADMIN_PERMISSION = "admin_issue_triage";
6
+ /** Sentinel scope_id stored for issues raised without a scope (global/super-admin issues). */
7
+ export declare const GLOBAL_ISSUE_SCOPE_ID = "global";
8
+ /**
9
+ * Resolve local admins of a given scope (recipients for a scoped issue).
10
+ * Defaults to the `admin_issue_triage` permission.
11
+ */
12
+ export declare function resolveScopeAdmins(connect: any, scope_id: string, permission?: string): Promise<string[]>;
13
+ /**
14
+ * Resolve global/super admins (recipients for a global issue, no scope).
15
+ * find_roles_with_permission is scope-bound, so global admins (who may hold the
16
+ * permission at any scope) are resolved via a direct join when the adapter
17
+ * exposes raw(). Best-effort: [] otherwise.
18
+ */
19
+ export declare function resolveGlobalAdmins(connect: any): Promise<string[]>;
20
+ //# sourceMappingURL=recipients.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recipients.d.ts","sourceRoot":"","sources":["../../src/issues/recipients.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAErB,2EAA2E;AAC3E,eAAO,MAAM,uBAAuB,0BAA0B,CAAC;AAE/D,4EAA4E;AAC5E,eAAO,MAAM,sBAAsB,uBAAuB,CAAC;AAE3D,8FAA8F;AAC9F,eAAO,MAAM,qBAAqB,WAAW,CAAC;AA2B9C;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,GAAG,EACZ,QAAQ,EAAE,MAAM,EAChB,UAAU,GAAE,MAA+B,GAC1C,OAAO,CAAC,MAAM,EAAE,CAAC,CAGnB;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAgBzE"}
@@ -0,0 +1,61 @@
1
+ import 'server-only';
2
+ /** Permission that identifies a super/global admin (matches hazo_auth). */
3
+ export const GLOBAL_ADMIN_PERMISSION = 'hazo_org_global_admin';
4
+ /** Permission that identifies a scope-local admin able to triage issues. */
5
+ export const SCOPE_ADMIN_PERMISSION = 'admin_issue_triage';
6
+ /** Sentinel scope_id stored for issues raised without a scope (global/super-admin issues). */
7
+ export const GLOBAL_ISSUE_SCOPE_ID = 'global';
8
+ /**
9
+ * User ids holding `permission` within a specific scope.
10
+ * Intersects find_roles_with_permission (roles that grant the permission in the
11
+ * scope) with get_users_by_scope (users assigned to the scope) on role_id.
12
+ * Best-effort: returns [] if hazo_auth is absent or anything throws.
13
+ */
14
+ async function usersWithPermissionInScope(connect, scope_id, permission) {
15
+ const authLib = await import('hazo_auth/server-lib').catch(() => null);
16
+ if (!authLib?.find_roles_with_permission || !authLib?.get_users_by_scope)
17
+ return [];
18
+ const roles = await authLib.find_roles_with_permission(connect, scope_id, permission).catch(() => []);
19
+ if (!Array.isArray(roles) || roles.length === 0)
20
+ return [];
21
+ const roleIds = new Set(roles.map((r) => r.id));
22
+ const res = await authLib.get_users_by_scope(connect, scope_id).catch(() => null);
23
+ const scopes = res?.scopes ?? [];
24
+ const userIds = scopes
25
+ .filter((s) => roleIds.has(s.role_id))
26
+ .map((s) => s.user_id)
27
+ .filter(Boolean);
28
+ return [...new Set(userIds)];
29
+ }
30
+ /**
31
+ * Resolve local admins of a given scope (recipients for a scoped issue).
32
+ * Defaults to the `admin_issue_triage` permission.
33
+ */
34
+ export async function resolveScopeAdmins(connect, scope_id, permission = SCOPE_ADMIN_PERMISSION) {
35
+ if (!scope_id)
36
+ return [];
37
+ return usersWithPermissionInScope(connect, scope_id, permission);
38
+ }
39
+ /**
40
+ * Resolve global/super admins (recipients for a global issue, no scope).
41
+ * find_roles_with_permission is scope-bound, so global admins (who may hold the
42
+ * permission at any scope) are resolved via a direct join when the adapter
43
+ * exposes raw(). Best-effort: [] otherwise.
44
+ */
45
+ export async function resolveGlobalAdmins(connect) {
46
+ if (typeof connect?.raw !== 'function')
47
+ return [];
48
+ try {
49
+ const rows = await connect.raw(`SELECT DISTINCT us.user_id
50
+ FROM hazo_user_scopes us
51
+ JOIN hazo_role_permissions rp ON rp.role_id = us.role_id
52
+ JOIN hazo_permissions p ON p.id = rp.permission_id
53
+ WHERE p.permission_name = $1`, [GLOBAL_ADMIN_PERMISSION]);
54
+ if (!Array.isArray(rows))
55
+ return [];
56
+ return [...new Set(rows.map((r) => r.user_id).filter(Boolean))];
57
+ }
58
+ catch {
59
+ return [];
60
+ }
61
+ }
@@ -2,6 +2,7 @@ import type React from 'react';
2
2
  export interface IssueCardData {
3
3
  id: string;
4
4
  type: string;
5
+ severity: string;
5
6
  status: string;
6
7
  subject_user_id: string;
7
8
  assigned_to: string | null;
@@ -1 +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"}
1
+ {"version":3,"file":"registry.client.d.ts","sourceRoot":"","sources":["../../src/issues/registry.client.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,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,qBAmDjE"}
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cx, severityMeta, relativeTime } from '../components/issues_panel/ui_helpers.js';
3
4
  const registry = new Map();
4
5
  export function registerIssueCardRenderer(typeKey, renderer) {
5
6
  registry.set(typeKey, renderer);
@@ -11,5 +12,7 @@ export function listIssueCardRenderers() {
11
12
  return Array.from(registry.entries()).map(([typeKey, renderer]) => ({ typeKey, renderer }));
12
13
  }
13
14
  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
+ const sev = severityMeta(issue.severity);
16
+ const when = relativeTime(issue.last_seen_at);
17
+ return (_jsxs("div", { className: cx('relative pl-3', 'before:absolute before:left-0 before:top-0.5 before:bottom-0.5 before:w-1 before:rounded-full', sev.rail), children: [_jsx("h4", { className: "text-[13px] font-semibold leading-snug text-slate-800 line-clamp-2", children: issue.title }), issue.summary && (_jsx("p", { className: "mt-1 text-xs leading-relaxed text-slate-500 line-clamp-2", children: issue.summary })), _jsxs("div", { className: "mt-2.5 flex flex-wrap items-center gap-1.5", children: [_jsxs("span", { className: cx('inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold', sev.badge), children: [_jsx("span", { className: cx('h-1.5 w-1.5 rounded-full', sev.dot) }), sev.label] }), _jsx("span", { className: "inline-flex items-center rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-medium text-slate-600 ring-1 ring-inset ring-slate-200/70", children: issue.type }), issue.occurrence_count > 1 && (_jsxs("span", { title: `Seen ${issue.occurrence_count} times`, className: "inline-flex items-center rounded-full bg-amber-50 px-1.5 py-0.5 text-[10px] font-semibold text-amber-700 ring-1 ring-inset ring-amber-100", children: ["\u00D7", issue.occurrence_count] })), when && (_jsx("span", { className: "ml-auto text-[10px] tabular-nums text-slate-400", children: when }))] })] }));
15
18
  }
@@ -35,9 +35,9 @@ export interface IssueTypeDef {
35
35
  title: string;
36
36
  summary: string;
37
37
  };
38
- resolveRecipients(issue: IssueRecord, ctx: IssueActionCtx): Promise<RecipientResult>;
39
- actions: IssueActionDef[];
40
- buildResolutionNotice(issue: IssueRecord, actionKey: string, result: IssueActionResult): NotifyPayload;
38
+ resolveRecipients?(issue: IssueRecord, ctx: IssueActionCtx): Promise<RecipientResult>;
39
+ actions?: IssueActionDef[];
40
+ buildResolutionNotice?(issue: IssueRecord, actionKey: string, result: IssueActionResult): NotifyPayload;
41
41
  }
42
42
  export declare function registerIssueType(def: IssueTypeDef): void;
43
43
  export declare function getIssueType(typeKey: string): IssueTypeDef | undefined;
@@ -1 +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"}
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,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IACtF,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B,qBAAqB,CAAC,CACpB,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"}
@@ -3,10 +3,12 @@ export interface IssueRecord {
3
3
  id: string;
4
4
  scope_id: string;
5
5
  type: string;
6
- status: 'new' | 'wip' | 'closed' | 'archived';
7
- subject_user_id: string;
6
+ severity: 'low' | 'medium' | 'high' | 'critical';
7
+ status: 'new' | 'wip' | 'on_hold' | 'closed' | 'archived';
8
+ subject_user_id: string | null;
8
9
  assigned_to: string | null;
9
10
  recipient_scope_id: string | null;
11
+ source: string | null;
10
12
  title: string;
11
13
  summary: string;
12
14
  payload: Record<string, unknown>;
@@ -25,12 +27,14 @@ export interface IssueRecord {
25
27
  export interface CreateIssueInput {
26
28
  scope_id: string;
27
29
  type: string;
28
- subject_user_id: string;
30
+ severity?: 'low' | 'medium' | 'high' | 'critical';
31
+ subject_user_id?: string | null;
29
32
  recipient_scope_id?: string;
33
+ source?: string;
30
34
  title: string;
31
35
  summary: string;
32
36
  payload: Record<string, unknown>;
33
- dedupe_key: string;
37
+ dedupe_key?: string;
34
38
  }
35
39
  export interface IssueStore {
36
40
  createOrBumpIssue(input: CreateIssueInput): Promise<{
@@ -47,8 +51,9 @@ export interface IssueStore {
47
51
  offset?: number;
48
52
  }): Promise<IssueRecord[]>;
49
53
  getIssue(id: string): Promise<IssueRecord | null>;
50
- transitionStatus(id: string, status: 'wip' | 'closed', actorUserId: string): Promise<IssueRecord>;
54
+ transitionStatus(id: string, status: IssueRecord['status'], actorUserId: string): Promise<IssueRecord>;
51
55
  setAssignee(id: string, userId: string | null): Promise<IssueRecord>;
56
+ setType(id: string, type: string): Promise<IssueRecord>;
52
57
  resolveIssue(id: string, opts: {
53
58
  resolution: 'granted' | 'denied';
54
59
  role_id?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/issues/store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/issues/store.ts"],"names":[],"mappings":"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"}
@@ -7,10 +7,12 @@ export function toIssueRecord(raw) {
7
7
  id: raw.id,
8
8
  scope_id: raw.scope_id,
9
9
  type: raw.type,
10
+ severity: raw.severity ?? 'medium',
10
11
  status: raw.status,
11
- subject_user_id: raw.subject_user_id,
12
+ subject_user_id: raw.subject_user_id ?? null,
12
13
  assigned_to: raw.assigned_to ?? null,
13
14
  recipient_scope_id: raw.recipient_scope_id ?? null,
15
+ source: raw.source ?? null,
14
16
  title: raw.title,
15
17
  summary: raw.summary,
16
18
  payload: typeof raw.payload === 'string'
@@ -30,21 +32,17 @@ export function toIssueRecord(raw) {
30
32
  };
31
33
  }
32
34
  /**
33
- * Legal status transitions:
34
- * new wip
35
- * new closed (direct close)
36
- * wip → closed
37
- * any → archived (only via archiveClosedOlderThan)
35
+ * Every known status may transition to any other known status. Admins move
36
+ * cards freely across the board — including reopening a closed issue (closed
37
+ * new/wip) and parking one ( on_hold). Only an unknown target status is
38
+ * rejected. `archived` is normally reached via archiveClosedOlderThan, but a
39
+ * manual move is permitted too.
40
+ *
41
+ * Side effect: a → wip that lands on an unassigned issue auto-assigns the actor.
38
42
  */
39
- const ALLOWED_TRANSITIONS = {
40
- new: ['wip', 'closed'],
41
- wip: ['closed'],
42
- closed: [],
43
- archived: [],
44
- };
43
+ const KNOWN_STATUSES = new Set(['new', 'wip', 'on_hold', 'closed', 'archived']);
45
44
  function assertTransition(from, to) {
46
- const allowed = ALLOWED_TRANSITIONS[from] ?? [];
47
- if (!allowed.includes(to)) {
45
+ if (!KNOWN_STATUSES.has(to)) {
48
46
  throw new Error(`hazo_admin:issues — illegal status transition: ${from} → ${to}`);
49
47
  }
50
48
  }
@@ -56,9 +54,13 @@ export function createIssueStore(adapter) {
56
54
  return adapter.raw(sql, params);
57
55
  }
58
56
  async function createOrBumpIssue(input) {
59
- log.debug('createOrBumpIssue', { dedupe_key: input.dedupe_key });
57
+ const dedupe_key = input.dedupe_key;
58
+ if (!dedupe_key) {
59
+ throw new Error('hazo_admin:issues — dedupe_key required at store layer');
60
+ }
61
+ log.debug('createOrBumpIssue', { dedupe_key });
60
62
  // Check for an existing open issue with this dedupe_key.
61
- const existing = await raw(`SELECT * FROM ${TABLE} WHERE dedupe_key = $1 AND status NOT IN ('closed', 'archived') LIMIT 1`, [input.dedupe_key]);
63
+ const existing = await raw(`SELECT * FROM ${TABLE} WHERE dedupe_key = $1 AND status NOT IN ('closed', 'archived') LIMIT 1`, [dedupe_key]);
62
64
  if (existing && existing.length > 0) {
63
65
  const ts = now();
64
66
  const bumped = await raw(`UPDATE ${TABLE}
@@ -74,27 +76,29 @@ export function createIssueStore(adapter) {
74
76
  const id = crypto.randomUUID();
75
77
  const payloadStr = JSON.stringify(input.payload);
76
78
  const inserted = await raw(`INSERT INTO ${TABLE} (
77
- id, scope_id, type, status, subject_user_id, assigned_to,
78
- recipient_scope_id, title, summary, payload, dedupe_key,
79
+ id, scope_id, type, severity, status, subject_user_id, assigned_to,
80
+ recipient_scope_id, source, title, summary, payload, dedupe_key,
79
81
  occurrence_count, first_seen_at, last_seen_at,
80
82
  resolution, resolution_role_id, resolution_reason,
81
83
  resolved_by, resolved_at, created_at, updated_at
82
84
  ) VALUES (
83
- $1, $2, $3, 'new', $4, NULL,
84
- $5, $6, $7, $8, $9,
85
- 1, $10, $11,
85
+ $1, $2, $3, $4, 'new', $5, NULL,
86
+ $6, $7, $8, $9, $10, $11,
87
+ 1, $12, $13,
86
88
  NULL, NULL, NULL,
87
- NULL, NULL, $12, $13
89
+ NULL, NULL, $14, $15
88
90
  ) RETURNING *`, [
89
91
  id,
90
92
  input.scope_id,
91
93
  input.type,
92
- input.subject_user_id,
94
+ input.severity ?? 'medium',
95
+ input.subject_user_id ?? null,
93
96
  input.recipient_scope_id ?? null,
97
+ input.source ?? null,
94
98
  input.title,
95
99
  input.summary,
96
100
  payloadStr,
97
- input.dedupe_key,
101
+ dedupe_key,
98
102
  ts,
99
103
  ts,
100
104
  ts,
@@ -179,6 +183,15 @@ export function createIssueStore(adapter) {
179
183
  throw new Error(`hazo_admin:issues — issue ${id} not found`);
180
184
  return toIssueRecord(row);
181
185
  }
186
+ // Dedupe-safe: the partial unique index is on dedupe_key only; changing type never touches it.
187
+ async function setType(id, type) {
188
+ const ts = now();
189
+ const updated = await raw(`UPDATE ${TABLE} SET type = $1, updated_at = $2 WHERE id = $3 RETURNING *`, [type, ts, id]);
190
+ const row = updated && updated.length > 0 ? updated[0] : null;
191
+ if (!row)
192
+ throw new Error(`hazo_admin:issues — issue ${id} not found`);
193
+ return toIssueRecord(row);
194
+ }
182
195
  async function resolveIssue(id, opts) {
183
196
  const ts = now();
184
197
  const updated = await raw(`UPDATE ${TABLE}
@@ -221,6 +234,7 @@ export function createIssueStore(adapter) {
221
234
  getIssue,
222
235
  transitionStatus,
223
236
  setAssignee,
237
+ setType,
224
238
  resolveIssue,
225
239
  archiveClosedOlderThan,
226
240
  };
@@ -248,12 +262,16 @@ export async function createIssueStoreFromConnect(adapter) {
248
262
  return new Date().toISOString();
249
263
  }
250
264
  async function createOrBumpIssue(input) {
251
- log.debug('createIssueStoreFromConnect:createOrBumpIssue', { dedupe_key: input.dedupe_key });
265
+ const dedupe_key = input.dedupe_key;
266
+ if (!dedupe_key) {
267
+ throw new Error('hazo_admin:issues — dedupe_key required at store layer');
268
+ }
269
+ log.debug('createIssueStoreFromConnect:createOrBumpIssue', { dedupe_key });
252
270
  // Atomically bump an existing open issue via claimRows.
253
271
  const bumped = await adapter.claimRows({
254
272
  table: TABLE,
255
273
  where: {
256
- dedupe_key: input.dedupe_key,
274
+ dedupe_key,
257
275
  status: { in: ['new', 'wip'] },
258
276
  },
259
277
  set: {
@@ -271,14 +289,16 @@ export async function createIssueStoreFromConnect(adapter) {
271
289
  const inserted = await crud.insert({
272
290
  scope_id: input.scope_id,
273
291
  type: input.type,
292
+ severity: input.severity ?? 'medium',
274
293
  status: 'new',
275
- subject_user_id: input.subject_user_id,
294
+ subject_user_id: input.subject_user_id ?? null,
276
295
  assigned_to: null,
277
296
  recipient_scope_id: input.recipient_scope_id ?? null,
297
+ source: input.source ?? null,
278
298
  title: input.title,
279
299
  summary: input.summary,
280
300
  payload: input.payload,
281
- dedupe_key: input.dedupe_key,
301
+ dedupe_key,
282
302
  occurrence_count: 1,
283
303
  first_seen_at: ts,
284
304
  last_seen_at: ts,
@@ -353,6 +373,15 @@ export async function createIssueStoreFromConnect(adapter) {
353
373
  throw new Error(`hazo_admin:issues — issue ${id} not found`);
354
374
  return toIssueRecord(row);
355
375
  }
376
+ // Dedupe-safe: the partial unique index is on dedupe_key only; changing type never touches it.
377
+ async function setType(id, type) {
378
+ const ts = now();
379
+ const updated = await crud.updateById(id, { type, updated_at: ts });
380
+ const row = Array.isArray(updated) ? updated[0] : updated;
381
+ if (!row)
382
+ throw new Error(`hazo_admin:issues — issue ${id} not found`);
383
+ return toIssueRecord(row);
384
+ }
356
385
  async function resolveIssue(id, opts) {
357
386
  const ts = now();
358
387
  const updated = await crud.updateById(id, {
@@ -395,6 +424,7 @@ export async function createIssueStoreFromConnect(adapter) {
395
424
  getIssue,
396
425
  transitionStatus,
397
426
  setAssignee,
427
+ setType,
398
428
  resolveIssue,
399
429
  archiveClosedOlderThan,
400
430
  };
@@ -0,0 +1,48 @@
1
+ import 'server-only';
2
+ export interface IssueTypeCatalogRecord {
3
+ type_key: string;
4
+ label: string;
5
+ color: string | null;
6
+ description: string | null;
7
+ scope_id: string | null;
8
+ sort_order: number;
9
+ created_at: string;
10
+ updated_at: string;
11
+ }
12
+ export interface CreateIssueTypeInput {
13
+ type_key: string;
14
+ label: string;
15
+ color?: string | null;
16
+ description?: string | null;
17
+ scope_id?: string | null;
18
+ sort_order?: number;
19
+ }
20
+ export interface UpdateIssueTypeInput {
21
+ label?: string;
22
+ color?: string | null;
23
+ description?: string | null;
24
+ sort_order?: number;
25
+ }
26
+ export interface IssueTypeCatalogStore {
27
+ listTypes(opts: {
28
+ scopeIds?: string[];
29
+ isGlobalAdmin?: boolean;
30
+ }): Promise<IssueTypeCatalogRecord[]>;
31
+ getType(type_key: string): Promise<IssueTypeCatalogRecord | null>;
32
+ createType(input: CreateIssueTypeInput): Promise<IssueTypeCatalogRecord>;
33
+ updateType(type_key: string, patch: UpdateIssueTypeInput): Promise<IssueTypeCatalogRecord>;
34
+ deleteType(type_key: string): Promise<void>;
35
+ }
36
+ export declare function toIssueTypeCatalogRecord(raw: Record<string, unknown>): IssueTypeCatalogRecord;
37
+ export declare function createIssueTypeCatalogStore(adapter: any): IssueTypeCatalogStore;
38
+ /**
39
+ * PostgREST-native implementation of IssueTypeCatalogStore.
40
+ * Uses createCrudService so no raw SQL is issued.
41
+ * Falls back to createIssueTypeCatalogStore when the adapter exposes a raw()
42
+ * method (i.e. it is a direct-DB adapter that already handles SQL).
43
+ *
44
+ * Returns a Promise because it lazily imports hazo_connect/server on the
45
+ * CRUD path (avoiding loading the full hazo_connect stack for raw adapters).
46
+ */
47
+ export declare function createIssueTypeCatalogStoreFromConnect(adapter: any): Promise<IssueTypeCatalogStore>;
48
+ //# sourceMappingURL=type_catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"type_catalog.d.ts","sourceRoot":"","sources":["../../src/issues/type_catalog.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAOrB,MAAM,WAAW,sBAAsB;IACrC,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;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,CAAC,IAAI,EAAE;QACd,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QACpB,aAAa,CAAC,EAAE,OAAO,CAAC;KACzB,GAAG,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAAC;IACtC,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAAC;IAClE,UAAU,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACzE,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC3F,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7C;AAED,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,sBAAsB,CAW7F;AAwBD,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,GAAG,GAAG,qBAAqB,CAuG/E;AAED;;;;;;;;GAQG;AACH,wBAAsB,sCAAsC,CAC1D,OAAO,EAAE,GAAG,GACX,OAAO,CAAC,qBAAqB,CAAC,CAyFhC"}