For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add a consistent PDF/HTML page header to both exam sprint reports that displays the student name from StudentName, with the logo area left empty for a future asset.
Architecture: The report renderers already own final HTML generation. Add template placeholders for studentName and generatedAtText, fill them from the existing report content or payload contract, and keep escaping at the renderer boundary. The left logo slot is CSS-only empty space in both templates.
Tech Stack: Java 17 records, Spring component renderers, static HTML templates, JUnit 5, AssertJ.
abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/outlook/ClasspathOutlookExamSprintReportRendererTest.javaabilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/achievement/ClasspathAchievementExamSprintReportRendererTest.javaabilities/exam-sprint/infrastructure/src/main/resources/templates/outlook-exam-sprint-report-template.htmlabilities/exam-sprint/infrastructure/src/main/resources/templates/achievement-exam-sprint-report-template.htmlabilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/outlook/ClasspathOutlookExamSprintReportRenderer.javaabilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/achievement/ClasspathAchievementExamSprintReportRenderer.javaIn ClasspathOutlookExamSprintReportRendererTest, add a test that renders callerVocabularyPayload() and asserts:
assertThat(html)
.contains("class=\"report-header\"")
.contains("class=\"header-logo\"")
.contains("个人学情报告")
.contains("class=\"header-student-name\">20260318测试</div>")
.contains("class=\"header-generated-at\"")
.doesNotContain("{{studentName}}")
.doesNotContain("{{generatedAtText}}");
In ClasspathAchievementExamSprintReportRendererTest, add a test that renders sampleContent() and asserts:
assertThat(html)
.contains("class=\"report-header\"")
.contains("class=\"header-logo\"")
.contains("个人学情报告")
.contains("class=\"header-student-name\">测试临考</div>")
.contains("class=\"header-generated-at\"")
.doesNotContain("{{studentName}}")
.doesNotContain("{{generatedAtText}}");
Extend existing escaping tests so malicious student names are escaped. For achievement, create content with studentName set to 测试<script>alert(1)</script> and assert escaped text appears. For outlook, mutate StudentName in the JsonNode to the same value and assert escaped text appears.
Run:
./mvnw -pl abilities/exam-sprint/infrastructure -Dtest=ClasspathOutlookExamSprintReportRendererTest,ClasspathAchievementExamSprintReportRendererTest test
Expected: tests fail because report-header, studentName, and generatedAtText placeholders do not exist or are not replaced yet.
Insert this block immediately after <div class="report-container"> in both templates:
<header class="report-header">
<div class="header-logo" aria-hidden="true"></div>
<div class="header-main">
<div class="header-report-type">个人学情报告</div>
<div class="header-student-name">{{studentName}}</div>
</div>
<div class="header-generated-at">{{generatedAtText}}</div>
</header>
Add CSS before </style> in both templates:
.report-header {
display: table;
width: 100%;
table-layout: fixed;
border-bottom: 3px solid #111;
margin-bottom: 28px;
padding-bottom: 10px;
}
.header-logo,
.header-main,
.header-generated-at {
display: table-cell;
vertical-align: top;
}
.header-logo {
width: 180px;
}
.header-main {
text-align: center;
color: #68768a;
}
.header-report-type {
font-size: 13px;
line-height: 1.5;
}
.header-student-name {
margin-top: 4px;
font-size: 13px;
line-height: 1.5;
}
.header-generated-at {
width: 260px;
color: #68768a;
font-size: 12px;
line-height: 1.5;
text-align: right;
white-space: nowrap;
}
In ClasspathOutlookExamSprintReportRenderer.render, add replacements for {{studentName}} and {{generatedAtText}} before returning the template string:
.replace("{{studentName}}", escape(payloadContract.studentName()))
.replace("{{generatedAtText}}", escape(formatGeneratedAt(generatedAt)))
Add a private formatter:
private String formatGeneratedAt(Instant generatedAt) {
return generatedAt == null ? "" : generatedAt.toString();
}
Reuse the existing escape(String value) method in this renderer.
In ClasspathAchievementExamSprintReportRenderer.placeholders, add:
placeholders.put("studentName", escape(reportContent.studentName()));
placeholders.put("generatedAtText", escape(formatGeneratedAt(generatedAt)));
Pass generatedAt into placeholders(...) from render(...). Add a private formatter:
private String formatGeneratedAt(Instant generatedAt) {
return generatedAt == null ? "" : generatedAt.toString();
}
Run:
./mvnw -pl abilities/exam-sprint/infrastructure -Dtest=ClasspathOutlookExamSprintReportRendererTest,ClasspathAchievementExamSprintReportRendererTest test
Expected: both renderer test classes pass.
Run:
./mvnw -pl abilities/exam-sprint/infrastructure test
Expected: infrastructure module tests pass.
Run:
git diff -- abilities/exam-sprint/infrastructure/src/main/resources/templates/outlook-exam-sprint-report-template.html abilities/exam-sprint/infrastructure/src/main/resources/templates/achievement-exam-sprint-report-template.html abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/outlook/ClasspathOutlookExamSprintReportRenderer.java abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/achievement/ClasspathAchievementExamSprintReportRenderer.java abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/outlook/ClasspathOutlookExamSprintReportRendererTest.java abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/achievement/ClasspathAchievementExamSprintReportRendererTest.java
Expected: only the planned header, placeholder replacement, and tests changed.
StudentName in the page header; logo area is empty; generated time is present; escaping is tested.TBD, TODO, or unresolved implementation placeholders in this plan.generatedAt is already part of renderer signatures.