@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.
- package/dist/index.js +25449 -0
- package/drizzle/0000_remarkable_charles_xavier.sql +210 -0
- package/drizzle/0001_wise_wolf_cub.sql +13 -0
- package/drizzle/0002_secret_spyke.sql +16 -0
- package/drizzle/0003_workspace_config_singleton.sql +16 -0
- package/drizzle/0004_terminal_claim_v120.sql +11 -0
- package/drizzle/0005_brainstorm_v124.sql +10 -0
- package/drizzle/0006_project_v140.sql +6 -0
- package/drizzle/0007_prd_wizard_v124.sql +14 -0
- package/drizzle/0008_workflow_artifact_v126.sql +24 -0
- package/drizzle/0009_project_import_v150.sql +7 -0
- package/drizzle/0010_quality_v151.sql +3 -0
- package/drizzle/meta/0000_snapshot.json +1335 -0
- package/drizzle/meta/0001_snapshot.json +1416 -0
- package/drizzle/meta/0002_snapshot.json +1510 -0
- package/drizzle/meta/_journal.json +83 -0
- package/package.json +41 -0
- package/plugin/.claude-plugin/marketplace.json +36 -0
- package/plugin/.claude-plugin/plugin.json +22 -0
- package/plugin/README.md +625 -0
- package/plugin/SKILLS.md +350 -0
- package/plugin/co2.svg +5 -0
- package/plugin/commands/brainstorm-loop.md +129 -0
- package/plugin/skills/conductor-defect/SKILL.md +898 -0
- package/plugin/skills/conductor-feature-develop/SKILL.md +1383 -0
- package/plugin/skills/conductor-feature-develop/references/implementation-domain-template.md +131 -0
- package/plugin/skills/conductor-feature-develop/references/implementation-master-template.md +89 -0
- package/plugin/skills/conductor-feature-develop/references/playwright-setup.md +224 -0
- package/plugin/skills/conductor-feature-prepare/SKILL.md +604 -0
- package/plugin/skills/depgen-k8s/SKILL.md +468 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-laravel.md +197 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-nodejs.md +163 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-spring-boot.md +174 -0
- package/plugin/skills/depgen-k8s/references/k8s-patterns.md +869 -0
- package/plugin/skills/mockgen-shadcn/SKILL.md +1067 -0
- package/plugin/skills/mockgen-shadcn/references/admin-layout-template.md +1467 -0
- package/plugin/skills/mockgen-shadcn/references/mockup-hub-template.md +498 -0
- package/plugin/skills/mockgen-shadcn/references/mockup-index-template.md +216 -0
- package/plugin/skills/mockgen-tailwind/SKILL.md +904 -0
- package/plugin/skills/mockgen-tailwind/references/admin-layout-template.md +720 -0
- package/plugin/skills/mockgen-tailwind/references/mockup-hub-template.md +498 -0
- package/plugin/skills/mockgen-tailwind/references/mockup-index-template.md +190 -0
- package/plugin/skills/modelgen-nosql/SKILL.md +617 -0
- package/plugin/skills/modelgen-nosql/references/model-extraction-methodology.md +497 -0
- package/plugin/skills/modelgen-nosql/references/nosql-design-guide.md +258 -0
- package/plugin/skills/modelgen-relational/SKILL.md +707 -0
- package/plugin/skills/modelgen-relational/references/model-extraction-methodology.md +499 -0
- package/plugin/skills/specgen-flutter-riverpod/SKILL.md +898 -0
- package/plugin/skills/specgen-flutter-riverpod/references/component-patterns.md +971 -0
- package/plugin/skills/specgen-flutter-riverpod/references/notification-patterns.md +457 -0
- package/plugin/skills/specgen-flutter-riverpod/references/routing-patterns.md +362 -0
- package/plugin/skills/specgen-flutter-riverpod/references/security-patterns.md +709 -0
- package/plugin/skills/specgen-flutter-riverpod/references/spec-template.md +1566 -0
- package/plugin/skills/specgen-flutter-riverpod/references/state-patterns.md +518 -0
- package/plugin/skills/specgen-flutter-riverpod/references/storage-patterns.md +370 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/SKILL.md +984 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/batch-patterns.md +359 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/messaging-patterns.md +468 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/modulith-patterns.md +461 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/reporting-patterns.md +803 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/security-patterns.md +501 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/spec-template.md +1268 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/web-patterns.md +301 -0
- package/plugin/skills/specgen-node-cli-web/SKILL.md +561 -0
- package/plugin/skills/specgen-node-cli-web/references/auth-patterns.md +293 -0
- package/plugin/skills/specgen-node-cli-web/references/cli-packaging-patterns.md +250 -0
- package/plugin/skills/specgen-node-cli-web/references/server-patterns.md +247 -0
- package/plugin/skills/specgen-node-cli-web/references/spec-template.md +401 -0
- package/plugin/skills/specgen-node-cli-web/references/testing-patterns.md +174 -0
- package/plugin/skills/specgen-react-mui/SKILL.md +789 -0
- package/plugin/skills/specgen-react-mui/references/component-patterns.md +1077 -0
- package/plugin/skills/specgen-react-mui/references/reporting-patterns.md +1998 -0
- package/plugin/skills/specgen-react-mui/references/routing-patterns.md +461 -0
- package/plugin/skills/specgen-react-mui/references/security-patterns.md +599 -0
- package/plugin/skills/specgen-react-mui/references/spec-template.md +1030 -0
- package/plugin/skills/specgen-react-mui/references/state-patterns.md +327 -0
- package/plugin/skills/specgen-react-tailwind/SKILL.md +859 -0
- package/plugin/skills/specgen-react-tailwind/references/component-patterns.md +1212 -0
- package/plugin/skills/specgen-react-tailwind/references/reporting-patterns.md +1529 -0
- package/plugin/skills/specgen-react-tailwind/references/routing-patterns.md +669 -0
- package/plugin/skills/specgen-react-tailwind/references/security-patterns.md +656 -0
- package/plugin/skills/specgen-react-tailwind/references/spec-template.md +1237 -0
- package/plugin/skills/specgen-react-tailwind/references/state-patterns.md +327 -0
- package/plugin/skills/specgen-sdk-java/SKILL.md +933 -0
- package/plugin/skills/specgen-sdk-java/references/api-extraction-patterns.md +247 -0
- package/plugin/skills/specgen-sdk-java/references/http-client-patterns.md +495 -0
- package/plugin/skills/specgen-sdk-java/references/packaging-patterns.md +366 -0
- package/plugin/skills/specgen-sdk-java/references/spec-template.md +956 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/SKILL.md +1284 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/batch-patterns.md +1224 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/jasper-patterns.md +1638 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/messaging-patterns.md +512 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/modulith-patterns.md +354 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/security-patterns.md +551 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/spec-template.md +2502 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/web-patterns.md +503 -0
- package/plugin/skills/specgen-spring-jpa-restapi/SKILL.md +990 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/batch-patterns.md +1224 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/messaging-patterns.md +512 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/modulith-patterns.md +279 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/reporting-patterns.md +1536 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/restapi-patterns.md +722 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/security-patterns.md +351 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/spec-template.md +553 -0
- package/plugin/skills/specgen-ts-cli/SKILL.md +861 -0
- package/plugin/skills/specgen-ts-cli/references/async-patterns.md +1101 -0
- package/plugin/skills/specgen-ts-cli/references/command-patterns.md +383 -0
- package/plugin/skills/specgen-ts-cli/references/config-patterns.md +430 -0
- package/plugin/skills/specgen-ts-cli/references/database-patterns.md +750 -0
- package/plugin/skills/specgen-ts-cli/references/packaging-patterns.md +426 -0
- package/plugin/skills/specgen-ts-cli/references/spec-template.md +1391 -0
- package/plugin/skills/specgen-ts-cli/references/testing-patterns.md +909 -0
- package/plugin/skills/testgen-functional/SKILL.md +1381 -0
- package/plugin/skills/tracegen-matrix/SKILL.md +243 -0
- package/plugin/skills/util-preparek8senv/SKILL.md +422 -0
- package/plugin/skills/util-projectinit/SKILL.md +307 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-loop.md +129 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-protocol.md +144 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-setup-guide.md +153 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-template-appdev.md +87 -0
- package/plugin/skills/util-projectinit/reference/co2-context-generation-guide.md +761 -0
- package/plugin/skills/util-projectsync/SKILL.md +501 -0
- package/plugin/skills/util-updprd/SKILL.md +324 -0
- package/plugin/skills/util-usanalyzer/SKILL.md +307 -0
- package/plugin/skills/util-ustagger/SKILL.md +154 -0
- package/static/assets/abnfDiagram-VRR7QNED-DvPuwyCV.js +1 -0
- package/static/assets/arc-DRKw7sjs.js +1 -0
- package/static/assets/architectureDiagram-ZJ3FMSHR-CaNcxUbC.js +36 -0
- package/static/assets/blockDiagram-677ZJIJ3-Demd4AOK.js +132 -0
- package/static/assets/c4Diagram-LMCZKHZV-PVKUfc2x.js +10 -0
- package/static/assets/channel-C2nVbuwv.js +1 -0
- package/static/assets/chunk-2Q5K7J3B-CExf8ww_.js +1 -0
- package/static/assets/chunk-32BRIVSS-CFPaoONF.js +1 -0
- package/static/assets/chunk-5VM5RSS4-DE2esM67.js +15 -0
- package/static/assets/chunk-EX3LRPZG-SON_YdDG.js +231 -0
- package/static/assets/chunk-JWPE2WC7-Qxah8LNr.js +1 -0
- package/static/assets/chunk-MOJQB5TN-BcWGQV3j.js +88 -0
- package/static/assets/chunk-RYQCIY6F-DkDj-OR5.js +1 -0
- package/static/assets/chunk-V7JOEXUC-BlI1Mrah.js +206 -0
- package/static/assets/chunk-VR4S4FIN-Agf__duj.js +1 -0
- package/static/assets/chunk-XXDRQBXY-B5CfAh85.js +1 -0
- package/static/assets/classDiagram-OUVF2IWQ-DXJIljsR.js +1 -0
- package/static/assets/classDiagram-v2-EOCWNBFH-DXJIljsR.js +1 -0
- package/static/assets/cose-bilkent-JH36ORCC-DO0cQkcy.js +1 -0
- package/static/assets/cynefin-VYW2F7L2-CfOnsnXc.js +178 -0
- package/static/assets/cynefinDiagram-TSTJHNR4-BJ8tULAv.js +62 -0
- package/static/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
- package/static/assets/dagre-VKFMJZFB-DfjwzHqP.js +4 -0
- package/static/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/static/assets/diagram-FQU43EPY-CndSOIIM.js +3 -0
- package/static/assets/diagram-G47NLZAW-Czf5hN0Z.js +24 -0
- package/static/assets/diagram-NH7WQ7WH-COAyojGh.js +24 -0
- package/static/assets/diagram-OA4YK3LP-H8PoiN2m.js +30 -0
- package/static/assets/diagram-WEI45ONY-DzcaVzjp.js +41 -0
- package/static/assets/ebnfDiagram-CCIWWBDH-DPazGYLJ.js +1 -0
- package/static/assets/erDiagram-Q63AITRT-Bo-fubgF.js +85 -0
- package/static/assets/flowDiagram-23GEKE2U-CSvxUDwh.js +156 -0
- package/static/assets/ganttDiagram-NO4QXBWP-DnBveWX6.js +292 -0
- package/static/assets/gitGraphDiagram-IHSO6WYX-B3rVL8c-.js +106 -0
- package/static/assets/graph-C9eacEi8.js +1 -0
- package/static/assets/index-LZSQkLE2.css +41 -0
- package/static/assets/index-y7XDSgMG.js +495 -0
- package/static/assets/infoDiagram-FWYZ7A6U-CvWV9yy2.js +2 -0
- package/static/assets/init-Gi6I4Gst.js +1 -0
- package/static/assets/ishikawaDiagram-FXEZZL3T-DF28a1N6.js +70 -0
- package/static/assets/journeyDiagram-5HDEW3XC-CB6CugYk.js +139 -0
- package/static/assets/kanban-definition-HUTT4EX6-ResSLF6d.js +89 -0
- package/static/assets/katex-C5jXJg4s.js +257 -0
- package/static/assets/layout-DEXfKzaS.js +1 -0
- package/static/assets/linear-CLO--ryk.js +1 -0
- package/static/assets/map-Czzmt4hB.js +1 -0
- package/static/assets/mindmap-definition-LN4V7U3C-BvW3EbxX.js +96 -0
- package/static/assets/ordinal-Cboi1Yqb.js +1 -0
- package/static/assets/pegDiagram-2B236MQR-BHCgqrrM.js +1 -0
- package/static/assets/pieDiagram-ENE6RG2P-DVmCQ5FA.js +39 -0
- package/static/assets/quadrantDiagram-ABIIQ3AL-B3e5KR52.js +7 -0
- package/static/assets/railroadDiagram-RFXS5EU6-Csv5ysLc.js +1 -0
- package/static/assets/requirementDiagram-TGXJPOKE-DOMVzvnB.js +84 -0
- package/static/assets/sankeyDiagram-HTMAVEWB-B1dcTOvy.js +40 -0
- package/static/assets/sequenceDiagram-DBY2YBRQ-BY414yeO.js +162 -0
- package/static/assets/sizeCapture-X5ZJPWSS-CuJfrfBN.js +1 -0
- package/static/assets/stateDiagram-2N3HPSRC-FX4rJkzV.js +1 -0
- package/static/assets/stateDiagram-v2-6OUMAXLB-BVufbO_H.js +1 -0
- package/static/assets/swimlanes-5IMT3BWC-Sfy58mmv.js +2 -0
- package/static/assets/swimlanesDiagram-G3AALYLV-wDCDw08H.js +8 -0
- package/static/assets/timeline-definition-FHXFAJF6-CmV7by8p.js +120 -0
- package/static/assets/vennDiagram-L72KCM5P-FqIyFZFt.js +34 -0
- package/static/assets/wardleyDiagram-EHGQE667-BZ06O45i.js +78 -0
- package/static/assets/xychartDiagram-FW5EYKEG-BOkffR06.js +7 -0
- 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.
|