diff --git a/.forge/features/blade-refinements/spec.md b/.forge/features/blade-refinements/spec.md new file mode 100644 index 000000000..614c7aa8f --- /dev/null +++ b/.forge/features/blade-refinements/spec.md @@ -0,0 +1,245 @@ +# Blade Refinements Spec + +Status: Complete — approved for implementation + +## User-facing purpose + +Blade should feel like one coherent Knight Hacks product rather than a collection +of admin pages and a separate mobile member experience. Navigation should be +deliberate, compact, and understandable; member pages should prioritize the next +useful action; admin pages should spend their space on the actual workspace; and +common failures should explain the real problem without disrupting the user. + +This refinement slice also closes several concrete production defects, improves +issue collaboration and attribution, adds managed screenshots to issue +descriptions, and completes the Forms presentation model with a top banner image. + +## Users / actors + +- Ordinary authenticated members, including dues-paid and unpaid members. +- Members completing initial signup or editing an existing profile. +- Administrators with one or more Blade capabilities. +- Officers with access to all administration destinations. +- Form editors and authenticated form respondents. +- Issue readers, editors, assignees, and Discord reminder recipients. +- System actors represented in issue history or the Admin log. +- Unauthenticated visitors to the public Blade landing page. + +## User-visible interface + +### Public landing and account chrome + +- `/` remains the public Blade landing page whether or not the visitor is + signed in. Its primary call to action adapts to the session: sign in when + unauthenticated and go to the member dashboard when authenticated. +- An ordinary member who has only Dashboard and Settings does not see a sidebar. + The authenticated chrome shows a Settings cog and Sign out together at the + top right. +- The product mark remains a recognizable route back to the public landing page. + +### Administrator navigation + +- Desktop administration uses a compact icon rail with an opener in the top-left + corner. Hover and focus do not expand it. +- Every collapsed icon is directly clickable and has an accessible name and + tooltip, so a user who recognizes it can navigate without opening the rail. +- Clicking the opener reveals labels and section headings. Selecting any + destination closes the expanded rail after navigation. +- Mobile retains its current drawer behavior and closes after a selection. +- External destinations use a consistent external-link marker and accessible + wording. This includes Guild in navigation and outbound Guild/social/profile + actions. +- Dashboard remains first and ungrouped. The administration map is: + - **Club:** Analytics, Members, Alumni, Companies, Events, and Event Check-in. + - **Team:** Issues, Forms, Email, Roles, Discord archive, and Admin logs. + - **Hackathon:** Hackathons, Hackers, Hackathon Events, and Hackathon Check-in. + - **External:** Guild. +- Settings and Sign out remain account utilities rather than administration + destinations. + +### Administration page hierarchy + +- A top-level admin page shows its title and relevant actions without the + visible purple eyebrow or a permanently visible descriptive paragraph. +- A title-adjacent accessible information control preserves useful page context + by placing the current page description in a tooltip. The former eyebrow is + not carried into the tooltip. +- Loading states do not reserve vertical space for the removed lines. +- Configuration panels remove subtitles that merely repeat the panel title or + available choices. Copy that explains consequences, permissions, payment, + deletion, or another non-obvious fact stays visible. + +### Member dashboard and Guild + +- Mobile and desktop present the same member sections, order, labels, and + available actions. Layout may reflow, but neither viewport is a separate + product. +- Guild remains a prominent, top-level part of the dashboard. Profile-picture, + resume, visibility, and Guild preference controls are not moved away solely + because they mutate data. +- The Guild card briefly explains that Guild is Knight Hacks' public member + directory/profile, clearly distinguishes public Guild content from private + Blade content, and marks the public Guild profile action as external. +- Avatar, visibility, name, tagline, company, links, resume controls, and empty + states remain aligned with missing as well as populated data. +- The dashboard includes a compact **Check in** surface with a **View QR code** + action on both mobile and desktop. +- An unpaid member sees a prominent dues banner near the top with a clear + payment action. +- A paid member does not see the dues banner. A compact green paid badge appears + beside `Welcome, ` and its hover/focus tooltip confirms that dues + are paid. +- Previous forms remains reachable at the bottom of the dashboard as a small, + low-emphasis history action rather than a primary tile. + +### Profile and career feedback + +- Uploading or replacing a resume during signup or from an existing profile + confirms success without automatically opening the document viewer. +- The success state offers an explicit **View** action. +- Removing a saved profile picture requires confirmation. Cancelling keeps the + picture and its saved state unchanged. +- Employment editing marks required fields, associates an error with the + precise employment entry and field, and focuses or scrolls to the first + problem. +- Legacy employment records still receive the intentional migration/confirmation + guidance, but a missing position title, experience type, company, or status is + never mislabeled as a different error. + +### Issues and audit identity + +- Typing in admin member search remains focused while results and URL state + update. +- Filtering Issues by assignee loads a valid result or a truthful empty state; + it never replaces the workspace with the generic "Issues could not be loaded" + failure for a valid selection. +- Author-entered line breaks remain visible in issue description preview and + detail views while Markdown continues to work. +- Issue editors can add managed screenshot images to a description through a + file picker, clipboard paste, or drag and drop. The image is inserted at the + description cursor. +- Managed issue images accept PNG, JPEG, WebP, and GIF files up to 10 MB each. + An issue retains at most 10 managed images. Animated GIFs are allowed and SVG + is rejected. +- Uploaded issue images appear in preview and detail, follow issue access, have + editable alternative text, and can be removed by an authorized editor. +- Issue history and Admin log surfaces prefer the linked member's current full + name. If no linked Member profile can be resolved, they fall back to the saved + Discord display name or username. Explicit system actors remain system actors. +- Existing history and log rows benefit from the same display rule; their saved + historical snapshots are not rewritten. + +### Issue reminders + +- Every issue reminder presents the Blade destination as the linked issue + title. +- When the issue has a Discord discussion thread, the title is followed by + ` | Chat`, with **Chat** linked to that thread. +- Without a thread, only the linked title is shown. Mentions, reminder cadence, + delivery idempotency, and message safety remain unchanged. + +### Forms presentation + +- Existing form instruction blocks continue to support text, uploaded images, + and browser-playable uploaded video. Editors can see that capability, upload + media, remove it, and verify its respondent presentation without needing + hidden knowledge of the builder. +- A form editor can upload, replace, or remove one banner image for a form. +- The banner appears once at the top of the respondent experience, above the + form title/description/instructions, in a responsive 4:1 frame with `cover` + cropping. The builder provides crop guidance, preview, and editable alt text. +- Draft preview and the published respondent view use the same banner behavior. +- Banner and instruction media are visible only to actors allowed to view that + form; possession of an object URL does not bypass form access. + +### Responsive and chart readability + +- Member surfaces remain usable at 320 px, common mobile widths, intermediate + desktop widths, and full desktop widths in Chromium-family browsers. +- Long member names, taglines, companies, URLs, event names, and empty-state + sentences wrap or truncate intentionally without being clipped. +- Nearby current/prior deadline labels on hackathon comparison charts remain + distinguishable. The existing textual/table alternative remains available. + +## Scope + +### In scope + +- Public landing behavior for authenticated visitors. +- Member-only header without a sidebar; explicit desktop admin rail behavior; + grouped admin information architecture; mobile close-on-select preservation. +- Central admin header compaction and repetitive configuration-copy cleanup. +- Unified member dashboard hierarchy, Guild explanation, external-link cues, + check-in surface, paid/unpaid dues treatment, and demoted form history. +- Resume success flow, photo-removal confirmation, employment validation, member + search focus, issue-assignee failure, issue newlines, responsive overflow, and + analytics label collisions. +- Managed images in issue descriptions. +- Existing form image/video instruction support verification and discoverability. +- One managed top-of-form banner image. +- Issue reminder `Title | Chat` presentation. +- Member-full-name resolution in issue history and Admin logs with Discord + fallback. + +### Out of scope + +- A Knight Hacks member-benefits page or benefits content. +- Moving all Guild/profile mutations into Settings or making the dashboard + profile read-only. +- Replacing Blade analytics with Grafana or adding observability infrastructure. +- Uploaded video in issue descriptions; arbitrary issue file attachments beyond + the managed image formats approved for this slice. +- Anonymous or public Forms behavior beyond the existing Forms platform. +- A new open-forms directory. +- Redesigning Discord reminder cadence, mentions, or delivery infrastructure. +- A browser-specific Zen workaround when standards-compliant responsive behavior + is already correct; Chrome and Zen reports are both valid reproduction inputs. + +## Vocabulary + +- `Blade`: Knight Hacks' member and administration product. +- `Guild`: Knight Hacks' public member directory and public-profile surface, + distinct from private Blade member data. +- `Icon rail`: the collapsed desktop administration navigation. +- `Instruction block`: ordered form guidance containing text, an image, or a + browser-playable video. +- `Form banner`: one editor-managed image displayed above a form's respondent + content. +- `Managed issue image`: an uploaded image owned and authorized by the issue + system rather than a permanently public third-party URL. +- `Member full name`: the linked Member profile's first and last name, not the + authentication provider/Discord username. +- `Saved actor snapshot`: the immutable fallback label recorded when a history + or audit event was written. + +## Acceptance criteria + +- Ordinary members can reach Dashboard, Settings, Sign out, and the public + landing page without seeing an otherwise two-item sidebar. +- Admins can activate collapsed icons directly, explicitly expand the rail, + understand grouped destinations, and observe it close after selection without + hover causing layout changes. +- Top-level admin workspaces reclaim the eyebrow/description height while useful + context remains keyboard- and screen-reader-accessible. +- Mobile and desktop member dashboards expose the same hierarchy and actions, + with Guild prominent and plainly defined. +- Unpaid members receive the prominent payment CTA; paid members see only the + compact paid badge beside their welcome name. +- Resume uploads across signup and existing-member flows confirm success and wait + for the user to select View; profile image deletion can be cancelled safely. +- Search, issue filtering, employment validation, Markdown line breaks, + responsive content, and comparison-chart labels pass their concrete regression + cases. +- Authorized issue editors can upload, describe, render, and remove managed + screenshots without exposing them to unauthorized users. +- Issue reminders render linked `Title | Chat` when a Discord thread exists and + linked `Title` alone otherwise. +- Issue history and Admin logs show current member full names when resolvable and + truthful fallbacks otherwise, including for pre-existing rows. +- Existing form image/video instructions remain operational and understandable; + a banner image can be managed and appears correctly in preview and response + views. + +## Open questions + +- None. The product bundle is approved for implementation. diff --git a/.forge/features/blade-refinements/srd.md b/.forge/features/blade-refinements/srd.md new file mode 100644 index 000000000..09a8463db --- /dev/null +++ b/.forge/features/blade-refinements/srd.md @@ -0,0 +1,291 @@ +# Blade Refinements SRD + +Status: Approved for implementation — technical discovery prerequisites remain + +## Technical purpose + +Refine the current production Blade shell and member/admin surfaces without +creating parallel product architectures. Fix localized regressions at their +owning boundaries, extend the existing attachment model for issue images and a +form banner, and preserve current permission, audit, Forms, and Discord delivery +invariants. + +This bundle starts from `origin/main` commit `78857b85` on branch +`forge/refinements` in `/Users/dvidal/Documents/forge-refinements`. + +## Relevant principles + +- Follow `docs/agentic-development/forge-engineering-principles.md`: make access + policy explicit, keep apps thin, place business logic in `@forge/api`, validate + at boundaries, preserve external-side-effect idempotency, and keep clients + responsive but non-authoritative. +- Follow `docs/agentic-development/frontend-design.md` and + `apps/blade/DESIGN_SYSTEM.md`: reuse Blade primitives, preserve the grid-shell + visual language, test 320 px and long-content states, and keep focus behavior + deliberate. +- This bundle supersedes the hover/focus desktop-rail decision in + `admin-member-dashboard`, the intentionally divergent mobile composition in + `mobile-member-experience`, and the prominent paid-dues/desktop-QR placement + decisions where they conflict with this spec. It does not erase their other + accepted behavior. +- The existing `forms-and-event-feedback` attachment and instruction-media + contracts remain authoritative. Extend them rather than build a second upload + system. +- The existing `club-operations-issues` contracts remain authoritative except + that native managed **image** attachments are now explicitly in scope and the + reminder link label changes from `Discuss` to `Chat`. + +## Access policy + +- `/` remains available without authentication. Session state changes its CTA, + not its visibility. +- Member Dashboard, Settings, resume/photo operations, QR, dues, previous forms, + and Guild preferences retain their current authenticated-member access. +- The shell derives ordinary-member versus admin navigation from the same + server-authoritative capability projection used today. Hiding the sidebar is + presentation only and grants no access. +- Admin routes and procedures retain their existing capability requirements. +- Issue image create/finalize/remove/read operations require the same issue edit + or read access respectively as the owning issue. A link or guessed attachment + ID is insufficient. +- Form banner upload/finalize/remove requires existing form edit access. Banner + read requires the same respondent/editor visibility required to read the + owning form in its current state. +- Issue history and Admin log reads retain their existing permissions. Name + enrichment must not expose Member data to a caller who cannot read the row. +- System actors stay nullable/non-member actors and never resolve through a + coincidental display-name match. + +## Architecture / data flow + +### Blade shell and page hierarchy + +- Keep Next.js page modules server-first. The server computes the session, + current member, and authorized admin navigation projection; focused client + components own transient rail/drawer state. +- Replace CSS `hover`/`focus-within` expansion in `AuthenticatedShell` with an + explicit desktop state. Do not persist the expanded state: selecting a link + closes it by product definition, and each full navigation begins collapsed. +- Model navigation as destination groups rather than infer sections from labels + in render components. Filter unauthorized children first and omit empty groups. +- Ordinary members render no desktop/mobile nav rail. Reuse the shared account + controls for the top-right Settings and Sign out actions. +- Link the product mark to `/`; remove the authenticated redirect from the root + page and select CTA copy/href from the session. +- Centralize compact title/help behavior in `AdminPageHeader`. Consumers continue + to provide meaningful descriptions during migration; the shared component + exposes the description alone through an accessible tooltip, does not project + the old eyebrow copy, and removes obsolete skeleton rows. + +### Member dashboard and localized regressions + +- Refactor duplicated mobile/desktop dashboard blocks into shared semantic + sections rendered once and laid out responsively. Keep Guild prominent and its + current mutation procedures authoritative. +- Derive paid/unpaid rendering from the existing dues status query. The paid + badge's tooltip is accessible by hover and keyboard focus; unpaid payment + behavior is unchanged. +- Resume upload mutations return/use the existing successful result but set an + inline success state instead of opening the viewer. The explicit View control + opens the existing preview. +- Gate saved-photo removal behind the shared confirmation dialog; do not issue + the mutation before confirmation. +- Remove the `key={input.query}` remount boundary from the admin members page and + preserve debounced URL/result updates without replacing the active input. +- Split legacy employment normalization errors from current entry validation. + Map Zod issues to entry/field UI and retain a form-level summary for assistive + technology. +- Apply issue line-break styling at the issue description renderer/preview + boundary rather than changing every `MarkdownContent` consumer. +- Reproduce the issue-assignee failure against the production-base code before + selecting a fix. Add a regression at the smallest failing UI/API boundary. +- Resolve comparison-chart label collisions through deterministic placement, + abbreviated labels, staggering, or collision-aware rendering; do not remove + the existing text alternative. + +### Managed issue images + +- Reuse the Forms attachment service/object-storage conventions where practical: + create intent, direct upload, finalize after metadata verification, reference + from the owning entity, authorize short-lived read, and durably clean abandoned + or removed objects. +- `@forge/api` owns issue access checks, upload lifecycle, ownership, cleanup + scheduling, and history/audit semantics. Blade only requests intents, uploads + bytes, finalizes, and inserts the returned stable attachment reference into the + description model. +- `@forge/validators` owns image MIME/size/count/alt-text inputs. Server-side + checks are authoritative and verify stored object metadata at finalization. +- Accept PNG, JPEG, WebP, and GIF up to 10 MB per image and at most 10 retained + managed images per issue. Allow animated GIFs and reject SVG. +- Blade supports file selection, clipboard paste, and drag/drop through one + upload pipeline, then inserts the finalized managed reference at the current + description cursor. +- Prefer an additive generic attachment row/ownership relation if the current + Forms model supports a safe new owner type. Do not store data URLs or permanent + public bucket URLs in issue Markdown. +- The rendered description resolves a managed attachment reference only after + issue-read authorization and returns a short-lived/download response. Sanitize + ordinary external Markdown URLs under the existing Markdown policy. +- Removing an image removes its description reference immediately and schedules + object cleanup only when no retained issue revision/reference owns it. Abandoned + intents expire. Issue history records a safe structural image change without + embedding signed URLs or file bytes. + +### Form banner and existing instruction media + +- The current `formDefinitionSchema.instructions` image/video blocks, builder + upload procedures, and authorized respondent rendering already satisfy the + instruction-media capability. Add focused regression coverage and improve + discoverability only; do not revive the unrelated legacy + `FormSchemaValidator.banner` URL field. +- Extend the active forms-platform definition with an optional managed banner + reference, ideally an attachment ID plus alt text. Reuse the existing form + attachment create/finalize/read/cleanup pipeline with a distinct `banner` + purpose. +- Form create/update validation verifies the banner belongs to the same form, + was finalized by an authorized editor, and is an allowed image. Replacing or + removing it schedules the old unreferenced object for cleanup. +- Draft preview and respondent acquisition both project the same authorized + banner model in a responsive 4:1 `cover` frame. The builder provides crop + guidance, preview, and editable alt text. Do not expose a raw object name, + storage key, or durable public URL to the client. +- A schema migration may be avoidable if the versioned JSON definition owns the + banner reference; confirm the current persisted form revision model before + changing `@forge/db`. Any required migration must be additive and separately + documented in the implementation plan/status before execution. + +### Actor display enrichment + +- Issue history and Admin log queries should batch-resolve non-null linked Member + IDs and project `firstName + lastName` when both/available name fields produce a + non-empty display value. +- Fall back to the immutable stored actor snapshot when no linked Member exists, + the profile was removed, or the resolved name is empty. Preserve explicit + system labels. +- Resolve at read time so historical rows improve without mutation or migration. + Avoid N+1 queries and do not join actors by Discord username/text. + +### Discord reminder projection + +- In `@forge/api` reminder presentation, render the sanitized issue title as the + Blade link and append a literal separator plus linked `Chat` only when a valid + thread URL is present. +- Preserve current Components V2 limits, allowed mentions, role/user pings, + grouping, delivery ledger, stable nonce/idempotency, and no-live-write test + defaults. + +## tRPC/API behavior + +- Existing procedures should remain backward-compatible unless a new result + field is required for attachment/banner projection. +- Add issue-image intent/finalize/remove/read procedures under the issue router or + shared attachment service, each with `.meta({ description })`, shared Zod input, + permission checks before object work, and non-enumerating `NOT_FOUND` behavior + where appropriate. +- Extend existing Forms upload procedures with `purpose: "banner"` rather than + creating an unrelated endpoint. Existing `instruction` and respondent-file + purposes remain unchanged. +- Name-enriched history/log results may add/replace the presentation label while + retaining stored snapshot data internally for fallback. Do not make clients + assemble Member names. +- The issue assignee filter fix must preserve pagination and all other filters; + capture the failing query before changing it. +- Business logic remains tRPC/platform logic. Do not add REST business endpoints. + +## Validation + +- Use shared Zod validators for navigation group shape where shared, active form + definition/banner data, attachment lifecycle inputs, image policy, and actor + projection outputs. +- Keep client file `accept` hints aligned with server policy, but validate size, + MIME, owner, authorization, and finalized state on the server. +- Normalize and reject empty alt text for managed images unless the approved UI + explicitly supports a decorative-image flag. +- Preserve existing issue title/description, employment, Forms, and reminder + limits. Newline presentation does not alter stored description text. +- Validate all external/thread URLs before Discord projection and escape user + text under existing message safety rules. + +## Data / migration / compatibility + +- Navigation, headers, member composition, local regressions, and reminder label + changes require no data migration. +- Actor display enrichment is read-time only and preserves all immutable history + and audit rows. +- Managed issue images require durable owner/reference metadata and cleanup + state. Prefer extending the existing attachment schema; if a new owner relation + or enum requires a migration, make it additive and keep old issues readable. +- Form banner references should live in the active versioned definition when that + maintains historical response rendering. Never reuse the stale legacy banner + URL shape as authority without a compatibility analysis. +- Old forms with no banner and old issues with external Markdown images remain + readable. Rollback hides new authoring controls but must not corrupt retained + references or make existing issue/form pages fail. + +## Discord integration + +- No role, permission, guild, cadence, or channel configuration changes. +- Guild links in Blade are visually identified as external, but Guild remains an + intentional prominent member feature. +- Reminder content changes only from the previous secondary link wording/layout + to linked `Title | Chat` when a thread exists. +- Actor name presentation in Blade does not rewrite Discord messages or Discord + identities. + +## Configurability review + +Would this require a developer change next year? + +- Answer: Navigation groups and stable platform destinations remain code-owned; + form banners, instruction media, issue images, dues state, profile content, and + names are data-driven. +- Hard-coding the information architecture is acceptable because destinations + and permission enums ship with code. Adding a new admin domain should require + assigning its route to a group in the same navigation registry. +- Upload limits should use shared named policy constants rather than scattered UI + literals once the human approves them. + +## React / frontend constraints + +- Do not add `"use client"` to route pages. Keep transient rail, tooltip, dialog, + upload, and focus behavior in the smallest existing client component. +- Use existing `@forge/ui` Button, Tooltip, Dialog/AlertDialog, Badge, Skeleton, + and Markdown primitives. Promote a new primitive only if a second real consumer + justifies it. +- Preserve semantic navigation, `aria-current`, accessible icon labels, keyboard + activation, focus visibility, tooltip focus support, dialog focus return, and + reduced-motion behavior. +- No hover-only action may be required. Rail opener, collapsed destinations, + paid tooltip, info tooltip, uploads, and removal remain keyboard-accessible. +- Avoid parallel mobile/desktop component trees for the member dashboard. Use + responsive CSS around one semantic composition. +- Test long strings and 320 px; do not rely on `overflow-x-hidden` to conceal + layout bugs. +- Reserve media geometry to avoid layout shift and provide loading, broken-media, + upload progress, success, and failure states. + +## Testing / verification strategy + +- Component/unit tests in `apps/blade` for header semantics, navigation state and + grouping, member dues/dashboard composition, resume/photo behavior, employment + errors, member-search focus, issue Markdown/images, Forms banner/instruction + media, and responsive class/DOM contracts. +- `@forge/api` integration/unit tests for issue filter reproduction, attachment + authorization/lifecycle, form banner ownership, actor enrichment, and reminder + formatting/idempotency preservation. +- Validator tests for managed-image and active form-definition banner shapes. +- Playwright desktop, intermediate-width, and 320 px journeys with long-content + fixtures, ordinary member/admin actors, paid/unpaid states, and authorized/ + unauthorized media access. +- Run focused package tests while implementing; then React analyzer for changed + frontend surfaces, `pnpm verify:precommit`, relevant reviewers selected from + the diff, and final `git diff --check`. +- No live Discord write during automated tests. Any staging reminder smoke must + be separately intentional and retain the approved no-ping safety path. + +## Open questions + +- Before implementation, confirm whether the active attachment schema can accept + Issue and Form-banner owner types without a database migration. +- Reproduce and record the exact issue-assignee failure before selecting its + repair. diff --git a/.forge/features/blade-refinements/status.md b/.forge/features/blade-refinements/status.md new file mode 100644 index 000000000..401492179 --- /dev/null +++ b/.forge/features/blade-refinements/status.md @@ -0,0 +1,253 @@ +# Blade Refinements Status + +Current phase: Bundle approved / ready for technical discovery + +## Decision log + +- 2026-08-12: Start from production `origin/main` in the isolated + `/Users/dvidal/Documents/forge-refinements` worktree at `78857b85`. +- 2026-08-12: Forge is the current product/project name. Reforge is retired and + must not be used for new branch or workflow naming. The shared branch is + `forge/refinements`. +- 2026-08-12: Use one `blade-refinements` bundle for the long refinement slice; + the human explicitly waived the preference to split unrelated small changes. +- 2026-08-12: Desktop admin navigation uses a top-left explicit opener, never + hover expansion. Collapsed icons navigate directly; an expanded rail closes + after selection. Mobile retains close-on-select. +- 2026-08-12: Ordinary members with only Dashboard and Settings get no sidebar; + Settings and Sign out live together at the top right. +- 2026-08-12: Admin destinations are grouped by product domain rather than + alphabetical order. The approved map is Club (Analytics, Members, Alumni, + Companies, Events, Event Check-in), Team (Issues, Forms, Email, Roles, Discord + archive, Admin logs), Hackathon (Hackathons, Hackers, Hackathon Events, + Hackathon Check-in), and External (Guild), with Dashboard first and ungrouped. +- 2026-08-12: `/` stays public for authenticated visitors and adapts its CTA. +- 2026-08-12: Unpaid dues stays prominent at the top. Paid dues becomes a green + badge beside the Welcome name with a hover/focus tooltip; no paid banner. +- 2026-08-12: Guild remains a prominent top-level member capability, and its + profile/photo/resume/preferences controls are not moved into Settings as part + of this slice. The dashboard must instead explain what Guild is and which data + is public/external. +- 2026-08-12: Resume upload success must not auto-open preview and applies to + signup plus existing-member flows. Success retains an explicit View action. +- 2026-08-12: Previous forms remains at the bottom as a small, low-emphasis + action. +- 2026-08-12: Managed issue screenshot uploads are in scope despite the earlier + `club-operations-issues` non-goal. The approved editor supports file picker, + clipboard paste, and drag/drop with cursor insertion. It accepts PNG, JPEG, + WebP, and GIF up to 10 MB each and 10 retained images per issue; animated GIFs + are allowed and SVG is rejected. +- 2026-08-12: Forms already supports managed image/video instruction blocks in + the active production platform. This slice verifies/polishes that behavior and + adds the genuinely missing managed top banner image. +- 2026-08-12: Issue reminders use linked `Title | Chat`; Chat is omitted when no + Discord thread exists. +- 2026-08-12: Issue history and Admin logs prefer linked current Member full + names at read time, fall back to saved Discord identity, preserve system actors, + and do not rewrite historical rows. +- 2026-08-12: Member benefits and Grafana are deferred. +- 2026-08-12: Remove visible admin-page eyebrows/descriptions centrally. Preserve + the current description alone through an accessible title tooltip, omit the + eyebrow from that tooltip, and reclaim matching skeleton space. +- 2026-08-12: Treat the Chrome and Zen clipping reports as responsive defects; + do not dismiss them as browser-specific without standards-compliant evidence. +- 2026-08-12: Form banners use a responsive 4:1 `cover` frame with builder crop + guidance, preview, and editable alt text. +- 2026-08-12: The human approved all five remaining product decisions and the + spec/SRD/test-case bundle for implementation. +- 2026-08-12: Contributors may implement directly on the shared + `forge/refinements` branch for this agent-first trial. +- 2026-08-12: Treat agent-assisted development as a first-class contribution + experience. Agent use is encouraged, while each contributor remains responsible + for the scope, code, decisions, validation, and result they submit. The bundle + exists to reduce errors and miscommunication between developers and agents. +- 2026-08-12: The contributor entrypoint is the feature bundle. A developer may + point an agent at it and let the agent load the repository instructions, + relevant skills, and code context. Humans may read those materials themselves + if useful, but are not expected to study agent skill files before contributing. + +## Open questions + +1. Technical discovery: confirm whether the existing attachment schema can gain + Issue/Form-banner owners without a database migration. +2. Technical reproduction: capture the exact production-base issue-assignee + filter failure before selecting a repair. + +## Contributor coordination + +- `forge/refinements` is the shared implementation branch and bundle source of + truth. Contributors work and push directly on this branch for now. +- This is an agent-first development trial. Contributors are encouraged to use + coding agents as real collaborators, not as an afterthought. The approved + artifacts give the developer and agent the same product, technical, and test + contract before code changes begin. +- The simplest way to start is to point an agent at + `.forge/features/blade-refinements/`, give it the refinement IDs being claimed, + and tell it to follow the repository's agent instructions. The agent should + read the bundle, `AGENTS.md`, and any relevant repo skills or code context it + needs. Contributors can read the bundle and supporting material themselves if + they are curious or prefer to work without an agent; they are not expected to + read skill files manually. +- Agent assistance does not transfer ownership. The contributor is responsible + for understanding and reviewing the changes, keeping the work in scope, asking + when the contract is unclear, and verifying the result. +- Before editing implementation code, announce the refinement IDs and expected + file areas you intend to claim in the shared development thread. Replace + `Unclaimed` below with your name/handle and claim date, set the row to + `In progress`, and make the claim visible to the other contributors. +- If someone already owns an overlapping row or file set, coordinate before + continuing. +- Keep changes limited to the claimed rows. If implementation reveals a spec, + SRD, test-case, access, upload, or schema conflict, stop and record it under + Open questions instead of silently changing the approved contract. +- Update each row to `In progress`, `Blocked`, or `Complete` as work moves, and + record the checks and visual evidence used to verify completed work. + +## Refinement inventory + +| ID | Refinement | State | Claim | Proof | +| ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ----------------------- | ------------------------------ | +| R-01 | Keep `/` public for signed-in users, adapt its CTA, and make the product mark return there. | Ready | Unclaimed | TC-001 | +| R-02 | Remove the sidebar for ordinary members; place Settings and Sign out together at the top right. | Ready | Unclaimed | TC-002 | +| R-03 | Replace hover expansion with a top-left admin rail opener; keep collapsed icons clickable, close after selection, and preserve mobile close-on-select. | Ready | Unclaimed | TC-003, TC-NEG-001 | +| R-04 | Group admin destinations into the approved Club, Team, Hackathon, and External map; omit empty groups and mark Guild/outbound destinations as external. | Ready | Unclaimed | TC-004 | +| R-05 | Remove visible admin eyebrows/descriptions, expose description-only title help, and shrink matching skeletons. | Ready | Unclaimed | TC-005 | +| R-06 | Remove repetitive configuration subtitles while preserving consequential guidance. | Ready | Unclaimed | TC-005 | +| R-07 | Use one member-dashboard hierarchy and action set across mobile and desktop. | Ready | Unclaimed | TC-007, TC-020 | +| R-08 | Keep Guild prominent and editable; define Guild, separate public Guild data from private Blade data, and mark its public actions as external. | Ready | Unclaimed | TC-007, TC-008 | +| R-09 | Replace the isolated QR action with a compact Check in surface and View QR code action on every viewport. | Ready | Unclaimed | TC-007 | +| R-10 | Keep unpaid dues prominent; replace the paid tile with a green paid badge and accessible tooltip beside the Welcome name. | Ready | Unclaimed | TC-006 | +| R-11 | Keep Previous forms as a small, low-emphasis action at the bottom of the dashboard. | Complete | hector1128 (2026-08-14) | TC-007 | +| R-12 | Align sparse and populated Guild/profile content and handle long names, links, companies, filenames, events, and empty states without clipping. | Ready | Unclaimed | TC-008, TC-020 | +| R-13 | Change resume upload/replace in signup and existing-member flows to success plus explicit View, without automatic preview. | Ready | Unclaimed | TC-009 | +| R-14 | Require confirmation before removing a saved profile picture. | Complete | hector1128 (2026-08-14) | TC-010 | +| R-15 | Mark employment fields required and report/focus the precise invalid entry and field without mislabeling legacy validation. | Ready | Unclaimed | TC-011 | +| R-16 | Preserve admin member-search focus and keystrokes while debounced results and URL state update. | Ready | Unclaimed | TC-012, TC-NEG-001 | +| R-17 | Reproduce and fix the Issue assignee filter failure without breaking other filters, pagination, or access policy. | Discovery | Unclaimed | TC-013 | +| R-18 | Preserve author-entered issue-description line breaks in preview/detail without changing unrelated Markdown consumers. | Ready | Unclaimed | TC-014 | +| R-19 | Add authorized managed issue images through picker, paste, and drag/drop with cursor insertion, alt text, approved limits, rendering, removal, and cleanup. | Discovery | Unclaimed | TC-015, TC-NEG-002, TC-NEG-003 | +| R-20 | Prefer linked current Member full names in Issue history and Admin logs; fall back to stored Discord labels and preserve system actors. | Ready | Unclaimed | TC-016, TC-NEG-003 | +| R-21 | Render issue reminders as linked `Title \| Chat` when a Discord thread exists and linked title alone otherwise. | Complete | hector1128 (2026-08-14) | TC-017 | +| R-22 | Prevent overlapping current/prior hackathon comparison labels while preserving the accessible text/table alternative. | Ready | Unclaimed | TC-021 | +| R-23 | Fix reported Chrome/Zen member and shell overflow at 320 px, intermediate widths, and desktop without hiding content behind overflow rules. | Ready | Unclaimed | TC-020 | +| R-24 | Verify and polish existing Forms text/image/video instruction-card authoring, upload feedback, ordering, cleanup, and respondent rendering. | Ready | Unclaimed | TC-018, TC-NEG-003 | +| R-25 | Add one managed form banner with upload/replace/remove, editable alt text, preview guidance, and responsive 4:1 `cover` presentation. | Discovery | Unclaimed | TC-019, TC-NEG-002, TC-NEG-003 | +| R-26 | Knight Hacks member-benefits content/page. | Deferred | Unclaimed | Out of scope | +| R-27 | Grafana analytics replacement or observability infrastructure. | Deferred | Unclaimed | Out of scope | + +## Task list + +- [x] Fetch current `origin/main` and create the isolated worktree/branch. +- [x] Review repository skills, engineering principles, design guidance, existing + feature artifacts, screenshots, issue reports, and production code paths. +- [x] Instantiate `.forge/features/blade-refinements/`. +- [x] Record the human's eight scoping/product decisions and later identity/ + reminder asks. +- [x] Discover that form image/video instruction media already exists and isolate + form banner as the new capability. +- [x] Draft `spec.md`, `srd.md`, and `test-cases.md`. +- [x] Resolve the five focused product questions and amend all artifacts. +- [x] Human approves the artifact bundle before implementation/test generation. +- [ ] Reproduce issue-assignee filter failure and document evidence. +- [ ] Inspect attachment schema compatibility and document migration/no-migration + decision before any schema change. +- [ ] Contributors announce and record refinement-ID claims before editing + implementation code. +- [ ] Implement claimed slices in checkpoints and keep the inventory current. +- [ ] Run focused verification, React analysis, `pnpm verify:precommit`, derived + reviewers, browser QA, and `git diff --check`. + +## Validation / commands + +- `git fetch origin main`: fetched production base successfully. +- Isolated worktree creation from `origin/main`: created + `/Users/dvidal/Documents/forge-refinements` at `78857b85`. +- `/Users/dvidal/Documents/forge/node_modules/.bin/tsx scripts/create-forge-feature.ts blade-refinements "Blade Refinements"`: + created the four-file bundle. The fresh worktree has no local `node_modules`, so + the already-installed runtime from the source worktree was used without + changing dependency files. +- Code/artifact inspection: confirmed centralized admin headers, hover/focus rail, + ordinary member/admin projections, dashboard duplication, resume auto-preview, + photo deletion without confirmation, search remount, employment error collapse, + current issue reminder/thread data, actor snapshots, existing Forms instruction + uploads, and lack of an active managed Forms banner field. +- `/Users/dvidal/Documents/forge/node_modules/.bin/prettier --no-config --write +.forge/features/blade-refinements/*.md`: all four Markdown files formatted; the + repository config could not resolve from a fresh worktree without its own + `node_modules`, so this artifact-only pass used Prettier defaults. +- `git diff --check --no-index /dev/null ` for all four files: passed. +- Direct Git object verification: worktree `HEAD` and `origin/main` both resolve + to `78857b85`; that commit contains the Reforge shell, `.forge` artifacts, + issue reminder code, and Forms platform inspected by this bundle. +- 2026-08-12 approval sync: updated all four artifacts with the five approved + decisions, formatted them with Prettier, confirmed no stale product-question + placeholders remain, and reran per-file whitespace checks successfully. +- `pnpm install --frozen-lockfile`: passed in the refinement worktree; pnpm + reported only the repository's existing warning about moving `pnpm.overrides` + to the newer settings location. +- `pnpm format`: passed across all 23 packages. +- `pnpm lint`: passed across all 27 tasks with 0 errors. Existing repository + size/style warnings remain and are unrelated to this artifact-only commit. +- `pnpm typecheck`: passed across all 29 tasks. +- `pnpm verify:push`: passed after the contributor inventory and coordination + instructions were added. It reran format, lint, and typecheck successfully. + +- 2026-08-14: R-14 implemented. Confirmation now gates saved-photo removal in + `MemberProfilePictureUpload` (used by the member dashboard and settings + page), which is the only shared component behind that surface. The signup + flow's `saveMode="deferred"` remove path is unaffected, since nothing is + saved yet there and the SRD scopes confirmation to saved-photo removal. + Cancel closes without a mutation call; confirm runs the existing + `saveMemberProfilePicture` mutation once and returns focus to the upload + file input (the only control still present after the remove button + unmounts). Checks run: `pnpm format` (pass), `pnpm --filter=@forge/blade +typecheck` (pass), `pnpm --filter=@forge/blade lint` (0 errors; pre-existing + unrelated file/function-length warnings only), `pnpm analyze:react:changed` + (pass, prop API unchanged), and the two existing Vitest files that + reference this component (`member-profile-settings-form.test.tsx`, + `member-dashboard.test.tsx`, 8/8 passing — both mock the component and + don't cover the new confirm flow). No automated browser/E2E verification + was performed. Manual verification in a real logged-in session is still + recommended before merge. +- 2026-08-14: `forge/refinements` was rebased onto latest `main` (through + `dfac35c1`, "Adding Leetcode and /eightball responses #487") and + force-pushed to origin at the human's explicit request, replacing an + already-pushed merge commit (`9583bdb5`, "Merge branch 'main' into + forge/refinements") with a linear history so `rename refinements branch +for forge` is the branch tip again. The merged content was verified + byte-identical before rewriting history. Any other local clone of this + branch (e.g. the `dvidal` worktree referenced below) will need to reset to + the new tip rather than merge/pull normally. +- 2026-08-14: R-11 confirmed already satisfied by existing dashboard code + (both the desktop `Card` and the mobile `lg:hidden` block already render + Previous forms as a small `dashboardNestedSurfaceClass` row with a `size="sm"` + outline button) and marked Complete; no code change was needed. +- 2026-08-14: R-21 implemented in `packages/api/src/utils/issues/reminders.ts` + (`targetBlock`). The Discord discussion link changed from ` · [Discuss](url)` + to ` | [Chat](url)`, appended after the bold linked-title heading only when + `target.discordThreadUrl` is set; omitted entirely otherwise. No other + reminder builder (events, hackathon events) shares this pattern, so the + change is scoped to issue reminders only. Existing `[Discuss]` assertions in + `packages/api/src/tests/issues/reminders.test.ts` were updated to `[Chat]`, + and a new `TC-017` test was added covering both the threaded and + threadless cases with a title containing a raw mention and a line break, to + confirm the existing sanitize/escape behavior around the new separator is + unchanged. Checks run: `pnpm --filter=@forge/api typecheck` (pass), + `pnpm --filter=@forge/cron typecheck` (pass, only consumer of + `deliverIssueReminders`), `pnpm --filter=@forge/api lint` (0 errors, + pre-existing unrelated file-length warning only), and + `packages/api/src/tests/issues/reminders.test.ts` (7/7 passing). No message + was sent to a live Discord channel; the cron job that calls this + (`apps/cron/src/crons/issue-reminders.ts`) was not run. + +## Links + +- Triage artifact: + `/Users/dvidal/Documents/Codex/2026-08-12/pleas/outputs/forge-refinements-triage.md` +- Worktree: `/Users/dvidal/Documents/forge-refinements` +- Branch: `forge/refinements` +- Remote branch: `https://github.com/KnightHacks/forge/tree/forge/refinements` +- Feature bundle: `.forge/features/blade-refinements/` +- PRs: none +- Issues: GitHub #503, GitHub #504, and the supplied Blade issue/thread context +- Discord/thread context: supplied in the Codex task on 2026-08-12 diff --git a/.forge/features/blade-refinements/test-cases.md b/.forge/features/blade-refinements/test-cases.md new file mode 100644 index 000000000..80851b3d7 --- /dev/null +++ b/.forge/features/blade-refinements/test-cases.md @@ -0,0 +1,471 @@ +# Blade Refinements Test Cases + +Status: Complete — approved for test generation and implementation + +## Scope + +These cases prove the revised Blade shell, member/admin hierarchy, localized +production regressions, issue collaboration/identity behavior, managed issue +images, form banner, existing instruction media, and responsive/chart behavior. + +They intentionally exclude a benefits page, Grafana, uploaded issue video, +arbitrary issue files, and changes to Discord cadence or mention rules. + +## Test placement plan + +- `apps/blade/src/tests`: shared header/navigation, dashboard/profile, issue, + Forms, and focused regression component tests. +- `apps/blade/src/tests/e2e`: authenticated public/member/admin journeys at 320 + px, intermediate desktop, and full desktop. +- `packages/api/src/**/__tests__`: issue filters/uploads/history projection, + form-banner lifecycle, audit actor projection, and reminder formatting. +- `packages/validators`: active form definition and managed issue-image policy. +- Expected focused commands will use package filters; final gates are React + analysis for changed surfaces, `pnpm verify:precommit`, reviewers selected from + the diff, and `git diff --check`. + +## Test cases + +### TC-001: Public root adapts to authentication + +Setup: + +- Prepare an unauthenticated browser and an authenticated ordinary member. + +Action: + +- Each opens `/` and follows the primary action. + +Expected observations: + +- Both see the public landing page. +- The unauthenticated action begins sign-in; the authenticated action opens the + member dashboard without a redirect loop. +- The authenticated product mark returns to `/`. + +### TC-002: Ordinary member chrome has no sidebar + +Setup: + +- Sign in as a member with no admin destinations. + +Action: + +- Open Dashboard and Settings at desktop and mobile widths, then sign out. + +Expected observations: + +- No icon rail or navigation drawer is present. +- Settings cog and Sign out are together at the top right and work by keyboard + and pointer. +- No admin destination is exposed or made accessible by direct requests. + +### TC-003: Desktop admin rail is explicit and closes on selection + +Setup: + +- Sign in as an admin with destinations in multiple groups. + +Action: + +- Hover and tab across the collapsed rail; activate a recognizable icon; return, + open the rail, and select a labeled destination. + +Expected observations: + +- Hover/focus never expands the rail. +- Collapsed icons expose tooltips/accessibility names and navigate directly. +- The opener expands the rail; grouped labels are visible. +- Selecting a destination closes it, active state remains understandable, and + focus is not stranded in hidden content. + +### TC-004: Navigation grouping follows access + +Setup: + +- Prepare an officer, a limited Club admin, a Hackathon-only admin, and an admin + with no entries in one approved group. + +Action: + +- Inspect desktop and mobile navigation. + +Expected observations: + +- Each actor sees only authorized destinations. +- Dashboard is first; non-empty domain headings follow the approved map; empty + headings are omitted. +- Guild and other outbound destinations are marked external. +- Mobile closes after selection. + +### TC-005: Admin page header reclaims content space accessibly + +Setup: + +- Open representative list, detail, analytics, issue, event, and configuration + admin pages plus their loading states. + +Action: + +- Inspect visually, tab to help controls, and query accessible names/descriptions. + +Expected observations: + +- No visible purple eyebrow or permanent description consumes header height. +- Title and actions remain aligned; the current page description, without the + former eyebrow, is available in an accessible tooltip. +- Skeletons do not reserve removed rows. +- Consequential configuration guidance stays visible while repetitive subtitles + do not. + +### TC-006: Paid and unpaid dashboards choose different hierarchy + +Setup: + +- Prepare otherwise equivalent paid and unpaid members. + +Action: + +- Open the member dashboard on mobile and desktop. + +Expected observations: + +- Unpaid shows the prominent top dues banner and working payment CTA. +- Paid has no dues banner and shows a compact green badge beside the Welcome + name; its hover/focus tooltip confirms payment. +- Neither state repeats an unnecessary explanatory dues sentence. + +### TC-007: Member sections are consistent across viewports + +Setup: + +- Prepare a member with Guild data, events, a resume, and previous forms. + +Action: + +- Compare 390 px and desktop presentations and use every available action. + +Expected observations: + +- Both expose the same ordered sections, labels, and actions. +- Guild stays prominent and is defined as the public member directory/profile; + public links are external-marked. +- Check in is a clear box with View QR code. +- Previous forms is a small bottom action, not a primary tile. + +### TC-008: Guild/profile content handles missing and long values + +Setup: + +- Prepare one sparse member and one member with long name, tagline, company, + portfolio URL, social values, and resume filename. + +Action: + +- Open the dashboard across tested widths and operate photo, resume, visibility, + preferences, and public Guild actions. + +Expected observations: + +- Guild remains plainly separate from private Blade/account data. +- Empty and populated layouts align without overlap or clipped controls. +- Long values wrap/truncate intentionally; outbound actions remain reachable and + identified as external. +- Existing Guild mutation actions remain available. + +### TC-009: Resume upload confirms before optional View + +Setup: + +- Prepare initial signup and an existing member with/without a resume. + +Action: + +- Upload and replace valid resumes in each flow. + +Expected observations: + +- The viewer does not open automatically. +- A clear successful-upload state appears and offers View. +- Selecting View opens the uploaded document; failure preserves the previous + saved resume and reports a truthful error. + +### TC-010: Profile picture removal is confirmable + +Setup: + +- Prepare a member with a saved picture. + +Action: + +- Start removal, cancel, then repeat and confirm. + +Expected observations: + +- Cancel issues no remove mutation and retains the image. +- Confirm removes it once, provides feedback, and returns focus safely. + +### TC-011: Employment errors identify the actual field + +Setup: + +- Prepare a legacy employment row and a current row missing position title, + company, experience type, or employment status one at a time. + +Action: + +- Submit each invalid state and then correct it. + +Expected observations: + +- Required labels are marked. +- Each error names and associates with the actual entry/field and the first error + receives focus/scroll treatment. +- Legacy confirmation guidance appears only for the legacy condition. +- Corrected valid values save successfully. + +### TC-012: Member search keeps focus through result updates + +Setup: + +- Open Members with data including `Alejandro` and a debounced URL query. + +Action: + +- Type the full name continuously while results update. + +Expected observations: + +- No keystrokes are lost, the search retains focus/caret, results converge, and + URL state remains shareable. + +### TC-013: Issue assignee filtering is safe and composable + +Setup: + +- Capture the current production-base failure and prepare teams with zero, one, + and many eligible assignees plus issues assigned across teams. + +Action: + +- Filter by team/assignee alone and alongside status, priority, search, and view + changes. + +Expected observations: + +- Valid choices return correct results or a truthful empty state, never the + generic workspace-load failure. +- Pagination and other filters remain correct; unauthorized data is not leaked. + +### TC-014: Issue Markdown preserves authored line breaks + +Setup: + +- Create descriptions containing adjacent plain lines, blank paragraphs, lists, + links, code, and long unbroken content. + +Action: + +- Compare edit preview and saved detail. + +Expected observations: + +- Author-entered plain line breaks remain visible and preview matches detail. +- Markdown semantics and long-content wrapping remain intact. +- Other shared Markdown consumers do not change unexpectedly. + +### TC-015: Authorized issue image lifecycle + +Setup: + +- Prepare an issue editor, read-only issue reader, unauthorized admin; valid PNG, + JPEG, WebP, static GIF, and animated GIF images; an SVG; a file above 10 MB; + and an issue already retaining 10 managed images. + +Action: + +- Upload/finalize through file picker, clipboard paste, and drag/drop; confirm + cursor insertion; add alt text; save/render the description; replace or remove + an image; and request it as each actor. + +Expected observations: + +- Only editors can create/finalize/remove; readers can view only when they may + read the issue; unauthorized/guessed IDs reveal no object. +- Preview and detail render authorized images with stable geometry and alt text. +- PNG, JPEG, WebP, and both GIF variants at or below 10 MB are accepted through + every gesture. SVG, oversized files, and an eleventh retained image are + rejected without partially mutating the issue. +- Removed and abandoned objects enter safe cleanup without breaking retained + references or history. + +### TC-016: Actor labels prefer current member names + +Setup: + +- Prepare issue-history and Admin-log rows for a linked renamed Member, a deleted + or missing Member with a saved Discord snapshot, and a system actor. + +Action: + +- Read list/detail/history pages, including rows created before this change. + +Expected observations: + +- Linked rows display current Member full name. +- Missing profiles display the saved Discord fallback. +- System rows retain their explicit system label. +- Reads are batched/no-N+1, access policy is unchanged, and stored rows are not + rewritten. + +### TC-017: Issue reminder renders `Title | Chat` + +Setup: + +- Build otherwise identical due/overdue reminders with a valid Discord thread, + no thread, long/special-character titles, assignees, and an owning-role fallback. + +Action: + +- Build reminder payloads under deterministic tests. + +Expected observations: + +- Threaded issue: linked sanitized title, literal `|`, linked `Chat`. +- No thread: linked sanitized title only. +- Existing mentions, limits, grouping, cadence, idempotency, and allowed-mention + behavior are unchanged; tests perform no live Discord write. + +### TC-018: Existing form instruction media remains complete + +Setup: + +- Prepare an authorized form editor and a draft with text, image, and browser- + playable video instruction blocks. + +Action: + +- Upload, remove, save, preview, publish, and view as an eligible respondent. + +Expected observations: + +- The builder makes media actions discoverable and reports progress/failure. +- Saved ordering, authorized playback/rendering, alt text, replacement cleanup, + and existing 100 MB server policy continue to work. + +### TC-019: Form banner lifecycle and presentation + +Setup: + +- Prepare an editable draft, published form, authorized respondent, unauthorized + actor, valid images, and rejected image fixtures. + +Action: + +- Upload/finalize a banner, preview it, publish, view/respond across widths, + replace it, then remove it. + +Expected observations: + +- Exactly one banner appears above respondent content in a responsive 4:1 + `cover` frame with its alt text. +- The builder provides crop guidance, preview, and editable alt text before + publication. +- Preview and respondent views match; loading/failure do not collapse the page. +- Only authorized actors can manage/read it; durable public storage URLs are not + exposed. +- Replacement/removal safely cleans the old unreferenced object; forms without a + banner remain unchanged. + +### TC-020: Responsive content does not clip + +Setup: + +- Use 320 px, common mobile, intermediate desktop/zoom-equivalent, and full + desktop viewports with the long/sparse member fixtures and Chrome-family + engines available in Playwright. + +Action: + +- Navigate member dashboard/settings, events empty/populated states, Guild links, + QR, resume, and account actions. + +Expected observations: + +- No horizontal page overflow or hidden actionable content. +- Long name, globe/portfolio row, event title, and empty-state text remain + readable; responsive layout does not depend on concealed overflow. + +### TC-021: Comparison chart labels remain readable + +Setup: + +- Prepare current/prior hackathons whose application and confirmation deadlines + coincide or fall on adjacent chart positions. + +Action: + +- Render the Application pace comparison at supported widths. + +Expected observations: + +- Current/prior labels remain distinguishable without covering each other or + essential data. +- Keyboard/screen-reader and textual table alternatives still identify every + deadline and series. + +## Negative / regression cases + +### TC-NEG-001: Focus and rail state do not regress through navigation + +Setup: + +- Open expanded admin navigation and a focused search/filter control. + +Action: + +- Navigate, use back/forward, and update query state. + +Expected observations: + +- The rail returns collapsed after selection; hover never opens it; active input + remounts only when structurally necessary and ordinary query updates retain + focus. + +### TC-NEG-002: Upload authorization cannot be bypassed + +Setup: + +- Obtain another actor's issue/form attachment ID, stale upload intent, malformed + reference, and mismatched owner/form/issue IDs. + +Action: + +- Attempt finalize, attach, read, replace, and remove operations. + +Expected observations: + +- Every operation fails with the approved non-enumerating authorization/error + class, no foreign object is exposed, and no partial entity/history change is + committed. + +### TC-NEG-003: Existing data remains readable + +Setup: + +- Load old issues with plain/external Markdown images, old forms without banners, + old form instruction media, and old actor snapshots. + +Action: + +- Read them after rollout and under a UI rollback with new authoring hidden. + +Expected observations: + +- Existing pages remain functional, old media retains authorized behavior, old + actor rows get truthful enrichment/fallback, and no migration fabricates or + rewrites history. + +## Open questions + +- None. These observable cases are approved for test generation. diff --git a/apps/blade/src/app/_components/member/guild-preferences-dialog.tsx b/apps/blade/src/app/_components/member/guild-preferences-dialog.tsx index 696248677..0816fd068 100644 --- a/apps/blade/src/app/_components/member/guild-preferences-dialog.tsx +++ b/apps/blade/src/app/_components/member/guild-preferences-dialog.tsx @@ -5,6 +5,7 @@ import { ExternalLink, Loader2, SlidersHorizontal } from "lucide-react"; import type { GUILD } from "@forge/consts"; import { GUILD as GUILD_CONSTS } from "@forge/consts"; +import { cn } from "@forge/ui"; import { Button } from "@forge/ui/button"; import { Checkbox } from "@forge/ui/checkbox"; import { @@ -19,6 +20,7 @@ import { import { Switch } from "@forge/ui/switch"; import type { CurrentMember } from "~/hooks/use-member"; +import { dashboardNestedSurfaceClass } from "~/app/_components/member/member-dashboard"; import { getGuildMemberUrl } from "~/lib/guild-urls"; import { api } from "~/trpc/react"; @@ -69,7 +71,13 @@ export function GuildPreferencesDialog({ member }: { member: CurrentMember }) { }} > - diff --git a/apps/blade/src/app/_components/member/member-dashboard.tsx b/apps/blade/src/app/_components/member/member-dashboard.tsx index 300ac3917..f22096e08 100644 --- a/apps/blade/src/app/_components/member/member-dashboard.tsx +++ b/apps/blade/src/app/_components/member/member-dashboard.tsx @@ -478,7 +478,7 @@ function GuildProfileCard({
diff --git a/apps/blade/src/app/_components/member/member-profile-picture-upload.tsx b/apps/blade/src/app/_components/member/member-profile-picture-upload.tsx index 5686dc14c..550ec45f4 100644 --- a/apps/blade/src/app/_components/member/member-profile-picture-upload.tsx +++ b/apps/blade/src/app/_components/member/member-profile-picture-upload.tsx @@ -1,10 +1,19 @@ "use client"; -import { useState } from "react"; +import { useRef, useState } from "react"; import { Camera, Loader2, X } from "lucide-react"; import { cn } from "@forge/ui"; import { Avatar, AvatarFallback, AvatarImage } from "@forge/ui/avatar"; +import { Button } from "@forge/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@forge/ui/dialog"; import { Input } from "@forge/ui/input"; import { checkUploadMetadata, @@ -52,6 +61,8 @@ export function MemberProfilePictureUpload({ ); const [uploadError, setUploadError] = useState(null); const [previewUrl, setPreviewFile] = useObjectPreviewUrl(); + const [confirmRemoveOpen, setConfirmRemoveOpen] = useState(false); + const fileInputRef = useRef(null); const savedProfilePicture = api.profilePicture.getProfilePicture.useQuery( undefined, @@ -130,6 +141,34 @@ export function MemberProfilePictureUpload({ } }; + const removeSavedPicture = async () => { + setUploadError(null); + + try { + await updateProfilePicture.mutateAsync({ profilePictureUrl: "" }); + setProfilePictureUrl(""); + setPreviewFile(null); + } catch { + setUploadError("Profile picture could not be removed."); + } finally { + setConfirmRemoveOpen(false); + fileInputRef.current?.focus(); + } + }; + + const handleRemoveClick = () => { + setUploadError(null); + + if (saveMode === "deferred") { + setProfilePictureUrl(""); + setPreviewFile(null); + onChange?.(""); + return; + } + + setConfirmRemoveOpen(true); + }; + return (
@@ -160,6 +199,7 @@ export function MemberProfilePictureUpload({ )} Upload profile picture { - setUploadError(null); - try { - if (saveMode === "deferred") { - setProfilePictureUrl(""); - setPreviewFile(null); - onChange?.(""); - return; - } - - await updateProfilePicture.mutateAsync({ - profilePictureUrl: "", - }); - setProfilePictureUrl(""); - setPreviewFile(null); - } catch { - setUploadError("Profile picture could not be removed."); - } - }} + onClick={handleRemoveClick} >
+ + {saveMode === "member" && ( + { + if (!updateProfilePicture.isPending) setConfirmRemoveOpen(open); + }} + > + + + Remove profile picture? + + This removes your saved profile picture. You can upload a new + one anytime. + + + + + + + + + )}
); } diff --git a/packages/api/src/tests/issues/reminders.test.ts b/packages/api/src/tests/issues/reminders.test.ts index 105f80c18..bfebcf618 100644 --- a/packages/api/src/tests/issues/reminders.test.ts +++ b/packages/api/src/tests/issues/reminders.test.ts @@ -149,7 +149,7 @@ describe("Club Operations Issues reminders", () => { expect(displays[1]).toContain("\n-# Alex"); expect(displays[1]).toContain("\n-# Development Team"); expect(displays[1]).toContain( - "[Discuss]()", + "| [Chat]()", ); expect(displays[1]).not.toContain("Open in Blade"); const [productionMessage] = splitIssueReminderMessages( @@ -169,7 +169,7 @@ describe("Club Operations Issues reminders", () => { expect(bladeOnlyText).toContain( `https://blade.knighthacks.org/admin/issues/${assigned.id}`, ); - expect(bladeOnlyText).not.toContain("[Discuss]"); + expect(bladeOnlyText).not.toContain("[Chat]"); expect(issueReminderAllowedMentions(message.targets)).toEqual({ parse: [], roles: ["333333333333333333"], @@ -177,6 +177,54 @@ describe("Club Operations Issues reminders", () => { }); }); + it("TC-017 renders the linked title followed by `| Chat` only when a Discord thread exists", () => { + const now = new Date("2026-07-21T13:00:00.000Z"); + const [threaded] = buildIssueReminderPlan( + [{ ...baseIssue, name: "Ship <@123> update\nnow" }], + now, + ); + const [threadless] = buildIssueReminderPlan( + [ + { + ...baseIssue, + discordThreadUrl: null, + id: "00000000-0000-4000-8000-000000000003", + name: "Ship <@123> update\nnow", + }, + ], + now, + ); + if (!threaded || !threadless) throw new Error("Expected reminder targets."); + + const [threadedMessage] = splitIssueReminderMessages( + [threaded], + "https://blade.test", + ); + const [threadlessMessage] = splitIssueReminderMessages( + [threadless], + "https://blade.test", + ); + if (!threadedMessage || !threadlessMessage) { + throw new Error("Expected reminder messages."); + } + + const threadedText = containerTextDisplays(threadedMessage).join("\n"); + const threadlessText = containerTextDisplays(threadlessMessage).join("\n"); + + const escapedTitle = sanitizeIssueReminderTitle( + "Ship <@123> update\nnow", + ).replace(/([\\`*_[\]{}()~|>])/g, "\\$1"); + + expect(threadedText).toContain( + `${escapedTitle} (8/4)]()** | [Chat](<${threaded.discordThreadUrl}>)`, + ); + expect(threadlessText).toContain( + `${escapedTitle} (8/4)]()**\n`, + ); + expect(threadlessText).not.toContain("| [Chat]"); + expect(threadlessText).not.toContain("[Chat]"); + }); + it("TC-REMINDER-005 splits deterministic messages at Discord component limits", () => { const targets = Array.from({ length: 80 }, (_, index) => { const [target] = buildIssueReminderPlan( diff --git a/packages/api/src/utils/issues/reminders.ts b/packages/api/src/utils/issues/reminders.ts index 50c99cb36..9ddb28433 100644 --- a/packages/api/src/utils/issues/reminders.ts +++ b/packages/api/src/utils/issues/reminders.ts @@ -194,7 +194,7 @@ function targetBlock(target: IssueReminderTarget, bladeUrl: string) { const url = targetUrl(target, bladeUrl); const title = escapeMarkdown(truncate(target.name, TARGET_TITLE_LIMIT)); const discussion = target.discordThreadUrl - ? ` · [Discuss](<${target.discordThreadUrl}>)` + ? ` | [Chat](<${target.discordThreadUrl}>)` : ""; const heading = `**(${priorityMarks(target.priority)}) [${title} (${shortDate(target.dueAt)})](<${url}>)**${discussion}`; const audience = truncate(