diff --git a/AGENTS.md b/AGENTS.md index ddab98d..5aa3275 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # GitNexus — Code Intelligence -This project is indexed by GitNexus as **cameleer-saas** (2838 symbols, 6037 relationships, 239 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **cameleer-saas** (3004 symbols, 6307 relationships, 253 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/CLAUDE.md b/CLAUDE.md index 6d3f6c7..87b33e9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -79,7 +79,7 @@ PostgreSQL (Flyway): `src/main/resources/db/migration/` # GitNexus — Code Intelligence -This project is indexed by GitNexus as **cameleer-saas** (2881 symbols, 6138 relationships, 243 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **cameleer-saas** (3004 symbols, 6307 relationships, 253 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/docs/superpowers/plans/2026-04-26-email-template-polish-plan.md b/docs/superpowers/plans/2026-04-26-email-template-polish-plan.md new file mode 100644 index 0000000..dd722f1 --- /dev/null +++ b/docs/superpowers/plans/2026-04-26-email-template-polish-plan.md @@ -0,0 +1,449 @@ +# Email Template Polish Implementation Plan + +> **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:** Replace inline HTML email templates with polished, branded HTML files loaded from classpath, featuring playful desert/caravan copy, structured card layout with watermark, and proper header/footer. + +**Architecture:** Extract 4 email templates from `EmailConnectorService.buildSmtpConfig()` into standalone HTML files at `src/main/resources/email-templates/`. Generate a pre-faded watermark PNG served as a static asset. Inject `ProvisioningProperties` to resolve the watermark URL at runtime. + +**Tech Stack:** Java 21, Spring Boot, ImageMagick (one-time asset generation), HTML email (inline styles only) + +--- + +### File Map + +| Action | File | Purpose | +|--------|------|---------| +| Create | `src/main/resources/email-templates/register.html` | Registration verification email | +| Create | `src/main/resources/email-templates/sign-in.html` | Sign-in verification email | +| Create | `src/main/resources/email-templates/forgot-password.html` | Password reset email | +| Create | `src/main/resources/email-templates/generic.html` | Generic verification email | +| Create | `src/main/resources/static/assets/email-watermark.png` | Pre-faded logo at 7% opacity | +| Modify | `src/main/java/net/siegeln/cameleer/saas/vendor/EmailConnectorService.java` | Load templates from classpath, inject watermark URL | +| Modify | `src/main/java/net/siegeln/cameleer/saas/config/SecurityConfig.java:49` | Permit `/assets/**` for unauthenticated email clients | +| Create | `src/test/java/net/siegeln/cameleer/saas/vendor/EmailTemplateLoadingTest.java` | Verify templates load and placeholders resolve | + +--- + +### Task 1: Generate the pre-faded watermark PNG + +**Files:** +- Create: `src/main/resources/static/assets/email-watermark.png` + +- [ ] **Step 1: Generate the faded watermark using ImageMagick** + +Source the logo from the design-system sibling repo. Apply 7% opacity on a transparent background, output to the static assets directory: + +```bash +magick "C:/Users/Hendrik/Documents/projects/design-system/assets/cameleer-logo.png" \ + -channel A -evaluate Multiply 0.07 +channel \ + -resize 320x320 \ + "src/main/resources/static/assets/email-watermark.png" +``` + +If `magick` is not available, use Python Pillow as fallback: + +```bash +python3 -c " +from PIL import Image +img = Image.open('C:/Users/Hendrik/Documents/projects/design-system/assets/cameleer-logo.png').convert('RGBA') +img = img.resize((320, 320), Image.LANCZOS) +r, g, b, a = img.split() +a = a.point(lambda x: int(x * 0.07)) +img = Image.merge('RGBA', (r, g, b, a)) +img.save('src/main/resources/static/assets/email-watermark.png') +print('Saved watermark') +" +``` + +- [ ] **Step 2: Verify the file exists and is reasonable size** + +```bash +ls -la src/main/resources/static/assets/email-watermark.png +``` + +Expected: File exists, roughly 5-30 KB. + +- [ ] **Step 3: Commit** + +```bash +git add src/main/resources/static/assets/email-watermark.png +git commit -m "feat: add pre-faded logo watermark for email templates" +``` + +--- + +### Task 2: Permit static assets in SecurityConfig + +**Files:** +- Modify: `src/main/java/net/siegeln/cameleer/saas/config/SecurityConfig.java:49` + +The watermark image must be loadable by email clients without authentication. The current security config has `.anyRequest().authenticated()` as catch-all, so `/assets/**` needs an explicit permit. + +- [ ] **Step 1: Add `/assets/**` to the permitAll list** + +In `SecurityConfig.java`, find the existing line: + +```java +.requestMatchers("/_app/**", "/favicon.ico", "/favicon.svg", "/logo.svg", "/logo-dark.svg").permitAll() +``` + +Change it to: + +```java +.requestMatchers("/_app/**", "/assets/**", "/favicon.ico", "/favicon.svg", "/logo.svg", "/logo-dark.svg").permitAll() +``` + +- [ ] **Step 2: Commit** + +```bash +git add src/main/java/net/siegeln/cameleer/saas/config/SecurityConfig.java +git commit -m "feat: permit /assets/** for unauthenticated access (email watermark)" +``` + +--- + +### Task 3: Create the 4 HTML email template files + +**Files:** +- Create: `src/main/resources/email-templates/register.html` +- Create: `src/main/resources/email-templates/sign-in.html` +- Create: `src/main/resources/email-templates/forgot-password.html` +- Create: `src/main/resources/email-templates/generic.html` + +All templates use the same card structure. The `{{code}}` placeholder is Logto's built-in substitution. The `{{watermarkUrl}}` placeholder is replaced by `EmailConnectorService` at runtime. + +- [ ] **Step 1: Create `register.html`** + +```html +
+
+ Cameleer.io +
+
+ +
+

Welcome to the caravan!

+

Enter this code to verify your email and claim your spot. The dunes wait for no one.

+
+
+ {{code}} +
+
+

This code expires in 10 minutes. If you didn't request this, you can safely ignore this email — no camels were harmed.

+
+
+
+

Questions? Contact your administrator

+

Cameleer — Apache Camel observability

+
+
+``` + +- [ ] **Step 2: Create `sign-in.html`** + +```html +
+
+ Cameleer.io +
+
+ +
+

Back at the oasis already?

+

Here's your sign-in code. The caravan master is checking credentials.

+
+
+ {{code}} +
+
+

This code expires in 10 minutes.

+
+
+
+

Questions? Contact your administrator

+

Cameleer — Apache Camel observability

+
+
+``` + +- [ ] **Step 3: Create `forgot-password.html`** + +```html +
+
+ Cameleer.io +
+
+ +
+

Lost in the dunes?

+

No worries — enter this code to reset your password and get back on the trail.

+
+
+ {{code}} +
+
+

This code expires in 10 minutes. If you didn't request a password reset, you can safely ignore this email.

+
+
+
+

Questions? Contact your administrator

+

Cameleer — Apache Camel observability

+
+
+``` + +- [ ] **Step 4: Create `generic.html`** + +```html +
+
+ Cameleer.io +
+
+ +
+

Quick checkpoint

+

Here's your verification code. Just making sure it's really you at the reins.

+
+
+ {{code}} +
+
+

This code expires in 10 minutes.

+
+
+
+

Questions? Contact your administrator

+

Cameleer — Apache Camel observability

+
+
+``` + +- [ ] **Step 5: Commit** + +```bash +git add src/main/resources/email-templates/ +git commit -m "feat: add branded HTML email templates with desert/caravan copy" +``` + +--- + +### Task 4: Refactor EmailConnectorService to load templates from classpath + +**Files:** +- Modify: `src/main/java/net/siegeln/cameleer/saas/vendor/EmailConnectorService.java` + +- [ ] **Step 1: Write the failing test** + +Create `src/test/java/net/siegeln/cameleer/saas/vendor/EmailTemplateLoadingTest.java`: + +```java +package net.siegeln.cameleer.saas.vendor; + +import org.junit.jupiter.api.Test; +import org.springframework.core.io.ClassPathResource; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; + +class EmailTemplateLoadingTest { + + private static final String[] TEMPLATE_FILES = { + "email-templates/register.html", + "email-templates/sign-in.html", + "email-templates/forgot-password.html", + "email-templates/generic.html" + }; + + @Test + void allTemplateFilesExistOnClasspath() { + for (String path : TEMPLATE_FILES) { + var resource = new ClassPathResource(path); + assertTrue(resource.exists(), "Template file missing: " + path); + } + } + + @Test + void templatesContainCodePlaceholder() throws IOException { + for (String path : TEMPLATE_FILES) { + String content = new ClassPathResource(path).getContentAsString(StandardCharsets.UTF_8); + assertTrue(content.contains("{{code}}"), + path + " must contain {{code}} placeholder"); + } + } + + @Test + void templatesContainWatermarkPlaceholder() throws IOException { + for (String path : TEMPLATE_FILES) { + String content = new ClassPathResource(path).getContentAsString(StandardCharsets.UTF_8); + assertTrue(content.contains("{{watermarkUrl}}"), + path + " must contain {{watermarkUrl}} placeholder"); + } + } + + @Test + void watermarkPlaceholderIsReplaced() throws IOException { + String content = new ClassPathResource("email-templates/register.html") + .getContentAsString(StandardCharsets.UTF_8); + String resolved = content.replace("{{watermarkUrl}}", + "https://example.com/platform/assets/email-watermark.png"); + assertFalse(resolved.contains("{{watermarkUrl}}")); + assertTrue(resolved.contains("https://example.com/platform/assets/email-watermark.png")); + } + + @Test + void templatesContainBrandElements() throws IOException { + for (String path : TEMPLATE_FILES) { + String content = new ClassPathResource(path).getContentAsString(StandardCharsets.UTF_8); + assertTrue(content.contains("Cameleer.io"), + path + " must contain Cameleer.io header"); + assertTrue(content.contains("Apache Camel observability"), + path + " must contain tagline"); + assertTrue(content.contains("#C6820E"), + path + " must use brand color"); + } + } +} +``` + +- [ ] **Step 2: Run tests to verify they pass (templates exist from Task 3)** + +```bash +./mvnw test -pl . -Dtest=EmailTemplateLoadingTest -Dspring.profiles.active=test +``` + +Expected: All 5 tests PASS. + +- [ ] **Step 3: Add `ProvisioningProperties` dependency to `EmailConnectorService`** + +Replace the constructor and add the template loading logic. The full updated `EmailConnectorService.java`: + +Change the imports and fields at the top of the class — add `ProvisioningProperties` import and field: + +```java +import net.siegeln.cameleer.saas.provisioning.ProvisioningProperties; +import org.springframework.core.io.ClassPathResource; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +``` + +Replace the constructor: + +```java + private final LogtoManagementClient logtoClient; + private final ProvisioningProperties provisioningProps; + + public EmailConnectorService(LogtoManagementClient logtoClient, ProvisioningProperties provisioningProps) { + this.logtoClient = logtoClient; + this.provisioningProps = provisioningProps; + } +``` + +Replace the `buildSmtpConfig` method (lines 157-191) with: + +```java + /** Load an email template from classpath and resolve the watermark URL placeholder. */ + private String loadTemplate(String filename) { + try { + String content = new ClassPathResource("email-templates/" + filename) + .getContentAsString(StandardCharsets.UTF_8); + String watermarkUrl = provisioningProps.publicProtocol() + "://" + + provisioningProps.publicHost() + "/platform/assets/email-watermark.png"; + return content.replace("{{watermarkUrl}}", watermarkUrl); + } catch (IOException e) { + throw new IllegalStateException("Failed to load email template: " + filename, e); + } + } + + /** Build the Logto SMTP connector config with Cameleer-branded email templates. */ + private Map buildSmtpConfig(SmtpConfig smtp) { + var config = new HashMap(); + config.put("host", smtp.host()); + config.put("port", smtp.port()); + config.put("auth", Map.of("user", smtp.username(), "pass", smtp.password())); + config.put("fromEmail", smtp.fromEmail()); + config.put("templates", List.of( + Map.of( + "usageType", "Register", + "contentType", "text/html", + "subject", "Your caravan pass is almost ready", + "content", loadTemplate("register.html") + ), + Map.of( + "usageType", "SignIn", + "contentType", "text/html", + "subject", "Your Cameleer sign-in code", + "content", loadTemplate("sign-in.html") + ), + Map.of( + "usageType", "ForgotPassword", + "contentType", "text/html", + "subject", "Reset your Cameleer password", + "content", loadTemplate("forgot-password.html") + ), + Map.of( + "usageType", "Generic", + "contentType", "text/html", + "subject", "Your Cameleer verification code", + "content", loadTemplate("generic.html") + ) + )); + return config; + } +``` + +- [ ] **Step 4: Verify the project compiles** + +```bash +./mvnw compile -pl . +``` + +Expected: BUILD SUCCESS + +- [ ] **Step 5: Run the template tests again to confirm nothing broke** + +```bash +./mvnw test -pl . -Dtest=EmailTemplateLoadingTest -Dspring.profiles.active=test +``` + +Expected: All 5 tests PASS. + +- [ ] **Step 6: Commit** + +```bash +git add src/main/java/net/siegeln/cameleer/saas/vendor/EmailConnectorService.java +git add src/test/java/net/siegeln/cameleer/saas/vendor/EmailTemplateLoadingTest.java +git commit -m "feat: load email templates from classpath with watermark URL resolution" +``` + +--- + +### Task 5: Run the full test suite + +**Files:** None (verification only) + +- [ ] **Step 1: Run all tests** + +```bash +./mvnw test -Dspring.profiles.active=test +``` + +Expected: BUILD SUCCESS, all tests pass. If any existing tests fail due to the new `ProvisioningProperties` constructor parameter on `EmailConnectorService`, they will need their mocks updated — but there are no existing tests for this class. + +- [ ] **Step 2: Verify the watermark is accessible without auth by checking SecurityConfig** + +Confirm the `/assets/**` matcher is in the `permitAll()` chain (done in Task 2). With context-path `/platform`, the full public URL will be `https:///platform/assets/email-watermark.png`. + +- [ ] **Step 3: Final commit if any fixes were needed** + +Only if test failures required changes: + +```bash +git add -A +git commit -m "fix: resolve test failures from email template refactor" +``` diff --git a/docs/superpowers/specs/2026-04-26-email-template-polish-design.md b/docs/superpowers/specs/2026-04-26-email-template-polish-design.md new file mode 100644 index 0000000..5eabd1b --- /dev/null +++ b/docs/superpowers/specs/2026-04-26-email-template-polish-design.md @@ -0,0 +1,108 @@ +# Email Template Polish + +Polish the 4 Logto SMTP connector email templates with branded visuals, playful desert/caravan copy, and extract them from inline Java strings to standalone HTML files. + +## Current State + +All 4 email templates are hardcoded as inline HTML strings in `EmailConnectorService.buildSmtpConfig()` (lines 164-189). They share a minimal structure: centered text "Cameleer" wordmark in `#C6820E`, a one-line message, a large verification code, and a small expiry note. No logo, no footer, no personality. + +## Design Decisions + +- **Tone:** Playful desert/caravan voice matching the sign-in page personality +- **Layout:** Structured card — amber header bar, white body with watermark, footer with separator +- **Footer:** Help link ("Questions? Contact your administrator") + tagline ("Cameleer — Apache Camel observability") +- **Header:** Text-only "Cameleer.io" centered on amber `#C6820E` bar, no logo image +- **Watermark:** Cameleer logo at ~7% opacity, oversized, positioned top-right showing compass + cameleer + camel head. For production: pre-faded PNG hosted at a public URL to avoid CSS opacity issues in Outlook desktop. +- **Storage:** External HTML template files, not inline Java strings + +## Template Structure + +All 4 templates share the same layout, differing only in subject, headline, body copy, and safety note. + +``` ++------------------------------------------+ +| [amber #C6820E header bar] | +| Cameleer.io (white) | ++------------------------------------------+ +| | +| Headline (bold, 16px) [watermark | +| Body text (14px, 1.6lh) logo at | +| 7% opacity| +| +-------------------+ | +| | VERIFICATION CODE | | +| | cream pill, amber | | +| | border, monospace | | +| +-------------------+ | +| | +| Expiry/safety note (13px, muted) | +| | ++------------------------------------------+ +| Questions? Contact your administrator | +| Cameleer — Apache Camel observability | ++------------------------------------------+ +``` + +## Copy + +| Type | Subject | Headline | Body | Safety note | +|------|---------|----------|------|-------------| +| Register | Your caravan pass is almost ready | Welcome to the caravan! | Enter this code to verify your email and claim your spot. The dunes wait for no one. | This code expires in 10 minutes. If you didn't request this, you can safely ignore this email — no camels were harmed. | +| SignIn | Your Cameleer sign-in code | Back at the oasis already? | Here's your sign-in code. The caravan master is checking credentials. | This code expires in 10 minutes. | +| ForgotPassword | Reset your Cameleer password | Lost in the dunes? | No worries — enter this code to reset your password and get back on the trail. | This code expires in 10 minutes. If you didn't request a password reset, you can safely ignore this email. | +| Generic | Your Cameleer verification code | Quick checkpoint | Here's your verification code. Just making sure it's really you at the reins. | This code expires in 10 minutes. | + +## Visual Specifications + +- **Font stack:** `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif` +- **Card max-width:** 480px, no fixed width (shrinks naturally on mobile) +- **Card border:** `1px solid #e8e0d4` +- **Header:** `background: #C6820E`, padding 20px 24px, text centered, 22px bold white +- **Code pill:** `background: #FDF6EC`, `border: 2px solid #C6820E`, border-radius 8px, padding 16px 32px, 32px bold monospace with 8px letter-spacing in `#C6820E` +- **Watermark:** Absolutely positioned `top:-30px; right:-50px`, 320x320px, 7% opacity, clipped by `overflow: hidden` on body container +- **Footer separator:** `1px solid #e8e0d4` +- **Footer text:** "Questions?" at 12px `#999`, tagline at 11px `#bbb` + +## Responsiveness + +No media queries needed. The single-column layout works from ~280px up: +- Card uses `max-width: 480px` with no fixed width +- Code pill uses `display: inline-block`, wraps within container +- All sizes in px (email clients handle rem/em inconsistently) +- Watermark clipped by `overflow: hidden`, never causes horizontal scroll + +## File Structure + +### Template files + +Create 4 HTML template files at `src/main/resources/email-templates/`: + +``` +src/main/resources/email-templates/ + register.html + sign-in.html + forgot-password.html + generic.html +``` + +Each file is a complete HTML email body (inline styles, self-contained). The verification code placeholder uses Logto's `{{code}}` syntax. + +### Watermark image + +Create a pre-faded PNG of the Cameleer logo at 7% opacity on a transparent background. Source the logo from `design-system/assets/cameleer-logo.png` and generate the faded version using ImageMagick or similar (one-time step, committed to the repo). + +Place the image at `src/main/resources/static/platform/assets/email-watermark.png`. Spring Boot serves `/platform/assets/**` as static resources automatically. The template files use a placeholder `{{watermarkUrl}}` that `EmailConnectorService` replaces with `https:///platform/assets/email-watermark.png` at runtime. + +### Java changes + +`EmailConnectorService.buildSmtpConfig()`: +- Read each template file from classpath (`src/main/resources/email-templates/*.html`) at startup or on first use +- Replace `{{watermarkUrl}}` with the configured public host URL +- Pass the HTML content as the `content` field in each Logto template config +- Keep subjects in Java (they're short strings, no benefit from externalizing) + +## Email Client Compatibility + +- **Gmail, Apple Mail, Outlook.com:** Full support — opacity, absolute positioning, border-radius all work +- **Outlook desktop (Word renderer):** CSS `opacity` is ignored. The pre-faded watermark PNG solves this — the transparency is baked into the image itself, not applied via CSS. Absolute positioning is supported via VML fallback that Outlook generates. +- **No CSS classes:** Everything uses inline styles (email clients strip `