feat(certification): surface grading rubric and appeal path in module dashboard#3405
feat(certification): surface grading rubric and appeal path in module dashboard#3405bokelley wants to merge 3 commits into
Conversation
… dashboard Adds a collapsible "About this assessment" panel to completed module cards in the certification dashboard. Dimensions are lazy-fetched from the module API on panel toggle and cached in state. A new POST /api/me/certification/review-request endpoint routes intake email to certification@agenticadvertising.org via Resend (rate-limited to 3 requests per user per 24 h). Closes #2371. https://claude.ai/code/session_01CDVH9Eh3fKShyfZ8JbScET
|
Important change to the email destination + flow: Route review requests to The plus-addressing pattern lets Addie pick it up as inbound email and turn it into an escalation thread rather than just landing in a generic inbox. Addie's inbound email is handled at What needs to happen
Why this mattersIf the email goes to a generic inbox, it gets lost. As an Addie escalation, it's:
Please update the destination address, verify the inbound handler captures |
… escalation flow Changes the review-request email destination from a generic inbox (certification@agenticadvertising.org) to addie+certification@updates.agenticadvertising.org so the inbound handler converts it into a structured Addie escalation thread. - certification.ts: update `to` address; add `Learner Email:` field to body so the inbound handler can ack the learner without a DB lookup - webhooks.ts: add `certification` context type; extend parseAddieContext to recognise `addie+certification` before the catch-all; add handleCertificationEmail that creates an addie_threads row (channel=email), an addie_escalations row (needs_human_action), and sends an ack to the learner - inbound-email-webhook.test.ts: sync AddieContext type and parseAddieContext; add four tests covering TO/CC/domain variants and the prospect-fallback guard https://claude.ai/code/session_017nGYdsDrfL6wFyV9bpLWY1
|
Done — pushed in 520ca69. Here's what changed across the three files:
One thing to verify locally before merge: confirm that the Resend inbound domain ( Generated by Claude Code |
|
Thanks — changes in The Resend inbound-domain check (confirming Generated by Claude Code |
|
Acknowledged — changes noted as received. The Resend inbound-domain check is logged as a pre-merge ops step; no further code action from this side. Generated by Claude Code |
| async function handleCertificationEmail(data: ResendInboundPayload['data']): Promise<void> { | ||
| const emailText = data.text || ''; | ||
|
|
||
| const userIdMatch = emailText.match(/^Learner ID:\s*(.+)$/m); |
| const emailText = data.text || ''; | ||
|
|
||
| const userIdMatch = emailText.match(/^Learner ID:\s*(.+)$/m); | ||
| const learnerEmailMatch = emailText.match(/^Learner Email:\s*(.+)$/m); |
|
|
||
| const userIdMatch = emailText.match(/^Learner ID:\s*(.+)$/m); | ||
| const learnerEmailMatch = emailText.match(/^Learner Email:\s*(.+)$/m); | ||
| const moduleIdMatch = emailText.match(/^Module:\s*(.+)$/m); |
| const userIdMatch = emailText.match(/^Learner ID:\s*(.+)$/m); | ||
| const learnerEmailMatch = emailText.match(/^Learner Email:\s*(.+)$/m); | ||
| const moduleIdMatch = emailText.match(/^Module:\s*(.+)$/m); | ||
| const statusMatch = emailText.match(/^Status:\s*(.+)$/m); |
|
Noted — no further code action on this end. PR stays as drafted; the Resend domain check is yours to clear pre-merge. Generated by Claude Code Generated by Claude Code |
The changeset description still referenced the original destination (certification@agenticadvertising.org). Commit 520ca69 changed it to addie+certification@updates.agenticadvertising.org; this keeps the description accurate. https://claude.ai/code/session_018ogaNYV4EqPqKtCFc6XPVX
|
Pushed one follow-up commit (2019291): the changeset description still named Remaining pre-merge ops step (cannot be done in code): in the Resend dashboard, confirm that Triaged by Claude Code. Session: https://claude.ai/code/session_018ogaNYV4EqPqKtCFc6XPVX Generated by Claude Code |
Closes #2371
What this does
Adds a collapsible "About this assessment" panel to every completed module card in the certification dashboard (
/certification). The panel surfaces:GET /api/certification/modules/:idon first toggle, cached instate.moduleDetails. Onlynameanddescriptionare shown; internal scores andscoring_guideare never exposed./ai-disclosure#review("About Addie's grading and your rights →").POST /api/me/certification/review-requestendpoint, which validates the learner has a completed assessment, then routes an intake email tocertification@agenticadvertising.orgvia Resend. A mailto fallback is shown alongside for resilience.tested_outmodules get different copy ("Completed via placement assessment — no rubric scoring was applied") and no dimension list (no scoring occurred for that path).Why this is non-breaking
assessment_criteriawas already returned by the module API; we're just surfacing it in the UI.Changes
server/public/certification.htmlrenderAssessmentDisclosure(),loadAssessmentDetails(),submitReviewRequest()functions; new CSS block for the panelserver/src/routes/certification.tsPOST /api/me/certification/review-requestroute; Resend import; per-user rate limiter (3 req / 24 h viaCachedPostgresStore).changeset/cert-rubric-appeal-disclosure.mdPre-PR expert review
Education expert (aee4e6d95e1f3ed5f): ✅ Approved with fixes applied
tested_outcopy expanded to clarify no rubric scoring occurredinnerHTMLin success state replaced with DOM-safecreateElement+replaceChildrenCode reviewer (a0f6e58b125df2924): ✅ Approved with fixes applied
border-top: 1px→var(--border-1)(design-system rule)reviewRequestLimiter(3 req / user / 24 h, Postgres-backed) to the email endpointontogglesuppressed fortested_outcards (nodims-div exists for that path)delete state.moduleDetails[modId]on fetch error so user can retrymodule_idformat validation tightened to/^[A-Z][0-9]{1,2}$/userIdvalidated as UUID before use in email bodyesc()removed from mailto href (was double-encoding);toLocaleDateString()replaced withtoISOString().split('T')[0]Triage-managed PR
This PR was created by the AdCP triage agent in response to
/triage executeon issue #2371 by @bokelley.@types/nodeerrors on main; no new errors introduced)https://claude.ai/code/session_01CDVH9Eh3fKShyfZ8JbScET
Generated by Claude Code