@rashidee/co2 1.2.5

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 (190) hide show
  1. package/dist/index.js +25449 -0
  2. package/drizzle/0000_remarkable_charles_xavier.sql +210 -0
  3. package/drizzle/0001_wise_wolf_cub.sql +13 -0
  4. package/drizzle/0002_secret_spyke.sql +16 -0
  5. package/drizzle/0003_workspace_config_singleton.sql +16 -0
  6. package/drizzle/0004_terminal_claim_v120.sql +11 -0
  7. package/drizzle/0005_brainstorm_v124.sql +10 -0
  8. package/drizzle/0006_project_v140.sql +6 -0
  9. package/drizzle/0007_prd_wizard_v124.sql +14 -0
  10. package/drizzle/0008_workflow_artifact_v126.sql +24 -0
  11. package/drizzle/0009_project_import_v150.sql +7 -0
  12. package/drizzle/0010_quality_v151.sql +3 -0
  13. package/drizzle/meta/0000_snapshot.json +1335 -0
  14. package/drizzle/meta/0001_snapshot.json +1416 -0
  15. package/drizzle/meta/0002_snapshot.json +1510 -0
  16. package/drizzle/meta/_journal.json +83 -0
  17. package/package.json +41 -0
  18. package/plugin/.claude-plugin/marketplace.json +36 -0
  19. package/plugin/.claude-plugin/plugin.json +22 -0
  20. package/plugin/README.md +625 -0
  21. package/plugin/SKILLS.md +350 -0
  22. package/plugin/co2.svg +5 -0
  23. package/plugin/commands/brainstorm-loop.md +129 -0
  24. package/plugin/skills/conductor-defect/SKILL.md +898 -0
  25. package/plugin/skills/conductor-feature-develop/SKILL.md +1383 -0
  26. package/plugin/skills/conductor-feature-develop/references/implementation-domain-template.md +131 -0
  27. package/plugin/skills/conductor-feature-develop/references/implementation-master-template.md +89 -0
  28. package/plugin/skills/conductor-feature-develop/references/playwright-setup.md +224 -0
  29. package/plugin/skills/conductor-feature-prepare/SKILL.md +604 -0
  30. package/plugin/skills/depgen-k8s/SKILL.md +468 -0
  31. package/plugin/skills/depgen-k8s/references/dockerfile-laravel.md +197 -0
  32. package/plugin/skills/depgen-k8s/references/dockerfile-nodejs.md +163 -0
  33. package/plugin/skills/depgen-k8s/references/dockerfile-spring-boot.md +174 -0
  34. package/plugin/skills/depgen-k8s/references/k8s-patterns.md +869 -0
  35. package/plugin/skills/mockgen-shadcn/SKILL.md +1067 -0
  36. package/plugin/skills/mockgen-shadcn/references/admin-layout-template.md +1467 -0
  37. package/plugin/skills/mockgen-shadcn/references/mockup-hub-template.md +498 -0
  38. package/plugin/skills/mockgen-shadcn/references/mockup-index-template.md +216 -0
  39. package/plugin/skills/mockgen-tailwind/SKILL.md +904 -0
  40. package/plugin/skills/mockgen-tailwind/references/admin-layout-template.md +720 -0
  41. package/plugin/skills/mockgen-tailwind/references/mockup-hub-template.md +498 -0
  42. package/plugin/skills/mockgen-tailwind/references/mockup-index-template.md +190 -0
  43. package/plugin/skills/modelgen-nosql/SKILL.md +617 -0
  44. package/plugin/skills/modelgen-nosql/references/model-extraction-methodology.md +497 -0
  45. package/plugin/skills/modelgen-nosql/references/nosql-design-guide.md +258 -0
  46. package/plugin/skills/modelgen-relational/SKILL.md +707 -0
  47. package/plugin/skills/modelgen-relational/references/model-extraction-methodology.md +499 -0
  48. package/plugin/skills/specgen-flutter-riverpod/SKILL.md +898 -0
  49. package/plugin/skills/specgen-flutter-riverpod/references/component-patterns.md +971 -0
  50. package/plugin/skills/specgen-flutter-riverpod/references/notification-patterns.md +457 -0
  51. package/plugin/skills/specgen-flutter-riverpod/references/routing-patterns.md +362 -0
  52. package/plugin/skills/specgen-flutter-riverpod/references/security-patterns.md +709 -0
  53. package/plugin/skills/specgen-flutter-riverpod/references/spec-template.md +1566 -0
  54. package/plugin/skills/specgen-flutter-riverpod/references/state-patterns.md +518 -0
  55. package/plugin/skills/specgen-flutter-riverpod/references/storage-patterns.md +370 -0
  56. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/SKILL.md +984 -0
  57. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/batch-patterns.md +359 -0
  58. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/messaging-patterns.md +468 -0
  59. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/modulith-patterns.md +461 -0
  60. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/reporting-patterns.md +803 -0
  61. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/security-patterns.md +501 -0
  62. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/spec-template.md +1268 -0
  63. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/web-patterns.md +301 -0
  64. package/plugin/skills/specgen-node-cli-web/SKILL.md +561 -0
  65. package/plugin/skills/specgen-node-cli-web/references/auth-patterns.md +293 -0
  66. package/plugin/skills/specgen-node-cli-web/references/cli-packaging-patterns.md +250 -0
  67. package/plugin/skills/specgen-node-cli-web/references/server-patterns.md +247 -0
  68. package/plugin/skills/specgen-node-cli-web/references/spec-template.md +401 -0
  69. package/plugin/skills/specgen-node-cli-web/references/testing-patterns.md +174 -0
  70. package/plugin/skills/specgen-react-mui/SKILL.md +789 -0
  71. package/plugin/skills/specgen-react-mui/references/component-patterns.md +1077 -0
  72. package/plugin/skills/specgen-react-mui/references/reporting-patterns.md +1998 -0
  73. package/plugin/skills/specgen-react-mui/references/routing-patterns.md +461 -0
  74. package/plugin/skills/specgen-react-mui/references/security-patterns.md +599 -0
  75. package/plugin/skills/specgen-react-mui/references/spec-template.md +1030 -0
  76. package/plugin/skills/specgen-react-mui/references/state-patterns.md +327 -0
  77. package/plugin/skills/specgen-react-tailwind/SKILL.md +859 -0
  78. package/plugin/skills/specgen-react-tailwind/references/component-patterns.md +1212 -0
  79. package/plugin/skills/specgen-react-tailwind/references/reporting-patterns.md +1529 -0
  80. package/plugin/skills/specgen-react-tailwind/references/routing-patterns.md +669 -0
  81. package/plugin/skills/specgen-react-tailwind/references/security-patterns.md +656 -0
  82. package/plugin/skills/specgen-react-tailwind/references/spec-template.md +1237 -0
  83. package/plugin/skills/specgen-react-tailwind/references/state-patterns.md +327 -0
  84. package/plugin/skills/specgen-sdk-java/SKILL.md +933 -0
  85. package/plugin/skills/specgen-sdk-java/references/api-extraction-patterns.md +247 -0
  86. package/plugin/skills/specgen-sdk-java/references/http-client-patterns.md +495 -0
  87. package/plugin/skills/specgen-sdk-java/references/packaging-patterns.md +366 -0
  88. package/plugin/skills/specgen-sdk-java/references/spec-template.md +956 -0
  89. package/plugin/skills/specgen-spring-jpa-jtehtmx/SKILL.md +1284 -0
  90. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/batch-patterns.md +1224 -0
  91. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/jasper-patterns.md +1638 -0
  92. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/messaging-patterns.md +512 -0
  93. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/modulith-patterns.md +354 -0
  94. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/security-patterns.md +551 -0
  95. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/spec-template.md +2502 -0
  96. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/web-patterns.md +503 -0
  97. package/plugin/skills/specgen-spring-jpa-restapi/SKILL.md +990 -0
  98. package/plugin/skills/specgen-spring-jpa-restapi/references/batch-patterns.md +1224 -0
  99. package/plugin/skills/specgen-spring-jpa-restapi/references/messaging-patterns.md +512 -0
  100. package/plugin/skills/specgen-spring-jpa-restapi/references/modulith-patterns.md +279 -0
  101. package/plugin/skills/specgen-spring-jpa-restapi/references/reporting-patterns.md +1536 -0
  102. package/plugin/skills/specgen-spring-jpa-restapi/references/restapi-patterns.md +722 -0
  103. package/plugin/skills/specgen-spring-jpa-restapi/references/security-patterns.md +351 -0
  104. package/plugin/skills/specgen-spring-jpa-restapi/references/spec-template.md +553 -0
  105. package/plugin/skills/specgen-ts-cli/SKILL.md +861 -0
  106. package/plugin/skills/specgen-ts-cli/references/async-patterns.md +1101 -0
  107. package/plugin/skills/specgen-ts-cli/references/command-patterns.md +383 -0
  108. package/plugin/skills/specgen-ts-cli/references/config-patterns.md +430 -0
  109. package/plugin/skills/specgen-ts-cli/references/database-patterns.md +750 -0
  110. package/plugin/skills/specgen-ts-cli/references/packaging-patterns.md +426 -0
  111. package/plugin/skills/specgen-ts-cli/references/spec-template.md +1391 -0
  112. package/plugin/skills/specgen-ts-cli/references/testing-patterns.md +909 -0
  113. package/plugin/skills/testgen-functional/SKILL.md +1381 -0
  114. package/plugin/skills/tracegen-matrix/SKILL.md +243 -0
  115. package/plugin/skills/util-preparek8senv/SKILL.md +422 -0
  116. package/plugin/skills/util-projectinit/SKILL.md +307 -0
  117. package/plugin/skills/util-projectinit/reference/brainstorm-loop.md +129 -0
  118. package/plugin/skills/util-projectinit/reference/brainstorm-protocol.md +144 -0
  119. package/plugin/skills/util-projectinit/reference/brainstorm-setup-guide.md +153 -0
  120. package/plugin/skills/util-projectinit/reference/brainstorm-template-appdev.md +87 -0
  121. package/plugin/skills/util-projectinit/reference/co2-context-generation-guide.md +761 -0
  122. package/plugin/skills/util-projectsync/SKILL.md +501 -0
  123. package/plugin/skills/util-updprd/SKILL.md +324 -0
  124. package/plugin/skills/util-usanalyzer/SKILL.md +307 -0
  125. package/plugin/skills/util-ustagger/SKILL.md +154 -0
  126. package/static/assets/abnfDiagram-VRR7QNED-DvPuwyCV.js +1 -0
  127. package/static/assets/arc-DRKw7sjs.js +1 -0
  128. package/static/assets/architectureDiagram-ZJ3FMSHR-CaNcxUbC.js +36 -0
  129. package/static/assets/blockDiagram-677ZJIJ3-Demd4AOK.js +132 -0
  130. package/static/assets/c4Diagram-LMCZKHZV-PVKUfc2x.js +10 -0
  131. package/static/assets/channel-C2nVbuwv.js +1 -0
  132. package/static/assets/chunk-2Q5K7J3B-CExf8ww_.js +1 -0
  133. package/static/assets/chunk-32BRIVSS-CFPaoONF.js +1 -0
  134. package/static/assets/chunk-5VM5RSS4-DE2esM67.js +15 -0
  135. package/static/assets/chunk-EX3LRPZG-SON_YdDG.js +231 -0
  136. package/static/assets/chunk-JWPE2WC7-Qxah8LNr.js +1 -0
  137. package/static/assets/chunk-MOJQB5TN-BcWGQV3j.js +88 -0
  138. package/static/assets/chunk-RYQCIY6F-DkDj-OR5.js +1 -0
  139. package/static/assets/chunk-V7JOEXUC-BlI1Mrah.js +206 -0
  140. package/static/assets/chunk-VR4S4FIN-Agf__duj.js +1 -0
  141. package/static/assets/chunk-XXDRQBXY-B5CfAh85.js +1 -0
  142. package/static/assets/classDiagram-OUVF2IWQ-DXJIljsR.js +1 -0
  143. package/static/assets/classDiagram-v2-EOCWNBFH-DXJIljsR.js +1 -0
  144. package/static/assets/cose-bilkent-JH36ORCC-DO0cQkcy.js +1 -0
  145. package/static/assets/cynefin-VYW2F7L2-CfOnsnXc.js +178 -0
  146. package/static/assets/cynefinDiagram-TSTJHNR4-BJ8tULAv.js +62 -0
  147. package/static/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
  148. package/static/assets/dagre-VKFMJZFB-DfjwzHqP.js +4 -0
  149. package/static/assets/defaultLocale-DX6XiGOO.js +1 -0
  150. package/static/assets/diagram-FQU43EPY-CndSOIIM.js +3 -0
  151. package/static/assets/diagram-G47NLZAW-Czf5hN0Z.js +24 -0
  152. package/static/assets/diagram-NH7WQ7WH-COAyojGh.js +24 -0
  153. package/static/assets/diagram-OA4YK3LP-H8PoiN2m.js +30 -0
  154. package/static/assets/diagram-WEI45ONY-DzcaVzjp.js +41 -0
  155. package/static/assets/ebnfDiagram-CCIWWBDH-DPazGYLJ.js +1 -0
  156. package/static/assets/erDiagram-Q63AITRT-Bo-fubgF.js +85 -0
  157. package/static/assets/flowDiagram-23GEKE2U-CSvxUDwh.js +156 -0
  158. package/static/assets/ganttDiagram-NO4QXBWP-DnBveWX6.js +292 -0
  159. package/static/assets/gitGraphDiagram-IHSO6WYX-B3rVL8c-.js +106 -0
  160. package/static/assets/graph-C9eacEi8.js +1 -0
  161. package/static/assets/index-LZSQkLE2.css +41 -0
  162. package/static/assets/index-y7XDSgMG.js +495 -0
  163. package/static/assets/infoDiagram-FWYZ7A6U-CvWV9yy2.js +2 -0
  164. package/static/assets/init-Gi6I4Gst.js +1 -0
  165. package/static/assets/ishikawaDiagram-FXEZZL3T-DF28a1N6.js +70 -0
  166. package/static/assets/journeyDiagram-5HDEW3XC-CB6CugYk.js +139 -0
  167. package/static/assets/kanban-definition-HUTT4EX6-ResSLF6d.js +89 -0
  168. package/static/assets/katex-C5jXJg4s.js +257 -0
  169. package/static/assets/layout-DEXfKzaS.js +1 -0
  170. package/static/assets/linear-CLO--ryk.js +1 -0
  171. package/static/assets/map-Czzmt4hB.js +1 -0
  172. package/static/assets/mindmap-definition-LN4V7U3C-BvW3EbxX.js +96 -0
  173. package/static/assets/ordinal-Cboi1Yqb.js +1 -0
  174. package/static/assets/pegDiagram-2B236MQR-BHCgqrrM.js +1 -0
  175. package/static/assets/pieDiagram-ENE6RG2P-DVmCQ5FA.js +39 -0
  176. package/static/assets/quadrantDiagram-ABIIQ3AL-B3e5KR52.js +7 -0
  177. package/static/assets/railroadDiagram-RFXS5EU6-Csv5ysLc.js +1 -0
  178. package/static/assets/requirementDiagram-TGXJPOKE-DOMVzvnB.js +84 -0
  179. package/static/assets/sankeyDiagram-HTMAVEWB-B1dcTOvy.js +40 -0
  180. package/static/assets/sequenceDiagram-DBY2YBRQ-BY414yeO.js +162 -0
  181. package/static/assets/sizeCapture-X5ZJPWSS-CuJfrfBN.js +1 -0
  182. package/static/assets/stateDiagram-2N3HPSRC-FX4rJkzV.js +1 -0
  183. package/static/assets/stateDiagram-v2-6OUMAXLB-BVufbO_H.js +1 -0
  184. package/static/assets/swimlanes-5IMT3BWC-Sfy58mmv.js +2 -0
  185. package/static/assets/swimlanesDiagram-G3AALYLV-wDCDw08H.js +8 -0
  186. package/static/assets/timeline-definition-FHXFAJF6-CmV7by8p.js +120 -0
  187. package/static/assets/vennDiagram-L72KCM5P-FqIyFZFt.js +34 -0
  188. package/static/assets/wardleyDiagram-EHGQE667-BZ06O45i.js +78 -0
  189. package/static/assets/xychartDiagram-FW5EYKEG-BOkffR06.js +7 -0
  190. package/static/index.html +24 -0
@@ -0,0 +1,2502 @@
1
+ # Specification Template — Web Monolith
2
+
3
+ This is the authoritative template for the generated specification. The specification is
4
+ split into **two types of files**:
5
+
6
+ 1. **`SPECIFICATION.md`** (root) — Table of Contents, shared infrastructure, and all
7
+ application-level sections. Generated once per application.
8
+ 2. **`<module>/SPEC.md`** (per-module) — Self-contained module blueprint.
9
+ Generated once per module from PRD.md.
10
+
11
+ ```
12
+ spec/
13
+ ├── SPECIFICATION.md
14
+ ├── location-information/
15
+ │ └── SPEC.md
16
+ ├── corridor/
17
+ │ └── SPEC.md
18
+ └── ...
19
+ ```
20
+
21
+ Placeholders use `{{VARIABLE}}` syntax and must be replaced with actual values gathered
22
+ from context files.
23
+
24
+ Sections marked **[CONDITIONAL]** should only be included when the corresponding
25
+ integration is selected. Within unconditional sections, some content blocks are
26
+ conditional — follow the inline guidance.
27
+
28
+ **CRITICAL: Sample code requirement.** Every component in the spec MUST include a complete,
29
+ continuous code sample in a fenced code block (Java, JTE, JavaScript, YAML, or HTML).
30
+ The code must be self-explanatory and directly usable as a reference for a coding agent.
31
+ Do not describe components with bullet points alone — always accompany descriptions with
32
+ full code.
33
+
34
+ ---
35
+
36
+ # Part A: Root SPECIFICATION.md
37
+
38
+ Everything from this point until "Part B" goes into the root `SPECIFICATION.md` file.
39
+ A coding agent implements these shared sections **first**, before any module work.
40
+
41
+ ---
42
+
43
+ ## Table of Contents
44
+
45
+ Generate a TOC with clickable Markdown anchor links to every H2 and H3 section in this
46
+ file. Additionally, include a **Modules** section in the TOC that links to each
47
+ module's `SPEC.md` file:
48
+
49
+ ```markdown
50
+ ## Table of Contents
51
+
52
+ ### Shared Infrastructure
53
+ - [1. Project Overview](#1-project-overview)
54
+ - [2. Maven Configuration](#2-maven-configuration)
55
+ - ...all shared sections...
56
+
57
+ ### Modules
58
+ - [Location Information](location-information/SPEC.md)
59
+ - [Corridor](corridor/SPEC.md)
60
+ - [Employer](employer/SPEC.md)
61
+ - ...(one link per module)...
62
+ ```
63
+
64
+ Only include conditional sections that apply based on user selections.
65
+
66
+ ---
67
+
68
+ ## Section 1: Project Overview
69
+
70
+ ```
71
+ # {{APPLICATION_NAME}} — Technical Specification
72
+
73
+ ## 1. Project Overview
74
+
75
+ **Application Name**: {{APPLICATION_NAME}} (from CLAUDE.md Custom Applications section)
76
+ **Artifact ID**: {{ARTIFACT_ID}} (kebab-case of application name)
77
+ **Group ID**: com.bestinet.urp
78
+ **Base Package**: {{BASE_PACKAGE}} (com.bestinet.urp.<artifactid>)
79
+ **Java Version**: 21
80
+ **Spring Boot Version**: 3.5.7
81
+ **Description**: {{APP_DESCRIPTION}} (from CLAUDE.md application description)
82
+ **Versions Covered**: v1.0.0 — v{{LATEST_VERSION}}
83
+
84
+ ### Optional Components (Auto-Determined)
85
+ **Database**: {{DATABASE}} (from CLAUDE.md dependencies)
86
+ **Authentication**: {{AUTH}} (from CLAUDE.md dependencies + PRD.md NFRs)
87
+ **Scheduling**: {{SCHEDULING}} (from PRD.md NFRs)
88
+ **Messaging**: {{MESSAGING}} (from CLAUDE.md dependencies)
89
+ **Internationalisation**: {{I18N}} (from PRD.md language requirements; e.g., `yes (en, ms)` or `no`)
90
+
91
+ ### Technology Stack
92
+ (Render the core stack version table from SKILL.md, plus selected optional integration versions)
93
+
94
+ ### User Roles
95
+ (List each user role extracted from mockup sidebar files, e.g., Hub Administrator, Hub Operation Support.
96
+ Include a role-to-module mapping table showing which modules each role can access.
97
+ This mapping drives `@PreAuthorize` annotations on controllers — NOT URL path prefixes.)
98
+
99
+ | Role | Modules | Spring Security Role |
100
+ |------|---------|---------------------|
101
+ | Hub Administrator | Location Information, Corridor, Recruitment Step, ... | `HUB_ADMINISTRATOR` |
102
+ | Hub Operation Support | Employer, Quota, Quota Allocation, ... | `HUB_OPERATION_SUPPORT` |
103
+
104
+ ### Sidebar Navigation Items
105
+ (For each role, list the navigation items with module-based hrefs — NOT role-prefixed hrefs.)
106
+
107
+ **Hub Administrator:**
108
+ | Label | Href | Section |
109
+ |-------|------|---------|
110
+ | Location Information | `/location-information` | Geography |
111
+ | Corridor | `/corridor` | Administration |
112
+ | ... | ... | ... |
113
+
114
+ **Hub Operation Support:**
115
+ | Label | Href | Section |
116
+ |-------|------|---------|
117
+ | Employer | `/employer` | Operations |
118
+ | Quota | `/quota` | Operations |
119
+ | ... | ... | ... |
120
+
121
+ ### Modules
122
+ (List each module from PRD.md organized by System Module and Business Module,
123
+ with description from the module heading and user story count from MODEL.md summary table.
124
+ Each module links to its dedicated SPEC.md file.)
125
+
126
+ | Module | Type | Stories | Versions | Spec |
127
+ |--------|------|---------|----------|------|
128
+ | Location Information | Business | 12 | 1.0.0, 1.0.1, 1.0.3 | [SPEC](location-information/SPEC.md) |
129
+ | Corridor | Business | 8 | 1.0.0, 1.0.3 | [SPEC](corridor/SPEC.md) |
130
+ | ... | ... | ... | ... | ... |
131
+
132
+ ### Input Sources
133
+ (List the context files read to produce this specification)
134
+ - CLAUDE.md: {{path}}
135
+ - PRD.md: {{path}}
136
+ - Module Model: {{path to MODEL.md}}
137
+ - Mockup Screens: {{path to MOCKUP.html}}
138
+ ```
139
+
140
+ ---
141
+
142
+ ## Section 2: Maven Configuration
143
+
144
+ Generate a complete `pom.xml` inside a code block. This section is critical — it must be
145
+ copy-pasteable and immediately functional.
146
+
147
+ Include:
148
+ - Parent: `spring-boot-starter-parent` 3.5.7
149
+ - Properties: java version, jte version, mapstruct version, lombok-mapstruct-binding version, frontend-maven-plugin version, node version
150
+ - Dependencies (with comments grouping them):
151
+ - **Web**: `spring-boot-starter-web`
152
+ - **Templating**: `gg.jte:jte-spring-boot-starter-3`, `gg.jte:jte`
153
+ - **[If Database = MongoDB]** `spring-boot-starter-data-mongodb`
154
+ - **[If Database = PostgreSQL]** `spring-boot-starter-data-jpa`, `org.postgresql:postgresql`, `org.flywaydb:flyway-core`
155
+ - **[If Database = MySQL]** `spring-boot-starter-data-jpa`, `com.mysql:mysql-connector-j`, `org.flywaydb:flyway-core`, `org.flywaydb:flyway-mysql`
156
+ - **[If Auth = Keycloak]** `spring-boot-starter-security`, `spring-boot-starter-oauth2-client`
157
+ - **[If Auth = form]** `spring-boot-starter-security`
158
+ - **Modulith**: `spring-modulith-starter-core`, `spring-modulith-events-api`
159
+ - **[If Database = MongoDB]** `spring-modulith-starter-mongodb`
160
+ - **[If Database = PostgreSQL/MySQL]** `spring-modulith-starter-jpa`
161
+ - **[If Scheduling = yes]** `spring-boot-starter-quartz`
162
+ - **[If Scheduling = yes AND Database = MongoDB]** `io.fluidsonic.mirror:fluidsonic-mirror-quartz`
163
+ - **[If Scheduling = yes AND Spring Batch = yes]** `spring-boot-starter-batch`
164
+ - **[If Scheduling = yes AND Spring Batch = yes AND Remote Partitioning = yes]** `spring-batch-integration`, `spring-integration-amqp`, `spring-boot-starter-amqp`
165
+ - **[If Messaging = yes]** `spring-boot-starter-amqp` *(shared with Remote Partitioning — if both selected, include once)*
166
+ - **Utilities**: `lombok`, `mapstruct`, `mapstruct-processor`
167
+ - **DevTools**: `spring-boot-devtools` (runtime scope)
168
+ - **Testing**: `spring-boot-starter-test`, `spring-modulith-starter-test`
169
+ - **[If Auth = Keycloak or form]** `spring-security-test`
170
+ - Build plugins:
171
+ - `spring-boot-maven-plugin` (exclude lombok)
172
+ - `maven-compiler-plugin` with annotation processor paths for Lombok + MapStruct, and
173
+ `<compilerArgs>` to set `-Aspring.modulith.metadata.output.directory=${project.basedir}/build/generated-spring-modulith`
174
+ so Spring Modulith writes its metadata inside the module directory instead of the repo root
175
+ - `frontend-maven-plugin` to run `npm ci && npm run build` in `frontend/` before `process-resources`
176
+ - `maven-clean-plugin` configured with a `<fileset>` to delete `${project.basedir}/jte-classes`
177
+ on `mvn clean` (this removes stale on-demand compiled JTE classes created at runtime in dev mode)
178
+ - `gg.jte:jte-maven-plugin` for template precompilation:
179
+ - Goal: `precompile` (generates AND compiles `.class` files, not just `.java` sources)
180
+ - Phase: `process-classes` (must run after `compile` so application classes are available)
181
+ - `<sourceDirectory>`: `${project.basedir}/src/main/jte`
182
+ - `<targetDirectory>`: `${project.build.directory}/classes` (outputs directly into `target/classes/`
183
+ so precompiled templates are included in the Spring Boot fat JAR by `spring-boot-maven-plugin`)
184
+ - `<contentType>`: `Html`
185
+ - **CRITICAL**: Do NOT use `${project.build.directory}/jte-classes` as targetDirectory. The
186
+ `spring-boot-maven-plugin` repackage goal only includes `target/classes/` in the fat JAR, NOT
187
+ `target/jte-classes/`. Using `jte-classes` causes precompiled templates to be missing at
188
+ runtime in the production container, resulting in `TemplateNotFoundException`.
189
+ - **CRITICAL**: Do NOT use `${project.basedir}/jte-classes` as targetDirectory — that path is
190
+ outside `target/` and `mvn clean` will not remove it, causing stale precompiled templates.
191
+
192
+ Specify the `dependencyManagement` section for Spring Modulith BOM.
193
+
194
+ ---
195
+
196
+ ## Section 3: Application Configuration
197
+
198
+ Generate complete configuration files for `src/main/resources/`.
199
+
200
+ The YAML content varies based on selected integrations. Include only sections relevant
201
+ to the user's choices.
202
+
203
+ ### application.yml (default profile)
204
+
205
+ **IMPORTANT: Environment variable externalization.** All configuration values that may
206
+ change between environments (ports, hostnames, credentials, URIs) MUST use Spring's
207
+ `${ENV_VAR:default}` syntax. This ensures the application works out-of-the-box in local
208
+ development while allowing production deployments to override via environment variables.
209
+
210
+ **Always include:**
211
+ ```yaml
212
+ server:
213
+ port: ${SERVER_PORT:{{SERVER_PORT}}}
214
+ servlet:
215
+ context-path: /
216
+
217
+ spring:
218
+ application:
219
+ name: {{ARTIFACT_ID}}
220
+ modulith:
221
+ events:
222
+ republish-outstanding-events-on-restart: true
223
+
224
+ gg:
225
+ jte:
226
+ developmentMode: ${JTE_DEV_MODE:false}
227
+ usePrecompiledTemplates: ${JTE_PRECOMPILED:true}
228
+ templateSuffix: .jte
229
+ contentType: Html
230
+
231
+ spring:
232
+ jte:
233
+ templateLocation: src/main/jte
234
+ templateSuffix: .jte
235
+ contentType: Html
236
+
237
+ app:
238
+ cors:
239
+ allowed-origins: ${APP_CORS_ALLOWED_ORIGINS:http://localhost:3000}
240
+ theme:
241
+ default: light
242
+ cookie-name: theme
243
+ cookie-max-age: 31536000
244
+
245
+ logging:
246
+ level:
247
+ root: ${LOG_LEVEL_ROOT:WARN}
248
+ {{BASE_PACKAGE}}: ${LOG_LEVEL_APP:INFO}
249
+ pattern:
250
+ console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] [%X{userId}] %-5level %logger{36} - %msg%n"
251
+ ```
252
+
253
+ **[If Database = MongoDB] add:**
254
+ ```yaml
255
+ spring:
256
+ data:
257
+ mongodb:
258
+ uri: ${MONGODB_URI:mongodb://localhost:27017/{{DB_NAME}}}
259
+ auto-index-creation: true
260
+
261
+ logging:
262
+ level:
263
+ org.springframework.data.mongodb: WARN
264
+ ```
265
+
266
+ **[If Database = PostgreSQL] add:**
267
+ ```yaml
268
+ spring:
269
+ datasource:
270
+ url: ${DB_URL:jdbc:postgresql://localhost:5432/{{DB_NAME}}}
271
+ username: ${DB_USERNAME:{{DB_USER}}}
272
+ password: ${DB_PASSWORD:{{DB_PASSWORD}}}
273
+ driver-class-name: org.postgresql.Driver
274
+ jpa:
275
+ hibernate:
276
+ ddl-auto: validate
277
+ open-in-view: false
278
+ properties:
279
+ hibernate:
280
+ dialect: org.hibernate.dialect.PostgreSQLDialect
281
+ flyway:
282
+ enabled: true
283
+ locations: classpath:db/migration
284
+
285
+ logging:
286
+ level:
287
+ org.springframework.data.jpa: WARN
288
+ org.hibernate.SQL: WARN
289
+ ```
290
+
291
+ **[If Database = MySQL] add:**
292
+ ```yaml
293
+ spring:
294
+ datasource:
295
+ url: ${DB_URL:jdbc:mysql://localhost:3306/{{DB_NAME}}}
296
+ username: ${DB_USERNAME:{{DB_USER}}}
297
+ password: ${DB_PASSWORD:{{DB_PASSWORD}}}
298
+ driver-class-name: com.mysql.cj.jdbc.Driver
299
+ jpa:
300
+ hibernate:
301
+ ddl-auto: validate
302
+ open-in-view: false
303
+ properties:
304
+ hibernate:
305
+ dialect: org.hibernate.dialect.MySQLDialect
306
+ flyway:
307
+ enabled: true
308
+ locations: classpath:db/migration
309
+
310
+ logging:
311
+ level:
312
+ org.springframework.data.jpa: WARN
313
+ org.hibernate.SQL: WARN
314
+ ```
315
+
316
+ **[If Auth = Keycloak] add:**
317
+ ```yaml
318
+ spring:
319
+ security:
320
+ oauth2:
321
+ client:
322
+ registration:
323
+ keycloak:
324
+ client-id: ${KEYCLOAK_CLIENT_ID:{{KEYCLOAK_CLIENT_ID}}}
325
+ client-secret: ${KEYCLOAK_CLIENT_SECRET:{{KEYCLOAK_CLIENT_SECRET}}}
326
+ scope: openid,profile,email
327
+ provider:
328
+ keycloak:
329
+ issuer-uri: ${KEYCLOAK_ISSUER_URI:{{KEYCLOAK_ISSUER_URI}}}
330
+
331
+ app:
332
+ security:
333
+ keycloak-client-id: ${KEYCLOAK_CLIENT_ID:{{KEYCLOAK_CLIENT_ID}}}
334
+ public-paths:
335
+ - /static/**
336
+ - /assets/**
337
+ - /error
338
+
339
+ logging:
340
+ level:
341
+ org.springframework.security: WARN
342
+ ```
343
+
344
+ **[If Auth = form] add:**
345
+ ```yaml
346
+ app:
347
+ security:
348
+ public-paths:
349
+ - /static/**
350
+ - /assets/**
351
+ - /login
352
+ - /register
353
+ - /error
354
+
355
+ logging:
356
+ level:
357
+ org.springframework.security: WARN
358
+ ```
359
+
360
+ **[If Scheduling = yes AND Database = MongoDB] add:**
361
+ ```yaml
362
+ spring:
363
+ quartz:
364
+ job-store-type: mongodb
365
+ properties:
366
+ org.quartz.threadPool.threadCount: 5
367
+ org.quartz.jobStore.class: io.fluidsonic.mirror.quartz.MongoDBJobStore
368
+ org.quartz.jobStore.mongoUri: ${MONGODB_URI:mongodb://localhost:27017/{{DB_NAME}}}
369
+ org.quartz.jobStore.dbName: {{DB_NAME}}
370
+ org.quartz.jobStore.collectionPrefix: qrtz_
371
+ ```
372
+
373
+ **[If Scheduling = yes AND Database = PostgreSQL/MySQL] add:**
374
+ ```yaml
375
+ spring:
376
+ quartz:
377
+ job-store-type: jdbc
378
+ jdbc:
379
+ initialize-schema: always
380
+ properties:
381
+ org.quartz.threadPool.threadCount: 5
382
+ org.quartz.jobStore.class: org.quartz.impl.jdbcjobstore.JobStoreTX
383
+ org.quartz.jobStore.driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
384
+ ```
385
+
386
+ **[If Scheduling = yes AND Database = none] add:**
387
+ ```yaml
388
+ spring:
389
+ quartz:
390
+ job-store-type: memory
391
+ properties:
392
+ org.quartz.threadPool.threadCount: 5
393
+ ```
394
+
395
+ **[If Scheduling = yes AND Spring Batch = yes AND Database = MongoDB] add:**
396
+ ```yaml
397
+ spring:
398
+ batch:
399
+ jdbc:
400
+ initialize-schema: never
401
+ job:
402
+ enabled: false
403
+ ```
404
+
405
+ **[If Scheduling = yes AND Spring Batch = yes AND Database = PostgreSQL/MySQL] add:**
406
+ ```yaml
407
+ spring:
408
+ batch:
409
+ jdbc:
410
+ initialize-schema: always
411
+ job:
412
+ enabled: false
413
+ ```
414
+
415
+ **[If Scheduling = yes AND Spring Batch = yes AND Database = none] add:**
416
+ ```yaml
417
+ spring:
418
+ batch:
419
+ jdbc:
420
+ initialize-schema: never
421
+ job:
422
+ enabled: false
423
+ ```
424
+
425
+ **[If Scheduling = yes AND Spring Batch = yes AND Remote Partitioning = yes] add:**
426
+ ```yaml
427
+ spring:
428
+ rabbitmq:
429
+ host: ${RABBITMQ_HOST:localhost}
430
+ port: ${RABBITMQ_PORT:5672}
431
+ username: ${RABBITMQ_USERNAME:guest}
432
+ password: ${RABBITMQ_PASSWORD:guest}
433
+ virtual-host: ${RABBITMQ_VHOST:/}
434
+
435
+ app:
436
+ batch:
437
+ partition:
438
+ grid-size: 4
439
+ request-queue: batch.partition.requests
440
+ reply-queue: batch.partition.replies
441
+ request-dlq: batch.partition.requests.dlq
442
+ exchange: batch.partition.exchange
443
+ ```
444
+
445
+ **[If Messaging = yes] add:**
446
+ ```yaml
447
+ spring:
448
+ rabbitmq:
449
+ host: ${RABBITMQ_HOST:localhost}
450
+ port: ${RABBITMQ_PORT:5672}
451
+ username: ${RABBITMQ_USERNAME:guest}
452
+ password: ${RABBITMQ_PASSWORD:guest}
453
+ virtual-host: ${RABBITMQ_VHOST:/}
454
+
455
+ app:
456
+ messaging:
457
+ events-exchange: app.events.exchange
458
+ commands-exchange: app.commands.exchange
459
+ dead-letter-exchange: app.dlx.exchange
460
+ ```
461
+ > **Note:** If Remote Partitioning is also selected, the `spring.rabbitmq.*` connection
462
+ > block is shared — include it only once. The batch partition queues (`batch.partition.*`)
463
+ > and messaging queues (`app.*`) use different exchanges and do not conflict.
464
+
465
+ ### .env File — JTE Configuration
466
+
467
+ The `.env` file for local development **MUST** include JTE environment variables:
468
+
469
+ ```properties
470
+ # --- JTE ---
471
+ JTE_DEV_MODE=true
472
+ JTE_PRECOMPILED=false
473
+ ```
474
+
475
+ - `JTE_DEV_MODE=true`: JTE compiles templates on-the-fly from `.jte` sources (hot-reload during development)
476
+ - `JTE_PRECOMPILED=false`: Disables precompiled template lookup so changes to `.jte` files are reflected immediately
477
+
478
+ **For production** (no `.env` file), the defaults in `application.yml` apply: `JTE_DEV_MODE=false`,
479
+ `JTE_PRECOMPILED=true` — the Maven `precompile` goal generates the precompiled classes in `target/jte-classes`.
480
+
481
+ **CRITICAL — Stale JTE cache warning:** When `JTE_DEV_MODE=true`, JTE creates on-demand compiled
482
+ `.class` files in a `jte-classes/` directory at the project root. These cached classes can become
483
+ stale after template edits. The `maven-clean-plugin` configuration (Section 2) ensures `mvn clean`
484
+ deletes this folder. Developers should always run `mvn clean` before `spring-boot:run` if they
485
+ suspect stale templates. The `jte-classes/` folder MUST be listed in `.gitignore`.
486
+
487
+ ### No Profile-Specific YAML Files
488
+
489
+ Do NOT generate `application-dev.yml` or `application-prod.yml`. All environment differences
490
+ are handled via environment variables with `${ENV_VAR:default}` syntax in `application.yml`.
491
+ JTE mode, log levels, and all other settings are controlled by the `.env` file locally and
492
+ system environment variables in production.
493
+
494
+ ---
495
+
496
+ ## Section 4: Build & Tooling
497
+
498
+ ### 4.1 Frontend Directory Structure
499
+ ```
500
+ frontend/
501
+ ├── package.json
502
+ ├── vite.config.js
503
+ ├── tailwind.config.js
504
+ ├── postcss.config.js
505
+ └── src/
506
+ ├── css/
507
+ │ ├── app.css # Main entry — imports all partials
508
+ │ ├── base/
509
+ │ │ ├── reset.css # Tailwind @layer base
510
+ │ │ └── typography.css # Font imports, text defaults
511
+ │ ├── components/
512
+ │ │ ├── buttons.css # @layer components — button styles
513
+ │ │ ├── forms.css # Input, select, textarea styles
514
+ │ │ ├── cards.css # Card component styles
515
+ │ │ ├── tables.css # Table styles
516
+ │ │ ├── modals.css # Modal/dialog styles
517
+ │ │ └── navigation.css # Nav, breadcrumb, tabs styles
518
+ │ ├── layouts/
519
+ │ │ ├── sidebar.css # Sidebar responsive behavior
520
+ │ │ ├── navbar.css # Header/navbar styles
521
+ │ │ └── footer.css # Footer styles
522
+ │ └── themes/
523
+ │ ├── light.css # Light theme CSS custom properties
524
+ │ └── dark.css # Dark theme CSS custom properties
525
+ └── js/
526
+ ├── app.js # Entry — registers Alpine, htmx, global stores
527
+ ├── alpine/
528
+ │ ├── stores/
529
+ │ │ ├── toast.js # Toast notification store
530
+ │ │ ├── modal.js # Modal state store
531
+ │ │ ├── drawer.js # Sidebar drawer store
532
+ │ │ └── theme.js # Theme toggle store
533
+ │ └── components/
534
+ │ ├── dropdown.js # Dropdown behavior
535
+ │ ├── tabs.js # Tab switching
536
+ │ ├── collapse.js # Accordion/collapse
537
+ │ └── carousel.js # Carousel component
538
+ └── htmx/
539
+ ├── index.js # Global error/loading handlers
540
+ └── extensions/
541
+ ├── toast-errors.js # Map htmx errors to toast notifications
542
+ └── loading-states.js # Toggle loading classes during requests
543
+ ```
544
+
545
+ ### 4.2 Vite Configuration
546
+ ```js
547
+ // frontend/vite.config.js
548
+ import { defineConfig } from 'vite';
549
+ import { resolve } from 'path';
550
+
551
+ export default defineConfig({
552
+ root: resolve(__dirname, 'src'),
553
+ build: {
554
+ outDir: resolve(__dirname, '../src/main/resources/static/dist'),
555
+ emptyOutDir: true,
556
+ manifest: true,
557
+ rollupOptions: {
558
+ input: {
559
+ app: resolve(__dirname, 'src/js/app.js'),
560
+ styles: resolve(__dirname, 'src/css/app.css'),
561
+ },
562
+ },
563
+ },
564
+ });
565
+ ```
566
+
567
+ ### 4.3 Tailwind Configuration
568
+ ```js
569
+ // frontend/tailwind.config.js
570
+ export default {
571
+ content: [
572
+ '../src/main/jte/**/*.jte',
573
+ './src/js/**/*.js',
574
+ ],
575
+ darkMode: ['class', '[data-theme="dark"]'],
576
+ theme: {
577
+ extend: {
578
+ colors: {
579
+ primary: { 50: '#eff6ff', /* ... full scale */ 900: '#1e3a5f' },
580
+ accent: { 50: '#f0fdf4', /* ... full scale */ 900: '#14532d' },
581
+ },
582
+ },
583
+ },
584
+ plugins: [],
585
+ };
586
+ ```
587
+
588
+ ### 4.4 ViteManifestService
589
+ ```java
590
+ package {{BASE_PACKAGE}}.shared.vite;
591
+
592
+ import com.fasterxml.jackson.core.type.TypeReference;
593
+ import com.fasterxml.jackson.databind.ObjectMapper;
594
+ import jakarta.annotation.PostConstruct;
595
+ import lombok.Getter;
596
+ import lombok.extern.slf4j.Slf4j;
597
+ import org.springframework.beans.factory.annotation.Value;
598
+ import org.springframework.core.io.ClassPathResource;
599
+ import org.springframework.stereotype.Service;
600
+ import java.io.IOException;
601
+ import java.util.Map;
602
+
603
+ @Service
604
+ @Slf4j
605
+ public class ViteManifestService {
606
+
607
+ @Value("${gg.jte.development-mode:false}")
608
+ private boolean devMode;
609
+
610
+ private Map<String, ManifestEntry> manifest;
611
+
612
+ @PostConstruct
613
+ void loadManifest() {
614
+ if (devMode) {
615
+ log.info("Vite dev mode — manifest not loaded");
616
+ return;
617
+ }
618
+ try {
619
+ var resource = new ClassPathResource("static/dist/.vite/manifest.json");
620
+ var mapper = new ObjectMapper();
621
+ manifest = mapper.readValue(resource.getInputStream(),
622
+ new TypeReference<Map<String, ManifestEntry>>() {});
623
+ log.info("Vite manifest loaded with {} entries", manifest.size());
624
+ } catch (IOException e) {
625
+ throw new IllegalStateException("Failed to load Vite manifest", e);
626
+ }
627
+ }
628
+
629
+ public String resolveAsset(String entryName) {
630
+ if (devMode) {
631
+ return "http://localhost:5173/src/" + entryName;
632
+ }
633
+ var entry = manifest.get(entryName);
634
+ if (entry == null) {
635
+ throw new IllegalArgumentException("No manifest entry for: " + entryName);
636
+ }
637
+ return "/static/dist/" + entry.getFile();
638
+ }
639
+
640
+ @Getter
641
+ static class ManifestEntry {
642
+ private String file;
643
+ private String src;
644
+ private boolean isEntry;
645
+ private String[] css;
646
+ }
647
+ }
648
+ ```
649
+
650
+ ### 4.5 Maven Frontend Plugin
651
+ ```xml
652
+ <!-- In pom.xml build/plugins -->
653
+ <plugin>
654
+ <groupId>com.github.eirslett</groupId>
655
+ <artifactId>frontend-maven-plugin</artifactId>
656
+ <version>${frontend-maven-plugin.version}</version>
657
+ <configuration>
658
+ <workingDirectory>frontend</workingDirectory>
659
+ <nodeVersion>v${node.version}</nodeVersion>
660
+ </configuration>
661
+ <executions>
662
+ <execution>
663
+ <id>install-node-and-npm</id>
664
+ <goals><goal>install-node-and-npm</goal></goals>
665
+ </execution>
666
+ <execution>
667
+ <id>npm-install</id>
668
+ <goals><goal>npm</goal></goals>
669
+ <configuration><arguments>ci</arguments></configuration>
670
+ </execution>
671
+ <execution>
672
+ <id>npm-build</id>
673
+ <goals><goal>npm</goal></goals>
674
+ <phase>process-resources</phase>
675
+ <configuration><arguments>run build</arguments></configuration>
676
+ </execution>
677
+ </executions>
678
+ </plugin>
679
+ ```
680
+
681
+ ---
682
+
683
+ ## Section 4b: `.gitignore`
684
+
685
+ Generate a `.gitignore` file at the project root to exclude all generated, downloaded,
686
+ and environment-specific files from version control.
687
+
688
+ ```gitignore
689
+ # Build output
690
+ target/
691
+
692
+ # IDE
693
+ .idea/
694
+ *.iml
695
+ .vscode/
696
+ .settings/
697
+ .project
698
+ .classpath
699
+ *.swp
700
+ *~
701
+
702
+ # OS
703
+ .DS_Store
704
+ Thumbs.db
705
+
706
+ # Frontend
707
+ frontend/node_modules/
708
+ frontend/.vite/
709
+ src/main/resources/static/dist/
710
+
711
+ # JTE generated
712
+ **/jte-classes/
713
+
714
+ # Spring Modulith generated
715
+ **/build/generated-spring-modulith/
716
+
717
+ # E2E
718
+ e2e/node_modules/
719
+ e2e/test-results/
720
+ e2e/playwright-report/
721
+ e2e/visual-baselines/
722
+
723
+ # Logs
724
+ *.log
725
+ logs/
726
+
727
+ # Environment
728
+ .env
729
+ .env.*
730
+
731
+ # [If Scheduling = yes AND Spring Batch = yes] Batch temp files
732
+ # *.tmp
733
+
734
+ # [If Reporting = yes] Compiled JasperReports templates (regenerated at build time)
735
+ # *.jasper
736
+ ```
737
+
738
+ > Remove the comment markers (`#`) and include the conditional entries only when
739
+ > their integration is selected. Delete the conditional blocks entirely when not selected.
740
+
741
+ ---
742
+
743
+ ## Section 5: Package Structure
744
+
745
+ Generate the full directory tree. The structure must follow Spring Modulith conventions
746
+ adapted for server-rendered web applications.
747
+
748
+ Adapt the config and shared packages based on user selections. Only include files relevant
749
+ to the chosen integrations.
750
+
751
+ ```
752
+ src/
753
+ ├── main/
754
+ │ ├── java/
755
+ │ │ └── {{BASE_PACKAGE}}/
756
+ │ │ ├── Application.java # @SpringBootApplication
757
+ │ │ ├── config/ # Shared configuration
758
+ │ │ │ ├── WebMvcConfiguration.java
759
+ │ │ │ ├── [If Auth = Keycloak] SecurityConfiguration.java
760
+ │ │ │ ├── [If Auth = Keycloak] KeycloakGrantedAuthoritiesMapper.java
761
+ │ │ │ ├── [If Auth = form] SecurityConfiguration.java
762
+ │ │ │ ├── [If Database = MongoDB] MongoConfiguration.java
763
+ │ │ │ ├── [If Database = PostgreSQL/MySQL] JpaConfiguration.java
764
+ │ │ │ ├── [If Scheduling = yes] QuartzConfig.java
765
+ │ │ │ ├── [If Scheduling = yes AND Spring Batch = yes] BatchConfig.java
766
+ │ │ │ ├── [If Remote Partitioning = yes] RabbitMQPartitionConfig.java
767
+ │ │ │ └── [If Remote Partitioning = yes] BatchIntegrationConfig.java
768
+ │ │ ├── shared/ # Shared kernel (cross-cutting)
769
+ │ │ │ ├── exception/
770
+ │ │ │ │ ├── GlobalExceptionHandler.java
771
+ │ │ │ │ ├── WebApplicationException.java
772
+ │ │ │ │ └── ErrorResponse.java
773
+ │ │ │ ├── model/
774
+ │ │ │ │ ├── [If Database = MongoDB] BaseDocument.java
775
+ │ │ │ │ └── [If Database = PostgreSQL/MySQL] BaseEntity.java
776
+ │ │ │ ├── security/ # [If Auth != none]
777
+ │ │ │ │ ├── CspNonceFilter.java
778
+ │ │ │ │ ├── AuthAware.java
779
+ │ │ │ │ ├── ThemeAware.java
780
+ │ │ │ │ └── PaginationAware.java
781
+ │ │ │ ├── vite/
782
+ │ │ │ │ └── ViteManifestService.java
783
+ │ │ │ ├── audit/ # [If Database != none]
784
+ │ │ │ │ └── AuditAwareImpl.java
785
+ │ │ │ ├── layout/
786
+ │ │ │ │ ├── MainLayout.java
787
+ │ │ │ │ ├── [If Auth != none] AuthLayout.java
788
+ │ │ │ │ └── ErrorLayout.java
789
+ │ │ │ ├── fragment/
790
+ │ │ │ │ ├── NavItem.java
791
+ │ │ │ │ ├── UserProfile.java
792
+ │ │ │ │ ├── SidebarViewModel.java
793
+ │ │ │ │ └── FooterViewModel.java
794
+ │ │ │ ├── component/
795
+ │ │ │ │ ├── Alert.java
796
+ │ │ │ │ ├── Pagination.java
797
+ │ │ │ │ ├── PaginationRequest.java
798
+ │ │ │ │ ├── PaginatedResult.java
799
+ │ │ │ │ └── (other component view models)
800
+ │ │ │ ├── [If Messaging = yes] messaging/ # INFRASTRUCTURE ONLY — no @RabbitListener here
801
+ │ │ │ │ ├── RabbitMQMessagingConfig.java # Exchange/queue/binding declarations
802
+ │ │ │ │ ├── RabbitMQPublisher.java # Generic publisher service
803
+ │ │ │ │ ├── MessageConverterConfig.java # Jackson2JsonMessageConverter bean
804
+ │ │ │ │ └── (event/command DTOs shared across modules, e.g., OrderExportedEvent.java)
805
+ │ │ │ └── auth/ # [If Auth != none]
806
+ │ │ │ ├── [If Auth = Keycloak] AuthService.java
807
+ │ │ │ ├── [If Auth = Keycloak] LoginController.java
808
+ │ │ │ ├── [If Auth = Keycloak] KeycloakLoginRequest.java
809
+ │ │ │ ├── [If Auth = form] AuthService.java
810
+ │ │ │ ├── [If Auth = form] LoginController.java
811
+ │ │ │ ├── [If Auth = form] CustomUserDetailsService.java
812
+ │ │ │ ├── LoginRequest.java
813
+ │ │ │ └── LoginView.java
814
+ │ │ │
815
+ │ │ ├── {{module1}}/ # Module — PUBLIC API
816
+ │ │ │ ├── {{Module1}}Service.java # Public service interface
817
+ │ │ │ ├── {{Module1}}DTO.java # Public return type
818
+ │ │ │ ├── {{Module1}}Exception.java # Public exception class
819
+ │ │ │ └── internal/ # INTERNAL — hidden from other modules
820
+ │ │ │ ├── {{Module1}}ServiceImpl.java # Service implementation
821
+ │ │ │ ├── [If Database = MongoDB] {{Module1}}Repository.java # MongoRepository
822
+ │ │ │ ├── [If Database = MongoDB] {{Module1}}Document.java # MongoDB document
823
+ │ │ │ ├── [If Database = PostgreSQL/MySQL] {{Module1}}Repository.java # JpaRepository
824
+ │ │ │ ├── [If Database = PostgreSQL/MySQL] {{Module1}}Entity.java # JPA entity
825
+ │ │ │ ├── {{Module1}}Mapper.java # MapStruct mapper
826
+ │ │ │ ├── page/
827
+ │ │ │ │ ├── {{Module1}}PageController.java# Page controller (@Controller)
828
+ │ │ │ │ ├── {{Module1}}ListView.java # List page view model
829
+ │ │ │ │ └── {{Module1}}DetailView.java # Detail page view model
830
+ │ │ │ ├── fragment/
831
+ │ │ │ │ └── {{Module1}}FragmentController.java # Fragment controller (htmx)
832
+ │ │ │ └── [If Messaging = yes] {{Module1}}EventConsumer.java # @RabbitListener (MQ inbound adapter — ALWAYS in module's internal/)
833
+ │ │ │
834
+ │ │ ├── {{module2}}/ # Another module
835
+ │ │ │ └── (same public/internal structure)
836
+ │ │ │
837
+ │ │ └── [If Scheduling = yes] scheduling/ # Scheduling module
838
+ │ │ ├── internal/
839
+ │ │ │ ├── job/
840
+ │ │ │ │ ├── SampleQuartzJob.java
841
+ │ │ │ │ └── [If Remote Partitioning = yes] PartitionedQuartzJob.java
842
+ │ │ │ └── [If Spring Batch = yes] batch/
843
+ │ │ │ ├── SampleBatchConfig.java
844
+ │ │ │ ├── SampleItemReader.java
845
+ │ │ │ ├── SampleItemProcessor.java
846
+ │ │ │ ├── SampleItemWriter.java
847
+ │ │ │ └── [If Remote Partitioning = yes] partition/
848
+ │ │ │ ├── SamplePartitioner.java
849
+ │ │ │ ├── PartitionChannelConfig.java
850
+ │ │ │ ├── PartitionedBatchConfig.java
851
+ │ │ │ ├── WorkerBatchConfig.java
852
+ │ │ │ └── PartitionedReaderConfig.java
853
+ │ │ └── SchedulingService.java
854
+ │ │
855
+ │ ├── jte/ # JTE templates
856
+ │ │ └── {{BASE_PACKAGE_PATH}}/
857
+ │ │ ├── shared/
858
+ │ │ │ ├── layout/
859
+ │ │ │ │ ├── MainLayout.jte
860
+ │ │ │ │ ├── [If Auth != none] AuthLayout.jte
861
+ │ │ │ │ └── ErrorLayout.jte
862
+ │ │ │ ├── fragment/
863
+ │ │ │ │ ├── HeaderFragment.jte
864
+ │ │ │ │ ├── SidebarFragment.jte
865
+ │ │ │ │ ├── FooterFragment.jte
866
+ │ │ │ │ └── ThemeSwitcherFragment.jte
867
+ │ │ │ └── component/
868
+ │ │ │ ├── alert/Alert.jte
869
+ │ │ │ ├── avatar/Avatar.jte
870
+ │ │ │ ├── badge/Badge.jte
871
+ │ │ │ ├── breadcrumb/Breadcrumb.jte
872
+ │ │ │ ├── button/Button.jte
873
+ │ │ │ ├── card/Card.jte
874
+ │ │ │ ├── form/FormControl.jte
875
+ │ │ │ ├── form/Input.jte
876
+ │ │ │ ├── form/Select.jte
877
+ │ │ │ ├── loading/Loading.jte
878
+ │ │ │ ├── modal/Modal.jte
879
+ │ │ │ ├── pagination/Pagination.jte
880
+ │ │ │ ├── table/Table.jte
881
+ │ │ │ ├── tabs/Tabs.jte
882
+ │ │ │ └── toast/ToastContainer.jte
883
+ │ │ ├── {{module1}}/
884
+ │ │ │ ├── page/
885
+ │ │ │ │ ├── {{Module1}}ListPage.jte
886
+ │ │ │ │ └── {{Module1}}DetailPage.jte
887
+ │ │ │ └── fragment/
888
+ │ │ │ └── {{Module1}}RowFragment.jte
889
+ │ │ └── {{module2}}/
890
+ │ │ └── (same page/fragment structure)
891
+ │ │
892
+ │ └── resources/
893
+ │ ├── application.yml
894
+ │ ├── application-dev.yml
895
+ │ ├── application-prod.yml
896
+ │ ├── logback-spring.xml
897
+ │ ├── [If Database = PostgreSQL/MySQL] db/migration/
898
+ │ │ └── V1__initial_schema.sql
899
+ │ └── static/
900
+ │ ├── assets/
901
+ │ │ └── logo.svg
902
+ │ └── dist/ # Vite build output (gitignored)
903
+
904
+ ├── frontend/ # Frontend build source
905
+ │ └── (see Section 4)
906
+
907
+ └── test/
908
+ └── java/
909
+ └── {{BASE_PACKAGE}}/
910
+ └── (test classes)
911
+ ```
912
+
913
+ Explain the Spring Modulith encapsulation rules and how they apply to web modules.
914
+ Refer to `references/modulith-patterns.md` for the detailed explanation.
915
+
916
+ ---
917
+
918
+ ## Section 6: Security Configuration [CONDITIONAL — Include only if Auth != none]
919
+
920
+ **[If Auth = Keycloak]:**
921
+
922
+ This section describes the Spring Security setup for Keycloak JWT auth.
923
+ Refer to `references/security-patterns.md` for the complete content to include.
924
+
925
+ Must cover:
926
+ - `SecurityConfiguration.java` — full `SecurityFilterChain` bean definition
927
+ - `KeycloakGrantedAuthoritiesMapper.java` — extracting Keycloak roles from OIDC ID token claims
928
+ - `CspNonceFilter.java` — CSP nonce generation per request
929
+ - `AuthAware.java` — mixin to extract user info from JWT
930
+ - Public vs. secured endpoint rules
931
+ - Role hierarchy mapping from Keycloak realm_access and resource_access claims
932
+ - `SecurityContextUtil.java` — helper to extract current user info from JWT
933
+ - CSRF handling (disabled for stateless)
934
+ - CORS configuration
935
+ - Session creation policy: `STATELESS`
936
+
937
+ **[If Auth = form]:**
938
+
939
+ This section describes the Spring Security setup for form-based login.
940
+
941
+ Must cover:
942
+ - `SecurityConfiguration.java` — full `SecurityFilterChain` bean with `formLogin()`, `logout()`, `rememberMe()`
943
+ - `CustomUserDetailsService.java` — `UserDetailsService` implementation backed by the selected database
944
+ - `CspNonceFilter.java` — CSP nonce generation per request
945
+ - `AuthAware.java` — mixin to extract user info from `SecurityContextHolder`
946
+ - Public vs. secured endpoint rules
947
+ - Role-based access control using `hasRole()` / `hasAuthority()`
948
+ - CSRF enabled (standard for form login)
949
+ - CORS configuration
950
+ - Password encoding with `BCryptPasswordEncoder` bean
951
+
952
+ ---
953
+
954
+ ## Section 7: Layouts (JTE)
955
+
956
+ ### 7.1 MainLayout
957
+ Purpose: authenticated pages with header, sidebar, footer; responsive.
958
+
959
+ **CRITICAL — JTE content injection pattern:** Layouts accept a `gg.jte.Content` parameter
960
+ (not `@insert`/`@endtemplate`). Pages pass their body via a content block `content = @`...``.
961
+ This is the only pattern that works reliably with JTE 3.x. Do NOT use `@insert`/`@endtemplate`.
962
+
963
+ ```java
964
+ @Getter @Setter
965
+ public class MainLayout {
966
+ private final String title;
967
+ private final List<NavItem> navItems;
968
+ private final UserProfile user;
969
+ private final String theme;
970
+ private final String cspNonce;
971
+ private final ViteManifestService vite;
972
+
973
+ public MainLayout defaultPage(String title, HttpServletRequest req) { /* factory */ }
974
+ }
975
+ ```
976
+ ```jte
977
+ @import com.bestinet.urp.techres.shared.layout.MainLayout
978
+ @import gg.jte.Content
979
+ @param MainLayout layout
980
+ @param Content content
981
+ <!DOCTYPE html>
982
+ <html lang="en" data-theme="${layout.getTheme()}">
983
+ <head>
984
+ <meta charset="UTF-8" />
985
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
986
+ <title>${layout.getTitle()} — {{APP_TITLE}}</title>
987
+ <link rel="stylesheet" href="${layout.getVite().resolveAsset("css/app.css")}" />
988
+ <script type="module" src="${layout.getVite().resolveAsset("js/app.js")}" nonce="${layout.getCspNonce()}"></script>
989
+ </head>
990
+ <body class="min-h-screen bg-gray-50 dark:bg-gray-900 font-body">
991
+ @template.{{BASE_PACKAGE_PATH_DOT}}.shared.fragment.HeaderFragment(layout = layout)
992
+ <div class="flex pt-16 min-h-[calc(100vh-4rem)]">
993
+ @template.{{BASE_PACKAGE_PATH_DOT}}.shared.fragment.SidebarFragment(navItems = layout.getNavItems())
994
+ <main class="flex-1 ml-64">
995
+ <div id="content-area" class="p-6 space-y-6">
996
+ ${content}
997
+ </div>
998
+ @template.{{BASE_PACKAGE_PATH_DOT}}.shared.fragment.FooterFragment()
999
+ </main>
1000
+ </div>
1001
+ @template.{{BASE_PACKAGE_PATH_DOT}}.shared.component.toast.ToastContainer()
1002
+ </body>
1003
+ </html>
1004
+ ```
1005
+
1006
+ **How pages use MainLayout** — pages wrap their body in `content = @`...``:
1007
+ ```jte
1008
+ @import {{BASE_PACKAGE}}.{{module}}.internal.page.{{Entity}}ListView
1009
+ @param {{Entity}}ListView view
1010
+ @template.{{BASE_PACKAGE_PATH_DOT}}.shared.layout.MainLayout(layout = view.getLayout(), content = @`
1011
+ <%-- Page content here --%>
1012
+ <h1>Page Title</h1>
1013
+ <div id="{{module}}-list">
1014
+ @template.{{BASE_PACKAGE_PATH_DOT}}.{{module}}.page.{{Entity}}ListFragment(items = view.getItems(), pagination = view.getPagination())
1015
+ </div>
1016
+ `)
1017
+ ```
1018
+
1019
+ ### 7.2 AuthLayout [CONDITIONAL — Include only if Auth != none]
1020
+ Purpose: split-screen login page for unauthenticated users.
1021
+ ```jte
1022
+ @import {{BASE_PACKAGE}}.shared.layout.AuthLayout
1023
+ @import gg.jte.Content
1024
+ @param AuthLayout layout
1025
+ @param Content content
1026
+ <!DOCTYPE html>
1027
+ <html lang="en" data-theme="${layout.getTheme()}">
1028
+ <body class="min-h-screen grid md:grid-cols-2">
1029
+ <section class="hidden md:flex items-center justify-center bg-gray-100 dark:bg-gray-800">
1030
+ <div class="max-w-md space-y-4">
1031
+ <img src="/static/assets/logo.svg" alt="Brand" class="h-10" />
1032
+ <h1 class="text-3xl font-bold text-gray-900 dark:text-white">Welcome back</h1>
1033
+ <p class="text-gray-500 dark:text-gray-400">Access your dashboard securely.</p>
1034
+ </div>
1035
+ </section>
1036
+ <section class="flex items-center justify-center p-8">
1037
+ ${content}
1038
+ </section>
1039
+ </body>
1040
+ </html>
1041
+ ```
1042
+
1043
+ ### 7.3 ErrorLayout
1044
+ Purpose: centered error display. ErrorLayout does NOT use the Content parameter pattern
1045
+ since it renders a self-contained error page with no injected body.
1046
+ ```jte
1047
+ @import {{BASE_PACKAGE}}.shared.layout.ErrorLayout
1048
+ @param ErrorLayout layout
1049
+ <!DOCTYPE html>
1050
+ <html lang="en" data-theme="${layout.getTheme()}">
1051
+ <body class="min-h-screen flex flex-col items-center justify-center bg-gray-50 dark:bg-gray-900">
1052
+ <div class="text-center space-y-3">
1053
+ <p class="text-6xl font-bold text-red-600">${layout.getStatus()}</p>
1054
+ <h1 class="text-2xl font-semibold text-gray-900 dark:text-white">${layout.getTitle()}</h1>
1055
+ <p class="text-gray-500 dark:text-gray-400">${layout.getMessage()}</p>
1056
+ <a class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors" href="/">Return Home</a>
1057
+ </div>
1058
+ </body>
1059
+ </html>
1060
+ ```
1061
+
1062
+ ---
1063
+
1064
+ ## Section 8: Fragments
1065
+
1066
+ **CRITICAL — Template invocation syntax:** All JTE templates MUST use the fully-qualified
1067
+ `@template.` syntax to call other templates. Do NOT use `@include()`. The correct syntax is:
1068
+ ```
1069
+ @template.{{BASE_PACKAGE_PATH_DOT}}.shared.fragment.HeaderFragment(layout = layout)
1070
+ ```
1071
+
1072
+ ### 8.1 HeaderFragment
1073
+ ```jte
1074
+ @import {{BASE_PACKAGE}}.shared.layout.MainLayout
1075
+ @param MainLayout layout
1076
+ <header class="fixed top-0 left-0 right-0 z-50 bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700">
1077
+ <div class="px-4 sm:px-6 lg:px-8">
1078
+ <div class="flex items-center justify-between h-16">
1079
+ <div class="flex items-center">
1080
+ <button class="lg:hidden p-2 rounded-md text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:bg-gray-700" x-data @click="$store.drawer.toggle()">
1081
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg>
1082
+ </button>
1083
+ <a class="flex items-center gap-2 ml-2 lg:ml-0" href="/">
1084
+ <img src="/static/assets/logo.svg" class="h-8" alt="Logo" />
1085
+ </a>
1086
+ </div>
1087
+ <div class="flex items-center gap-3">
1088
+ @template.{{BASE_PACKAGE_PATH_DOT}}.shared.fragment.ThemeSwitcherFragment()
1089
+ @template.{{BASE_PACKAGE_PATH_DOT}}.shared.component.avatar.Avatar(user = layout.getUser())
1090
+ </div>
1091
+ </div>
1092
+ </div>
1093
+ </header>
1094
+ ```
1095
+
1096
+ ### 8.2 SidebarFragment
1097
+ The sidebar receives `navItems` directly (not a separate ViewModel). It is a fixed left
1098
+ panel (w-64) that works with the `ml-64` offset on `<main>` in MainLayout.
1099
+ ```jte
1100
+ @import {{BASE_PACKAGE}}.shared.fragment.NavItem
1101
+ @import java.util.List
1102
+ @param List<NavItem> navItems
1103
+ <aside class="fixed top-16 left-0 bottom-0 z-40 w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 overflow-y-auto"
1104
+ x-show="$store.drawer.open || window.innerWidth >= 1024" x-transition>
1105
+ <div class="py-4 px-3">
1106
+ <ul class="space-y-1">
1107
+ @for(var item : navItems)
1108
+ <li>
1109
+ <a href="${item.href()}" class="flex items-center px-3 py-2 rounded-lg text-sm font-medium ${item.active() ? "bg-blue-50 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300" : "text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"} transition-colors">
1110
+ ${item.label()}
1111
+ </a>
1112
+ </li>
1113
+ @endfor
1114
+ </ul>
1115
+ </div>
1116
+ </aside>
1117
+ ```
1118
+
1119
+ ### 8.3 FooterFragment
1120
+ The footer takes no parameters. It is rendered inside `<main>` after the content area.
1121
+ ```jte
1122
+ <footer class="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-auto">
1123
+ <div class="px-4 sm:px-6 lg:px-8 py-4">
1124
+ <div class="flex flex-col sm:flex-row items-center justify-between gap-2">
1125
+ <p class="text-sm text-gray-500 dark:text-gray-400">© {{CURRENT_YEAR}} {{COMPANY_NAME}}. All rights reserved.</p>
1126
+ <div class="flex gap-4">
1127
+ <a href="/privacy" class="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors">Privacy</a>
1128
+ <a href="/terms" class="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors">Terms</a>
1129
+ </div>
1130
+ </div>
1131
+ </div>
1132
+ </footer>
1133
+ ```
1134
+
1135
+ ### 8.4 ThemeSwitcherFragment
1136
+ ```jte
1137
+ <button type="button"
1138
+ class="p-2 rounded-lg text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:bg-gray-700 transition-colors"
1139
+ x-data @click="$store.theme.toggle()"
1140
+ :aria-label="$store.theme.isDark ? 'Switch to light mode' : 'Switch to dark mode'">
1141
+ <svg x-show="!$store.theme.isDark" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/></svg>
1142
+ <svg x-show="$store.theme.isDark" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/></svg>
1143
+ </button>
1144
+ ```
1145
+
1146
+ ---
1147
+
1148
+ ## Section 9: UI Components (Tailwind-based)
1149
+
1150
+ Pattern: each component has a Java view model (record or class) and a JTE template.
1151
+ All styling uses pure Tailwind CSS utility classes. No DaisyUI.
1152
+
1153
+ ### 9.1 Alert
1154
+ ```java
1155
+ public record Alert(String type, String message) {}
1156
+ // type: "success" | "info" | "warning" | "error"
1157
+ ```
1158
+ ```jte
1159
+ @param Alert alert
1160
+ !{var colors = switch(alert.type()) {
1161
+ case "success" -> "bg-green-50 text-green-800 border-green-200 dark:bg-green-900/20 dark:text-green-400 dark:border-green-800";
1162
+ case "info" -> "bg-blue-50 text-blue-800 border-blue-200 dark:bg-blue-900/20 dark:text-blue-400 dark:border-blue-800";
1163
+ case "warning" -> "bg-yellow-50 text-yellow-800 border-yellow-200 dark:bg-yellow-900/20 dark:text-yellow-400 dark:border-yellow-800";
1164
+ case "error" -> "bg-red-50 text-red-800 border-red-200 dark:bg-red-900/20 dark:text-red-400 dark:border-red-800";
1165
+ default -> "bg-gray-50 text-gray-800 border-gray-200";
1166
+ };}
1167
+ <div class="flex items-center p-4 rounded-lg border ${colors}" role="alert">
1168
+ <span class="text-sm font-medium">${alert.message()}</span>
1169
+ </div>
1170
+ ```
1171
+
1172
+ ### 9.2 Avatar
1173
+ ```java
1174
+ public record Avatar(String imageUrl, String fallback, String size) {}
1175
+ // size: "sm" | "md" | "lg"
1176
+ ```
1177
+ ```jte
1178
+ @param Avatar avatar
1179
+ !{var dim = switch(avatar.size()) { case "sm" -> "w-8 h-8"; case "lg" -> "w-12 h-12"; default -> "w-10 h-10"; };}
1180
+ <div class="relative inline-flex items-center justify-center ${dim} rounded-full overflow-hidden bg-gray-200 dark:bg-gray-600">
1181
+ @if(avatar.imageUrl() != null)
1182
+ <img src="${avatar.imageUrl()}" alt="${avatar.fallback()}" class="w-full h-full object-cover" />
1183
+ @else
1184
+ <span class="text-sm font-medium text-gray-600 dark:text-gray-300">${avatar.fallback()}</span>
1185
+ @endif
1186
+ </div>
1187
+ ```
1188
+
1189
+ ### 9.3 Badge
1190
+ ```java
1191
+ public record Badge(String text, String tone) {}
1192
+ // tone: "primary" | "success" | "warning" | "error" | "neutral"
1193
+ ```
1194
+ ```jte
1195
+ @param Badge badge
1196
+ !{var colors = switch(badge.tone()) {
1197
+ case "primary" -> "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400";
1198
+ case "success" -> "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400";
1199
+ case "warning" -> "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400";
1200
+ case "error" -> "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400";
1201
+ default -> "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300";
1202
+ };}
1203
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${colors}">${badge.text()}</span>
1204
+ ```
1205
+
1206
+ ### 9.4 Breadcrumb
1207
+ ```java
1208
+ public record BreadcrumbItem(String label, String href, boolean active) {}
1209
+ public record Breadcrumb(List<BreadcrumbItem> items) {}
1210
+ ```
1211
+ ```jte
1212
+ @param Breadcrumb breadcrumb
1213
+ <nav class="flex" aria-label="Breadcrumb">
1214
+ <ol class="flex items-center space-x-2">
1215
+ @for(var item : breadcrumb.items())
1216
+ <li class="flex items-center">
1217
+ @if(!item.active())
1218
+ <a href="${item.href()}" class="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">${item.label()}</a>
1219
+ <svg class="ml-2 w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/></svg>
1220
+ @else
1221
+ <span class="text-sm font-medium text-gray-900 dark:text-white">${item.label()}</span>
1222
+ @endif
1223
+ </li>
1224
+ @endfor
1225
+ </ol>
1226
+ </nav>
1227
+ ```
1228
+
1229
+ ### 9.5 Button
1230
+ ```java
1231
+ public record Button(String label, String href, String tone, boolean outline) {}
1232
+ ```
1233
+ ```jte
1234
+ @param Button button
1235
+ !{var base = "inline-flex items-center px-4 py-2 text-sm font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2";}
1236
+ !{var colors = button.outline()
1237
+ ? switch(button.tone()) {
1238
+ case "primary" -> "border border-blue-600 text-blue-600 hover:bg-blue-50 focus:ring-blue-500 dark:border-blue-400 dark:text-blue-400 dark:hover:bg-blue-900/20";
1239
+ case "danger" -> "border border-red-600 text-red-600 hover:bg-red-50 focus:ring-red-500 dark:border-red-400 dark:text-red-400 dark:hover:bg-red-900/20";
1240
+ default -> "border border-gray-300 text-gray-700 hover:bg-gray-50 focus:ring-gray-500 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700";
1241
+ }
1242
+ : switch(button.tone()) {
1243
+ case "primary" -> "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500";
1244
+ case "danger" -> "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500";
1245
+ default -> "bg-white text-gray-700 border border-gray-300 hover:bg-gray-50 focus:ring-gray-500 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600";
1246
+ };}
1247
+ <a class="${base} ${colors}" href="${button.href()}">${button.label()}</a>
1248
+ ```
1249
+
1250
+ ### 9.6 Card
1251
+ ```java
1252
+ public record Card(String title, String subtitle, String content, List<Button> actions) {}
1253
+ ```
1254
+ ```jte
1255
+ @param Card card
1256
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
1257
+ <div class="p-6">
1258
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-white">${card.title()}</h3>
1259
+ @if(card.subtitle() != null)
1260
+ <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">${card.subtitle()}</p>
1261
+ @endif
1262
+ @if(card.content() != null)
1263
+ <p class="mt-3 text-gray-700 dark:text-gray-300">${card.content()}</p>
1264
+ @endif
1265
+ @if(card.actions() != null && !card.actions().isEmpty())
1266
+ <div class="mt-4 flex justify-end gap-2">
1267
+ @for(btn : card.actions())
1268
+ @template.{{BASE_PACKAGE_PATH_DOT}}.shared.component.button.Button(button = btn)
1269
+ @endfor
1270
+ </div>
1271
+ @endif
1272
+ </div>
1273
+ </div>
1274
+ ```
1275
+
1276
+ ### 9.7 FormControl & Input
1277
+ ```java
1278
+ public record FormControl(String label, String name, String help, boolean required) {}
1279
+ public record Input(String name, String type, String value, String placeholder) {}
1280
+ public record Select(String name, List<Option> options) {}
1281
+ public record Option(String value, String label, boolean selected) {}
1282
+ public record Checkbox(String name, boolean checked, String label) {}
1283
+ public record Radio(String name, String value, boolean checked, String label) {}
1284
+ public record Toggle(String name, boolean enabled, String label) {}
1285
+ ```
1286
+ ```jte
1287
+ @!-- FormControl.jte --@
1288
+ @import gg.jte.Content
1289
+ @param FormControl ctrl
1290
+ @param Content content
1291
+ <div class="space-y-1">
1292
+ <label for="${ctrl.name()}" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
1293
+ ${ctrl.label()} @if(ctrl.required()) <span class="text-red-500">*</span> @endif
1294
+ </label>
1295
+ ${content}
1296
+ @if(ctrl.help() != null)
1297
+ <p class="text-xs text-gray-500 dark:text-gray-400">${ctrl.help()}</p>
1298
+ @endif
1299
+ </div>
1300
+ ```
1301
+ ```jte
1302
+ @!-- Input.jte --@
1303
+ @param Input input
1304
+ <input type="${input.type()}" name="${input.name()}" id="${input.name()}" value="${input.value()}"
1305
+ placeholder="${input.placeholder()}"
1306
+ class="block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-400 dark:focus:ring-blue-400" />
1307
+ ```
1308
+ ```jte
1309
+ @!-- Select.jte --@
1310
+ @param Select select
1311
+ <select name="${select.name()}" id="${select.name()}"
1312
+ class="block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:focus:border-blue-400">
1313
+ @for(opt : select.options())
1314
+ <option value="${opt.value()}" ${opt.selected() ? "selected" : ""}>${opt.label()}</option>
1315
+ @endfor
1316
+ </select>
1317
+ ```
1318
+ ```jte
1319
+ @!-- Checkbox.jte --@
1320
+ @param Checkbox checkbox
1321
+ <label class="flex items-center gap-3 cursor-pointer">
1322
+ <input type="checkbox" name="${checkbox.name()}" class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700" ${checkbox.checked() ? "checked" : ""} />
1323
+ <span class="text-sm text-gray-700 dark:text-gray-300">${checkbox.label()}</span>
1324
+ </label>
1325
+ ```
1326
+
1327
+ ### 9.8 Loading & Skeleton
1328
+ ```java
1329
+ public record Loading(boolean spinner) {}
1330
+ public record Skeleton(String width, String height) {}
1331
+ ```
1332
+ ```jte
1333
+ @param Loading loading
1334
+ @if(loading.spinner())
1335
+ <div class="flex justify-center">
1336
+ <svg class="animate-spin h-5 w-5 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
1337
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
1338
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
1339
+ </svg>
1340
+ </div>
1341
+ @else
1342
+ <div class="flex space-x-1">
1343
+ <div class="w-2 h-2 bg-blue-600 rounded-full animate-bounce [animation-delay:-0.3s]"></div>
1344
+ <div class="w-2 h-2 bg-blue-600 rounded-full animate-bounce [animation-delay:-0.15s]"></div>
1345
+ <div class="w-2 h-2 bg-blue-600 rounded-full animate-bounce"></div>
1346
+ </div>
1347
+ @endif
1348
+ ```
1349
+
1350
+ ### 9.9 Modal
1351
+ ```java
1352
+ public record ModalAction(String label, String tone, String js) {}
1353
+ public record Modal(String id, String title, String body, List<ModalAction> actions) {}
1354
+ ```
1355
+ ```jte
1356
+ @param Modal modal
1357
+ <div id="${modal.id()}" class="fixed inset-0 z-50 hidden" x-data="{ open: false }" x-show="open" x-cloak>
1358
+ <div class="fixed inset-0 bg-black/50 transition-opacity" @click="open = false"></div>
1359
+ <div class="fixed inset-0 flex items-center justify-center p-4">
1360
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-md w-full" @click.stop>
1361
+ <div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
1362
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-white">${modal.title()}</h3>
1363
+ </div>
1364
+ <div class="px-6 py-4">
1365
+ <p class="text-gray-600 dark:text-gray-300">${modal.body()}</p>
1366
+ </div>
1367
+ <div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end gap-2">
1368
+ @for(action : modal.actions())
1369
+ <button class="px-4 py-2 text-sm font-medium rounded-lg transition-colors" @click="${action.js()}">${action.label()}</button>
1370
+ @endfor
1371
+ </div>
1372
+ </div>
1373
+ </div>
1374
+ </div>
1375
+ ```
1376
+
1377
+ ### 9.10 Pagination
1378
+ ```java
1379
+ public record Pagination(int page, int totalPages, String baseHref) {}
1380
+ ```
1381
+ ```jte
1382
+ @param Pagination pagination
1383
+ <nav class="flex items-center justify-center gap-1" aria-label="Pagination">
1384
+ @if(pagination.page() > 1)
1385
+ <a href="${pagination.baseHref()}?page=${pagination.page() - 1}" class="px-3 py-2 text-sm rounded-lg text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700 transition-colors">Previous</a>
1386
+ @endif
1387
+ @for(int i = 1; i <= pagination.totalPages(); i++)
1388
+ <a href="${pagination.baseHref()}?page=${i}"
1389
+ class="px-3 py-2 text-sm rounded-lg transition-colors ${i == pagination.page() ? "bg-blue-600 text-white font-medium" : "text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"}">
1390
+ ${i}
1391
+ </a>
1392
+ @endfor
1393
+ @if(pagination.page() < pagination.totalPages())
1394
+ <a href="${pagination.baseHref()}?page=${pagination.page() + 1}" class="px-3 py-2 text-sm rounded-lg text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700 transition-colors">Next</a>
1395
+ @endif
1396
+ </nav>
1397
+ ```
1398
+
1399
+ ### 9.11 Table
1400
+ ```java
1401
+ public record TableHead(List<String> columns) {}
1402
+ public record TableRow(List<String> cells) {}
1403
+ public record TableBody(List<TableRow> rows) {}
1404
+ public record Table(TableHead head, TableBody body) {}
1405
+ ```
1406
+ ```jte
1407
+ @param Table table
1408
+ <div class="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700">
1409
+ <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
1410
+ <thead class="bg-gray-50 dark:bg-gray-800">
1411
+ <tr>
1412
+ @for(col : table.head().columns())
1413
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">${col}</th>
1414
+ @endfor
1415
+ </tr>
1416
+ </thead>
1417
+ <tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
1418
+ @for(row : table.body().rows())
1419
+ <tr class="hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors">
1420
+ @for(cell : row.cells())
1421
+ <td class="px-6 py-4 text-sm text-gray-900 dark:text-gray-300 whitespace-nowrap">${cell}</td>
1422
+ @endfor
1423
+ </tr>
1424
+ @endfor
1425
+ </tbody>
1426
+ </table>
1427
+ </div>
1428
+ ```
1429
+
1430
+ ### 9.12 Tabs
1431
+ ```java
1432
+ public record Tab(String id, String label, boolean active) {}
1433
+ public record Tabs(List<Tab> items) {}
1434
+ ```
1435
+ ```jte
1436
+ @param Tabs tabs
1437
+ <div class="border-b border-gray-200 dark:border-gray-700">
1438
+ <nav class="flex space-x-4" role="tablist">
1439
+ @for(tab : tabs.items())
1440
+ <a role="tab" href="#${tab.id()}"
1441
+ class="px-3 py-2 text-sm font-medium border-b-2 transition-colors ${tab.active() ? "border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400" : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-200"}">
1442
+ ${tab.label()}
1443
+ </a>
1444
+ @endfor
1445
+ </nav>
1446
+ </div>
1447
+ ```
1448
+
1449
+ ### 9.13 Toast & ToastContainer
1450
+ ```java
1451
+ public record Toast(String type, String message) {}
1452
+ public record ToastContainer(List<Toast> toasts) {}
1453
+ ```
1454
+ ```jte
1455
+ @param ToastContainer container
1456
+ <div class="fixed bottom-4 right-4 z-50 space-y-2" x-data>
1457
+ <template x-for="(toast, i) in $store.toast.items" :key="i">
1458
+ <div class="flex items-center p-4 rounded-lg shadow-lg border max-w-sm"
1459
+ :class="toast.type === 'error' ? 'bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-400' :
1460
+ toast.type === 'success' ? 'bg-green-50 border-green-200 text-green-800 dark:bg-green-900/20 dark:border-green-800 dark:text-green-400' :
1461
+ 'bg-blue-50 border-blue-200 text-blue-800 dark:bg-blue-900/20 dark:border-blue-800 dark:text-blue-400'">
1462
+ <span class="text-sm font-medium" x-text="toast.message"></span>
1463
+ <button @click="$store.toast.remove(i)" class="ml-3 text-current opacity-50 hover:opacity-100">
1464
+ <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>
1465
+ </button>
1466
+ </div>
1467
+ </template>
1468
+ </div>
1469
+ ```
1470
+
1471
+ ### 9.14 Stats
1472
+ ```java
1473
+ public record Stat(String label, String value, String desc) {}
1474
+ public record Stats(List<Stat> items) {}
1475
+ ```
1476
+ ```jte
1477
+ @param Stats stats
1478
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-${stats.items().size()} gap-4">
1479
+ @for(stat : stats.items())
1480
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
1481
+ <p class="text-sm text-gray-500 dark:text-gray-400">${stat.label()}</p>
1482
+ <p class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white">${stat.value()}</p>
1483
+ @if(stat.desc() != null)
1484
+ <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">${stat.desc()}</p>
1485
+ @endif
1486
+ </div>
1487
+ @endfor
1488
+ </div>
1489
+ ```
1490
+
1491
+ ### 9.15 Steps
1492
+ ```java
1493
+ public record Step(String label, boolean done) {}
1494
+ public record Steps(List<Step> items) {}
1495
+ ```
1496
+ ```jte
1497
+ @param Steps steps
1498
+ <ol class="flex items-center">
1499
+ @for(var step : steps.items())
1500
+ <li class="flex items-center ${step.done() ? "text-blue-600 dark:text-blue-400" : "text-gray-500 dark:text-gray-400"}">
1501
+ <span class="flex items-center justify-center w-8 h-8 rounded-full border-2 text-sm font-medium ${step.done() ? "border-blue-600 bg-blue-600 text-white dark:border-blue-400 dark:bg-blue-400" : "border-gray-300 dark:border-gray-600"}">
1502
+ @if(step.done()) ✓ @endif
1503
+ </span>
1504
+ <span class="ml-2 text-sm font-medium">${step.label()}</span>
1505
+ </li>
1506
+ @endfor
1507
+ </ol>
1508
+ ```
1509
+
1510
+ ### 9.16 Tooltip
1511
+ ```java
1512
+ public record Tooltip(String text) {}
1513
+ ```
1514
+ ```jte
1515
+ @import gg.jte.Content
1516
+ @param Tooltip tooltip
1517
+ @param Content content
1518
+ <div class="relative group inline-block">
1519
+ ${content}
1520
+ <div class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-1.5 bg-gray-900 text-white text-xs rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap dark:bg-gray-700">
1521
+ ${tooltip.text()}
1522
+ <div class="absolute top-full left-1/2 -translate-x-1/2 -mt-1 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"></div>
1523
+ </div>
1524
+ </div>
1525
+ ```
1526
+
1527
+ ### 9.17 Progress
1528
+ ```java
1529
+ public record Progress(int value, int max) {}
1530
+ ```
1531
+ ```jte
1532
+ @param Progress progress
1533
+ !{var pct = (int) ((double) progress.value() / progress.max() * 100);}
1534
+ <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5">
1535
+ <div class="bg-blue-600 h-2.5 rounded-full transition-all" style="width: ${pct}%"></div>
1536
+ </div>
1537
+ ```
1538
+
1539
+ ---
1540
+
1541
+ *(Module Blueprints are generated as separate files — see Part B below.
1542
+ Each module gets its own `<module>/SPEC.md` file. The Module Index in
1543
+ Section 1 links to each module's spec.)*
1544
+
1545
+ ---
1546
+
1547
+ ## Section 10: Frontend JS Structure
1548
+
1549
+ ### 10.1 App Entry
1550
+ ```js
1551
+ // frontend/src/js/app.js
1552
+ import Alpine from 'alpinejs';
1553
+ import htmx from 'htmx.org';
1554
+ import { toastStore } from './alpine/stores/toast.js';
1555
+ import { themeStore } from './alpine/stores/theme.js';
1556
+ import { drawerStore } from './alpine/stores/drawer.js';
1557
+ import { modalStore } from './alpine/stores/modal.js';
1558
+ import './htmx/index.js';
1559
+
1560
+ Alpine.store('toast', toastStore());
1561
+ Alpine.store('theme', themeStore());
1562
+ Alpine.store('drawer', drawerStore());
1563
+ Alpine.store('modal', modalStore());
1564
+ Alpine.start();
1565
+ window.htmx = htmx;
1566
+ ```
1567
+
1568
+ ### 10.2 Alpine Stores
1569
+ ```js
1570
+ // toast.js
1571
+ export function toastStore() {
1572
+ return {
1573
+ items: [],
1574
+ push(type, message) {
1575
+ this.items.push({ type, message });
1576
+ setTimeout(() => this.items.shift(), 5000);
1577
+ },
1578
+ remove(index) { this.items.splice(index, 1); }
1579
+ };
1580
+ }
1581
+
1582
+ // theme.js
1583
+ export function themeStore() {
1584
+ return {
1585
+ isDark: document.cookie.includes('theme=dark'),
1586
+ toggle() {
1587
+ this.isDark = !this.isDark;
1588
+ const theme = this.isDark ? 'dark' : 'light';
1589
+ document.documentElement.dataset.theme = theme;
1590
+ document.cookie = `theme=${theme};path=/;max-age=31536000;SameSite=Lax`;
1591
+ }
1592
+ };
1593
+ }
1594
+
1595
+ // drawer.js
1596
+ export function drawerStore() {
1597
+ return {
1598
+ open: false,
1599
+ toggle() { this.open = !this.open; },
1600
+ close() { this.open = false; }
1601
+ };
1602
+ }
1603
+ ```
1604
+
1605
+ ### 10.3 htmx Global Handlers
1606
+ ```js
1607
+ // frontend/src/js/htmx/index.js
1608
+ document.addEventListener('htmx:responseError', (event) => {
1609
+ const status = event.detail.xhr.status;
1610
+ const message = status >= 500 ? 'Server error. Please try again.' : 'Request failed.';
1611
+ Alpine.store('toast').push('error', message);
1612
+ });
1613
+
1614
+ document.addEventListener('htmx:sendError', () => {
1615
+ Alpine.store('toast').push('error', 'Network error. Check your connection.');
1616
+ });
1617
+ ```
1618
+
1619
+ ---
1620
+
1621
+ ## Section 11: Authentication Pages [CONDITIONAL — Include only if Auth != none]
1622
+
1623
+ **[If Auth = Keycloak]:**
1624
+
1625
+ Keycloak handles all login flows externally. The application redirects unauthenticated
1626
+ users to Keycloak's login page via the OAuth2 authorization code flow. Keycloak manages:
1627
+ - Login form (email/password)
1628
+ - Social login providers (Google, etc.)
1629
+ - User registration (if enabled in Keycloak realm)
1630
+ - Password reset
1631
+
1632
+ The application only needs to:
1633
+ - Configure the OAuth2 client for Keycloak
1634
+ - Handle the JWT token returned after successful authentication
1635
+ - Provide a logout endpoint that invalidates the Keycloak session
1636
+
1637
+ **[If Auth = form]:**
1638
+
1639
+ The application provides its own login page. Include:
1640
+ - Login page with email/password form, CSRF token, remember-me checkbox
1641
+ - Login controller handling GET (render form) and POST (Spring Security handles via `formLogin()`)
1642
+ - Logout endpoint clearing the session
1643
+ - Optionally: registration page if user self-registration is desired
1644
+ - Error display for invalid credentials
1645
+
1646
+ ---
1647
+
1648
+ ## Section 12: Error Handling & Exceptions
1649
+
1650
+ ### 13.1 Base Exception
1651
+ ```java
1652
+ package {{BASE_PACKAGE}}.shared.exception;
1653
+
1654
+ import lombok.Getter;
1655
+ import org.springframework.http.HttpStatus;
1656
+
1657
+ @Getter
1658
+ public class WebApplicationException extends RuntimeException {
1659
+ private final HttpStatus status;
1660
+ private final String code;
1661
+ private final String userMessage;
1662
+
1663
+ public WebApplicationException(HttpStatus status, String code, String userMessage) {
1664
+ super(userMessage);
1665
+ this.status = status;
1666
+ this.code = code;
1667
+ this.userMessage = userMessage;
1668
+ }
1669
+ }
1670
+ ```
1671
+
1672
+ ### 13.2 Global Exception Handler
1673
+ ```java
1674
+ package {{BASE_PACKAGE}}.shared.exception;
1675
+
1676
+ import jakarta.servlet.http.HttpServletRequest;
1677
+ import lombok.extern.slf4j.Slf4j;
1678
+ import org.springframework.http.ResponseEntity;
1679
+ import org.springframework.web.bind.annotation.ControllerAdvice;
1680
+ import org.springframework.web.bind.annotation.ExceptionHandler;
1681
+ import org.springframework.web.servlet.ModelAndView;
1682
+ import java.util.Map;
1683
+
1684
+ @ControllerAdvice
1685
+ @Slf4j
1686
+ public class GlobalExceptionHandler {
1687
+
1688
+ @ExceptionHandler(WebApplicationException.class)
1689
+ Object handleWeb(WebApplicationException ex, HttpServletRequest req) {
1690
+ log.warn("Web error [{}]: {}", ex.getCode(), ex.getUserMessage());
1691
+ if (isHtmxRequest(req)) {
1692
+ return ResponseEntity.status(ex.getStatus())
1693
+ .header("HX-Trigger", toastEvent("error", ex.getUserMessage()))
1694
+ .body("");
1695
+ }
1696
+ return errorView(ex.getStatus().value(), ex.getStatus().getReasonPhrase(), ex.getUserMessage());
1697
+ }
1698
+
1699
+ @ExceptionHandler(Exception.class)
1700
+ Object handleUnknown(Exception ex, HttpServletRequest req) {
1701
+ log.error("Unhandled error", ex);
1702
+ if (isHtmxRequest(req)) {
1703
+ return ResponseEntity.internalServerError()
1704
+ .header("HX-Trigger", toastEvent("error", "An unexpected error occurred"))
1705
+ .body("");
1706
+ }
1707
+ return errorView(500, "Internal Server Error", "An unexpected error occurred");
1708
+ }
1709
+
1710
+ private boolean isHtmxRequest(HttpServletRequest req) {
1711
+ return "true".equals(req.getHeader("HX-Request"));
1712
+ }
1713
+
1714
+ private ModelAndView errorView(int status, String title, String message) {
1715
+ return new ModelAndView("shared/layout/ErrorLayout",
1716
+ Map.of("layout", ErrorLayout.of(status, title, message)));
1717
+ }
1718
+
1719
+ private String toastEvent(String type, String message) {
1720
+ return "{\"showToast\":{\"type\":\"" + type + "\",\"message\":\"" + message + "\"}}";
1721
+ }
1722
+ }
1723
+ ```
1724
+
1725
+ ---
1726
+
1727
+ ## Section 13: Data Access [CONDITIONAL content varies by database]
1728
+
1729
+ **[If Database = MongoDB]:**
1730
+
1731
+ Describe MongoDB repositories per module with Spring Data MongoRepository.
1732
+ DTO mapping via MapStruct. Pagination helpers with `PaginationAware` mixin.
1733
+ All list endpoints paginated. Include `BaseDocument` with audit fields.
1734
+
1735
+ **[If Database = PostgreSQL/MySQL]:**
1736
+
1737
+ Describe JPA repositories per module with Spring Data JpaRepository.
1738
+ JPA entities with `@Entity`/`@Table` annotations extending `BaseEntity`.
1739
+ Flyway migration scripts for schema management.
1740
+ DTO mapping via MapStruct. Pagination helpers. All list endpoints paginated.
1741
+ Include `BaseEntity` with audit fields and `@MappedSuperclass`.
1742
+
1743
+ Include `BaseEntity` definition using `@UuidGenerator(style = UuidGenerator.Style.TIME)` with `@Column(columnDefinition = "CHAR(36)")` and `private String id`.
1744
+ All UUID/foreign-key columns in entities MUST use `columnDefinition = "CHAR(36)"` (not `length = 36`) to match the database `CHAR(36)` type and pass Hibernate schema validation.
1745
+
1746
+ Include a sample Flyway migration script:
1747
+
1748
+ **[If Database = PostgreSQL]:**
1749
+ ```sql
1750
+ -- V1__initial_schema.sql
1751
+ CREATE TABLE {{module_name}} (
1752
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1753
+ field_one VARCHAR(255) NOT NULL,
1754
+ field_two VARCHAR(255),
1755
+ active BOOLEAN NOT NULL DEFAULT TRUE,
1756
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
1757
+ updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
1758
+ created_by VARCHAR(255),
1759
+ updated_by VARCHAR(255)
1760
+ );
1761
+ ```
1762
+
1763
+ **[If Database = MySQL]:**
1764
+ ```sql
1765
+ -- V1__initial_schema.sql
1766
+ CREATE TABLE {{module_name}} (
1767
+ id CHAR(36) NOT NULL PRIMARY KEY,
1768
+ field_one VARCHAR(255) NOT NULL,
1769
+ field_two VARCHAR(255),
1770
+ active BOOLEAN NOT NULL DEFAULT TRUE,
1771
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
1772
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
1773
+ created_by VARCHAR(255) NOT NULL,
1774
+ updated_by VARCHAR(255) NOT NULL
1775
+ );
1776
+ ```
1777
+ **MySQL UUID rule:** All UUID primary keys and foreign keys MUST use `CHAR(36)` (not `VARCHAR(36)`). In JPA entities, map these with `columnDefinition = "CHAR(36)"` to ensure Hibernate `ddl-auto: validate` passes.
1778
+
1779
+ **[If Database = none]:**
1780
+
1781
+ Note that no database is configured. Modules should use in-memory collections
1782
+ or stub implementations. The spec should indicate where database integration can be
1783
+ added later.
1784
+
1785
+ ---
1786
+
1787
+ ## Section 14: Theming
1788
+
1789
+ Describe:
1790
+ - Light/dark theme using `data-theme` attribute on `<html>`
1791
+ - CSS custom properties in `themes/light.css` and `themes/dark.css`
1792
+ - Tailwind `darkMode: ['class', '[data-theme="dark"]']` config
1793
+ - Server reads `theme` cookie on request; if missing, defaults to `light`
1794
+ - Layout view model carries theme string; template sets `data-theme="${layout.getTheme()}"`
1795
+ - Alpine `themeStore` syncs toggle with cookie and `data-theme` attribute
1796
+
1797
+ ```css
1798
+ /* themes/light.css */
1799
+ [data-theme="light"] {
1800
+ --color-bg-primary: 255 255 255;
1801
+ --color-bg-secondary: 249 250 251;
1802
+ --color-text-primary: 17 24 39;
1803
+ --color-text-secondary: 107 114 128;
1804
+ --color-border: 229 231 235;
1805
+ }
1806
+
1807
+ /* themes/dark.css */
1808
+ [data-theme="dark"] {
1809
+ --color-bg-primary: 17 24 39;
1810
+ --color-bg-secondary: 31 41 55;
1811
+ --color-text-primary: 243 244 246;
1812
+ --color-text-secondary: 156 163 175;
1813
+ --color-border: 55 65 81;
1814
+ }
1815
+ ```
1816
+
1817
+ ---
1818
+
1819
+ ## Section 15: Pagination Support
1820
+
1821
+ ```java
1822
+ public record PaginationRequest(int page, int size) {}
1823
+
1824
+ public record PaginatedResult<T>(List<T> data, int page, int size, long totalItems) {
1825
+ public int totalPages() { return Math.max(1, (int) Math.ceil((double) totalItems / size)); }
1826
+ public Pagination toPagination(String baseHref) { return new Pagination(page, totalPages(), baseHref); }
1827
+ }
1828
+ ```
1829
+
1830
+ ```java
1831
+ public interface PaginationAware {
1832
+ default PaginationRequest pagination(HttpServletRequest req, int defaultSize, int maxSize) {
1833
+ int page = Math.max(1, parseIntParam(req, "page", 1));
1834
+ int size = Math.min(maxSize, Math.max(1, parseIntParam(req, "size", defaultSize)));
1835
+ return new PaginationRequest(page, size);
1836
+ }
1837
+ private int parseIntParam(HttpServletRequest req, String name, int defaultValue) {
1838
+ String val = req.getParameter(name);
1839
+ if (val == null) return defaultValue;
1840
+ try { return Integer.parseInt(val); } catch (NumberFormatException e) { return defaultValue; }
1841
+ }
1842
+ }
1843
+ ```
1844
+
1845
+ All list endpoints must call services with pagination and render the Pagination component
1846
+ above and below listings.
1847
+
1848
+ ---
1849
+
1850
+ ## Section 16: Logging Strategy
1851
+
1852
+ Same structure as API spec — `logback-spring.xml` with Spring profiles, correlation ID
1853
+ (from JWT if auth is enabled, or request-scoped UUID if no auth), MDC context filter.
1854
+ Include full logback-spring.xml sample:
1855
+
1856
+ ```xml
1857
+ <configuration>
1858
+ <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
1859
+ <property name="LOG_PATTERN"
1860
+ value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{userId}] %-5level %logger{36} - %msg%n"/>
1861
+ <springProfile name="dev">
1862
+ <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
1863
+ <encoder><pattern>${LOG_PATTERN}</pattern></encoder>
1864
+ </appender>
1865
+ <root level="INFO"><appender-ref ref="CONSOLE"/></root>
1866
+ <logger name="{{BASE_PACKAGE}}" level="DEBUG"/>
1867
+ </springProfile>
1868
+ <springProfile name="prod">
1869
+ <root level="WARN"/>
1870
+ <logger name="{{BASE_PACKAGE}}" level="INFO"/>
1871
+ </springProfile>
1872
+ </configuration>
1873
+ ```
1874
+
1875
+ ---
1876
+
1877
+ ## Section 17: Scheduling and Batch Processing [CONDITIONAL — Include only if Scheduling = yes]
1878
+
1879
+ Refer to `references/batch-patterns.md` for the full content.
1880
+
1881
+ This section has three tiers of functionality:
1882
+
1883
+ ### Tier 1 — Quartz Only (Scheduling = yes, Spring Batch = no)
1884
+ Quartz scheduler manages cron-triggered jobs that execute business logic directly
1885
+ (service calls, API calls, file operations). No reader/processor/writer pipeline.
1886
+
1887
+ Adapt the Quartz job store configuration based on the selected database:
1888
+ - **MongoDB**: Use `fluidsonic-mirror-quartz` for MongoDB-backed job store
1889
+ - **PostgreSQL/MySQL**: Use built-in Quartz JDBC job store
1890
+ - **none**: Use in-memory job store
1891
+
1892
+ ### Tier 2 — Quartz + Spring Batch (Scheduling = yes, Spring Batch = yes)
1893
+ **[If Spring Batch = yes]:**
1894
+
1895
+ Quartz triggers Spring Batch jobs with full reader/processor/writer pipelines for
1896
+ ETL-style processing. Include all Spring Batch configuration, item reader, item
1897
+ processor, item writer, and batch YAML config from `references/batch-patterns.md`.
1898
+
1899
+ ### Tier 3 — Remote Partitioning (Scheduling = yes, Spring Batch = yes, Remote Partitioning = yes)
1900
+ **[If Spring Batch = yes AND Remote Partitioning = yes]:**
1901
+
1902
+ Also include the remote partitioning content from `references/batch-patterns.md`
1903
+ (section "Remote Partitioning with RabbitMQ"). This adds:
1904
+ - RabbitMQ infrastructure configuration (exchange, request queue, reply queue, DLQ)
1905
+ - `SamplePartitioner` — splits data into ranges for parallel processing
1906
+ - Manager step using `RemotePartitioningManagerStepBuilderFactory`
1907
+ - Worker step using `RemotePartitioningWorkerStepBuilderFactory`
1908
+ - Spring Integration channels and AMQP adapters
1909
+ - `PartitionedQuartzJob` — Quartz job that launches the partitioned batch job
1910
+ - `@EnableBatchIntegration` configuration
1911
+ - Worker deployment notes (same JAR, `--spring.profiles.active=worker`)
1912
+ - Failure/retry semantics and DLQ monitoring
1913
+
1914
+ **Note:** If Database = none and Remote Partitioning = yes, the spec must include a
1915
+ prominent warning that this is an invalid combination. Remote partitioning requires a
1916
+ shared `JobRepository` database accessible by all manager and worker nodes.
1917
+
1918
+ ---
1919
+
1920
+ ## Section 18: Event-Driven Architecture
1921
+
1922
+ Refer to `references/modulith-patterns.md` for the full content. Same patterns apply
1923
+ regardless of database or auth selection.
1924
+
1925
+ ---
1926
+
1927
+ ## Section 19: Messaging (RabbitMQ Pub/Sub) [CONDITIONAL — Include only if Messaging = yes]
1928
+
1929
+ Refer to `references/messaging-patterns.md` for the full content.
1930
+
1931
+ Provides standalone RabbitMQ pub/sub messaging for inter-system communication:
1932
+ - Topic exchange for broadcasting domain events to external systems
1933
+ - Direct exchange for point-to-point command delivery
1934
+ - JSON message serialization via Jackson2JsonMessageConverter
1935
+ - Dead-letter exchange with DLQ for failed message handling
1936
+ - RabbitMQPublisher service for sending messages with routing keys
1937
+ - @RabbitListener consumers with error handling and retry
1938
+ - Separate from Spring Modulith internal events (those remain in-process)
1939
+
1940
+ **Note:** If Remote Partitioning = yes is also selected, the `spring.rabbitmq.*`
1941
+ connection config is shared. Only include the connection block once in the YAML.
1942
+ The batch partition queues (`batch.partition.*`) and messaging queues (`app.*`)
1943
+ use different exchanges and do not conflict.
1944
+
1945
+ ---
1946
+
1947
+ ## Section 20: MapStruct Usage
1948
+
1949
+ Describe per-module MapStruct mappers for transforming between data models and DTOs.
1950
+ Mapping flows adapt based on database selection:
1951
+
1952
+ - **MongoDB**: `Document -> DTO` (via mapper) and `DTO -> ViewModel` (via constructor or mapper)
1953
+ - **PostgreSQL/MySQL**: `Entity -> DTO` (via mapper) and `DTO -> ViewModel` (via constructor or mapper)
1954
+ - **none**: Direct DTO construction
1955
+
1956
+ ---
1957
+
1958
+ ## Section 21: Testing Strategy
1959
+
1960
+ - **Unit tests**: Mockito for service layer
1961
+ - **Module tests**: `@ApplicationModuleTest` for module boundary verification
1962
+ - **View model tests**: Verify view model construction and pagination
1963
+ - **[If Auth = Keycloak]** Security tests: Custom JWT test utilities for Keycloak token mocking
1964
+ - **[If Auth = form]** Security tests: `@WithMockUser` for testing secured endpoints
1965
+ - **Architecture tests**: `ApplicationModules.of(Application.class).verify()`
1966
+
1967
+ ---
1968
+
1969
+ ## Section 22: Reporting (Jasper Reports) [CONDITIONAL — Include only if Reporting = yes]
1970
+
1971
+ Refer to `references/jasper-patterns.md` for the full content.
1972
+
1973
+ Provides a JasperReports-based reporting infrastructure with DTO data sources:
1974
+
1975
+ ### Architecture
1976
+
1977
+ - **`ReportDefinition` interface** — modules implement this to define their reports.
1978
+ Each implementation is a `@Component` auto-discovered by the `ReportRegistry`.
1979
+ Includes `buildDesign()` method that returns a `JasperDesign` built via JRDesign API.
1980
+ - **`ReportDesignHelper`** — static utility class with builders for common report patterns
1981
+ (A4 portrait/landscape, title band, column headers, detail band, page footer)
1982
+ - **`ReportService`** — compiles `JasperDesign` from `buildDesign()` via `JasperCompileManager`,
1983
+ fills with data via `JRBeanCollectionDataSource` from DTO collections, and exports to PDF/XLSX/CSV
1984
+ - **`ReportRegistry`** — auto-discovers all `ReportDefinition` beans at startup and persists
1985
+ their metadata (name, description, module) to the database
1986
+ - **`ReportPageController`** — serves the report list page (`GET /reports`), parameter form
1987
+ (`GET /reports/{id}`), and generate/download endpoint (`POST /reports/{id}/generate`)
1988
+ - **JTE templates** — report list page (grouped by module) and parameter form page with
1989
+ dynamic fields based on `ReportParameter` descriptors
1990
+
1991
+ ### Data Flow
1992
+
1993
+ 1. Module defines a `@Component` implementing `ReportDefinition`
1994
+ 2. At startup, `ReportRegistry` discovers it and persists to the `report` table
1995
+ 3. User selects report → parameter form rendered from `ReportParameter` descriptors
1996
+ 4. User submits → `ReportDefinition.generateData(params)` calls module services to produce `List<DTO>`
1997
+ 5. `ReportService` wraps DTOs in `JRBeanCollectionDataSource`, compiles `JasperDesign` + fills + exports
1998
+ 6. File returned as download (PDF, XLSX, or CSV)
1999
+
2000
+ ### Key Constraint: DTO as Data Source (no SQL in templates)
2001
+
2002
+ Report definitions call **module services** (never repositories) to produce DTO collections.
2003
+ Report layouts are built programmatically via JRDesign API — no `.jrxml` templates, no SQL
2004
+ in reports. All data comes via `JRBeanCollectionDataSource`. This preserves Spring Modulith
2005
+ module boundaries and enables unit testing of report data generation independently from Jasper.
2006
+
2007
+ DTO field names must use **camelCase** and match `JRDesignField` names declared in `buildDesign()`
2008
+ exactly (JavaBean getter convention). Nested objects should be flattened into the report DTO
2009
+ (e.g., `staff.department.name` → `departmentName`).
2010
+
2011
+ ### Maven Dependencies
2012
+
2013
+ ```xml
2014
+ <!-- Reporting (included when Reporting = yes) -->
2015
+ <dependency>
2016
+ <groupId>net.sf.jasperreports</groupId>
2017
+ <artifactId>jasperreports</artifactId>
2018
+ <version>7.0.3</version>
2019
+ </dependency>
2020
+ <dependency>
2021
+ <groupId>net.sf.jasperreports</groupId>
2022
+ <artifactId>jasperreports-fonts</artifactId>
2023
+ <version>7.0.3</version>
2024
+ </dependency>
2025
+ <dependency>
2026
+ <groupId>com.github.librepdf</groupId>
2027
+ <artifactId>openpdf</artifactId>
2028
+ <version>2.0.4</version>
2029
+ </dependency>
2030
+ <dependency>
2031
+ <groupId>org.apache.poi</groupId>
2032
+ <artifactId>poi-ooxml</artifactId>
2033
+ <version>5.4.1</version>
2034
+ </dependency>
2035
+ ```
2036
+
2037
+ ### Application Configuration
2038
+
2039
+ ```yaml
2040
+ app:
2041
+ reporting:
2042
+ temp-path: ${java.io.tmpdir}/reports
2043
+ default-format: PDF
2044
+ ```
2045
+
2046
+ ### Package Structure
2047
+
2048
+ ```
2049
+ shared/
2050
+ └── reporting/
2051
+ ├── ReportDefinition.java
2052
+ ├── ReportParameter.java
2053
+ ├── ReportFormat.java
2054
+ ├── ReportResult.java
2055
+ ├── ReportDesignHelper.java
2056
+ ├── ReportService.java
2057
+ ├── ReportRegistry.java
2058
+ ├── ReportConfig.java
2059
+ ├── ReportGenerationException.java
2060
+ ├── ReportEntity.java / ReportDocument.java
2061
+ ├── ReportRepository.java
2062
+ ├── page/ReportPageController.java
2063
+ ├── fragment/ReportFragmentController.java
2064
+ └── view/ReportListView.java, ReportGenerateView.java
2065
+ ```
2066
+
2067
+ ### Database Table/Collection
2068
+
2069
+ **[If PostgreSQL/MySQL]:**
2070
+ ```sql
2071
+ CREATE TABLE report (
2072
+ report_id VARCHAR(100) PRIMARY KEY,
2073
+ name VARCHAR(255) NOT NULL,
2074
+ description VARCHAR(500),
2075
+ domain VARCHAR(100) NOT NULL,
2076
+ active BOOLEAN NOT NULL DEFAULT TRUE
2077
+ );
2078
+ ```
2079
+
2080
+ **[If MongoDB]:**
2081
+ Collection `report` with fields: `reportId`, `name`, `description`, `module`, `active`.
2082
+
2083
+ ### Module Integration Pattern
2084
+
2085
+ Each module registers reports by creating `@Component` classes implementing `ReportDefinition`.
2086
+ Report layouts are built programmatically using JRDesign API — no `.jrxml` template files:
2087
+
2088
+ ```java
2089
+ @Component
2090
+ @RequiredArgsConstructor
2091
+ public class StaffAllocationSummaryReport implements ReportDefinition {
2092
+
2093
+ private final StaffService staffService;
2094
+
2095
+ @Override public String getReportId() { return "staff-allocation-summary"; }
2096
+ @Override public String getName() { return "Staff Allocation Summary"; }
2097
+ @Override public String getDescription() { return "Lists all staff with allocation status."; }
2098
+ @Override public String getDomain() { return "Staff"; }
2099
+
2100
+ @Override
2101
+ public List<ReportParameter> getParameters() {
2102
+ return List.of(
2103
+ ReportParameter.select("departmentId", "Department", false, List.of(/* options */)),
2104
+ ReportParameter.select("allocationStatus", "Status", false, List.of(/* options */))
2105
+ );
2106
+ }
2107
+
2108
+ @Override
2109
+ public JasperDesign buildDesign(Map<String, Object> parameters) {
2110
+ var columns = List.of(
2111
+ new ReportDesignHelper.ColumnDef("Staff No.", "staffNumber", 80),
2112
+ new ReportDesignHelper.ColumnDef("Full Name", "fullName", 150),
2113
+ new ReportDesignHelper.ColumnDef("Department", "departmentName", 130),
2114
+ new ReportDesignHelper.ColumnDef("Status", "allocationStatus", 80)
2115
+ );
2116
+
2117
+ JasperDesign design = ReportDesignHelper.createA4LandscapeDesign("staff-allocation-summary");
2118
+ ReportDesignHelper.addField(design, "staffNumber", String.class);
2119
+ ReportDesignHelper.addField(design, "fullName", String.class);
2120
+ ReportDesignHelper.addField(design, "departmentName", String.class);
2121
+ ReportDesignHelper.addField(design, "allocationStatus", String.class);
2122
+ design.setTitle(ReportDesignHelper.createTitleBand("Staff Allocation Summary", 50));
2123
+ design.setColumnHeader(ReportDesignHelper.createColumnHeaderBand(columns));
2124
+ design.getDetailSection().getBands()[0] = ReportDesignHelper.createDetailBand(columns);
2125
+ design.setPageFooter(ReportDesignHelper.createPageFooterBand());
2126
+ return design;
2127
+ }
2128
+
2129
+ @Override
2130
+ public Collection<?> generateData(Map<String, Object> parameters) {
2131
+ return staffService.getStaffForReport(/* extract params */);
2132
+ }
2133
+ }
2134
+ ```
2135
+
2136
+ The report DTO (e.g., `StaffReportDTO`) is a flat bean whose field names map 1:1 to the
2137
+ `JRDesignField` names declared in `buildDesign()`. See `references/jasper-patterns.md`
2138
+ for the complete `ReportDefinition` interface, `ReportDesignHelper`, `ReportService`,
2139
+ `ReportRegistry`, `ReportPageController`, view models, and JTE templates.
2140
+
2141
+ ---
2142
+
2143
+ ## Section 23: Internationalisation (i18n) [CONDITIONAL — Include only if i18n = yes]
2144
+
2145
+ > Omit this entire section when i18n = no. When omitted, do NOT render a locale/language
2146
+ > switcher anywhere — JTE templates use literal copy.
2147
+
2148
+ Uses Spring's **built-in `MessageSource`** — no third-party dependency. The active locale is
2149
+ resolved per request from a cookie (switchable via a `lang` request param), and message keys
2150
+ are exposed to JTE templates through a `Localizer` helper registered as a global model
2151
+ attribute (mirroring how `appVersion` is exposed for the footer).
2152
+
2153
+ ### 23.1 `app.i18n` Configuration (`application.yml`)
2154
+
2155
+ Application-owned i18n config lives under the `app:` namespace (see Section 3a). Spring's own
2156
+ message-bundle settings live under `spring.messages`.
2157
+
2158
+ ```yaml
2159
+ spring:
2160
+ messages:
2161
+ basename: i18n/messages
2162
+ encoding: UTF-8
2163
+ fallback-to-system-locale: false
2164
+
2165
+ app:
2166
+ i18n:
2167
+ default-locale: ${APP_DEFAULT_LOCALE:en}
2168
+ supported-locales: ${APP_SUPPORTED_LOCALES:en,ms}
2169
+ ```
2170
+
2171
+ `.env` additions:
2172
+
2173
+ ```properties
2174
+ APP_DEFAULT_LOCALE=en
2175
+ APP_SUPPORTED_LOCALES=en,ms
2176
+ ```
2177
+
2178
+ Bind once via a record in the application-level `config` subpackage:
2179
+
2180
+ ```java
2181
+ @ConfigurationProperties(prefix = "app.i18n")
2182
+ public record I18nProperties(String defaultLocale, List<String> supportedLocales) {
2183
+ public List<Locale> locales() {
2184
+ return supportedLocales.stream().map(Locale::forLanguageTag).toList();
2185
+ }
2186
+ public Locale defaultLocaleAsLocale() {
2187
+ return Locale.forLanguageTag(defaultLocale);
2188
+ }
2189
+ }
2190
+ ```
2191
+
2192
+ ### 23.2 I18n Configuration (`shared/config/I18nConfig.java`)
2193
+
2194
+ ```java
2195
+ @Configuration
2196
+ @RequiredArgsConstructor
2197
+ public class I18nConfig implements WebMvcConfigurer {
2198
+
2199
+ private final I18nProperties i18nProperties;
2200
+
2201
+ @Bean
2202
+ public MessageSource messageSource() {
2203
+ ResourceBundleMessageSource source = new ResourceBundleMessageSource();
2204
+ source.setBasename("i18n/messages");
2205
+ source.setDefaultEncoding("UTF-8");
2206
+ source.setFallbackToSystemLocale(false);
2207
+ return source;
2208
+ }
2209
+
2210
+ @Bean
2211
+ public LocaleResolver localeResolver() {
2212
+ CookieLocaleResolver resolver = new CookieLocaleResolver("LOCALE");
2213
+ resolver.setDefaultLocale(i18nProperties.defaultLocaleAsLocale());
2214
+ resolver.setCookieMaxAge(Duration.ofDays(365));
2215
+ return resolver;
2216
+ }
2217
+
2218
+ @Bean
2219
+ public LocaleChangeInterceptor localeChangeInterceptor() {
2220
+ LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
2221
+ interceptor.setParamName("lang"); // /any/path?lang=ms
2222
+ return interceptor;
2223
+ }
2224
+
2225
+ @Override
2226
+ public void addInterceptors(InterceptorRegistry registry) {
2227
+ registry.addInterceptor(localeChangeInterceptor());
2228
+ }
2229
+
2230
+ // Wire bean-validation messages to the same bundle so @NotBlank etc. are localized.
2231
+ @Bean
2232
+ public LocalValidatorFactoryBean getValidator(MessageSource messageSource) {
2233
+ LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
2234
+ bean.setValidationMessageSource(messageSource);
2235
+ return bean;
2236
+ }
2237
+ }
2238
+ ```
2239
+
2240
+ ### 23.3 Localizer Helper + Global Model Attribute
2241
+
2242
+ JTE has no native i18n, so a `Localizer` resolves keys against the current request locale and
2243
+ is exposed to every template as `${t}` (alongside `${appVersion}`).
2244
+
2245
+ ```java
2246
+ // shared/i18n/Localizer.java
2247
+ @Component
2248
+ @RequiredArgsConstructor
2249
+ public class Localizer {
2250
+ private final MessageSource messageSource;
2251
+
2252
+ public String msg(String code, Object... args) {
2253
+ return messageSource.getMessage(code, args, code, LocaleContextHolder.getLocale());
2254
+ }
2255
+ }
2256
+ ```
2257
+
2258
+ ```java
2259
+ // shared/web/GlobalModelAttributes.java
2260
+ @ControllerAdvice
2261
+ @RequiredArgsConstructor
2262
+ public class GlobalModelAttributes {
2263
+ private final AppProperties appProperties;
2264
+ private final Localizer localizer;
2265
+ private final I18nProperties i18nProperties;
2266
+
2267
+ @ModelAttribute("appVersion")
2268
+ public String appVersion() { return appProperties.version(); }
2269
+
2270
+ @ModelAttribute("t")
2271
+ public Localizer localizer() { return localizer; }
2272
+
2273
+ @ModelAttribute("currentLocale")
2274
+ public String currentLocale() { return LocaleContextHolder.getLocale().toLanguageTag(); }
2275
+
2276
+ @ModelAttribute("supportedLocales")
2277
+ public List<String> supportedLocales() { return i18nProperties.supportedLocales(); }
2278
+ }
2279
+ ```
2280
+
2281
+ ### 23.4 Resource Bundles (`src/main/resources/i18n/`)
2282
+
2283
+ Default bundle (`messages.properties`) holds English; one `messages_<locale>.properties` per
2284
+ additional locale. Keys are namespaced by module slug.
2285
+
2286
+ ```properties
2287
+ # i18n/messages.properties (default / en)
2288
+ app.title=Hub Middleware
2289
+ common.create=Create
2290
+ common.save=Save
2291
+ common.cancel=Cancel
2292
+ location-information.title=Location Information
2293
+ location-information.create=Create Location
2294
+
2295
+ # i18n/messages_ms.properties
2296
+ app.title=Hub Middleware
2297
+ common.create=Cipta
2298
+ common.save=Simpan
2299
+ common.cancel=Batal
2300
+ location-information.title=Maklumat Lokasi
2301
+ location-information.create=Cipta Lokasi
2302
+ ```
2303
+
2304
+ ### 23.5 Language Switcher Fragment (`fragments/locale-switcher.jte`)
2305
+
2306
+ ```jte
2307
+ @import {{base}}.shared.i18n.Localizer
2308
+ @param Localizer t
2309
+ @param String currentLocale
2310
+ @param java.util.List<String> supportedLocales
2311
+
2312
+ <div x-data="{ open: false }" class="relative">
2313
+ <button @click="open = !open" type="button"
2314
+ class="inline-flex items-center gap-1 rounded-md px-2 py-1 text-sm text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
2315
+ <span>${currentLocale.toUpperCase()}</span>
2316
+ </button>
2317
+ <div x-show="open" @click.outside="open = false" x-cloak
2318
+ class="absolute right-0 mt-1 w-40 rounded-md border border-gray-200 bg-white py-1 shadow-lg dark:border-gray-700 dark:bg-gray-800">
2319
+ @for(String loc : supportedLocales)
2320
+ <a hx-get="?lang=${loc}" hx-on::after-request="window.location.reload()"
2321
+ href="?lang=${loc}"
2322
+ class="block px-3 py-1.5 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 ${loc.equals(currentLocale) ? "font-semibold text-primary-600" : "text-gray-700 dark:text-gray-200"}">
2323
+ ${loc.toUpperCase()}
2324
+ </a>
2325
+ @endfor
2326
+ </div>
2327
+ </div>
2328
+ ```
2329
+
2330
+ The `lang` param is consumed by `LocaleChangeInterceptor`, which asks `CookieLocaleResolver`
2331
+ to persist the choice in the `LOCALE` cookie. Include this fragment in `HeaderFragment`
2332
+ beside the theme switcher. **Only when i18n = yes.**
2333
+
2334
+ ### 23.6 JTE Usage Conventions
2335
+
2336
+ ```jte
2337
+ <h1>${t.msg("location-information.title")}</h1>
2338
+ <button>${t.msg("common.create")}</button>
2339
+ ```
2340
+
2341
+ > **Per-module keys.** Each module adds its display strings to every
2342
+ > `messages_<locale>.properties` bundle under its own slug namespace
2343
+ > (e.g., `corridor.title`, `corridor.empty-state`) and references them via
2344
+ > `${t.msg("corridor.title")}`. See the per-module SPEC.md template.
2345
+
2346
+ ---
2347
+
2348
+ # Part B: Module SPEC.md Template
2349
+
2350
+ Everything below is the template for each **`<module>/SPEC.md`** file. Generate one
2351
+ file per module from PRD.md and MODEL.md. Each file is placed in its own
2352
+ folder named after the module (kebab-case): `spec/<module-name>/SPEC.md`.
2353
+
2354
+ Each module SPEC.md is **self-contained** — a coding agent picks it up and implements
2355
+ the entire module independently (after the shared infrastructure from
2356
+ `SPECIFICATION.md` is in place).
2357
+
2358
+ **File:** `spec/{{module-name}}/SPEC.md`
2359
+
2360
+ ---
2361
+
2362
+ ## Module SPEC.md Structure
2363
+
2364
+ Generate each module SPEC.md with the following structure:
2365
+
2366
+ ```markdown
2367
+ # {{ModuleName}} Module — Module Specification
2368
+
2369
+ > Part of [{{APPLICATION_NAME}} Technical Specification](../SPECIFICATION.md)
2370
+
2371
+ ## Overview
2372
+
2373
+ **Module:** {{ModuleName}}
2374
+ **Package:** {{BASE_PACKAGE}}.{{modulename}}
2375
+ **Type:** {{System Module | Business Module}}
2376
+ ```
2377
+
2378
+ ### Traceability
2379
+
2380
+ List all tagged IDs this module implements, organized by type with version tags:
2381
+
2382
+ ```markdown
2383
+ ## Traceability
2384
+
2385
+ ### User Stories
2386
+ | ID | Version | Description |
2387
+ |---|---|---|
2388
+ | USHM00012 | v1.0.0 | View list of countries |
2389
+ | USHM00222 | v1.0.3 | Manage provinces/states |
2390
+
2391
+ ### Non-Functional Requirements
2392
+ | ID | Version | Description |
2393
+ |---|---|---|
2394
+ | NFRHM0006 | v1.0.0 | Country code is unique identifier |
2395
+
2396
+ ### Constraints
2397
+ | ID | Version | Description |
2398
+ |---|---|---|
2399
+ | CONSHM003 | v1.0.0 | Only supported country types |
2400
+
2401
+ ### Removed / Replaced
2402
+ | ID | Type | Removed In | Replaced By | Reason |
2403
+ |---|---|---|---|---|
2404
+ | USHM00015 | User Story | v1.0.3 | USHM00222 | Reimplemented with expanded scope |
2405
+ | NFRHM0039 | NFR | v1.0.3 | NFRHM0198 | Source changed from JD to Quota messages |
2406
+
2407
+ ### Collections
2408
+ (flat list — unchanged)
2409
+
2410
+ ### Mockup Screens
2411
+ | Screen | Version |
2412
+ |---|---|
2413
+ | location_information.html | v1.0.0 |
2414
+ | location_information_province_create.html | v1.0.3 |
2415
+ ```
2416
+
2417
+ ### Public API
2418
+
2419
+ Include complete code samples for:
2420
+
2421
+ - **Service Interface** — `{{ModuleName}}Service.java`
2422
+ Methods derived from user stories (e.g., "view list" → `list(PaginationRequest)`)
2423
+ - **DTOs** — `{{ModuleName}}DTO.java`, `{{ModuleName}}CreateDTO.java`, `{{ModuleName}}UpdateDTO.java`
2424
+ Fields from `model/<module>/model.md` and `model/<module>/schemas.json`
2425
+ - **Exception** — `{{ModuleName}}Exception.java`
2426
+ Module-specific exception with NotFound inner class
2427
+
2428
+ ### Internal Implementation
2429
+
2430
+ Include complete code samples for:
2431
+
2432
+ - **[If MongoDB] Document** — `{{ModuleName}}Document.java` with `@Document(collection = "{{actual_collection_name}}")`
2433
+ - **[If PostgreSQL/MySQL] Entity** — `{{ModuleName}}Entity.java` with `@Entity @Table(name = "{{actual_table_name}}")`
2434
+ - **Repository** — `{{ModuleName}}Repository.java` with query methods from user stories
2435
+ - **Mapper** — `{{ModuleName}}Mapper.java` with `@Mapper(componentModel = "spring")`
2436
+ - **Service Implementation** — `{{ModuleName}}ServiceImpl.java` with full CRUD logic
2437
+ - **Page Controller** — `{{ModuleName}}PageController.java` with endpoints matching mockup screens
2438
+ - **Fragment Controller** — `{{ModuleName}}FragmentController.java` for htmx partial updates
2439
+ - **View Models** — `{{ModuleName}}ListView.java`, `{{ModuleName}}DetailView.java` matching mockup data
2440
+
2441
+ ### JTE Templates
2442
+
2443
+ **CRITICAL — JTE syntax rules for all module templates:**
2444
+ 1. Use `@template.{{BASE_PACKAGE_PATH_DOT}}.path.to.Template(param = value)` to call other templates — NEVER `@include()`
2445
+ 2. Page templates wrap content in `content = @`...`)` when calling MainLayout — NEVER use `@insert`/`@endtemplate`
2446
+ 3. Every template must have `@import` statements for all referenced classes
2447
+ 4. Fragment templates do NOT wrap in MainLayout — they render partial HTML for htmx swaps
2448
+ 5. Use `@param` for all template parameters, including `@param Content content` when accepting child content (import `gg.jte.Content`)
2449
+
2450
+ Include complete JTE templates for:
2451
+
2452
+ - **`page/{{Entity}}ListPage.jte`** — List page wrapping MainLayout with `content = @`...``, containing a div with id for htmx target, calling the list fragment
2453
+ - **`page/{{Entity}}DetailPage.jte`** — Detail view wrapping MainLayout
2454
+ - **`page/{{Entity}}FormPage.jte`** — Create/edit form wrapping MainLayout (single template, mode-aware via `view.isEditMode()`)
2455
+ - **`page/{{Entity}}ListFragment.jte`** — List table fragment (no layout wrapper) for htmx partial updates, includes pagination component
2456
+ - Additional fragments as needed for tabs, sub-sections, or repeating rows
2457
+
2458
+ ### Critical Rules for Module SPEC.md
2459
+
2460
+ **CRITICAL: Use real module data.** Every module SPEC.md must:
2461
+ 1. Use actual field names from `model/<module>/model.md` and `model/<module>/schemas.json`
2462
+ 2. Use actual collection/table names from the module model
2463
+ 3. Define service methods that map to the actual user stories (by tag ID)
2464
+ 4. Define page controllers that map to actual mockup screens
2465
+ 5. List the user story IDs, NFR IDs, and constraint IDs that this module implements
2466
+ 6. Include complete, continuous code samples — no "// ..." gaps
2467
+ 7. Follow all constraints from the root `SPECIFICATION.md` (constructor injection,
2468
+ `@Getter @Setter` not `@Data`, no DaisyUI, MapStruct for mapping, paginated lists)
2469
+ 8. Tag every user story ID, NFR ID, constraint ID, and mockup screen with its version
2470
+ from PRD.md (e.g., `USHM00228 [v1.0.3]`)
2471
+ 9. Include a "Removed / Replaced" subsection listing deprecated items from previous
2472
+ versions — showing the removed ID, the version that removed it, the replacement ID
2473
+ (if any), and a brief reason. This ensures the spec captures the full evolution of
2474
+ requirements and what was removed from the implementation.
2475
+
2476
+ **CRITICAL: URL routing must be module-based, NOT role-prefixed.** Controller URLs
2477
+ use the module name only:
2478
+ - Page controller: `@RequestMapping("/corridor")` — NOT `@RequestMapping("/hub_administrator/corridor")`
2479
+ - Fragment controller: `@RequestMapping("/corridor/fragments")` — NOT `@RequestMapping("/api/content/hub_administrator/corridor")`
2480
+ - Sidebar nav href: `/corridor` — NOT `/hub_administrator/corridor`
2481
+ - Pagination base URL: `/corridor` — NOT `/hub_administrator/corridor`
2482
+
2483
+ The mockup role folder (e.g., `mockup/hub_administrator/content/corridor.html`) determines
2484
+ which role can access the page, expressed as `@PreAuthorize` annotations on the controller
2485
+ class or methods:
2486
+ ```java
2487
+ @Controller
2488
+ @RequestMapping("/corridor")
2489
+ @PreAuthorize("hasRole('HUB_ADMINISTRATOR')")
2490
+ @RequiredArgsConstructor
2491
+ class CorridorPageController implements PaginationAware, ThemeAware, AuthAware { ... }
2492
+ ```
2493
+
2494
+ If a module is accessible by multiple roles, use `hasAnyRole()`:
2495
+ ```java
2496
+ @PreAuthorize("hasAnyRole('HUB_ADMINISTRATOR', 'HUB_OPERATION_SUPPORT')")
2497
+ ```
2498
+
2499
+ The code sample templates for Service, DTOs, Document/Entity, Repository, Mapper,
2500
+ ServiceImpl, PageController, View Models, and JTE templates follow the same patterns
2501
+ shown in the original Section 10 code samples above (now moved to this Part B).
2502
+ Substitute actual module data from context files for every placeholder.