Error
+Request could not be completed
+The requested operation could not be completed.
+ +From 8388b4cc9bcad6fb756fd1a310f8c2871991d344 Mon Sep 17 00:00:00 2001 From: sechmachine <97589681+sechmachine727@users.noreply.github.com> Date: Sat, 15 Aug 2026 02:17:01 +0700 Subject: [PATCH] fix(ui): resolve round one shell findings --- .../tests/web/attendance-shell-integration.md | 6 +- .../web/project-task-shell-integration.md | 2 +- docs/tests/web/review-round-1-shared-ui.md | 93 +++++++++++++++ docs/tests/web/ui-shell-components.md | 4 +- .../controller/DashboardController.java | 20 ++++ .../DashboardAccessDeniedException.java | 10 ++ .../reporting/model/dto/DashboardView.java | 44 +++++++ .../reporting/service/DashboardService.java | 49 ++++++++ .../templates/attendance/history.html | 13 ++- .../resources/templates/error/generic.html | 19 +++ .../resources/templates/fragments/layout.html | 6 +- .../resources/templates/projects/form.html | 12 +- src/main/resources/templates/tasks/form.html | 6 +- .../AttendanceTemplateIntegrationTest.java | 39 +++++++ .../ProjectTaskFormAccessibilityWebTest.java | 108 ++++++++++++++++++ .../RoleDashboardWebIntegrationTest.java | 29 +++++ .../SharedErrorTemplateWebTest.java | 69 +++++++++++ .../labtimesheet/ui/UiContractWebTest.java | 21 +++- 18 files changed, 526 insertions(+), 24 deletions(-) create mode 100644 docs/tests/web/review-round-1-shared-ui.md create mode 100644 src/main/resources/templates/error/generic.html create mode 100644 src/test/java/com/lab/labtimesheet/feature/reporting/controller/ProjectTaskFormAccessibilityWebTest.java create mode 100644 src/test/java/com/lab/labtimesheet/feature/reporting/controller/SharedErrorTemplateWebTest.java diff --git a/docs/tests/web/attendance-shell-integration.md b/docs/tests/web/attendance-shell-integration.md index 29741f5..80f5634 100644 --- a/docs/tests/web/attendance-shell-integration.md +++ b/docs/tests/web/attendance-shell-integration.md @@ -1,18 +1,18 @@ # Test Evidence: attendance shell integration - **Test type:** Web -- **Requirement IDs:** `UI-001`, `UI-002`, `UI-003`, `UI-008`, `I1-ATT-03`, `I1-UI-04` +- **Requirement IDs:** `UI-001`, `UI-002`, `UI-003`, `UI-008`, `UI-013`, `I1-ATT-03`, `I1-UI-04` - **Scenario IDs:** `AC-ATT-003`, `AC-ATT-004`, `AC-UI-001`, `AC-UI-005` - **Test class/method:** `com.lab.labtimesheet.feature.reporting.controller.AttendanceTemplateIntegrationTest` - **Implementation commit:** `3064485` ## Protected behavior -The Intern attendance-history and Admin global-calendar pages consume the role-aware shared shell while preserving their existing routes, CSRF-protected mutation forms, filter values, empty states, and local theme assets. +The Intern attendance-history and Admin global-calendar pages consume the role-aware shared shell while preserving their existing routes, CSRF-protected mutation forms, filter values, empty states, and local theme assets. Populated history presents `dd/MM/yyyy` dates and 24-hour times in the attached policy timezone and does not collapse simultaneous violations. ## Test method -A focused MockMvc slice supplies empty production-shaped models to the two production Attendance templates and renders them with role-specific Spring Security principals. The owning feature's `AttendanceControllerTest` remains the affected behavioral suite for authorization, punch actions, calendar mutation, and view selection. +A focused MockMvc slice supplies empty and populated production-shaped models to the two production Attendance templates and renders them with role-specific Spring Security principals. The populated fixture uses UTC instants, the attached `Asia/Ho_Chi_Minh` seeded policy, and late-plus-early and late-plus-missing combinations. The owning feature's `AttendanceControllerTest` remains the affected behavioral suite for authorization, punch actions, calendar mutation, and view selection. ## Hand-derived expected result diff --git a/docs/tests/web/project-task-shell-integration.md b/docs/tests/web/project-task-shell-integration.md index 8aa0773..e8a5ea9 100644 --- a/docs/tests/web/project-task-shell-integration.md +++ b/docs/tests/web/project-task-shell-integration.md @@ -9,7 +9,7 @@ ## Protected behavior Every Iteration 1 Project and Task page uses the same authenticated desktop shell, local assets, role-aware Project navigation, table containment, form controls, empty states, status badges, and `dd/MM/yyyy` date presentation. Existing capability-gated actions, server routes, validation, authentication, and CSRF contracts remain unchanged. -Project and Task forms provide both an error summary and inline field errors for failed server validation. +Project and Task forms provide both an error summary and inline field errors for failed server validation. Every inline error has a stable ID and every invalid control references that ID through `aria-describedby`. ## Test method diff --git a/docs/tests/web/review-round-1-shared-ui.md b/docs/tests/web/review-round-1-shared-ui.md new file mode 100644 index 0000000..74d74ce --- /dev/null +++ b/docs/tests/web/review-round-1-shared-ui.md @@ -0,0 +1,93 @@ +# Test Evidence: round-one shared UI corrections + +- **Test type:** Web +- **Requirement IDs:** `AUTH-002`, `UI-003`, `UI-004`, `UI-010`, `UI-013`, `UI-014`, `ERR-001`, `I1-UI-01`, `I1-UI-02`, `I1-UI-04` +- **Scenario IDs:** `AC-AUTH-001`, `AC-UI-002`, `AC-UI-003`, `AC-UI-005` +- **Test class/method:** `com.lab.labtimesheet.ui.UiContractWebTest`, `com.lab.labtimesheet.feature.reporting.controller.AttendanceTemplateIntegrationTest#populatedHistoryUsesPolicyLocalPresentationAndListsEveryViolation`, `com.lab.labtimesheet.feature.reporting.controller.SharedErrorTemplateWebTest`, `com.lab.labtimesheet.feature.reporting.controller.ProjectTaskFormAccessibilityWebTest`, `com.lab.labtimesheet.feature.reporting.controller.RoleDashboardWebIntegrationTest#mentorAndInternDashboardsRenderRealScopedProjectTaskAndAttendanceData` +- **Implementation commit:** `pending` + +## Protected behavior + +The authenticated shell exposes only reachable role-authorized links. Intern attendance uses `/attendance`; Mentor attendance, profile, and notification links remain hidden until their authorized destination flows exist. Every rendered role-navigation link resolves through an actual authenticated GET. Attendance history uses the row's attached policy timezone for 24-hour times, formats business dates as `dd/MM/yyyy`, and renders every simultaneous violation. Project and Task field errors have stable IDs associated to invalid controls. Generic 404 and 409 pages use the shared shell and safe caller-supplied copy without rendering exception details. + +## Test method + +MockMvc renders the production shell and templates with real Spring Security principals and production-shaped Attendance DTOs. Project and Task invalid POSTs pass through their real controllers and validation, with only feature services replaced at the slice boundary. The full Spring/PostgreSQL role journey creates accounts, internship, Project, and Task through public services, renders each role's real dashboard, extracts every visible shell link, and performs an authenticated GET against each extracted path. + +## Hand-derived expected result + +Mentor navigation contains only overview and owned Projects; Intern navigation contains overview, `/attendance`, and Projects; Admin navigation contains overview, account creation, and global calendar. No role receives `/attendance/me`, `/profile`, `/notifications`, or a selector-less Mentor attendance destination. `2026-08-14T02:05:00Z` under `Asia/Ho_Chi_Minh` renders as `14/08/2026 09:05`; `09:00:00Z` renders as `16:00`. Late plus early-departure and late plus missing-checkout labels are both retained. Every rendered validation message has a stable referenced ID. Error pages expose only status and generic copy. + +## RED + +**Command** + +```text +export JAVA_HOME=/opt/homebrew/opt/openjdk@25 +export PATH="/opt/homebrew/opt/node@24/bin:$JAVA_HOME/bin:$PATH" +export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock +./mvnw -Dtest=UiContractWebTest,AttendanceTemplateIntegrationTest,SharedErrorTemplateWebTest,ProjectTaskFormAccessibilityWebTest test +./mvnw -Dtest=RoleDashboardWebIntegrationTest test +``` + +**Observed result** + +```text +Focused templates: Tests run: 13, Failures: 6, Errors: 2, Skipped: 0 +Navigation exposed /attendance/me, selector-less Mentor attendance, /profile, and /notifications. +Attendance rendered ISO dates/raw UTC instants and only one violation. +error/generic did not exist. +Invalid controls had no aria-describedby and inline errors had no stable IDs. + +PostgreSQL 18.4 role journey: Tests run: 1, Failures: 1, Errors: 0, Skipped: 0 +Following the Admin shell's visible /profile link returned 404 instead of 200. +BUILD FAILURE +``` + +The failures occurred after real template rendering and controller validation; they identify the missing reviewed behavior rather than fixture or environment failure. + +## GREEN + +**Command** + +```text +export JAVA_HOME=/opt/homebrew/opt/openjdk@25 +export PATH="/opt/homebrew/opt/node@24/bin:$JAVA_HOME/bin:$PATH" +export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock +./mvnw -Dtest=UiContractWebTest,AttendanceTemplateIntegrationTest,SharedErrorTemplateWebTest,ProjectTaskFormAccessibilityWebTest test +./mvnw -Dtest=RoleDashboardWebIntegrationTest test +``` + +**Observed result** + +```text +Focused templates: Tests run: 13, Failures: 0, Errors: 0, Skipped: 0 +PostgreSQL 18.4 role journey: Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 +BUILD SUCCESS +``` + +## Affected suite + +**Command and result** + +```text +export JAVA_HOME=/opt/homebrew/opt/openjdk@25 +export PATH="/opt/homebrew/opt/node@24/bin:$JAVA_HOME/bin:$PATH" +export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock +npm run build +./mvnw -Dtest=UiContractWebTest,AttendanceTemplateIntegrationTest,AttendanceControllerTest,SharedErrorTemplateWebTest,ProjectTaskFormAccessibilityWebTest,ProjectControllerTest,TaskControllerTest,RoleDashboardWebIntegrationTest test +./mvnw -DskipTests compile +./mvnw -DskipTests -Ddoclint=all javadoc:javadoc + +Node v24.19.0; npm 11.17.0 +Tailwind CSS v4.3.3: Done in 68ms +PostgreSQL 18.4 via Testcontainers +Tests run: 40, Failures: 0, Errors: 0, Skipped: 0 +Compile: success +Javadoc/doclint: success +BUILD SUCCESS +``` + +## External-test boundaries + +The automated checks prove rendering, controller validation, role-scoped navigation targets, attached-policy formatting, and generic error copy. They do not prove first-paint timing, keyboard focus/tooltips, runtime `aria-expanded` synchronization, or viewport overflow; those remain mandatory live desktop browser gates after the corrected producer pins are merged. diff --git a/docs/tests/web/ui-shell-components.md b/docs/tests/web/ui-shell-components.md index 97afb28..3d381cc 100644 --- a/docs/tests/web/ui-shell-components.md +++ b/docs/tests/web/ui-shell-components.md @@ -8,7 +8,7 @@ ## Protected behavior -Domain-owned Thymeleaf pages can render inside one desktop shell with role-filtered navigation, accessible controls/states, pre-paint local theme loading, and committed local CSS/JavaScript/Lucide assets. The tests catch missing fragments, unauthorized navigation leakage, inaccessible shared form/status markup, remote icon references, or a theme bootstrap loaded after CSS. +Domain-owned Thymeleaf pages can render inside one desktop shell with role-filtered navigation, accessible controls/states, pre-paint local theme loading, and committed local CSS/JavaScript/Lucide assets. The tests catch missing fragments, unauthorized or dead navigation links, inaccessible shared form/status markup, remote icon references, or a theme bootstrap loaded after CSS. ## Test method @@ -16,7 +16,7 @@ A test-only domain page consumes the production layout fragment through MockMvc ## Hand-derived expected result -A Mentor sees `Owned Projects`, theme, profile identity, and logout, but not Admin `Accounts` or Intern `My attendance`. The theme script occurs before the stylesheet. Form label/control IDs match, errors use `role="alert"`, status includes a textual accessible name, confirmation copy is described, and the reduced sprite contains the selected symbols without remote resource references. +A Mentor sees `Owned Projects`, account identity, theme, and logout, but not Admin `Accounts`, Intern `My attendance`, or selector-less Intern attendance. An Intern's attendance link targets the real `/attendance` route. Unimplemented profile and notification destinations are not exposed. The theme script occurs before the stylesheet. Form label/control IDs match, errors use `role="alert"`, status includes a textual accessible name, confirmation copy is described, and the reduced sprite contains the selected symbols without remote resource references. ## RED diff --git a/src/main/java/com/lab/labtimesheet/feature/reporting/controller/DashboardController.java b/src/main/java/com/lab/labtimesheet/feature/reporting/controller/DashboardController.java index 18f5506..6d1710f 100644 --- a/src/main/java/com/lab/labtimesheet/feature/reporting/controller/DashboardController.java +++ b/src/main/java/com/lab/labtimesheet/feature/reporting/controller/DashboardController.java @@ -7,15 +7,35 @@ import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; +/** + * Selects the dashboard view for the authenticated global authority. + * + *
The authority selects only which role-specific flow to invoke. The reporting service then + * reloads and revalidates the persisted account role and lifecycle before returning any data. + */ @Controller public class DashboardController { private final DashboardService dashboardService; + /** + * Creates the dashboard endpoint backed by the reporting composition service. + * + * @param dashboardService service that authorizes and assembles role-scoped dashboard data + */ public DashboardController(DashboardService dashboardService) { this.dashboardService = dashboardService; } + /** + * Renders the dashboard permitted by the caller's authenticated global role. + * + * @param authentication authenticated caller whose name is the persisted account email + * @param model Thymeleaf model populated with the role-specific {@code dashboard} projection + * @return the Admin, Mentor, or Intern dashboard template name + * @throws DashboardAccessDeniedException when the authority is unsupported or does not match + * an active persisted account identity + */ @GetMapping("/dashboard") public String dashboard(Authentication authentication, Model model) { String email = authentication.getName(); diff --git a/src/main/java/com/lab/labtimesheet/feature/reporting/exception/DashboardAccessDeniedException.java b/src/main/java/com/lab/labtimesheet/feature/reporting/exception/DashboardAccessDeniedException.java index 3173389..a882394 100644 --- a/src/main/java/com/lab/labtimesheet/feature/reporting/exception/DashboardAccessDeniedException.java +++ b/src/main/java/com/lab/labtimesheet/feature/reporting/exception/DashboardAccessDeniedException.java @@ -4,9 +4,19 @@ import org.springframework.security.access.AccessDeniedException; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; +/** + * Non-disclosing denial raised when an authenticated identity cannot access a role dashboard. + * + *
Callers must not include protected record identifiers or lifecycle details in the message. + */ @ResponseStatus(HttpStatus.FORBIDDEN) public class DashboardAccessDeniedException extends AccessDeniedException { + /** + * Creates a safe dashboard denial. + * + * @param message generic reason suitable for server-side diagnosis without protected details + */ public DashboardAccessDeniedException(String message) { super(message); } diff --git a/src/main/java/com/lab/labtimesheet/feature/reporting/model/dto/DashboardView.java b/src/main/java/com/lab/labtimesheet/feature/reporting/model/dto/DashboardView.java index 2b41638..436e61e 100644 --- a/src/main/java/com/lab/labtimesheet/feature/reporting/model/dto/DashboardView.java +++ b/src/main/java/com/lab/labtimesheet/feature/reporting/model/dto/DashboardView.java @@ -3,23 +3,62 @@ package com.lab.labtimesheet.feature.reporting.model.dto; import java.time.LocalDate; import java.util.List; +/** + * Closed set of immutable, role-specific dashboard projections rendered by Reporting. + * + *
Each projection contains only data authorized and calculated by its owning feature service.
+ */
public sealed interface DashboardView {
+ /**
+ * System-wide counts visible to an active Admin.
+ *
+ * @param activeAccounts active account count
+ * @param pendingActivations accounts awaiting activation
+ * @param activeInternships active internship count
+ * @param activeProjects active Projects visible to an Admin
+ */
record Admin(long activeAccounts, long pendingActivations,
long activeInternships, long activeProjects) implements DashboardView {
}
+ /**
+ * Owning-Mentor operational summary; empty authorized scopes are represented by zero counts.
+ *
+ * @param displayName persisted Mentor display name
+ * @param activeProjects active owned Project count
+ * @param activeMembers distinct eligible active members across owned Projects
+ * @param blockedTasks blocked Tasks visible within active owned Projects
+ */
record Mentor(String displayName, long activeProjects, long activeMembers,
long blockedTasks) implements DashboardView {
}
+ /**
+ * Eligible Intern summary for the Attendance policy's current business date.
+ *
+ * @param displayName persisted Intern display name
+ * @param attendanceState current policy-local attendance state
+ * @param activeProjects active Projects containing a current eligible membership
+ * @param assignedTasks current assigned Task count
+ * @param priorityTasks ordered Task-owned priority items; empty when none are assigned
+ */
record Intern(String displayName, AttendanceState attendanceState, long activeProjects,
long assignedTasks, List This service owns no persistence mapping or business-date calculation. Account lifecycle and
+ * role are revalidated from persisted identity data, while Project, Task, and Attendance retain
+ * ownership of their query scope, ordering, and attendance-policy business date.
+ */
@Service
@Transactional(readOnly = true)
public class DashboardService {
@@ -28,6 +35,14 @@ public class DashboardService {
private final TaskDashboardService tasks;
private final AttendanceApplicationService attendance;
+ /**
+ * Creates a reporting coordinator over the concrete feature query boundaries.
+ *
+ * @param accounts account identity and Admin summary boundary
+ * @param projects role-scoped Project summary boundary
+ * @param tasks role-scoped Task dashboard boundary
+ * @param attendance attendance state boundary using the active policy business date
+ */
public DashboardService(
AccountService accounts,
ProjectQueryService projects,
@@ -39,6 +54,16 @@ public class DashboardService {
this.attendance = attendance;
}
+ /**
+ * Builds system-wide Admin counts after confirming an active persisted Admin identity.
+ *
+ * Counts are zero when the corresponding feature has no matching records.
+ *
+ * @param email authenticated account email
+ * @return account lifecycle counts and the Admin-visible active Project count
+ * @throws DashboardAccessDeniedException when the persisted account is missing, inactive, or
+ * not an Admin
+ */
public DashboardView.Admin admin(String email) {
AccountIdentity admin = activeAccount(email, GlobalRole.ADMIN);
AccountSummary accountSummary = accounts.summary();
@@ -50,6 +75,17 @@ public class DashboardService {
projectSummary.activeProjectCount());
}
+ /**
+ * Builds the owning-Mentor dashboard after persisted-role revalidation.
+ *
+ * Project and member counts are scoped by the Project service; blocked Task count is scoped
+ * by the Task service. Each empty scope is represented by a zero count.
+ *
+ * @param email authenticated account email
+ * @return Mentor display name and role-scoped Project, member, and blocked-Task counts
+ * @throws DashboardAccessDeniedException when the persisted account is missing, inactive, or
+ * not a Mentor
+ */
public DashboardView.Mentor mentor(String email) {
AccountIdentity mentor = activeAccount(email, GlobalRole.MENTOR);
ProjectDashboardSummary projectSummary = projects.dashboardSummary(mentor.id());
@@ -61,6 +97,19 @@ public class DashboardService {
taskSummary.blockedTaskCount());
}
+ /**
+ * Builds the eligible Intern dashboard after persisted-role revalidation.
+ *
+ * The Attendance feature determines today's state from its policy-owned business date. The
+ * Task feature owns assigned count and priority ordering; no matching Tasks produce an empty
+ * priority list. Attendance ineligibility is converted to the same non-disclosing dashboard
+ * denial as other invalid Intern lifecycle states.
+ *
+ * @param email authenticated account email
+ * @return Intern attendance state, scoped counts, and at most the Task service's priority items
+ * @throws DashboardAccessDeniedException when the persisted account or internship is not
+ * eligible for the Intern dashboard
+ */
public DashboardView.Intern intern(String email) {
AccountIdentity intern = activeAccount(email, GlobalRole.INTERN);
AttendanceCurrentState attendanceState;
diff --git a/src/main/resources/templates/attendance/history.html b/src/main/resources/templates/attendance/history.html
index af4cbb7..7644860 100644
--- a/src/main/resources/templates/attendance/history.html
+++ b/src/main/resources/templates/attendance/history.html
@@ -39,12 +39,17 @@
Error The requested operation could not be completed. Name error Name error Start date error End date error Date range error Start date error End date error Date range error Leader error Leader error Assign work to a current eligible Project member.
-
diff --git a/src/main/resources/templates/error/generic.html b/src/main/resources/templates/error/generic.html
new file mode 100644
index 0000000..41d68ab
--- /dev/null
+++ b/src/main/resources/templates/error/generic.html
@@ -0,0 +1,19 @@
+
+
+
+
-
-
+
+
+
-
+
+ On time
+ Late
+ Early departure
+ Missing checkout
+
Request could not be completed
+