create-windy 0.2.33 → 0.3.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 (284) hide show
  1. package/dist/cli.js +173 -17
  2. package/package.json +1 -1
  3. package/template/.agents/skills/windy-business-module/SKILL.md +10 -1
  4. package/template/.windy-template.json +2 -2
  5. package/template/AGENTS.md +9 -0
  6. package/template/README.md +3 -1
  7. package/template/apps/agent-server/Dockerfile +1 -0
  8. package/template/apps/agent-server/README.md +28 -5
  9. package/template/apps/agent-server/adk_web_agents/__init__.py +1 -0
  10. package/template/apps/agent-server/adk_web_agents/safe_debug/__init__.py +3 -0
  11. package/template/apps/agent-server/adk_web_agents/safe_debug/agent.py +40 -0
  12. package/template/apps/agent-server/pyproject.toml +1 -1
  13. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +191 -16
  14. package/template/apps/agent-server/src/southwind_agent_server/adk_failure.py +13 -0
  15. package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +225 -0
  16. package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +76 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/app.py +63 -3
  18. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +35 -0
  19. package/template/apps/agent-server/src/southwind_agent_server/engine.py +2 -0
  20. package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +31 -3
  21. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +22 -9
  22. package/template/apps/agent-server/src/southwind_agent_server/provider_audit.py +144 -0
  23. package/template/apps/agent-server/src/southwind_agent_server/provider_governance.py +227 -0
  24. package/template/apps/agent-server/src/southwind_agent_server/service.py +1 -0
  25. package/template/apps/agent-server/tests/test_adk_adapter.py +305 -0
  26. package/template/apps/agent-server/tests/test_adk_web_agent.py +12 -0
  27. package/template/apps/agent-server/tests/test_api.py +16 -1
  28. package/template/apps/agent-server/tests/test_openai_provider.py +79 -0
  29. package/template/apps/agent-server/tests/test_provider_governance.py +215 -0
  30. package/template/apps/server/package.json +4 -1
  31. package/template/apps/server/src/agent/provider-input-governance.test.ts +82 -0
  32. package/template/apps/server/src/agent/provider-input-governance.ts +140 -0
  33. package/template/apps/server/src/agent/remote-runtime.test.ts +22 -0
  34. package/template/apps/server/src/agent/remote-runtime.ts +1 -0
  35. package/template/apps/server/src/agent/run-contracts.ts +2 -0
  36. package/template/apps/server/src/agent/run-facade.test.ts +52 -2
  37. package/template/apps/server/src/agent/run-facade.ts +38 -9
  38. package/template/apps/server/src/agent/tool-registry.ts +2 -6
  39. package/template/apps/server/src/application-services.ts +50 -0
  40. package/template/apps/server/src/audit/drizzle-log-store.ts +25 -0
  41. package/template/apps/server/src/audit/search-summary.ts +2 -6
  42. package/template/apps/server/src/auth/credential-restriction.ts +1 -0
  43. package/template/apps/server/src/auth/password.ts +30 -21
  44. package/template/apps/server/src/auth/routes.test.ts +44 -3
  45. package/template/apps/server/src/auth/routes.ts +6 -1
  46. package/template/apps/server/src/auth/runtime.ts +4 -1
  47. package/template/apps/server/src/auth/service.ts +25 -7
  48. package/template/apps/server/src/configuration/bootstrap.ts +20 -1
  49. package/template/apps/server/src/configuration/definition.test.ts +71 -0
  50. package/template/apps/server/src/configuration/definition.ts +242 -12
  51. package/template/apps/server/src/configuration/drizzle-repository.ts +36 -2
  52. package/template/apps/server/src/configuration/repository.ts +27 -2
  53. package/template/apps/server/src/configuration/routes.ts +4 -1
  54. package/template/apps/server/src/configuration/service.test.ts +100 -0
  55. package/template/apps/server/src/configuration/service.ts +50 -10
  56. package/template/apps/server/src/data-access/scoped-repository.integration.test.ts +3 -2
  57. package/template/apps/server/src/data-governance/export/service.ts +2 -6
  58. package/template/apps/server/src/example-modules.ts +4 -1
  59. package/template/apps/server/src/guards.ts +18 -5
  60. package/template/apps/server/src/http/request-source.test.ts +85 -0
  61. package/template/apps/server/src/http/request-source.ts +90 -0
  62. package/template/apps/server/src/index.ts +45 -45
  63. package/template/apps/server/src/license/routes.ts +2 -0
  64. package/template/apps/server/src/license/runtime-service.ts +10 -11
  65. package/template/apps/server/src/module-bootstrap.ts +2 -2
  66. package/template/apps/server/src/module-host/initialize-contracts.ts +6 -0
  67. package/template/apps/server/src/module-host/initialize.ts +30 -44
  68. package/template/apps/server/src/module-host/request-context.ts +29 -23
  69. package/template/apps/server/src/module-host/resource-file-policies.test.ts +197 -0
  70. package/template/apps/server/src/module-host/resource-file-policies.ts +108 -0
  71. package/template/apps/server/src/module-host/route-declaration.ts +45 -0
  72. package/template/apps/server/src/module-host/route-installer.ts +27 -3
  73. package/template/apps/server/src/module-host/storage-port.test.ts +170 -1
  74. package/template/apps/server/src/module-host/storage-port.ts +134 -1
  75. package/template/apps/server/src/module-host/storage-scope.ts +89 -0
  76. package/template/apps/server/src/module-host/tool-handlers.test.ts +305 -0
  77. package/template/apps/server/src/module-host/tool-handlers.ts +38 -4
  78. package/template/apps/server/src/module-host/user-directory-composition.test.ts +126 -0
  79. package/template/apps/server/src/module-host/user-directory-port.test.ts +238 -0
  80. package/template/apps/server/src/module-host/user-directory-port.ts +139 -0
  81. package/template/apps/server/src/module-host.ts +4 -0
  82. package/template/apps/server/src/notification/channel-adapter.ts +46 -0
  83. package/template/apps/server/src/notification/delivery-policy.test.ts +59 -0
  84. package/template/apps/server/src/notification/delivery-policy.ts +37 -0
  85. package/template/apps/server/src/notification/delivery-repository.ts +198 -0
  86. package/template/apps/server/src/notification/delivery-worker.test.ts +87 -0
  87. package/template/apps/server/src/notification/delivery-worker.ts +88 -0
  88. package/template/apps/server/src/notification/drizzle-management.ts +180 -0
  89. package/template/apps/server/src/notification/drizzle-query.ts +160 -0
  90. package/template/apps/server/src/notification/drizzle-repository.integration.test.ts +301 -0
  91. package/template/apps/server/src/notification/drizzle-repository.ts +156 -90
  92. package/template/apps/server/src/notification/extension-routes.test.ts +119 -0
  93. package/template/apps/server/src/notification/extension-routes.ts +111 -0
  94. package/template/apps/server/src/notification/memory-delivery-repository.ts +137 -0
  95. package/template/apps/server/src/notification/memory-support.ts +72 -0
  96. package/template/apps/server/src/notification/preference-repository.ts +95 -0
  97. package/template/apps/server/src/notification/recipient-directory.ts +104 -0
  98. package/template/apps/server/src/notification/repository.test.ts +171 -0
  99. package/template/apps/server/src/notification/repository.ts +179 -32
  100. package/template/apps/server/src/notification/revision-routes.test.ts +186 -0
  101. package/template/apps/server/src/notification/route-support.ts +225 -0
  102. package/template/apps/server/src/notification/routes.test.ts +182 -0
  103. package/template/apps/server/src/notification/routes.ts +225 -132
  104. package/template/apps/server/src/notification/service.test.ts +104 -0
  105. package/template/apps/server/src/notification/service.ts +140 -0
  106. package/template/apps/server/src/notification/smtp-adapter.test.ts +65 -0
  107. package/template/apps/server/src/notification/smtp-adapter.ts +68 -0
  108. package/template/apps/server/src/persistence.integration.test.ts +41 -2
  109. package/template/apps/server/src/route-guards.ts +1 -0
  110. package/template/apps/server/src/runtime-audit-list.test.ts +39 -0
  111. package/template/apps/server/src/runtime-development.ts +2 -0
  112. package/template/apps/server/src/runtime.test.ts +23 -9
  113. package/template/apps/server/src/runtime.ts +39 -2
  114. package/template/apps/server/src/scheduler/audit.ts +2 -11
  115. package/template/apps/server/src/scheduler/definitions.test.ts +6 -0
  116. package/template/apps/server/src/scheduler/definitions.ts +7 -0
  117. package/template/apps/server/src/scheduler/recovery-service.ts +2 -11
  118. package/template/apps/server/src/scheduler/routes.ts +2 -7
  119. package/template/apps/server/src/search/opensearch-client.test.ts +45 -0
  120. package/template/apps/server/src/search/opensearch-client.ts +145 -0
  121. package/template/apps/server/src/search/service.ts +2 -6
  122. package/template/apps/server/src/settings/routes.ts +15 -3
  123. package/template/apps/server/src/settings/runtime.ts +10 -1
  124. package/template/apps/server/src/storage/runtime.ts +13 -0
  125. package/template/apps/server/src/system/audit-query.ts +33 -4
  126. package/template/apps/server/src/system/audit-routes.test.ts +58 -4
  127. package/template/apps/server/src/system/audit-routes.ts +6 -5
  128. package/template/apps/server/src/system/built-in-roles.ts +1 -0
  129. package/template/apps/server/src/system/department-organization.ts +320 -0
  130. package/template/apps/server/src/system/department-routes.test.ts +207 -48
  131. package/template/apps/server/src/system/department-routes.ts +114 -33
  132. package/template/apps/server/src/system/drizzle-repository.ts +15 -7
  133. package/template/apps/server/src/system/drizzle-scoped-repository.ts +5 -3
  134. package/template/apps/server/src/system/drizzle-system.ts +37 -9
  135. package/template/apps/server/src/system/drizzle-transaction-context.ts +14 -0
  136. package/template/apps/server/src/system/identity-route-config.ts +7 -2
  137. package/template/apps/server/src/system/mutation-runner.ts +23 -0
  138. package/template/apps/server/src/system/organization-postgres.integration.test.ts +176 -0
  139. package/template/apps/server/src/system/resource-access-response.ts +12 -6
  140. package/template/apps/server/src/system/role-department-scope-policy.ts +55 -0
  141. package/template/apps/server/src/system/route-helpers.ts +2 -5
  142. package/template/apps/server/src/system/route-types.ts +12 -3
  143. package/template/apps/server/src/system/routes-validation.test.ts +26 -0
  144. package/template/apps/server/src/system/routes.test.ts +10 -2
  145. package/template/apps/server/src/system/routes.ts +70 -36
  146. package/template/apps/server/src/system/seed.ts +7 -0
  147. package/template/apps/server/src/work-order/data-scope.integration.test.ts +9 -2
  148. package/template/apps/server/src/work-order/opensearch-index.test.ts +97 -0
  149. package/template/apps/server/src/work-order/opensearch-index.ts +313 -0
  150. package/template/apps/server/src/work-order/search-provider.ts +60 -1
  151. package/template/apps/server/src/work-order/service.ts +42 -10
  152. package/template/apps/web/src/app/error-pages.ts +9 -0
  153. package/template/apps/web/src/components/auth/PasswordChangeForm.vue +16 -11
  154. package/template/apps/web/src/components/auth/PasswordChangeForm.webtest.ts +14 -0
  155. package/template/apps/web/src/components/common/FormDialog.vue +16 -2
  156. package/template/apps/web/src/components/common/ModalLayout.webtest.ts +20 -1
  157. package/template/apps/web/src/composables/useGlobalSearch.ts +68 -0
  158. package/template/apps/web/src/composables/usePlatformSettings.ts +8 -7
  159. package/template/apps/web/src/layout/GlobalSearchResults.vue +58 -0
  160. package/template/apps/web/src/layout/NavigationSearchDialog.vue +42 -6
  161. package/template/apps/web/src/layout/NavigationSearchDialog.webtest.ts +71 -0
  162. package/template/apps/web/src/layout/NotificationMenu.vue +73 -31
  163. package/template/apps/web/src/layout/NotificationMenu.webtest.ts +44 -1
  164. package/template/apps/web/src/layout/NotificationMenuItem.vue +61 -0
  165. package/template/apps/web/src/layout/NotificationPreferencesDialog.vue +181 -0
  166. package/template/apps/web/src/layout/NotificationPreferencesDialog.webtest.ts +80 -0
  167. package/template/apps/web/src/lib/date-time.ts +22 -10
  168. package/template/apps/web/src/lib/date-time.webtest.ts +9 -1
  169. package/template/apps/web/src/main.ts +14 -1
  170. package/template/apps/web/src/pages/auth/ChangePasswordPage.vue +16 -1
  171. package/template/apps/web/src/pages/configuration/components/ModuleConfigVersionList.vue +1 -0
  172. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationEditor.vue +10 -0
  173. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationWorkbench.vue +37 -6
  174. package/template/apps/web/src/pages/errors/PlatformErrorPage.vue +115 -0
  175. package/template/apps/web/src/pages/errors/PlatformErrorPage.webtest.ts +44 -0
  176. package/template/apps/web/src/pages/errors/error-page-extension.ts +26 -0
  177. package/template/apps/web/src/pages/system/SystemNotificationsPage.vue +136 -68
  178. package/template/apps/web/src/pages/system/SystemNotificationsPage.webtest.ts +224 -12
  179. package/template/apps/web/src/pages/system/audit-log-presenter.ts +86 -0
  180. package/template/apps/web/src/pages/system/audit-time-range.ts +28 -0
  181. package/template/apps/web/src/pages/system/components/AuditActionCell.vue +10 -69
  182. package/template/apps/web/src/pages/system/components/AuditActionCell.webtest.ts +20 -27
  183. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.vue +134 -0
  184. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.webtest.ts +76 -0
  185. package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +116 -52
  186. package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +52 -15
  187. package/template/apps/web/src/pages/system/components/NotificationHistoryDialog.vue +129 -0
  188. package/template/apps/web/src/pages/system/components/NotificationManagementCard.vue +91 -0
  189. package/template/apps/web/src/pages/system/components/NotificationPublishDialog.vue +121 -0
  190. package/template/apps/web/src/pages/system/components/NotificationPublishForm.vue +162 -1
  191. package/template/apps/web/src/pages/system/components/SystemResourceActions.vue +68 -0
  192. package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +74 -0
  193. package/template/apps/web/src/pages/system/components/SystemResourceFilters.vue +3 -4
  194. package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +91 -98
  195. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +68 -0
  196. package/template/apps/web/src/pages/system/components/SystemResourceTable.vue +20 -1
  197. package/template/apps/web/src/pages/system/components/SystemResourceTable.webtest.ts +28 -7
  198. package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.vue +210 -0
  199. package/template/apps/web/src/pages/system/components/department/DepartmentTransitionDialog.vue +112 -0
  200. package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +17 -4
  201. package/template/apps/web/src/pages/system/composables/useDepartmentTransitions.ts +79 -0
  202. package/template/apps/web/src/pages/system/composables/useNotificationPermissions.ts +14 -0
  203. package/template/apps/web/src/pages/system/composables/useSystemResource.ts +2 -0
  204. package/template/apps/web/src/pages/system/composables/useSystemResource.webtest.ts +15 -2
  205. package/template/apps/web/src/pages/system/resource-config.ts +12 -4
  206. package/template/apps/web/src/router/error-navigation.ts +50 -0
  207. package/template/apps/web/src/router/error-navigation.webtest.ts +106 -0
  208. package/template/apps/web/src/router/index.ts +71 -0
  209. package/template/apps/web/src/router/index.webtest.ts +29 -0
  210. package/template/apps/web/src/router/manifest-routes.webtest.ts +4 -0
  211. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
  212. package/template/apps/web/src/services/auth-api.ts +5 -0
  213. package/template/apps/web/src/services/http.ts +43 -6
  214. package/template/apps/web/src/services/http.webtest.ts +70 -0
  215. package/template/apps/web/src/services/notification-api.ts +85 -2
  216. package/template/apps/web/src/services/notification-api.webtest.ts +110 -0
  217. package/template/apps/web/src/services/search-api.ts +39 -0
  218. package/template/apps/web/src/services/system-api.ts +18 -0
  219. package/template/apps/web/vitest.config.ts +1 -0
  220. package/template/docker-compose.yml +68 -0
  221. package/template/docs/architecture/ai-egress.md +49 -7
  222. package/template/docs/architecture/ai-runtime.md +34 -6
  223. package/template/docs/architecture/data-scope-query-enforcement.md +4 -0
  224. package/template/docs/architecture/object-storage.md +22 -2
  225. package/template/docs/architecture/search-provider-protocol.md +38 -2
  226. package/template/docs/development/custom-error-pages.md +78 -0
  227. package/template/docs/development/custom-login-page.md +3 -3
  228. package/template/docs/platform/agent-runtime.md +76 -7
  229. package/template/docs/platform/audit-reliability.md +46 -2
  230. package/template/docs/platform/business-user-directory.md +82 -0
  231. package/template/docs/platform/module-configuration.md +13 -4
  232. package/template/docs/platform/notifications.md +67 -11
  233. package/template/docs/platform/organization-membership.md +17 -3
  234. package/template/docs/platform/system-crud-api.md +16 -0
  235. package/template/docs/platform/web-system-crud.md +6 -2
  236. package/template/package.json +4 -0
  237. package/template/packages/config/index.test.ts +45 -0
  238. package/template/packages/config/package.json +1 -1
  239. package/template/packages/config/src/platform.ts +169 -48
  240. package/template/packages/database/drizzle/0035_material_nightshade.sql +10 -0
  241. package/template/packages/database/drizzle/0036_hesitant_speed.sql +35 -0
  242. package/template/packages/database/drizzle/0037_absent_nick_fury.sql +2 -0
  243. package/template/packages/database/drizzle/0038_uneven_wasp.sql +38 -0
  244. package/template/packages/database/drizzle/meta/0035_snapshot.json +7331 -0
  245. package/template/packages/database/drizzle/meta/0036_snapshot.json +7436 -0
  246. package/template/packages/database/drizzle/meta/0037_snapshot.json +7457 -0
  247. package/template/packages/database/drizzle/meta/0038_snapshot.json +7749 -0
  248. package/template/packages/database/drizzle/meta/_journal.json +28 -0
  249. package/template/packages/database/package.json +1 -1
  250. package/template/packages/database/src/schema/identity.ts +7 -0
  251. package/template/packages/database/src/schema/notifications.ts +135 -0
  252. package/template/packages/database/src/schema-notification-delivery.test.ts +54 -0
  253. package/template/packages/database/src/schema-workflow-notification.test.ts +40 -1
  254. package/template/packages/modules/package.json +1 -1
  255. package/template/packages/modules/src/ai-operation-composition.test.ts +68 -0
  256. package/template/packages/modules/src/ai-operation-composition.ts +115 -0
  257. package/template/packages/modules/src/manifest.ts +26 -0
  258. package/template/packages/modules/src/provider-data-policy-composition.test.ts +74 -0
  259. package/template/packages/modules/src/system-api-permissions.ts +54 -0
  260. package/template/packages/modules/src/system-audit-actions.ts +15 -0
  261. package/template/packages/modules/src/system-features.ts +6 -1
  262. package/template/packages/modules/src/system-module-catalog.test.ts +16 -0
  263. package/template/packages/modules/src/system-module-catalog.ts +28 -4
  264. package/template/packages/modules/src/system-modules.test.ts +13 -1
  265. package/template/packages/modules/src/system-modules.ts +12 -0
  266. package/template/packages/modules/src/system-permissions.ts +11 -5
  267. package/template/packages/server-sdk/index.ts +11 -0
  268. package/template/packages/server-sdk/package.json +3 -2
  269. package/template/packages/server-sdk/src/ai-operation-registration.ts +2 -1
  270. package/template/packages/server-sdk/src/module-host.ts +8 -0
  271. package/template/packages/server-sdk/src/request-context.ts +2 -0
  272. package/template/packages/server-sdk/src/storage-port.ts +66 -0
  273. package/template/packages/server-sdk/src/tool-registration.ts +6 -0
  274. package/template/packages/server-sdk/src/user-directory-port.ts +42 -0
  275. package/template/packages/shared/package.json +1 -1
  276. package/template/packages/shared/src/audit.ts +25 -0
  277. package/template/packages/shared/src/module-configuration.ts +5 -0
  278. package/template/packages/shared/src/notification.ts +92 -1
  279. package/template/packages/shared/src/password.ts +7 -2
  280. package/template/packages/shared/src/platform-settings.ts +2 -0
  281. package/template/packages/storage/package.json +1 -1
  282. package/template/packages/storage/src/upload-service.test.ts +16 -0
  283. package/template/packages/storage/src/upload-service.ts +5 -1
  284. package/template/scripts/notification-postgres-regression.ts +27 -0
@@ -0,0 +1,26 @@
1
+ import type { Component } from "vue";
2
+
3
+ export type ErrorPageKind =
4
+ | "not-found"
5
+ | "internal"
6
+ | "maintenance"
7
+ | "network";
8
+
9
+ export interface ErrorPageProps {
10
+ kind: ErrorPageKind;
11
+ from: string;
12
+ requestId?: string;
13
+ }
14
+
15
+ export interface ErrorPageComponents {
16
+ notFound: Component;
17
+ internal: Component;
18
+ maintenance: Component;
19
+ network: Component;
20
+ }
21
+
22
+ export function defineErrorPageComponents(
23
+ components: ErrorPageComponents,
24
+ ): ErrorPageComponents {
25
+ return components;
26
+ }
@@ -1,15 +1,16 @@
1
1
  <script setup lang="ts">
2
2
  import type {
3
3
  PlatformNotification,
4
- PublishNotificationInput,
4
+ UpdateNotificationInput,
5
5
  } from "@southwind-ai/shared";
6
6
  import { DEFAULT_PAGE_SIZE } from "@southwind-ai/shared";
7
- import { ArchiveIcon, RefreshCwIcon, SendIcon } from "@lucide/vue";
8
- import { onMounted, shallowRef } from "vue";
7
+ import { RefreshCwIcon, SendIcon } from "@lucide/vue";
8
+ import { computed, onBeforeUnmount, onMounted, shallowRef } from "vue";
9
+ import { onBeforeRouteLeave } from "vue-router";
9
10
  import ConfirmDialog from "@/components/common/ConfirmDialog.vue";
10
11
  import DataPagination from "@/components/common/DataPagination.vue";
11
12
  import FormDialog from "@/components/common/FormDialog.vue";
12
- import { Badge } from "@/components/ui/badge";
13
+ import UnsavedChangesDialog from "@/components/common/UnsavedChangesDialog.vue";
13
14
  import { Button } from "@/components/ui/button";
14
15
  import {
15
16
  Card,
@@ -23,34 +24,62 @@ import {
23
24
  TooltipContent,
24
25
  TooltipTrigger,
25
26
  } from "@/components/ui/tooltip";
26
- import { formatDateTime } from "@/lib/date-time";
27
+ import { useUnsavedChangesConfirmation } from "@/composables/useUnsavedChangesConfirmation";
27
28
  import {
28
29
  archiveNotification,
29
30
  listManagedNotifications,
30
- publishNotification,
31
+ updateNotification,
31
32
  } from "@/services/notification-api";
33
+ import NotificationHistoryDialog from "./components/NotificationHistoryDialog.vue";
34
+ import NotificationManagementCard from "./components/NotificationManagementCard.vue";
35
+ import NotificationPublishDialog from "./components/NotificationPublishDialog.vue";
32
36
  import NotificationPublishForm from "./components/NotificationPublishForm.vue";
37
+ import { useNotificationPermissions } from "./composables/useNotificationPermissions";
33
38
 
34
- const emptyForm = (): PublishNotificationInput => ({
39
+ const emptyForm = (): UpdateNotificationInput => ({
35
40
  title: "",
36
41
  summary: "",
37
42
  content: "",
38
43
  category: "系统",
39
44
  });
40
- const form = shallowRef(emptyForm());
41
45
  const notifications = shallowRef<PlatformNotification[]>([]);
42
46
  const loading = shallowRef(false);
43
- const publishing = shallowRef(false);
44
47
  const error = shallowRef("");
45
48
  const resultMessage = shallowRef("");
46
49
  const confirmOpen = shallowRef(false);
47
50
  const publishOpen = shallowRef(false);
48
51
  const pendingArchive = shallowRef<PlatformNotification>();
52
+ const editingNotification = shallowRef<PlatformNotification>();
53
+ const editForm = shallowRef<UpdateNotificationInput>(emptyForm());
54
+ const editBaseline = shallowRef<UpdateNotificationInput>(emptyForm());
55
+ const editOpen = shallowRef(false);
56
+ const editing = shallowRef(false);
57
+ const historyNotification = shallowRef<PlatformNotification>();
58
+ const historyOpen = shallowRef(false);
59
+ const confirmation = useUnsavedChangesConfirmation();
60
+ const { canUpdate } = useNotificationPermissions();
49
61
  const page = shallowRef(1);
50
62
  const pageSize = shallowRef(DEFAULT_PAGE_SIZE);
51
63
  const total = shallowRef(0);
52
64
 
53
- onMounted(loadNotifications);
65
+ const editDirty = computed(
66
+ () =>
67
+ editOpen.value &&
68
+ (editForm.value.title !== editBaseline.value.title ||
69
+ editForm.value.summary !== editBaseline.value.summary ||
70
+ editForm.value.content !== editBaseline.value.content ||
71
+ editForm.value.category !== editBaseline.value.category),
72
+ );
73
+ onMounted(() => {
74
+ window.addEventListener("beforeunload", preventDirtyUnload);
75
+ void loadNotifications();
76
+ });
77
+ onBeforeUnmount(() =>
78
+ window.removeEventListener("beforeunload", preventDirtyUnload),
79
+ );
80
+ onBeforeRouteLeave(() =>
81
+ editDirty.value ? confirmation.request(saveEdit) : true,
82
+ );
54
83
 
55
84
  async function loadNotifications() {
56
85
  loading.value = true;
@@ -68,34 +97,65 @@ async function loadNotifications() {
68
97
  }
69
98
  }
70
99
 
71
- async function publish() {
72
- if (publishing.value) return;
73
- publishing.value = true;
100
+ function onPublished(notification: PlatformNotification) {
101
+ page.value = 1;
102
+ notifications.value = [notification, ...notifications.value].slice(
103
+ 0,
104
+ pageSize.value,
105
+ );
106
+ total.value += 1;
107
+ resultMessage.value =
108
+ notification.status === "scheduled"
109
+ ? "通知已进入定时发布队列。"
110
+ : "通知已发布,用户现在可以在右上角通知中心阅读。";
111
+ }
112
+
113
+ function openEdit(notification: PlatformNotification) {
114
+ editingNotification.value = notification;
115
+ editForm.value = editableContent(notification);
116
+ editBaseline.value = editableContent(notification);
117
+ editOpen.value = true;
118
+ }
119
+
120
+ async function saveEdit(): Promise<boolean> {
121
+ const notification = editingNotification.value;
122
+ if (!notification || editing.value) return false;
123
+ editing.value = true;
74
124
  error.value = "";
75
- resultMessage.value = "";
76
125
  try {
77
- const notification = await publishNotification({ ...form.value });
78
- page.value = 1;
79
- notifications.value = [notification, ...notifications.value].slice(
80
- 0,
81
- pageSize.value,
126
+ const result = await updateNotification(notification.id, {
127
+ ...editForm.value,
128
+ });
129
+ notifications.value = notifications.value.map((item) =>
130
+ item.id === result.notification.id ? result.notification : item,
82
131
  );
83
- total.value += 1;
84
- form.value = emptyForm();
85
- publishOpen.value = false;
86
- resultMessage.value = "通知已发布,用户现在可以在右上角通知中心阅读。";
132
+ editBaseline.value = editableContent(result.notification);
133
+ editForm.value = editableContent(result.notification);
134
+ editOpen.value = false;
135
+ resultMessage.value = result.changed
136
+ ? `公告已更新为 v${result.notification.revision}。`
137
+ : "公告内容没有变化。";
138
+ return true;
87
139
  } catch (currentError) {
88
140
  error.value = message(currentError);
141
+ return false;
89
142
  } finally {
90
- publishing.value = false;
143
+ editing.value = false;
91
144
  }
92
145
  }
93
146
 
147
+ function beforeEditClose() {
148
+ return editDirty.value ? confirmation.request(saveEdit) : true;
149
+ }
150
+ function openHistory(notification: PlatformNotification) {
151
+ historyNotification.value = notification;
152
+ historyOpen.value = true;
153
+ }
154
+
94
155
  function setPage(value: number) {
95
156
  page.value = value;
96
157
  void loadNotifications();
97
158
  }
98
-
99
159
  function setPageSize(value: number) {
100
160
  pageSize.value = value;
101
161
  page.value = 1;
@@ -129,6 +189,21 @@ async function confirmArchive() {
129
189
  function message(errorValue: unknown) {
130
190
  return errorValue instanceof Error ? errorValue.message : String(errorValue);
131
191
  }
192
+
193
+ const editableContent = (
194
+ notification: PlatformNotification,
195
+ ): UpdateNotificationInput => ({
196
+ title: notification.title,
197
+ summary: notification.summary,
198
+ content: notification.content,
199
+ category: notification.category,
200
+ });
201
+
202
+ function preventDirtyUnload(event: BeforeUnloadEvent) {
203
+ if (!editDirty.value) return;
204
+ event.preventDefault();
205
+ event.returnValue = "";
206
+ }
132
207
  </script>
133
208
 
134
209
  <template>
@@ -139,7 +214,7 @@ function message(errorValue: unknown) {
139
214
  <div class="space-y-2">
140
215
  <h1 class="text-2xl font-semibold text-foreground">通知管理</h1>
141
216
  <p class="text-sm text-muted-foreground">
142
- 发布站内通知、管理生命周期,并与用户已读回执形成闭环。
217
+ 发布和修改站内公告、查看不可变版本历史,并管理通知生命周期。
143
218
  </p>
144
219
  </div>
145
220
  <Button @click="publishOpen = true">
@@ -183,43 +258,16 @@ function message(errorValue: unknown) {
183
258
  </Tooltip>
184
259
  </CardHeader>
185
260
  <CardContent class="space-y-3">
186
- <article
261
+ <NotificationManagementCard
187
262
  v-for="(notification, index) in notifications"
188
263
  :key="notification.id"
189
- class="flex flex-col gap-3 rounded-md border p-4 md:flex-row md:items-start md:justify-between"
190
- >
191
- <div class="min-w-0 space-y-2">
192
- <div class="flex flex-wrap items-center gap-2">
193
- <h2 class="font-medium text-foreground">
194
- {{ (page - 1) * pageSize + index + 1 }}.
195
- {{ notification.title }}
196
- </h2>
197
- <Badge
198
- :variant="
199
- notification.status === 'published' ? 'default' : 'secondary'
200
- "
201
- >
202
- {{ notification.status === "published" ? "已发布" : "已归档" }}
203
- </Badge>
204
- <Badge variant="outline">{{ notification.category }}</Badge>
205
- </div>
206
- <p class="text-sm text-muted-foreground">
207
- {{ notification.summary }}
208
- </p>
209
- <p class="text-xs text-muted-foreground">
210
- 发布时间 {{ formatDateTime(notification.publishedAt) }}
211
- </p>
212
- </div>
213
- <Button
214
- v-if="notification.status === 'published'"
215
- variant="outline"
216
- size="sm"
217
- @click="requestArchive(notification)"
218
- >
219
- <ArchiveIcon data-icon="inline-start" />
220
- 归档
221
- </Button>
222
- </article>
264
+ :notification="notification"
265
+ :sequence="(page - 1) * pageSize + index + 1"
266
+ :can-edit="canUpdate"
267
+ @archive="requestArchive"
268
+ @edit="openEdit"
269
+ @history="openHistory"
270
+ />
223
271
  <p
224
272
  v-if="!loading && notifications.length === 0"
225
273
  class="py-8 text-center text-sm text-muted-foreground"
@@ -246,17 +294,37 @@ function message(errorValue: unknown) {
246
294
  @confirm="confirmArchive"
247
295
  />
248
296
 
249
- <FormDialog
297
+ <NotificationPublishDialog
250
298
  v-model:open="publishOpen"
251
- title="发布新通知"
252
- description="发布后立即进入所有已授权用户的通知中心。"
253
- submit-text="确认发布"
254
- :loading="publishing"
299
+ @published="onPublished"
300
+ @error="error = $event"
301
+ />
302
+
303
+ <FormDialog
304
+ v-model:open="editOpen"
305
+ title="修改公告"
306
+ :description="`当前 v${editingNotification?.revision || 1},保存后生成新版本。`"
307
+ submit-text="保存修改"
308
+ :loading="editing"
309
+ :before-close="beforeEditClose"
255
310
  prevent-outside-close
256
311
  content-class="sm:max-w-2xl"
257
- @submit="publish"
312
+ @submit="saveEdit"
258
313
  >
259
- <NotificationPublishForm v-model="form" />
314
+ <NotificationPublishForm v-model="editForm" />
260
315
  </FormDialog>
316
+
317
+ <NotificationHistoryDialog
318
+ v-model:open="historyOpen"
319
+ :notification="historyNotification"
320
+ />
321
+
322
+ <UnsavedChangesDialog
323
+ :open="confirmation.open.value"
324
+ :saving="confirmation.saving.value"
325
+ description="公告修改尚未保存。取消会留在编辑状态;保存成功后才会继续关闭或离开。"
326
+ @cancel="confirmation.cancel"
327
+ @save="confirmation.save"
328
+ />
261
329
  </div>
262
330
  </template>
@@ -1,31 +1,62 @@
1
1
  import { flushPromises, mount } from "@vue/test-utils";
2
- import { describe, expect, test, vi } from "vitest";
2
+ import type { PlatformNotification } from "@southwind-ai/shared";
3
+ import { beforeEach, describe, expect, test, vi } from "vitest";
3
4
  import SystemNotificationsPage from "./SystemNotificationsPage.vue";
4
5
  import FormDialog from "@/components/common/FormDialog.vue";
5
6
  import NotificationPublishForm from "./components/NotificationPublishForm.vue";
6
7
 
7
8
  const mocks = vi.hoisted(() => ({
8
- list: vi.fn(async () => ({ items: [], total: 0, page: 1, pageSize: 10 })),
9
- publish: vi.fn(async (input) => ({
10
- id: "notice_1",
11
- ...input,
12
- status: "published",
13
- publishedAt: "2026-07-14T08:00:00.000Z",
14
- createdAt: "2026-07-14T08:00:00.000Z",
15
- createdBy: "admin",
16
- updatedAt: "2026-07-14T08:00:00.000Z",
17
- updatedBy: "admin",
18
- })),
9
+ items: [] as PlatformNotification[],
10
+ list: vi.fn(),
11
+ publish: vi.fn(),
12
+ update: vi.fn(),
13
+ history: vi.fn(),
19
14
  archive: vi.fn(),
15
+ audience: vi.fn(),
16
+ permissionKeys: ["system.notification.update"],
17
+ }));
18
+
19
+ vi.mock("@/composables/useAccessSnapshot", () => ({
20
+ useAccessSnapshot: () => ({
21
+ snapshot: {
22
+ value: { actor: { permissionKeys: mocks.permissionKeys } },
23
+ },
24
+ }),
20
25
  }));
21
26
 
22
27
  vi.mock("@/services/notification-api", () => ({
23
28
  listManagedNotifications: mocks.list,
24
29
  publishNotification: mocks.publish,
30
+ updateNotification: mocks.update,
31
+ listNotificationRevisions: mocks.history,
25
32
  archiveNotification: mocks.archive,
33
+ getNotificationAudienceOptions: mocks.audience,
26
34
  }));
27
35
 
28
36
  describe("通知管理", () => {
37
+ beforeEach(() => {
38
+ mocks.items = [];
39
+ mocks.list.mockReset();
40
+ mocks.publish.mockReset();
41
+ mocks.update.mockReset();
42
+ mocks.history.mockReset();
43
+ mocks.archive.mockReset();
44
+ mocks.audience.mockReset();
45
+ mocks.audience.mockResolvedValue({
46
+ users: [],
47
+ roles: [],
48
+ departments: [],
49
+ });
50
+ mocks.permissionKeys = ["system.notification.update"];
51
+ mocks.list.mockImplementation(async () => ({
52
+ items: mocks.items,
53
+ total: mocks.items.length,
54
+ page: 1,
55
+ pageSize: 10,
56
+ }));
57
+ mocks.publish.mockImplementation(async (input) => notification(input));
58
+ });
59
+
29
60
  test("已发布通知使用右上角纯图标按钮刷新", async () => {
30
61
  const wrapper = mountPage();
31
62
  await flushPromises();
@@ -68,11 +99,165 @@ describe("通知管理", () => {
68
99
  summary: "今晚进行例行维护",
69
100
  content: "请提前保存工作。",
70
101
  category: "系统",
102
+ recipientUserIds: [],
103
+ recipientRoleCodes: [],
104
+ recipientDepartmentIds: [],
105
+ deliveryChannels: [],
106
+ includeChildDepartments: false,
71
107
  });
72
108
  expect(wrapper.text()).toContain("维护通知");
73
109
  expect(wrapper.text()).toContain("已发布");
74
110
  expect(wrapper.getComponent(FormDialog).props("open")).toBe(false);
75
111
  });
112
+
113
+ test("管理员修改公告后生成新版本并保留修改时间", async () => {
114
+ const original = notification({
115
+ title: "初始公告",
116
+ summary: "初始摘要",
117
+ content: "初始正文",
118
+ category: "系统",
119
+ });
120
+ mocks.items = [original];
121
+ mocks.update.mockImplementation(async (_id, input) => ({
122
+ changed: true,
123
+ notification: {
124
+ ...original,
125
+ ...input,
126
+ revision: 2,
127
+ updatedAt: "2026-07-14T09:00:00.000Z",
128
+ },
129
+ }));
130
+ const wrapper = mountPage();
131
+ await flushPromises();
132
+
133
+ await buttonByText(wrapper, "编辑").trigger("click");
134
+ const forms = wrapper.findAllComponents(NotificationPublishForm);
135
+ const editForm = forms.at(-1)!;
136
+ await editForm.get('[name="title"]').setValue("修改公告");
137
+ wrapper.findAllComponents(FormDialog).at(-1)!.vm.$emit("submit");
138
+ await flushPromises();
139
+
140
+ expect(mocks.update).toHaveBeenCalledWith("notice_1", {
141
+ title: "修改公告",
142
+ summary: "初始摘要",
143
+ content: "初始正文",
144
+ category: "系统",
145
+ });
146
+ expect(wrapper.text()).toContain("v2");
147
+ expect(wrapper.text()).toContain("公告已更新为 v2");
148
+ });
149
+
150
+ test("角色未获修改权限时不显示编辑入口", async () => {
151
+ mocks.permissionKeys = ["system.notification.manage"];
152
+ mocks.items = [
153
+ notification({
154
+ title: "只读管理公告",
155
+ summary: "只读摘要",
156
+ content: "只读正文",
157
+ category: "系统",
158
+ }),
159
+ ];
160
+ const wrapper = mountPage();
161
+ await flushPromises();
162
+
163
+ expect(
164
+ wrapper
165
+ .findAll("button")
166
+ .some((button) => button.text().includes("编辑")),
167
+ ).toBe(false);
168
+ expect(
169
+ wrapper
170
+ .findAll("button")
171
+ .some((button) => button.text().includes("历史")),
172
+ ).toBe(true);
173
+ expect(mocks.update).not.toHaveBeenCalled();
174
+ });
175
+
176
+ test("定时通知显示计划时间并可在到期前归档取消", async () => {
177
+ mocks.items = [
178
+ {
179
+ ...notification({
180
+ title: "定时公告",
181
+ summary: "定时摘要",
182
+ content: "定时正文",
183
+ category: "系统",
184
+ }),
185
+ status: "scheduled",
186
+ scheduledFor: "2030-01-01T00:00:00.000Z",
187
+ publishedAt: "2030-01-01T00:00:00.000Z",
188
+ },
189
+ ];
190
+ const wrapper = mountPage();
191
+ await flushPromises();
192
+
193
+ expect(wrapper.text()).toContain("已定时");
194
+ expect(wrapper.text()).toContain("计划发布");
195
+ expect(
196
+ wrapper
197
+ .findAll("button")
198
+ .some((button) => button.text().includes("归档")),
199
+ ).toBe(true);
200
+ });
201
+
202
+ test("历史入口展示版本、修改人和修改时间", async () => {
203
+ const original = notification({
204
+ title: "历史公告",
205
+ summary: "历史摘要",
206
+ content: "历史正文",
207
+ category: "系统",
208
+ });
209
+ mocks.items = [original];
210
+ mocks.history.mockResolvedValue({
211
+ items: [
212
+ {
213
+ notificationId: original.id,
214
+ revision: 1,
215
+ title: original.title,
216
+ summary: original.summary,
217
+ content: original.content,
218
+ category: original.category,
219
+ editedAt: original.publishedAt,
220
+ editedBy: "admin",
221
+ },
222
+ ],
223
+ total: 1,
224
+ page: 1,
225
+ pageSize: 10,
226
+ });
227
+ const wrapper = mountPage();
228
+ await flushPromises();
229
+
230
+ await buttonByText(wrapper, "历史").trigger("click");
231
+ await flushPromises();
232
+
233
+ expect(mocks.history).toHaveBeenCalledWith("notice_1", 1, 10);
234
+ expect(document.body.textContent).toContain("公告修改历史");
235
+ expect(document.body.textContent).toContain("admin");
236
+ expect(document.body.textContent).toContain("v1");
237
+ wrapper.unmount();
238
+ });
239
+
240
+ test("编辑草稿触发浏览器离开保护", async () => {
241
+ mocks.items = [
242
+ notification({
243
+ title: "保护公告",
244
+ summary: "保护摘要",
245
+ content: "保护正文",
246
+ category: "系统",
247
+ }),
248
+ ];
249
+ const wrapper = mountPage();
250
+ await flushPromises();
251
+ await buttonByText(wrapper, "编辑").trigger("click");
252
+ const editForm = wrapper.findAllComponents(NotificationPublishForm).at(-1)!;
253
+ await editForm.get('[name="title"]').setValue("尚未保存");
254
+ const event = new Event("beforeunload", { cancelable: true });
255
+
256
+ window.dispatchEvent(event);
257
+
258
+ expect(event.defaultPrevented).toBe(true);
259
+ wrapper.unmount();
260
+ });
76
261
  });
77
262
 
78
263
  function mountPage() {
@@ -86,3 +271,30 @@ function mountPage() {
86
271
  },
87
272
  });
88
273
  }
274
+
275
+ function notification(
276
+ input: Pick<
277
+ PlatformNotification,
278
+ "title" | "summary" | "content" | "category"
279
+ >,
280
+ ): PlatformNotification {
281
+ return {
282
+ id: "notice_1",
283
+ ...input,
284
+ status: "published",
285
+ publishedAt: "2026-07-14T08:00:00.000Z",
286
+ createdAt: "2026-07-14T08:00:00.000Z",
287
+ createdBy: "admin",
288
+ updatedAt: "2026-07-14T08:00:00.000Z",
289
+ updatedBy: "admin",
290
+ revision: 1,
291
+ };
292
+ }
293
+
294
+ function buttonByText(wrapper: ReturnType<typeof mountPage>, text: string) {
295
+ const button = wrapper
296
+ .findAll("button")
297
+ .find((candidate) => candidate.text().includes(text));
298
+ if (!button) throw new Error(`缺少按钮 ${text}`);
299
+ return button;
300
+ }
@@ -0,0 +1,86 @@
1
+ import type { SystemRecord } from "@/services/system-api";
2
+
3
+ const OUTCOME_LABELS: Record<string, string> = {
4
+ attempted: "已尝试",
5
+ success: "成功",
6
+ failure: "失败",
7
+ denied: "已拒绝",
8
+ };
9
+
10
+ export function auditRecord(value: unknown): Record<string, unknown> {
11
+ return value && typeof value === "object" && !Array.isArray(value)
12
+ ? (value as Record<string, unknown>)
13
+ : {};
14
+ }
15
+
16
+ export function auditText(value: unknown): string | undefined {
17
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
18
+ }
19
+
20
+ export function auditOutcomeLabel(value: unknown): string {
21
+ const key = auditText(value);
22
+ return key ? OUTCOME_LABELS[key] || key : "未记录";
23
+ }
24
+
25
+ export function auditTargetLabel(item: SystemRecord): string {
26
+ const target = auditRecord(item.target);
27
+ const type = auditText(target.type);
28
+ const id = auditText(target.id);
29
+ const label = auditText(target.label);
30
+ if (!type && !id && !label) return "未记录";
31
+ const primary = label || type || "未标注对象";
32
+ const identity = [type && type !== primary ? type : undefined, id]
33
+ .filter(Boolean)
34
+ .join(" / ");
35
+ return identity ? `${primary}(${identity})` : primary;
36
+ }
37
+
38
+ export function auditActorLabel(item: SystemRecord): string {
39
+ const actor = auditRecord(item.actor);
40
+ const name = auditText(actor.name);
41
+ const id = auditText(actor.id);
42
+ if (!name && !id) return "未记录";
43
+ return name && id && name !== id ? `${name}(${id})` : name || id || "未记录";
44
+ }
45
+
46
+ export function auditOperationSummary(
47
+ item: SystemRecord,
48
+ actionLabel?: string,
49
+ ): string {
50
+ const message = auditText(item.message);
51
+ const target = auditTargetLabel(item);
52
+ if (message) return target === "未记录" ? message : `${message};对象:${target}`;
53
+
54
+ const action = auditText(item.action) || "unknown";
55
+ const metadata = auditRecord(item.metadata);
56
+ const targetSuffix = target === "未记录" ? "" : ` ${target}`;
57
+ if (action === "crud.create") return `新增${targetSuffix || "记录"}`;
58
+ if (action === "crud.update") return `更新${targetSuffix || "记录"}`;
59
+ if (action === "crud.delete") return `删除${targetSuffix || "记录"}`;
60
+ if (action === "data.search" || action === "search.query") {
61
+ const query = auditText(metadata.query);
62
+ return query
63
+ ? `搜索${targetSuffix || "已记录资源"}:${query}`
64
+ : `搜索${targetSuffix || "已记录资源"}`;
65
+ }
66
+ if (action === "feature.update") {
67
+ const enabled = metadata.enabled;
68
+ const verb =
69
+ typeof enabled === "boolean" ? (enabled ? "启用" : "停用") : "更新";
70
+ return `${verb}${targetSuffix || "功能开关"}`;
71
+ }
72
+ if (target !== "未记录") {
73
+ return `${actionLabel || action}:${target}`;
74
+ }
75
+ const module = auditText(item.module);
76
+ return module
77
+ ? `${actionLabel || action}(模块:${module})`
78
+ : `${actionLabel || action}(未记录操作对象)`;
79
+ }
80
+
81
+ export function formatAuditMetadata(value: unknown): string {
82
+ const metadata = auditRecord(value);
83
+ return Object.keys(metadata).length
84
+ ? JSON.stringify(metadata, null, 2)
85
+ : "未记录";
86
+ }