feat: native iOS/macOS app shells, self-hosted contact form, system/light/dark theme, and JavaChat rebrand - #161
Conversation
The message pane and messages wrapper could overflow the column flex container and push the input area out of view. Give both min-height: 0 so they shrink within the column, and pin the input area with flex-shrink: 0.
Lesson summary text in the learn view was clipped with an ellipsis, hiding the description. Allow the summary to overflow visibly, clip no text, and wrap normally so the full summary stays readable.
The welcome footer was hidden only below a fixed viewport width, so it still appeared on touch devices with wide viewports. Hide it by default and reveal it only on viewports wider than 640px that report hover: hover and pointer: fine.
Inside the native iOS and macOS shells, OAuth must round-trip through the system authentication session instead of a web redirect. When the shell injects window.javaChatNativeOAuth, pass Clerk the internal OAuth transport that delegates the redirect URL and authorization open to the native bridge; plain web sessions keep the default transport.
Each guided lesson now ends with a Read link to the authoritative source for its topic (JLS chapter, Java 25 API, or the official Spring, Quarkus, Groovy, and Clojure references), so learners land on canonical HTTPS documentation instead of searching on their own. A new GuidedLearningServiceCitationTest case walks the full table of contents and asserts every lesson publishes at least one canonical HTTPS citation without touching retrieval.
Both shells are native WebKit containers that package the hosted web product without reimplementing its UI, routing, auth semantics, or data layer. They inject the Clerk native OAuth transport at the WebKit boundary so Google, LinkedIn, and Apple authorization round-trip through the system authentication session via javachat://sso-callback. The desktop shell adds Copy Link and Open in Default Browser page actions; the iOS target is a universal iPhone and iPad app. Navigation, OAuth admission, shell state, and configuration policies are covered by the Swift test suites under __tests__/mobile.
…am guards The site had no way for visitors to reach the owner without exposing an external mailto or a third-party form, and an unauthenticated public inbox needs abuse resistance. This adds a public POST /api/contact endpoint that delivers submissions over self-hosted SMTP (STARTTLS submission port) while silently dropping bots so they learn nothing from the response. - Add spring-boot-starter-mail dependency and spring.mail.* properties; SMTP credentials stay environment-only (SPRING_MAIL_USERNAME/PASSWORD) per the secrets policy, never in tracked properties - Add AppProperties.Contact with validated recipient/sender email routing - Add RequiredMailCredentialValidation that fails fast in prod when SMTP credentials are absent, so an unsendable inbox never accepts messages - Add ContactSubmission record owning every field invariant at the application boundary (length bounds, RFC 822 email, line-break/header- injection rejection) - Add ContactSubmissionUseCase with honeypot field, render-time trap, and per-IP fixed-window rate limit (Caffeine cache); spam drops answer 202 identically to real sends while genuine MailExceptions propagate - Add ContactMessageAcknowledgement sealed-permit member and ContactController mapping 202/400/429/500; ContactMessageRequest carries raw transport values - Add ContactControllerTest and RequiredMailCredentialValidationTest covering delivery, silent spam drops, rate limiting, header-injection, and fail-fast
…ta and sitemap entry The privacy policy needed a public, indexable URL with crawlable metadata, but the SPA shell and SEO controller only knew about /chat, /guided, and /learn, so the page would land on a 404 and stay out of the sitemap. This adds /privacy to the view-controller forwards, the SEO metadata map, and the sitemap's public routes so crawlers and direct links resolve correctly. - Register /privacy as an SPA view-controller forward in WebMvcConfig - Add a Privacy Policy PageMetadata entry and add /privacy to the SeoController @GetMapping path set so crawlers receive dedicated title/description/og metadata - Add /privacy to SitemapController PUBLIC_ROUTES - Extend SeoControllerTest, SitemapControllerTest, and BrowserErrorResponseIntegrationTest to cover the privacy metadata, sitemap loc, and SPA forwarding
…submission The SPA had no privacy policy view and no support contact surface, and the new /api/contact backend needs a client that validates before sending and maps the fixed response contract onto explicit UI states. This adds the privacy page as a third application view and a modal contact dialog that submits through the validated contact service. Header and App carry both changes together because the nav bar renders the privacy tab and the contact trigger, and App renders both the privacy view and the dialog. - Add PrivacyPage with the current data-practices policy and a contact aside, plus a PrivacyPage test asserting the effective date, providers, and absence of stale copied language - Add the privacy view to the pageMetadata catalog (with descendant-path recovery staying on chat) and extend App to render PrivacyPage and its tests for the direct /privacy route - Add ContactSubmissionSchema (with honeypot and renderedAt trap) and ContactAcceptedSchema to the single validation source of truth - Add contact service that POSTs to /api/contact with CSRF retry and maps 202/400/429/5xx and network failure onto a discriminated union, never swallowing errors or assuming a malformed acknowledgement succeeded - Add ContactFormDialog (native dialog, honeypot field captured at open, inline validation, success/rate-limit/rejected states) and its tests - Add privacy and contact nav buttons to Header with an onContactOpen callback prop, and extend Header tests for the new states and trigger
…orial text The thinking indicator truncated every status line (nowrap + ellipsis inside a 320px card) and stacked five ambient animations (bouncing dots, shimmer bar, pulsing avatar, wiggling phase icons), so retrieval statuses like the reranker and documentation-search notices were unreadable. Renders the pending state as chromeless typography in the message flow: full-wrapping status and detail lines, a static avatar optically aligned to the first line, and one slow text shine as the only live cue. - Add role="status" with aria-live for screen readers - Fall back to the phase default when the idle empty-string status would render a blank message - Update streaming-stability tests to assert the rendered status text instead of the removed data-phase hook
The app was dark-only, and citation badges keyed off the raw OS media query, so no manual scheme choice could exist, let alone stay in sync. Adds a header toggle (System/Light/Dark) persisted to localStorage, applied pre-paint by an inline boot script in index.html, and a full light "Warm Precision" palette behind :root[data-theme="light"]. Citation badges and syntax highlighting now follow the data-theme attribute instead of prefers-color-scheme. - ThemePreferenceSchema (Zod) validates the persisted preference - Compact header: icon-only nav and sign-in on narrow viewports so brand, nav, toggle, and auth controls fit within 360px - Toast shadow uses the themeable --shadow-xl token - global.css also drops the loading-dot/bounce animation tokens obsoleted by the status-indicator redesign (shared file; indicator landed separately)
Both shells hardcoded a dark interface (dark toolbar color scheme, white toolbar icons, dark-only body color). Adds a shared AppAppearance enum (System/Light/Dark) persisted via @AppStorage, wired through preferredColorScheme on iOS and macOS, an iOS Settings.bundle pane, a macOS Settings scene, an adaptive JavaChatBody color, and appearance-aware toolbar styling. - iOS: Settings.bundle multi-value pane plus AppAppearanceTests - macOS: radio-group Settings scene; toolbar follows the selected scheme
The contact form sends mail per-request and surfaces send failures at request time; SMTP reachability is not an application health dependency. An unreachable SMTP host (e.g. dev environments without a mail server) otherwise flips the aggregate health check to DOWN and trips monitors even though the app is fine. Disables the mail health indicator via management.health.mail.enabled=false.
…tions
oxlint's type-aware no-unsafe-type-assertion rule flagged two assertions
narrowing the unknown rejection to { cause?: unknown }.
Asserts the cause through vitest's toHaveProperty instead, which checks the
property on an unknown value without a cast and keeps the same identity
expectation.
…ss-tab sync The adversarial review of the theme toggle surfaced defects the dev-server dogfood could not catch: the inline boot script violated the backend's script-src 'self' Content-Security-Policy, so the no-flash mechanism would have died silently in production; an unguarded localStorage read could prevent the SPA from mounting in hardened privacy modes; and Clerk's primary button failed WCAG AA contrast in the light theme. - Serve the pre-paint boot logic as a classic render-blocking asset (public/theme-boot.js) so it passes script-src 'self'; ignore the generated copy under static/ - Tolerate localStorage read/write/remove failures with logged warnings and session-only theming instead of a boot crash or torn toggle state - Synchronize the preference across tabs via the storage event - Add --color-accent-foreground and use it for Clerk's colorPrimaryForeground (5.4:1 on the light accent, up from 3.0:1) - Return focus to the trigger when the mobile menu closes (WCAG 2.4.3) and drop the mismatched aria-haspopup from the disclosure - Route the new-content glow and error-bubble tint through color-mix tokens so they track each theme's accent/error colors - Guard boot-asset/composable constant drift with a parity test
With the header now carrying the settings menu, the web content could slip under the home indicator and horizontal safe areas, making the composer and header controls hard to reach on edge-to-edge devices. - Move ignoresSafeArea(.container) to the shell level and include the bottom edge so layout insets come from the web document itself - Stop the scroll view from double-applying the adjustment with contentInsetAdjustmentBehavior = .never
…to a menu The header could not fit four nav tabs, a theme toggle, and auth controls within a 360px row, and the contact form lived in a dialog that crawlers and deep links could not reach. A single settings-and-pages menu now holds the color-scheme choice and the Privacy/Contact links on every viewport, and Contact becomes a real /contact route with SEO metadata and sitemap entry. - Add HeaderMenu: hamburger trigger + panel with the System/Light/Dark color-scheme rows and crawlable Privacy/Contact anchors, closing on outside click and Escape with focus returned to the trigger - Replace ContactFormDialog with ContactPage rendered at the new contact application view; Privacy/Contact links navigate through real hrefs - Trim main navigation to the learning surfaces (Chat, Learn) - Subsume ThemeToggle into HeaderMenu and delete the standalone component - Forward /contact to the SPA in WebMvcConfig, add SeoController metadata, and list it in the sitemap
Spreading the auth controls and the menu trigger as separate flex `space-between` children let the signed-in avatar drift across the header on wide viewports. Wrap both in a single `.header-actions` flex cluster so the avatar sits immediately left of the menu trigger at every viewport width. - Add `.header-actions` wrapper grouping auth controls and `HeaderMenu` - Add `.header-actions` flex layout (align-items center, gap)
The App Store record and web app metadata used inconsistent spellings, letting SEO snippets and install prompts show "Java Chat" while the brand is JavaChat. Unify every user-facing metadata surface to the JavaChat spelling and refresh descriptions to name the flagship guided-lesson topics so search snippets carry the curriculum keywords. - Rename title/site-name/application-name to JavaChat across index.html, site.webmanifest, and pageMetadata.ts - Rewrite default and guided-lesson descriptions to cite JDK docs and Spring Boot, Quarkus, Kotlin, virtual threads, records, and pattern matching - Update the global.css design-system banner to JavaChat
…avaChat The privacy policy body still used the legacy "Java Chat" spelling while the app metadata and header now read JavaChat, leaving the legal page inconsistent with the rest of the surface. Update every body reference and the matching test description to JavaChat. - Replace "Java Chat" with JavaChat throughout PrivacyPage.svelte copy - Update PrivacyPage.test.ts description string to JavaChat
…ports The header brand still read "Java Chat" and the nav sat in a `space-between` flex row, so Chat/Learn were only centered when the brand and action clusters happened to match widths. Rename the brand to JavaChat and switch the header to a three-column grid so the nav stays optically centered on mobile, tablet, and desktop.
…t" to JavaChat The backend-served site.webmanifest still carried the legacy "Java Chat" name while the frontend copy and the rest of the app metadata now use JavaChat, leaving the installed PWA's name inconsistent with its source of truth. Match the frontend rebrand so both copies agree. - Rename name and short_name from "Java Chat" to JavaChat in src/main/resources/static/site.webmanifest
|
Important Review skippedToo many files! This PR contains 228 files, which is 128 over the limit of 100. To get a review, narrow the scope: Upgrade to a paid plan to raise the limit. This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (12)
📒 Files selected for processing (228)
You can disable this status message by setting the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7fd693caff
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Concurrent submissions from one IP could each observe the counter below the hourly allowance and all send before any increment landed, exceeding the advertised per-IP limit by an arbitrary burst. Reserve one slot with a single atomic increment before sending and release it when the reservation exceeds the allowance or delivery fails, so the check and increment cannot interleave. Flagged by Codex review on PR #161.
The ${SPRING_MAIL_HOST:...}/${SPRING_MAIL_PORT:...} placeholders declared a
new env-var-driven settings contract; repository policy [EV1c] keeps non-secret
defaults in Spring property files. Plain defaults suffice because Spring
relaxed binding already maps SPRING_MAIL_HOST/SPRING_MAIL_PORT onto
spring.mail.host/spring.mail.port, the same mechanism the credentials use.
Flagged by Codex review on PR #161.
SeoController still emitted the old "Java Chat" titles, descriptions, and JSON-LD name, so crawlers and social-preview clients that never run the SPA saw stale branding. Mirror the frontend pageMetadata catalog so the HTTP boundary and the SPA emit identical metadata for every routed path. Flagged by Codex review on PR #161.
The unconditional preventDefault() suppressed the anchor's new-tab behavior, so Command/Ctrl-clicking Privacy or Contact replaced the current SPA view instead of opening a new tab. Intercept only unmodified primary-button clicks so the real hrefs retain standard browser navigation semantics. Flagged by Codex review on PR #161.
Summary
JavaChat now ships as native iOS and macOS apps wrapping the hosted web product, gains a self-hosted contact form and public privacy page, supports a user-selectable system/light/dark theme, and rebrands from "Java Chat" to JavaChat across the UI and app metadata.
Changes
Features
mobile/iosApp/JavaChat/,mobile/desktopApp/JavaChatDesktop/)javachat://sso-callbackinstead of a web redirect; plain web sessions keep the default transport (clerkAuthentication.svelte.ts,OAuthPolicy.swift,WebViewCoordinator.swift)/api/contactdelivered over self-hosted SMTP (STARTTLS), with a honeypot field, render-time trap, per-IP rate limiting, and header-injection rejection — spam drops answer 202 identically to real sends so bots learn nothing (ContactController,ContactSubmissionUseCase,ContactSubmission,ContactPage.svelte)themePreference.svelte.ts,public/theme-boot.js,global.css)/contactroute with SEO metadata and sitemap entry instead of an uncrawlable dialog (HeaderMenu.svelte,WebMvcConfig,SeoController)/privacyrenders through the SPA shell with dedicated crawler metadata and a sitemap entry (PrivacyPage.svelte,SeoController,SitemapController)AppAppearance.swift,Settings.bundle/Root.plist)src/main/resources/guided/lessons/*.md,GuidedLearningServiceCitationTest)Bug Fixes
/actuator/healthto DOWN and trips monitors in environments without a mail server — mail reachability is not an application health dependency since send failures surface at request time (application.propertiesmanagement.health.mail.enabled=false)script-src 'self'Content-Security-Policy in production; it is now served as a classic render-blocking asset (public/theme-boot.js,index.html)themePreference.svelte.ts)role="status"for screen readers (ThinkingIndicator.svelte)ChatView.svelte)LearnView.svelte)JavaChatWebView.swift,WebViewCoordinator.swift)Header.svelte)WelcomeScreen.svelte)Refactoring
Header.svelte,pageMetadata.ts,PrivacyPage.svelte,static/site.webmanifest)~/.agents/skillslocation, removing ~180 duplicated files from the repository (.agents/skills/clerk-*)Breaking Changes
None
Test Coverage
ContactControllerTest,RequiredMailCredentialValidationTest,GuidedLearningServiceCitationTest,SeoControllerTestContactPage.test.ts,HeaderMenu.test.ts,themePreference.svelte.test.ts,contact.test.ts,App.test.ts__tests__/mobilecovering navigation policy, OAuth admission, shell state, and configuration