---
name: Session Status
description: Current in-progress work and next steps for appza_community
type: project
---

## Last session: 2026-05-21 (iOS build pipeline fix — firebase.json + flutterfire_cli + log preservation)

Diagnosed and fixed the iOS archive failure that killed builds 52 and 53. Two latent bugs in `build_flow.sh` + one missing dependency on the build server. Verified end-to-end — build 54 (`com.nusratappza.live`) archived cleanly. 3 commits on `dev`, 3 squash commits on `main`.

### Commits this session (3 on `dev`, 3 on `main`)

1. `63f69649` — fix(build-flow): generate firebase.json in update_firebase(). `remove_firebase()` deletes `firebase.json` at repo root but `update_firebase()` only regenerated `firebase_options.dart` — so any push-notification build done after a no-firebase build was missing `firebase.json`. The iOS archive's FlutterFire script phase invokes `flutterfire upload-crashlytics-symbols` which throws `PathNotFoundException` without it. New `generate_firebase_json()` reads project/app IDs from the platform configs already validated by `update_firebase()` and writes a valid `firebase.json` with `uploadDebugSymbols=true` for iOS. Reproduced the failure locally against the build_52 snapshot, then verified the patch produces JSON that flutterfire accepts.
2. `f3dd9180` — fix(build-flow): activate flutterfire_cli if missing on build host. SSH'd into build server 71768.local (208.52.189.220, user `administrator`) and found `$HOME/.pub-cache/bin/flutterfire` did not exist and `dart pub global list` was empty — the CLI was never activated. xcodebuild's compact output swallows the resulting `command not found` line, so the failure appeared as an empty PhaseScriptExecution error with no diagnostic. One-shot fix on the server (`fvm dart pub global activate flutterfire_cli` → installed 1.3.2), plus patched `update_firebase()` to self-heal by activating if missing.
3. `d899e024` — feat(build-flow): preserve iOS Runner-Runner.log so it survives zip-up. The build pipeline zips the project dir post-build but excludes `ios/build/` contents — exactly where fastlane writes `Runner-Runner.log`. That's why we couldn't see the actual flutterfire stderr on builds 52/53. `build_ios()` now copies `ios/build/Runner-Runner.log` → `ios/Runner-Runner.log` on both success and failure paths, so future failures leave a forensic trail in the snapshot zip.

Squash commits on `main`: `ed0b838d` (firebase.json), `1c17fbec` (flutterfire_cli), `65cf200e` (log preservation).

### Diagnostic flow (worth remembering)

- **Build server**: ssh as `administrator@208.52.189.220` (host: `71768.local`). Build dirs at `/Users/administrator/projects/appza_builder/storage/app/builds/{build_id}_{platform}_{timestamp}/`. After each build the dir is zipped to a snapshot and the dir is deleted, so `Runner-Runner.log` is permanently lost unless preserved into the zip.
- **Snapshot zips** are password-protected (password shared in conversation, not committed). Standard macOS `unzip` is PKZip v4.5 only — the snapshots are PKZip v5.1 (AES-encrypted) so use `7zz x -p'<pw>'` to extract.
- **xcodebuild script-phase failures hide stderr** in fastlane/compact mode. The full raw output lives only in `ios/build/Runner-Runner.log`. Always preserve it.
- **`flutterfire upload-crashlytics-symbols` requires `firebase.json` at project root** — even when `firebase_options.dart` is present. The CLI throws `PathNotFoundException` at `appleConfigFromFirebaseJson` (utils.dart:291) before doing anything else.
- **FlutterFire Xcode script extends PATH with `$HOME/.pub-cache/bin`** so it finds globally-activated `flutterfire`. If the CLI was never activated, it exits 127 with no parseable error.

### Architectural notes

- **`update_firebase()` is the single source of truth for Firebase setup** when `HAS_PUSH_NOTIFICATION=true`. It must: (a) validate platform configs, (b) regenerate `firebase_options.dart`, (c) regenerate `firebase.json`, (d) ensure `flutterfire_cli` is activated, (e) flip `hasFirebase=true`. The third was the latent miss — `remove_firebase()` removes `firebase.json` symmetrically but `update_firebase()` didn't recreate it.
- **Build server diagnostics-first workflow**: before SSHing in, exhausted the snapshot — confirmed `firebase.json` missing, confirmed patched script ran (53.log line 93 "✅ firebase.json generated successfully"), confirmed archive failed after my fix — narrowing to the second bug. Then SSHed in for the last-mile verification.
- **Self-heal vs. one-time fix**: applied both. The one-time activate unblocked the current build host; the script-level guard makes future machine rebuilds / new hosts safe.

### Pending iOS build work

None — pipeline is healthy. Next iOS build that fails will leave `ios/Runner-Runner.log` in the snapshot zip for direct inspection.

### Server-side TODOs still pending (carry-forward, unchanged)

- **Hasan vai — guest 401 on `/members/followers` and `/members/followings`** — still blocks 3224.
- **`text_decoration.text_decoration` placeholder** `'lineThrough'` → `'none'`.
- **`avatar_decoration.width/height` fractional `'0.3'` → pixel value.**
- **`FCommunity_UserAvatar` schema** needs `icon_decoration` + `button_decoration`.
- **`is_verified` field** missing from edit-profile views' items[0].
- **`avatar_decoration.width/height/radius` on `FCommunity_BasicProfile`** currently `40` — Nusrat to bump to `96`.
- **`follow` field on `FCommunity_EditProfile.items[0]`** — backend committed but not shipped.
- **Multi-word `last_name` server splitter** still corrupting names.

### Untracked PII JSON dumps in working tree (DO NOT git add)

Same four files — `create-post-page.json`, `profile-page-details.json`, `profile-page-details-demo123.json`, `profile-page-edit.json`. Still worth `.gitignore`-ing the pattern.

---

## Previous session: 2026-05-21 (4 High-priority closures: 3404/3405/3395/3406 + main squash)

Closed the entire remaining High-priority bug column on the edit-profile/profile-page set. Three commits on `dev`, all shipped to Nusrat; 3406 closed against `ba57ae34` from yesterday. Capped the day by squash-merging ~115 dev commits into `main` as `0b071c48`.

### Commits this session (3 on `dev`, 1 on `main`)

1. `cc4579e8` — fix(profile-username): use `hint_text_decoration` when not editing. `_UsernameField` TextFormField switches from `inputTextStyle` to `hintStyle` when `!_canEdit`; `_ReadOnlyUsername` (server-disallowed path) always uses `hintStyle`. Closes LazyTask 3404.
2. `6df8aacc` — fix(edit-profile-button): apply bell icon button decoration for selected state. Notification bell picks up state-dependent backdrops: notifying ON → `decline_button_decoration`, notifying OFF → `secondary_button_decoration`. Icon color stays sourced from `icon_decoration` in both states. Closes LazyTask 3405.
3. `852ba53d` — fix(profile-social-link): drop outer box wrapper, tint icon, use `primary_text` for prefix. Removed outer `Container(box_decoration)` wrap; SVG provider icon tinted with `icon_decoration.color` via `ColorFilter(BlendMode.srcIn)`; URL/`@` prefix label now reads `primary_text_decoration`. Closes LazyTask 3395.
4. `0b071c48` (on `main`) — squash of ~115 `dev` commits since `21b8584b` covering SDUI rebuilds (login, profile, edit-profile, create-post, chat-details), forms playground, AppConfigModel json_serializable migration, `card_views` page-grouped reorg, and the entire High-priority decoration sweep.

### LazyTask status delta this session

| ID | Title | Status |
|---|---|---|
| 3404 | Profile username hint text decoration is missing | Complete |
| 3405 | Edit profile button selected bell icon decoration missing | Complete |
| 3395 | Profile social links issues | Complete |
| 3406 | User avatar avatar decoration didn't applied | Complete (closed against `ba57ae34` from 2026-05-20) |

All four reassigned to Nusrat via `/ship`. Supervisor clone pulled to `852ba53d`; SSH tunnel up; `supervisord` restarted; `build-worker_00` RUNNING.

### Architectural notes

- **Bell icon: state-dependent backdrop, state-independent icon color.** User decided after iteration: ON and OFF states use different `*_decoration` backdrops, but the icon glyph itself stays a single color from `icon_decoration`. First attempt wired an `onIconTextDeco` slot for white-icon-on-dark-approve; user redirected to always use `icon_decoration`. Pattern: don't auto-derive icon color from backdrop; let server pick a single contrast color in `icon_decoration` and use it everywhere.
- **`_ReadOnlyUsername` now uses `hintStyle`** when the server marks the username as non-editable (`can_change_username == false`). Matches the editable-but-not-yet-tapped visual.
- **Social-link card lost its outer `box_decoration` wrap.** The page chrome handles padding/margin; the card itself is just a Column. Consistent with how `profile_username` ended up after `40521e29`. The 2026-05-20 "cards always own their own box_decoration" rule now has two documented exceptions — when the screen renders cards directly (not via engine ListView), the chrome wrapper moves up to the page.
- **SVG tinting via `ColorFilter(BlendMode.srcIn)`** is the canonical way to apply `icon_decoration.color` to provider SVGs (social-link icons here, reusable for any other server-shipped SVG).
- **Squash-merge workflow confirmed:** `git checkout main && git merge --squash dev && git commit && git push origin main`. Non-destructive — main fast-forwards from previous release commit (`21b8584b` here) and adds one new release commit on top. No force-push needed. Previous release squash commits stay in main's history as an audit trail.

### Outstanding High-priority work

None — High-priority column is empty. Next-session pickup is either:
- Unprioritized "App Bugs" (2828, 2834, 2835, 2836)
- "App New Works" backlog (13 items: 3205, 3271–3285)
- Carry-forwards: 3224 (still backend-blocked on Hasan vai's `/members/followers` 401 fix)

### Server-side TODOs still pending (carry-forward, unchanged from previous session)

- **Hasan vai — guest 401 on `/members/followers` and `/members/followings`** — still blocks 3224.
- **`text_decoration.text_decoration` placeholder** `'lineThrough'` → `'none'`.
- **`avatar_decoration.width/height` fractional `'0.3'` → pixel value.**
- **`FCommunity_UserAvatar` schema** needs `icon_decoration` + `button_decoration`.
- **`is_verified` field** missing from edit-profile views' items[0].
- **`avatar_decoration.width/height/radius` on `FCommunity_BasicProfile`** currently `40` — Nusrat to bump to `96`.
- **`follow` field on `FCommunity_EditProfile.items[0]`** — backend committed but not shipped.
- **Multi-word `last_name` server splitter** still corrupting names.

### Untracked PII JSON dumps in working tree (DO NOT git add)

Same four files — `create-post-page.json`, `profile-page-details.json`, `profile-page-details-demo123.json`, `profile-page-edit.json`. Still worth `.gitignore`-ing the pattern.

---

## Previous session: 2026-05-21 (3400 finish + cast-crash sweep + 3391 username rewrite + /ship skill + delivery)

Closed 3 more High-priority LazyTasks (3400, 3393, 3391), investigated 3224 (still backend-blocked), promoted the ship-to-nusrat runbook into a proper `/ship` skill, and shipped the whole 2026-05-20 + 2026-05-21 batch to Nusrat. 4 commits on `dev`, all pushed.

### Commits this session (4, all on `dev`)

1. `57ab952c` — fix(post-information): finish 3400 — swap Allow-Multi-Poll Checkbox → `_OptionCheckbox`; extend `CustomDropdown` with optional `iconColor`/`iconSize` (null-fallback, other consumers unaffected); wire `icon_decoration` to the space-dropdown arrow.
2. `e93d64f5` — fix(card-views): type the styleMap fallback const map to stop cast crash. `?? const {})` → `?? const <String, dynamic>{})` across 36 occurrences in 9 files. Closes 3391 (the actual `_ConstMap<dynamic, dynamic>` crash on FCommunity_ProfileUsername) and 3393 (Basic Profile Information on the edit page).
3. `40521e29` — fix(profile-username): full 3391 functional fix — added Change button (approve_button_decoration / approve_button_text_decoration) inside the input's trailing slot with confirmation modal → unlocks editing; added helper text shown while editing (primary_text_decoration); swapped outer `box_decoration` → `general_decoration` to match basic_profile / edit_profile_button cards; split typed text (`secondary_button_text_decoration`, dark) from hint placeholder (`hint_text_decoration`, gray). 3 Translator keys added.
4. `0043aa17` — chore(skills): promote ship-to-nusrat runbook from `.claude/memory/shipping_to_nusrat.md` into `/ship` skill. Cross-referenced in tasks SKILL.md.

### LazyTask status delta this session

| ID | Title | Status |
|---|---|---|
| 3400 | Post information issues | Complete (was In Progress carry-over) |
| 3393 | Basic profile information on profile edit page | Complete (closed by cast-crash sweep) |
| 3391 | Profile username issue on edit profile page | Complete |
| 3224 | Following, following permission issue | User marked Complete (waiting on Hasan vai's backend fix) |

### Ship batch reassigned to Nusrat (10 tasks)

Delivered via `/ship`. Supervisor clone pulled to `0043aa17`; SSH tunnel up; `supervisord` restarted; `build-worker_00` RUNNING.

- Today: **3391, 3393, 3400**
- Last session carry-over: **3332, 3333, 3386, 3389, 3390, 3397, 3401**

**Held back from reassignment**: **3224** — backend 401 still active on guest `/members/followers` per curl test today. Client empty-state fallback from 2026-04-29 is intact. Hasan vai's backend fix has NOT shipped; user is waiting.

### Architectural decisions / patterns this session

- **`?? const {})` is a crash-in-waiting** when followed by `as Map<String, dynamic>` — the untyped const map is `_ConstMap<dynamic, dynamic>` and Dart strict-generic casts reject it. Mandatory pattern: `?? const <String, dynamic>{})`. Already swept across every card vertical.dart + new_profile_view + new_create_post_view; future cards must use the typed form.
- **`general_decoration` IS valid for outer card wrap when the page is NOT engine-rendered.** The 2026-05-20 rule ("general_decoration is for ListView (engine wrap), box_decoration is per-card") applies to engine-rendered pages. The new edit-profile screen renders cards directly via `GetSelectedCardView(...)` with NO ListView wrap, so each card has to apply general_decoration itself. `basic_profile`, `edit_profile_button`, and now `profile_username` all read `general_decoration` for the outer wrap. The pre-2026-05-20 inconsistency was actually the right call for this screen.
- **`approve_button_decoration` / `approve_button_text_decoration` are server keys for Change/Approve-style trailing buttons** inside an input field. First wired on profile_username — pattern reusable for future "tap to unlock then edit" inputs.
- **Reassignment must skip backend-blocked tasks.** The `/ship` skill enforces this; today's 3224 is the textbook case. Marking complete locally is fine (you've done your part), but reassigning would put it back in Nusrat's queue with nothing testable.
- **/ship skill** — discovery via skill list, includes the supervisor-clone fallback (SSH tunnel + supervisord + start all). Use this any time the user says "ship to nusrat" / "deliver this batch" / "we pushed the build".

### Server-side TODOs still pending (carry-forward)

- **Hasan vai — guest 401 on `/members/followers` and `/members/followings`**: still active. Confirmed by curl today. Blocks 3224.
- **`text_decoration.text_decoration` placeholder `'lineThrough'` → `'none'`**: still pending (would unlock create-post body input visuals + several other pages).
- **`avatar_decoration.width/height` fractional value `'0.3'` → pixel value (e.g. `'40'`)**: still pending.
- **`FCommunity_UserAvatar` schema needs `icon_decoration` + `button_decoration`** keys (Nusrat pinned both in Figma, server doesn't ship them).
- **`is_verified` field** still missing from any of the 4 edit-profile views' items[0].
- **`avatar_decoration.width/height/radius` on `FCommunity_BasicProfile`** currently `40` — Nusrat to bump to `96`.
- **`follow` field on `FCommunity_EditProfile.items[0]`** — backend committed but not shipped; cold visits to other users' profiles render "Follow" for already-following relationships until interaction.
- **Multi-word `last_name` server splitter**: still corrupting names.

### Outstanding High-priority work

- **3395** — Profile social links issues (untouched — next session pickup).
- **3224** — held until Hasan vai's backend ships. No client work needed.

### Untracked PII JSON dumps in working tree (DO NOT git add)

- `create-post-page.json`
- `profile-page-details.json`
- `profile-page-details-demo123.json`
- `profile-page-edit.json` (refreshed today — used to inspect FCommunity_ProfileUsername server keys including `approve_button_decoration`)

These are curl dumps used during audits. Contain user emails, gravatar URLs, etc. Worth `.gitignore`-ing the pattern if we keep doing this — carried over from 2026-05-20.

---

## Previous session: 2026-05-20 (Profile + signin + chat + post-info SDUI decoration sweep — 7 commits)

Took on the entire High-priority bug column. Closed seven LazyTasks (3333, 3390, 3386, 3389, 3332, 3401-duplicate, 3397) and shipped a partial fix on 3400. Established the Figma-pin → server-key mapping discipline as the default workflow for these decoration-routing bugs.

### Commits this session (7, all on `dev` → `origin/dev`)

1. `77a49355` — fix(chat-details): wire 3-dots message menu color from server icon_decoration (LazyTask 3333)
2. `ebaa4187` — fix(edit-profile): route page through Renderer so engine wraps general_decoration; drop social-link bridge; entity field shape matches new server contract (LazyTask 3390)
3. `4af9780b` — fix(basic-profile): layout overhaul (port from fc_mobile profile_screen_1) + Figma-pin decoration wiring; record profile-page-details parent frame `1408:2018` in memory (LazyTask 3386)
4. `11d5a0e8` — fix(edit-profile-button): decoration rewiring per Figma + drop follow_state bridge; rename entity field `followState` → `follow` to match `/members/profile` shape (LazyTask 3389)
5. `9f8439c0` — fix(signin-info): drop primary-button text color from input focus border (LazyTask 3332; also closes 3401 as duplicate)
6. `fcfedddf` — fix(user-avatar): wire general / icon / button / avatar decorations on create-post FCommunity_UserAvatar (LazyTask 3397)
7. `f99c1814` — fix(post-information): wire dropdown trigger to primary_text_decoration + Send Announcement checkbox to selected_option / un_selected_option decoration. Partial LazyTask 3400.

### LazyTask status delta this session

| ID | Title | Status |
|---|---|---|
| 3333 | Chatinput component 3 dots | Complete |
| 3390 | Save button on edit profile page | Complete |
| 3386 | Basic profile components issue | Complete |
| 3389 | Edit profile button issue | Complete |
| 3332 | Signin info primary button border color | Complete |
| 3401 | login page input border | Complete (duplicate of 3332) |
| 3397 | User avatar issues | Complete |
| 3400 | Post information issues | **In Progress** (partial — see below) |
| 3331 | SignIn header box decoration | Reassigned back to Nusrat after she clarified the spec |

### Outstanding for next session

**3400 — Post Information** (partial commit shipped). Remaining:
- Allow-Multi-Poll checkbox (vertical.dart:1090) — swap Flutter `Checkbox` → `_OptionCheckbox` (the helper added in `f99c1814` is private to the file, ready to reuse).
- Apply `icon_decoration` to the space-selection dropdown's arrow icon. Requires extending `CustomDropdown` with an optional `iconColor` / `iconSize` (the `textStyle` parameter pattern from this session is the template).
- Confirm with Nusrat where `text_decoration` should land — likely on the two Checkbox labels (currently hardcoded `Color(0xFF525866)` 14 w500 at vertical.dart:1108).

**Other High-priority bugs untouched:**
- 3391 — Profile username issue on edit profile page
- 3393 — Basic profile information / general decoration cleanup (probably knock-on once we migrate `NewProfileScreen` to Renderer)
- 3395 — Profile social links issues

### Backend coordination outstanding

- **`follow` field on `FCommunity_EditProfile.items[0]`** (profile-page-details endpoint). Backend confirmed will ship. Until it does, cold visits render "Follow" for already-following relationships; behavior self-corrects after any interaction.
- **`avatar_decoration.width / height / radius`** on `FCommunity_BasicProfile` currently ship as `40` — Nusrat to bump to `96` to match fc_mobile reference size.
- **`is_verified`** still missing from any of the four edit-profile views' items. Save endpoint receives `false` until backend bundles it. Carried over from prior 3390 follow-up.

### Architectural decisions / patterns established

- **Figma pin → server key mapping is the canonical first step** for any "decoration not applied" bug. Pull all Nusrat-authored "X decoration" pins on the parent frame via Figma comments API, cluster by spatial X-coordinate when two components share a frame (profile-page-details has BasicProfile at x<400 and EditProfile at x>400 on the same canvas, `1408:2018`).
- **"general_decoration is for ListView (engine wrap), box_decoration is for each card view"** — the rule the user spelled out mid-session. Most cards respect it. Two violators audited (`basic_profile_card_views`, `edit_profile_button_card_views`) read `general_decoration` for their own outer wrap; cleanup is gated on `NewProfileScreen` moving to Renderer (out of scope for the visible-bug fixes done today).
- **"both icon & text" Figma annotation** = the same `*_button_text_decoration.color` drives both the Icon glyph and the count Text in stat chips. Codified in `_StatChip` in basic_profile.
- **Bridge cleanup pattern**: when the backend ships a field directly inside `items[0]`, the in-memory patch path (`_seedFollowStateIntoEditProfileView`, social-link bridge in NewEditProfileController) can be deleted entirely. Both bridges removed this session.
- **`CustomDropdown` opt-in style overrides** — added `textStyle: TextStyle?` parameter that falls back to current hardcoded styling when null. Other consumers (`search`, `edit_post`, `old_create_post`) unaffected. Template for adding `iconColor` / `iconSize` next time.

### Untracked files left in the working tree (DO NOT git add — PII)

- `create-post-page.json`
- `profile-page-details.json`
- `profile-page-details-demo123.json`
- `profile-page-edit.json`

These are curl dumps used during audits. Contain user emails, gravatar URLs, etc. Worth `.gitignore`-ing the pattern if we keep doing this.

---

## Previous session: 2026-05-18 (Create-post parity audit + dynamic styles wiring — 10 commits)

Audited the new create-post screen (committed yesterday) against the fc_mobile_stand_alone reference, closed every meaningful parity gap, then flipped both cards from hardcoded constants to server-driven `parseTextStyle / parseBoxDecoration / parseImageDecoration` lookups. Verified on iOS simulator. 10 commits on `dev`, all pushed to `origin/dev`.

### Commits this session (10, all on `dev`)

1. `a6fb122` — feat(create-post-parity): inject "On my profile" group when global posts enabled (**BLOCKER** — users on global-posts-enabled sites had no way to post to their own profile)
2. `1ea162f` — feat(create-post-parity): unify card scroll — title + body scroll together (matches fc_mobile's outer 0.78×height + single SingleChildScrollView)
3. `ed994d9` — chore(ios): regenerate Podfile.lock to match firebase_performance ^0.11.1+1 (Firebase 12.4 → 12.13 across the family)
4. `1d1b0e5` — fix(create-post-parity): swap hardcoded "Save"/"Topic" for Translator keys
5. `2782f6c` — feat(create-post-parity): success snackbar after video embed / Custom HTML
6. `88445d1` — fix(create-post-parity): stricter client-side video URL validation (Uri.parse + scheme + host check)
7. `790e462` — fix(create-post-parity): defensive maxMedia re-check at submit time
8. `f92427b` — refactor(create-post): drop dead if/else in applySpaceSelection
9. `968ea13` — feat(create-post): wire server-driven styles via parseTextStyle / parseBoxDecoration
10. `b268063` — feat(create-post): wire body input style → text_decoration
11. `170d03f` — docs(memory): figma → server-styles workflow + create-post pending list (memory only, no code)

### Outcome

- **Every audit-flagged parity gap closed.** Eight items from the fc_mobile_stand_alone diff (one blocker, seven minor/cosmetic) addressed in single-purpose commits. Three "suspicious" items investigated and dismissed (WebView smart handling, `is_from_space_details` defensive Map check, package import path).
- **Both create-post cards now consume server styles.** `PostInformationCard.styleMap` static slot threads the styleMap through sub-widgets without constructor plumbing (mirrors existing `bodyController` pattern). Pin → key mapping confirmed via Figma comments API (file `MtDxxWCRgzanTKcV6qG6Nu`, frame `1412:2600`).
- **Server-side TODOs documented for Nusrat** in [[create-post-styles-pending-nusrat]]. Reference JSON generated at `/tmp/create_post_styles_for_nusrat.json` via Figma node API. Visuals will resolve automatically when she publishes real per-key values.

### Architecture pieces shipped / decisions made

**`PostInformationCard.styleMap` static slot** — set at the top of `PostInformationDesign1Vertical.build`, read by all sub-widgets (including `PostInformationFooterToolbar` when hosted in the FAB by `NewCreatePostScreen`). Mirrors `bodyController` static-slot pattern. Cleaner than threading 7+ keys through 6+ constructor chains.

**Sanity guard on `avatar_decoration.width/height`** — server placeholder is `'0.3'` (the documented fractional bug). `_Avatar` widget falls back to 40 when `width >= 1` check fails, so the avatar stays visible even with bad server values. When Nusrat fixes the placeholder, values flow through automatically.

**Figma → server-styles workflow documented** in [[figma-styles-workflow]]. Self-contained recipe (Figma file key, API endpoints, color/weight/height conversions, caveats) for future SDUI pages.

**Skipped intentionally:**
- UserAvatar's calendar/eye `IconButton`s — `FCommunity_UserAvatar` styleMap doesn't expose `icon_decoration` or `button_decoration` (Nusrat pinned both in Figma but server schema missing). TODO for Nusrat.
- `selected_option_decoration` / `un_selected_option_decoration` — would require widget API changes to `CustomDropdown` / `MultiTopicSelection` / `CustomToolTipMenu` to accept style overrides. Defer.
- "Topic" label — no direct Figma pin (the runtime-conditional row isn't in the static mockup). Currently inferred as `secondary_text_decoration` by analogy with sibling "Posting in:" label. Flag for Nusrat to add Topic to the Figma mockup + pin.

### Audit findings worth carrying forward

- **fc_mobile_stand_alone is the source of truth for create-post parity.** The diff caught one blocker (global posts injection), three real UX gaps (i18n drift, video success feedback, weak URL validation), three layout/defensive items (card scroll shape, maxMedia recheck, dead branch), and several non-bugs the audit incorrectly flagged (WebView URL chain, pageState toggle architecture, Map-not-DropdownItem defense). Useful pattern for the rest of the SDUI migration backlog: agent-driven side-by-side diff against fc_mobile, then triage by severity.
- **The fc_mobile reference is occasionally wrong** — appza_community correctly diverged on a few points (form-state survival on failure, `CacheControllerManager.clearSpaceDetailsController` invocation). Don't blindly copy fc_mobile when the local code has better behavior.
- **WebView widget is smart enough for both URL and iframe HTML.** `_PickVideoStub` reads `video_url → url → html` from the upload Map; `CustomWebViewWidget` detects `<iframe>`/`<embed>` in the input and routes to `loadHtmlString` automatically. The audit's concern that iframe HTML could be passed to a URL loader was wrong.
- **`text_decoration` server bug affects ~6 pages, not just create-post.** Course, login, and signin pages all consume `styleMap['text_decoration']` directly. When Nusrat fixes `text_decoration.text_decoration` from `'lineThrough'` to `'none'`, the whole batch lights up.

### Server-side TODOs (carrying forward — flag to Nusrat)

Tracked in [[create-post-styles-pending-nusrat]] but worth surfacing here:

1. **Publish real per-key values** for `FCommunity_UserAvatar` (4 keys) and `FCommunity_PostInformation` (9 keys) — reference JSON at `/tmp/create_post_styles_for_nusrat.json` extracted directly from Figma.
2. **Fix `avatar_decoration.width/height`** from `'0.3'` to a real pixel value (e.g. `'40'`). Cross-page issue — same workaround in chat avatar from last session.
3. **Fix `text_decoration.text_decoration`** from `'lineThrough'` to `'none'`. Unlocks create-post body input + ~6 other pages.
4. **Add `icon_decoration` and `button_decoration`** to `FCommunity_UserAvatar` schema. Nusrat pinned both on the calendar/eye buttons in Figma but the server response doesn't include them.
5. **Pin the Topic row decoration** in the create-post Figma mockup. The row only exists at runtime (when space has topics) so there's no element to pin in the static design.
6. **Publish `create-post-page` on the public mobile endpoint** — admin endpoint works; public `/wp-json/appza/fcom-mobile/api/v1/screen?slug=...` still 404s. (Carry-forward from last session.)

### What's open / next-session pickup

- **MemberCard manual device test** — STILL outstanding. Carry-forward 9+ sessions now.
- **Multi-word `last_name` server fix** — still unfixed; flag to Nusrat from earlier sessions.
- **`selected_option_decoration` / `un_selected_option_decoration` wiring** — needs CustomDropdown / MultiTopicSelection / CustomToolTipMenu API extension to accept style overrides. Server schema exposes both keys for `FCommunity_PostInformation`, just no consumer.
- **SQA hand-off comments on LazyTasks 3269 + 3270** — tell Nusrat what's been built (audit + dynamic styles), what's deferred (her server-side list above), and which build will carry it.
- **Untrack `.DS_Store`** — carry-forward; `git rm --cached .DS_Store` + commit cleanup. Not urgent.
- **Promote `/tmp/build_create_post_styles.py` to `scripts/figma-styles.py`** — if/when we do the same swap for another SDUI page, a reusable script would save time. Today it's a one-off.

### Files of note

| Concern | Path |
|---|---|
| PostInformation card | `lib/presentation/core/renderer/card_views/feed/post_information_card_views/` |
| UserAvatar card | `lib/presentation/core/renderer/card_views/feed/user_avatar_card_views/` |
| Form store | `lib/core/cache/create_post_form_store.dart` |
| Controller + screen | `lib/presentation/features/new_create_post/` |
| Style slot | `PostInformationCard.styleMap` (in `shared/post_information_card_base.dart`) |
| Reference JSON for Nusrat | `/tmp/create_post_styles_for_nusrat.json` (regenerable via `python3 /tmp/build_create_post_styles.py`) |
| Workflow doc | [[figma-styles-workflow]] |
| Pending list | [[create-post-styles-pending-nusrat]] |

---

## Previous session: 2026-05-17 (Create-post SDUI rebuild — LazyTasks 3269 + 3270, full parity in one session)

Rebuilt the entire hand-built `features/create_post/` screen (~3,000 lines) as two SDUI cards (`FCommunity_UserAvatar` + `FCommunity_PostInformation`) and ported every legacy widget verbatim. 19 commits on `dev`. Confirmed end-to-end on emulator — text post submitted successfully.

### Commits (19, all on `dev`)

1. `0a5fc48a` — scaffold createPostPage fetcher + CreatePostFormStore + screen
2. `a1aa1ac7` — FCommunity_UserAvatar card (3269)
3. `d8e1a510` — FCommunity_PostInformation card skeleton (3270)
4. `ccb28bd0` — submitPost handler with full payload parity
5. `5a766c05` — repoint feed entry-card to NewCreatePostScreen
6. `827d4851` — retire features/create_post → pages/old_create_post (git mv)
7. `cb20e7a0` — fix: store DropdownItem directly (toJson Topic-cast crash)
8. `a554e49a` — wording + AppBar pill + avatar icons parity
9. `6657eafa` — CustomDropdown space picker + isFromSpaceDetails read-only
10. `e75806ad` — MultiTopicSelection/CustomToolTipMenu topic picker + Checkbox announcement gate
11. `7ef26e20` — visual layout matches legacy create-post card
12. `626dd282` — footer toolbar + image picker + poll editor (video/attach/emoji stubbed)
13. `e0a9d549` — video embed (AttachViewModal) + file attachments (DocumentService)
14. `e63d8672` — BuildViewPost preview modal port
15. `520b39d3` — ScheduleViewModal port + emoji picker
16. `9598c957` — move BuildViewPost out of retired old_create_post (seal the parking-lot)
17. `6498b3ce` — keyboard-aware floating toolbar (FAB shift)

### Outcome

- `lib/presentation/features/new_create_post/` is the live create-post screen. The `FCommunity_CreatePost` feed entry-card's vertical_view.dart now opens `NewCreatePostScreen` (with optional `preselected_space: DropdownItem` arg from space-details). Old `features/create_post/` retired to `pages/old_create_post/`; **no external imports reach into it** (BuildViewPost was hoisted to `lib/presentation/core/widgets/`).
- Two cards under `lib/presentation/core/renderer/card_views/feed/`:
  - `user_avatar_card_views/` — avatar + display_name + "Schedule for [date]" line (only when scheduled) + admin-gated calendar icon (opens ScheduleViewModal) + eye icon (opens BuildViewPost preview).
  - `post_information_card_views/` — space picker (CustomDropdown OR read-only Text if isFromSpaceDetails), topic picker (MultiTopicSelection if maxTopicsPerPost > 1, CustomToolTipMenu otherwise), conditional title field (TextFormField maxLength 120, gated on postTitlePref), body textbox (10 max / 1 min lines, fixed-height 0.58 * screen scroll), inline pickers (image grid / WebView video / attachment list / poll editor / emoji keyboard), 5-icon footer toolbar (gated paperclip + emoji), Checkbox announcement (gated on pro + admin/mod + email mention).
- Submit goes through `FeedService.createPost` with the same payload shape as the legacy controller — space slug, topic_ids, send_announcement_email, media_images, media (video), scheduled_at, content_type=document + document_ids, survey {options, end_date, type}. Cache invalidation: DatabaseHelper.deleteAllWhr + CacheControllerManager.clearSpaceDetailsController. Pop-before-snackbar order preserved.

### Architecture pieces shipped

**`lib/core/cache/create_post_form_store.dart`** — `CreatePostFormStore` singleton mirroring `EditProfileFormStore`. Holds: `selected_space` (DropdownItem object), `selected_topics` (List<Topic>), `title`, `message`, `pick_option`, `image_upload_list`, `video_upload`, `selected_attachments`, `poll_options`, `poll_end_date`, `allow_multi_poll`, `is_send_announcement`, `is_post_scheduled`, `schedule_date`, `is_from_space_details`. Cleared on screen `onClose()`. Key-mapping docs at top of file.

**`UserAvatarCard.bodyController` static slot** — exposes the body field's TextEditingController so the emoji picker (separate Obx tree, no constructor coupling) can insertEmoji / backspaceBody at cursor position. `_BodyField` assigns in initState, clears in dispose. Same pattern would work for any cross-card text injection.

**`PostInformationFooterToolbar`** — public widget reused by both the in-card position (when keyboard collapsed) and the screen's floatingActionButton (when keyboard visible). Single source of truth for the 5-icon row.

**Preselected-space path** — vertical_view.dart of FCommunity_CreatePost passes the DropdownItem object directly via `Get.arguments['preselected_space']`. Controller's `processArguments` reads it (DropdownItem, not Map — `toJson` doesn't recursively serialize nested Topic objects so a fromJson roundtrip would crash). Sets both `kSelectedSpace` and `kIsFromSpaceDetails: true` in initializePage, so the space row renders read-only Text instead of the dropdown.

### Server contract (confirmed live via admin endpoint with Basic auth)

`GET /wp-json/appza/api/v1/screen?slug=create-post-page` returns 2 views. Public `/wp-json/appza/fcom-mobile/api/v1/screen?slug=...` still 404s (not published for mobile yet) — fall back to the appza admin endpoint until Nusrat publishes.

| Class type | items[0] |
|---|---|
| `FCommunity_UserAvatar` | `{user_id, username, display_name, avatar, schedule_date}` (schedule_date format: "YYYY-MM-DD HH:mm:ss") |
| `FCommunity_PostInformation` | `{spaces: [{group_name, spaces: [DropdownItem...]}]}` — grouped space list |

`page_decoration.background_color: 0xfff5f7fa`. All `styles[*]` blocks ship Lato 24pt bold placeholders today — same situation as edit-profile. Card visuals are **hardcoded** to match legacy until Nusrat publishes real per-key values (see commit `7ef26e20`). When she does: swap hardcoded constants for `parseTextStyle(server-style-map)` calls.

### Insights worth carrying forward

- **DropdownItem.toJson does NOT recursively serialize.** Storing `DropdownItem.toJson()` in form store and reading it back via `DropdownItem.fromJson()` crashes because `topics: List<Topic>` lands in the JSON Map as Topic objects. Fix: store the DropdownItem object directly. Same trap applies to any class whose toJson embeds non-serializable child objects.
- **Server `styles` map ships placeholder defaults.** Same as edit-profile last session — Lato 24pt bold for any text decoration that Nusrat hasn't customized. Cards hardcode legacy visual constants for now; migrate to server-driven styles as a follow-up when Nusrat publishes meaningful values.
- **Old folder seal protocol.** When retiring a feature via `git mv` to `pages/old_*/`, hoist any sharable widgets OUT to `lib/presentation/core/widgets/` so the retired folder has zero external importers. The retired folder's internal cross-imports stay intact (parked code keeps compiling).
- **Emoji picker insertion needs a controller bridge.** Static TextEditingController slot on the base class is the cleanest pattern — set in initState, clear in dispose, mutate from anywhere with insertEmoji helper that also mirrors back to the form-store key so submit / preview stay consistent.
- **Floating toolbar via Builder.** Design widget's `build(model, styleMap, args)` doesn't get BuildContext, so MediaQuery checks (e.g. keyboard visibility) need to be wrapped in a `Builder(builder: (innerContext) => ...)`.
- **`emoji_picker_flutter.Config` collides with `google_fonts.Config`.** Both are re-exported via importer.dart paths. Alias the emoji import (`as ep`) — last session pattern.

### Server-side TODOs for Nusrat (flag in next standup)

- Publish `create-post-page` on the public `/wp-json/appza/fcom-mobile/api/v1/screen?slug=...` endpoint (admin endpoint already works; mobile clients hit the public one).
- Attach `FCommunity_PostInformation.json` spec file to LazyTask 3270 for documentation parity with 3269 (comment 411 already posted asking for this).
- Customize the create-post-page `styles` map — Lato 24pt bold defaults today; need per-key values for `box_decoration`, `primary_text_decoration`, `hint_text_decoration`, `text_decoration`, `icon_decoration`, `avatar_decoration`, `author_text_decoration`, etc. Once shipped, cards swap hardcoded constants for server-driven `parseTextStyle` calls.

### What's open / next-session pickup

- **Hot restart + thorough device verification.** Image upload, video embed (URL validation), file attachment (with edit name / delete), poll submit, schedule date, preview dialog, FAB shift on keyboard, announcement checkbox, topic chips, custom HTML video tab (admin-only). Aim: each picker exercised once.
- **Untrack `.DS_Store`.** Commit `9598c957` accidentally committed `.DS_Store` (already in .gitignore but was previously tracked). Run `git rm --cached .DS_Store` + commit cleanup. Not urgent.
- **SQA hand-off comments on 3269 + 3270.** Tell Nusrat what's been built, what's deferred (server-side TODOs above), and which build channel will carry it (when shipping).
- **MemberCard manual device test** — STILL outstanding (carry-forward 8+ sessions).
- **Multi-word last_name server fix** — still unfixed; flag to Nusrat from previous session.

### Files of note (in case anything regresses)

| Concern | Path |
|---|---|
| Page fetcher | `lib/data/services/network/appza_pages.dart` (`createPostPage`) |
| Form store | `lib/core/cache/create_post_form_store.dart` |
| Controller + screen | `lib/presentation/features/new_create_post/` |
| UserAvatar card | `lib/presentation/core/renderer/card_views/feed/user_avatar_card_views/` |
| PostInformation card | `lib/presentation/core/renderer/card_views/feed/post_information_card_views/` |
| Class type registration | `lib/presentation/core/renderer/card_views/get_selected_card_view.dart:377` (PostInformation), `:392` (UserAvatar) |
| Preview widget (hoisted) | `lib/presentation/core/widgets/build_view_post.dart` |
| Retired old | `lib/presentation/pages/old_create_post/` (sealed — zero external imports) |
| Feed entry-card | `lib/presentation/core/renderer/card_views/feed/create_post_card_views/designs/create_post_card_view_1/orientations/vertical_view.dart` |

---

## Previous session: 2026-05-17 (Edit-profile SDUI rebuild — LazyTasks 3264-3267 + dynamic styles + server-bug diagnosis)

Built the entire new SDUI edit-profile screen from scratch, retired the hand-built one, wired dynamic styles from Figma pins, and diagnosed a server-side name-splitter bug via direct curl. 10 commits, all on `dev`, in sync with origin.

### Commits this session (10, all on `dev`)

1. `f0fd0cec` — scaffold profile-page-edit fetch + placeholder view
2. `7664ef67` — EditProfileFormStore + ProfileUpdated event
3. `45f60164` — FCommunity_ProfileInformation card (3264)
4. `0ba8cfff` — FCommunity_ProfileSocialLink card (3265) + getUserInfo+OptionsService bridge
5. `62c11bb6` — FCommunity_ProfileUsername card (3266)
6. `0dd4d23f` — FCommunity_ProfileSavebutton card (3267)
7. `26a92960` — retire features/edit_profile/ → pages/old_edit_profile/ + rewire caller
8. `ff061037` — fix Get.back order (pop before snackbar) + preserve is_verified
9. `eb7200b1` — wire dynamic styles from SDUI styles map per Figma pins
10. `0c741908` — add member_id to update payload (parity with fc_mobile_stand_alone)

Plus `24f711d0` "refactor: remove redundant decoration wrappers from login card views" (not from this session, but lives at HEAD).

### Outcome

- `lib/presentation/features/new_edit_profile/` is the live edit-profile screen. The Edit Profile button on the new profile screen (`edit_profile_button_card_base.dart:20`) opens `NewEditProfileScreen` directly. Old `features/edit_profile/` retired to `pages/old_edit_profile/` (git mv preserved blame).
- Four self-contained card factories live under `lib/presentation/core/renderer/card_views/member/`:
  - `profile_information_card_views/` — first/last/email/website/short_description
  - `profile_social_link_card_views/` — dynamic providers list
  - `profile_username_card_views/` — editable username gated by `can_change_username`
  - `profile_savebutton_card_views/` — submits via `ProfileService.updateProfile`
- Single fetch: `profileEditPage(userId)` → `/screen?slug=profile-page-edit` → 4 views rendered top-to-bottom via `GetSelectedCardView` in `new_edit_profile_view.dart`.

### Architecture pieces shipped

**`lib/core/cache/edit_profile_form_store.dart`** — `EditProfileFormStore` singleton. `Rx<Map<String, dynamic>>` form snapshot, `setField` / `getField<T>` / `seed` / `snapshot` / `clear`. Cards write field values on text change; Save card reads snapshot to build the payload. Cleared on screen `onClose()` (ephemeral, screen-scoped — NOT wired into `clearAllCardActionStates` because that's for cross-screen action singletons).

**`lib/core/events/app_events.dart`** — added `ProfileUpdated(userId)`. Save card fires on success → `ProfileCacheStore` invalidates the user's cached profile-details snapshot (drops the entry from the LRU map) → next visit to profile-details refetches fresh.

**`lib/data/services/network/appza_pages.dart`** — added `profileEditPage({required String userId})` fetcher.

### Server contract (live, observed via curl `nusrat.appza.net`)

`GET /screen?user_id=11&slug=profile-page-edit` returns 4 views in this order:

| Class type | `items[0]` |
|---|---|
| `FCommunity_ProfileInformation` | `{user_id, username, first_name, last_name, email, avatar, short_description, short_description_rendered, website, can_change_email}` |
| `FCommunity_ProfileSocialLink` | **`[]`** — empty, see bridge below |
| `FCommunity_ProfileUsername` | `{user_id, username, can_change_username}` |
| `FCommunity_ProfileSavebutton` | `{user_id, username}` (no label payload) |

`page_decoration: {background_color: "0xfff5f7fa"}`. All 24 style decoration values are placeholder defaults (Lato 24pt bold black-on-white, 4px paddings). Tightens automatically once Nusrat customizes per-key values.

### Bridges (drop when server bundles natively)

**Social link providers** — server returns `items: []` for `FCommunity_ProfileSocialLink`. Controller's `_bridgeSocialLinkItems()` calls `OptionsService.getSocialLinkProviders()` (cached after first call into `AppConfigModel`) + `ProfileService.getUserInfo()`, merges them client-side into `items[0] = {providers: [{key, title, icon_svg, domain, placeholder, value}, ...]}`, patches the view via `_patchViewItems`.

**`is_verified`** — none of the four views ship it server-side, but the update endpoint expects it (the hand-built screen always sent it). Same `_bridgeSocialLinkItems` flow stashes `is_verified` from `getUserInfo` into `EditProfileFormStore` under the literal key `is_verified`. Save card reads it back into the payload.

### Save payload contract

Mirrors fc_mobile_stand_alone's `edit_profile_screen` exactly:

```
username, member_id, first_name, last_name, email, website,
short_description, user_id, is_verified, social_links
```

Strips empty strings via `payload.removeWhere`. Hits `/wp-json/appza/fcom-mobile/api/v1/members/update`. On success: pop screen first (`Get.back(result: true)`), then `successSnackBar` (snackbar lands on landing). Fires `ProfileUpdated` event so `ProfileCacheStore` invalidates the user's profile-details snapshot.

### Dynamic style mapping (Figma pin discovery via API)

Pulled Nusrat's pinned comments off the design file via `https://api.figma.com/v1/files/MtDxxWCRgzanTKcV6qG6Nu/comments` (token in `~/.zshrc` as `FIGMA_TOKEN`). Parent frame for all 4 edit-profile components: `1412:2180` at (2598, 7760), 1713x829. Children relative bounds:

| Component | Frame node | Relative bounds |
|---|---|---|
| BasicProfile | `1412:2193` | x=60-435, y=281-717 |
| SocialLink | `1412:2223` | x=466-841, y=281-757 |
| Username | `1412:2275` | x=872-1247, y=281-405 |
| Save | `1412:2294` | x=1278-1653, y=281-345 |

50 pins on the parent frame; filtered by relative offset to the right child. Mapping locked in commit `eb7200b1`:

```
ProfileInformation:
  outer (card body):    box_decoration
  field label:          primary_text_decoration
  input box + text:     textbox_licon_decoration
  hint:                 hint_text_decoration

ProfileSocialLink:
  outer:                box_decoration
  subtitle:             secondary_text_decoration
  provider title:       name_text_decoration
  domain prefix:        primary_button_decoration + primary_button_text_decoration
  domain icon size:     icon_decoration
  input box:            secondary_button_decoration
  input text:           secondary_button_text_decoration
  hint:                 hint_text_decoration

ProfileUsername:
  outer:                box_decoration
  section label:        name_text_decoration
  @ prefix:             primary_button_decoration + primary_button_text_decoration
  input box:            secondary_button_decoration
  typed text:           hint_text_decoration (per pin — Nusrat uses one style for both placeholder and typed text)

ProfileSavebutton:
  button (box+text):    textbox_licon_decoration
```

**Hard rule (user-corrected mid-session)**: card views never use `general_decoration` — that key is scaffold/page-level only. `box_decoration` is the single outer wrapper per card.

### Server bug — multi-word last_name (open, needs Nusrat fix)

Confirmed via direct curl: the `/members/update` endpoint runs an unconditional name-splitter on `last_name`. Multi-word values get their first words leaked into `first_name`:

| Sent | Stored |
|---|---|
| `last_name: "test saiful"` | `first_name: "Nusrat test"`, `last_name: "saiful"` |
| `last_name: "Jahan"` | `first_name: "Nusrat"`, `last_name: "Jahan"` ✓ |

Adding `member_id` to the payload (which fc_mobile_stand_alone sends but the hand-built appza_community didn't) does **NOT** fix this. The bug is server-side and exists for fc_mobile_stand_alone too — it just wasn't exercised with multi-word last names. Server-side TODO: stop normalizing `last_name`.

### Debug FAB kept on landing

Amber pencil FAB on `landing_page.dart` opens NewEditProfileScreen with `{user_id: <logged>}`. Kept in `kDebugMode` for parity with the orange/green new_profile debug FABs.

### What's open / next-session pickup

- **ProfileUsername approve button** — Figma has it (`approve_button_decoration` + `approve_button_text_decoration` pins on the right of the input). Not wired yet — separate scope. Adding it changes the username flow from "type + auto-save on form save" to "type + tap approve to confirm change". Decide UX before building.
- **Multi-word last_name server fix** — flag to Nusrat; both clients (this + fc_mobile_stand_alone) are affected.
- **Style customization on dashboard** — Nusrat needs to ship meaningful per-key values for the 24 style decoration groups. Today the UI renders with Lato 24pt bold defaults; visuals will tighten automatically when she sets values.
- **Bundle `social_links` providers into `FCommunity_ProfileSocialLink.items[0]` server-side** — drops `_bridgeSocialLinkItems` in the controller.
- **Bundle `is_verified` into one of the items[0]** — drops the is_verified seeding from the bridge.
- **Avatar in ProfileInformation items[0]** — server bundles avatar URL; not rendered today. Easy to add tappable upload using `BasicProfileCardBase.pickAvatarImage` if we want it on the edit screen.
- **LazyTask SQA hand-off comments** owed for 3264-3267 (plus 3262/3263/3268 still carry-over).
- **Device end-to-end save verification with single-word last names** — confirmed working via log trace; do a fresh manual pass on next session and close the loop with SQA.
- **MemberCard manual device test** — STILL outstanding (carry-forward 7+ sessions).

### Insights worth carrying forward

- **Form-state store is screen-scoped, not action-singleton-scoped.** Cleared on `onClose()`, not in `clearAllCardActionStates`. Don't add ephemeral form stores to the landing-onInit clear path — that path is for cross-screen mutation state (SpaceActions, ProfileCacheStore), not transient form input.
- **Verify server contract via curl before designing client.** Did this mid-session after committing scaffold; confirmed all four `items[0]` shapes match LazyTask specs (except SocialLink which is empty). 1-minute curl saves hours of speculation.
- **Figma comments are the source of truth for style mapping.** Use `https://api.figma.com/v1/files/<key>/comments`, filter by parent frame node_id, map relative `node_offset` to child component bounds. Saved hours vs. eyeballing the design.
- **Card views use `box_decoration` only — never `general_decoration`.** User corrected this mid-session. `general_decoration` is for scaffold-level / page-wrapper styling, not per-card.
- **Get.back BEFORE successSnackBar.** GetX's pop on an active overlay can consume the snackbar instead of the route. Pop first, snackbar second — the snackbar then lands on the parent route which is what the user sees post-navigation.
- **Don't trust SDK-level error swallowing on multi-word fields.** The server-side name-splitter silently mangles `last_name` with no error response; only a follow-up GET reveals the corruption. Always round-trip-test writes when the field shape isn't trivial.

---

## Previous session: 2026-05-16 (NEW profile page from scratch — full feature parity with old, plus SWR caching)

Big session. Built the entire new SDUI profile screen end-to-end as a card-owned architecture, retired the old profile screen, and shipped a sophisticated stale-while-revalidate cache layer. 12 commits, all on `dev`, all pushed.

### Outcome

- `lib/presentation/features/new_profile/` is the live profile screen for the whole app. All 9 navigation call sites (AppBar, MemberCard, MemberActions, leaderboard cards, schedule_post card, feed card author/comment tap, space-member card, firebase deep-link) route through `NewProfileScreen.openFromMemberArg(memberArg)` — a single bridge from the legacy `{item: {xprofile: {user_id, username}}}` shape to the flat `{username, member_id, user_id}` shape the screen expects.
- Old `features/profile/` retired to `lib/presentation/pages/old_profile/` (git mv preserved blame). Still compiles — 4 type-only imports of `ProfileController` remain in `cache_controller_manager.dart` + 3 card views' `Get.isRegistered<ProfileController>()` no-op checks. All dead at runtime, user chose to leave as parking lot.
- Two card factories live on the new `BaseCardFactory<T, C>` pattern: `FCommunity_BasicProfile` and `FCommunity_EditProfile` under `lib/presentation/core/renderer/card_views/member/`. They self-contain ALL actions (image upload, cover/avatar edit, follow/unfollow/message/block/unblock, notification toggle, count-chip dialog) via `static` methods on their base class. No reach into a host page controller; mutation goes through `AppEventBus` events → `ProfileCacheStore` snapshot patches.

### Architecture pieces shipped (new)

**`lib/core/events/app_events.dart`** — Tiny broadcast `AppEventBus` singleton + sealed `AppEvent`s: `CoverChanged`, `AvatarChanged`, `FollowStateChanged` (follow nullable; semantics: null/`'0'`/`'1'`/`'2'`). Mutation sites fire one event in their success branch; subscribers patch.

**`lib/core/cache/profile_cache_store.dart`** — In-memory LRU(10) snapshot store with SWR semantics. Each entry is `Rx<ProfileSnapshot>`; subscribes once to `AppEventBus` and patches the affected snapshot via immutable `copyWith`. Header TTL + about TTL both 5min. On `FollowStateChanged` it patches BOTH `about.items[0].follow` AND `headerViews[EditProfile].items[0].follow_state` so the AppBar kebab and the EditProfile button stay in sync. `clearAll()` wired into `clearAllCardActionStates()` (the OLD landing controller's onInit) — catches logout-relogin + login-without-logout.

**Scoped Tier 2 (intentionally limited)** — Only header + about JSON cached. Tab JSON (Posts/Spaces/Comments) refetches every visit; pagination state preservation deferred to Tier 2.5; cross-mutation events (`PostCreated`, `LikeChanged`) deferred to Tier 3. Memory ceiling ~100 KB. User explicitly chose scoped-down after a "won't this be bloated?" check — math: ~10KB per profile × 10 profiles, negligible.

**`lib/presentation/features/new_profile/profile_cache_prewarm.dart`** — Fire-and-forget warmer called from `LandingController.onInit` 2s after init. Fetches `profile-page-details` + `profile-page-about` for the logged-in user in parallel, primes the store. Skipped if logged out or both already fresh. First tap into own profile after app start = cache hit = instant paint.

### Cards (lib/presentation/core/renderer/card_views/member/)

**`basic_profile_card_views/`** (`FCommunity_BasicProfile`)
- `models/basic_profile_entity.dart` — `@JsonSerializable` with `kApiConverters`, snake-rename. Fields: user_id, username, display_name, avatar, cover_photo, total_points, followers_count, followings_count, blocked_count.
- `shared/basic_profile_card_base.dart` — statics: `isOwnProfile`, `pickCoverImage`, `removeCoverImage`, `pickAvatarImage`, `openUsersDialog`. Image upload flow: gallery → MediaUploadService → ProfileService.updatePhoto → fires `CoverChanged` / `AvatarChanged`.
- `design_1/vertical.dart` — Cover photo (180px, with kebab top-right on own profile when cover non-empty) + avatar (88px overhanging, with camera-plus edit badge bottom-right on own profile) + name/handle + clickable stat chips: Points (static) | Followers → opens dialog tab 1 | Following → tab 0 | Blocked (own profile only) → tab 2.
- `design_1/widget/profile_users_dialog.dart` — Self-contained `StatefulWidget` modal with 3 tabs (Following/Followers/Blocked, Blocked own-profile-only). Per-tab fetch via `MemberService.getFollowings/getFollowers/getBlockedUsers`. Server response shape: `items.followed[]{followed: {...}}` for following+blocked, `items.follower[]{follower: {...}}` for followers. Tap a row → close dialog + navigate to that user's profile.

**`edit_profile_button_card_views/`** (`FCommunity_EditProfile`)
- `models/edit_profile_button_entity.dart` — `@JsonSerializable`. Fields: is_my_profile, loggedInUserId (irregular camelCase, @JsonKey), request_user_id, dropdown_items (List<ProfileDropdownEntity> via readListValue + custom fromJson), follow_state.
- `shared/edit_profile_button_card_base.dart` — statics: `openEditProfile`, `followUser`, `unfollowUser`, `unblockUser` (with confirmation), `openMessageThread`, `toggleNotification`, `handleMenuItemTap`. Each action calls the appropriate service and fires the matching event on success.
- `design_1/vertical.dart` — own profile: Edit Profile pill + kebab (with server-driven items from `dropdown_items`). Other profile: Message + (Unblock | Unfollow | Follow depending on followState) + bell toggle when followState is '1' or '2'.

### Entities lifted to data layer

`lib/data/models/profile/about_member.dart` and `profile_dropdown_entity.dart` — `@JsonSerializable` with `kApiConverters` + snake-rename. AboutMember kept a `copyWith` for legacy `old_profile/profile_controller.dart` mutation paths (avatar/coverPhoto/follow/followersCount/followingsCount). Inner `Route` renamed to `ProfileNavRoute` to dodge Flutter Route collision. `canViewUserSpaces` keeps its `@JsonKey(name:)` override (server sends camelCase for that one field).

### Controller — `NewProfileController extends BaseController`

- Args (required): `username`, `member_id`, `user_id`
- `initializePage` — cache lookup → HIT: hydrate snapshot, `pageState = success` immediately, background SWR revalidate; MISS: cold fetch header, prime cache, load tab 0
- Snapshot worker (`ever<ProfileSnapshot>`) mirrors store changes into `headerViews` + re-derives `aboutMember`/`isRestricted`/`canBlockUser`
- Tabs (`tabRenderers` Rx<List<Widget?>>([4 slots])) — lazy load on tab tap, IndexedStack swap. `_preloadOtherTabs()` fires 1.5s after tab 0 settles, filtered by `aboutMember.profileNavs.slug`, skipped when restricted
- `_deriveAboutFields` bridges `aboutMember.follow` into `headerViews[EditProfile].items[0].follow_state` (`_seedFollowStateIntoEditProfileView`) so the card sees follow state via items[0] until the server ships it natively
- `blockUser` / `unblockUser` fire `FollowStateChanged` with the right value (block='0', unblock=null)

### Screen — `NewProfileScreen extends BasePageView<NewProfileController>`

- AppBar: back arrow, title "Profile", Block/Unblock kebab (`_ProfileKebab` — gated on followersModule + logged-in + isOtherUser)
- Body when restricted: `_HeaderViews` + tab strip (decorative) + `_RestrictedOverlay` (lock + "This profile is private" + Login link for guests)
- Body normally: `_HeaderViews(views)` + `_TabStrip(profileNavs)` + Expanded(`_TabBody(IndexedStack)`)
- `_HeaderViews` renders each view via `GetSelectedCardView(classType, selectedDesign, args)` — server controls view order; no client reordering
- `_ProfileShimmer` for `buildLoadingView` — cover-shape block + avatar overhang + name/handle bars + 3 stat chips + a button bar

### Server ↔ client contract (today)

- `GET /screen?slug=profile-page-details&username=&member_id=&user_id=` returns header view list (BasicProfile + EditProfile). EditProfile items[0] includes `dropdown_items` (server shipped this during the session).
- All 24 style decoration values are server defaults (placeholder Lato bold black-on-white 24pt, 4px paddings, 0.3 fractional avatar/image w/h). Nusrat hasn't customized yet — when she does, UI will tighten automatically via the existing parse* hooks.
- `avatar_decoration.width / height = "0.3"` server bug still present — workaround in card: ignore those values, hardcode 88px avatar, 180px cover. **Server-side TODO: pixel values, not fractions.**

### Debug FABs on landing

- Orange person — opens own profile
- Green person_search — opens mahdi's profile (user_id 61) for testing the other-user paths (Follow/Unfollow/Message/Block/Unblock/bell)
- Plus the existing playground / signin / chat experiment FABs from prior sessions

### EditProfileController disposed-race fix (drive-by)

`_populateFields()` was writing to disposed TextEditingControllers when the screen was reopened before the previous fetch settled. Pre-existing, exposed by faster navigation through the new screen. Added `_disposed` flag set in onClose, checked at top of `_populateFields`.

### Commits this session (12, all on `dev`, all pushed)

1. `4d96205b` — feat(new-profile): scaffold SDUI page with BasicProfile + EditProfile cards
2. `80bdc7c3` — feat(new-profile): pinned header + lazy-loaded tab renderers
3. `43eccb62` — feat(new-profile): kebab block/unblock + privacy overlay + shimmer loader
4. `e996d697` — feat(new-profile): card-owned kebabs, image actions, tab preload, restricted parity
5. `281f9e97` — refactor(edit-profile-card): read dropdown_items from items[0], drop /options fetch
6. `3a879598` — feat(profile-cache): scoped Tier-2 SWR snapshot store + event bus
7. `e4320898` — feat(profile-cache): prewarm logged-in user's snapshot from landing onInit
8. `8137d722` — refactor(profile): retire features/profile, route all callers to NewProfileScreen, lift entities to data/models
9. `28364de9` — feat(basic-profile-card): avatar camera-plus edit badge on own profile [Tier B]
10. `148da15f` — feat(edit-profile-card): wire Follow / Unfollow / Message / Unblock to real services [Tier A] (+ EditProfile disposed-race fix)
11. `130a51f1` — feat(basic-profile-card): clickable count chips + self-contained Users dialog [Tier C]
12. `51033f24` — feat(edit-profile-card): notification bell toggle when following [Tier D]

### What's open / next-session pickup

### NEXT SESSION FOCUS — Edit Profile batch (LazyTasks 3264–3267)

User explicitly named edit-profile as the next session's task at wrap-up. The four V:2.1.5 New Components are:

| ID | Component | Class Type | Scope | Slug |
|---|---|---|---|---|
| 3264 | Basic Profile Information | `FCommunity_ProfileInformation` | profile-page-edit | basic-profile-information |
| 3265 | Profile Social Link | `FCommunity_ProfileSocialLink` | profile-page-edit | profile-social-links |
| 3266 | Profile Username | `FCommunity_ProfileUsername` | profile-page-edit | profile-username |
| 3267 | Save Button | `FCommunity_ProfileSavebutton` | profile-page-edit | save-button |

All 4 share the same `profile-page-edit` SDUI page — one fetch, four views. Existing hand-built screen at `lib/presentation/features/edit_profile/edit_profile_screen.dart` (417 lines, uses `_SectionCard` / `_LabeledField` / `_buildSaveButton` private widgets, `BasePageView<EditProfileController>` base, NOT SDUI-rendered).

**First decision to make at session start**: do we go the same route as the new profile page (build pure SDUI from scratch, four card factories, retire the hand-built edit_profile screen to old/) or wire styles into the existing hand-built widgets (like we did for chat-details before profile)?

Recommendation: pure SDUI rebuild — consistent with where the new profile landed, gets to component-self-contained state immediately, no half-measure. ~4 card factories + an `edit_profile_v2` screen using `SduiPageController` would mirror the new_profile shape. The existing hand-built screen retires to `pages/old_edit_profile/`.

Before coding: curl `/screen?slug=profile-page-edit&member_id=<self>` to confirm what items[0] each of the four views returns. Server may need extensions if `Save Button` needs validation results, etc.

LazyTask creator: Nusrat (id 11). Auto-reassign back to her per workflow when the build is delivered.

### Other open items

- **Device verification of bell toggle** — user did the screenshot after Follow but bell didn't appear; likely hot-reload artifact (new top-level widget class needs hot restart). User said "commit and push" without confirming the restart. Verify on next session.
- **LazyTask SQA hand-off comments** — 3262 (Basic Profile) and 3263 (Edit Profile Button) owed. Both effectively complete pending verification. Plus 3268 (ChatInput) still owed from previous session.
- **Server-side asks**:
  - Style customization on `profile-page-details` (everything is defaults right now; Nusrat needs to set per-key values via dashboard)
  - Fix fractional `avatar_decoration` / `image_decoration` width/height to pixel values
  - Ship `follow_state` directly inside `FCommunity_EditProfile.items[0]` for other-user profiles (today we bridge it client-side from AboutMember.follow)
- **Tier 2.5 (deferred)** — Tab data caching with pagination state hooks (engine has `onSavePaginationState` / `onGetPaginationState` / `onClearPaginationState` callbacks ready) + scroll position preservation per tab. Currently posts/spaces/comments refetch every visit.
- **Tier 3 (deferred)** — Event broadcasts from external mutation sites: `PostCreated` (so a new post you created elsewhere prepends into the cached Posts tab instantly), `LikeChanged`, `CommentAdded`. Touches many files outside profile.
- **`my_spaces` slug in edit-row kebab** — intentional no-op since tab navigation is page-coupled. Could be resolved by an event bus mechanism for "request tab switch" or by having the server emit a `target_tab` field in dropdown items.
- **Edit-profile batch (LazyTasks 3264–3267)** — original V:2.1.5 follow-up after profile-details. Same pattern as 3268 ChatInput / 3262/3263 BasicProfile+EditProfile. Existing edit_profile screen is hand-built (`lib/presentation/features/edit_profile/`); needs SDUI style wiring (or pure-SDUI rebuild?). Discuss approach at start of next session.
- **User Avatar (LazyTask 3269)** on create-post-page — separate scope.
- **MemberCard manual device test** — STILL outstanding (carry-forward 6+ sessions now).
- **Pull-to-refresh** on SDUI tab content — not verified in new screen; engine's `onRefresh` should fire `refreshPage()` but the wiring is generic. Test when SQA flags.

### Insights worth carrying forward

- **Self-contained cards** is the architecture target. When the user said "card should manage this via api response, no needed manually passing", the right response was to move state out of cards' static Rx maps and into `ProfileCacheStore` snapshots + events. Cards still own action methods but state lives in one place.
- **SWR > stampede prevention via lifecycle hooks.** The cache subscribes ONCE to the event bus and patches affected snapshots; controllers subscribe to per-userId snapshot changes via `ever<ProfileSnapshot>`. No locks, no debouncing, no controller fanout.
- **Bridge layer for server-shape evolution.** When server hasn't shipped a needed field (`follow_state` on EditProfile items[0]), bridge it client-side in the controller (`_seedFollowStateIntoEditProfileView`) so the card stays clean. Once server ships, drop the bridge. Same pattern used for `dropdown_items` (lasted ~10min before server shipped it).
- **Component flow contained in component folder.** When the Users dialog initially lived in `features/new_profile/widgets/`, the user pushed it into `card_views/.../basic_profile_card_views/design_1/widget/profile_users_dialog.dart`. Card folder structure ([page_family]/[card]/{models, shared, design_N/{widget?}, [card]_factory.dart}) is the convention.
- **LRU snapshot cache cost is tiny** vs. perceived cost. ~10KB × 10 profiles = 100KB. Always do this math before fearmongering about "bloat".
- **Mutation events > direct state writes** for cross-screen consistency. AppBar Block/Unblock + EditProfile row Block/Unblock both fire `FollowStateChanged`; both surfaces update from the single store patch. No "make sure both code paths update both".

## Previous session: 2026-05-14 (chat-details refactor + SDUI style wiring + perf)

Two-act session: (1) tear apart the spaghetti `chat_details_view.dart` into per-feature widgets; (2) wire the SDUI `chat-page-details` styles map into every visible surface.

### Act 1 — view extraction (1532 → 378 lines, −75%)

Split `lib/presentation/features/chat_details/chat_details_view.dart` into 8 public widget classes under `widgets/`. Pure UI extraction, no behavior change beyond dropping an unused `fullMessage` param on `ChatReplyBubble` and an unused `messageText` local in delete-confirmation. Controller untouched.

| # | Commit | Widget | Style keys bound later |
|---|---|---|---|
| 1 | `a05897bf` | `ChatAppBarAvatar` (+ private `_DefaultAvatar`) | `avatar_decoration` |
| 2 | `a6f9d9a9` | `ChatReplyPreview` | `textbox_licon_decoration` |
| 3 | `3bf34643` | `ChatInlineMessageStatus` | (none) |
| 4 | `6254fcb9` | `ChatImageGrid` + `ChatImageGridFromMediaItems` | (none yet — server owes `image_decoration`) |
| 5 | `01161bd4` | `ChatMessageMenuButton` (with private helpers `_canShowDelete`/`_handleAction`/`_confirmDelete`) | (none) |
| 6 | `9874433c` | `ChatReplyBubble` | `textbox_licon_decoration` |
| 7 | `3219435e` | `ChatMessageBubble` | `box_decoration`, `primary_button_decoration`, `primary_text_decoration`, `primary_button_text_decoration`, `author_text_decoration`, `secondary_text_decoration`, `text_decoration` |
| 8 | `f456d955` | `ChatMessageInput` | `general_decoration`, `text_decoration`, `hint_text_decoration`, `textbox_licon_decoration`, `icon_decoration`, `approve_button_*`, `button_*` |

Pattern locked: public class in `widgets/`, controller via `Get.find<ChatDetailsController>()` (mirrors the existing `mvc` shortcut), commit per widget. Linter formatter pass on the trimmed view added as `f01c1218` (user-initiated).

### Act 2 — SDUI style wiring

**Endpoint:** `GET /screen?slug=chat-page-details` returns ONE view (`FCommunity_ChatInput`) with all 16 style groups packed into its `styles` map. Single card styles the entire screen — explicit design choice on the server side.

**Fetcher** (`61eee142`): added `chatPageDetails()` in `appza_pages.dart` mirroring `chatPage()`. Returns `PageResult`.

**Controller** (`61eee142`): added `final chatInputStyles = <String, dynamic>{}.obs;`. `initializePage()` awaits `_loadChatInputStyles()` **before first paint**, then `getString()`, then community members if applicable. `_loadChatInputStyles()` finds `views.firstWhere(class_type == 'FCommunity_ChatInput')` and writes the styles into the Rx map. Map populated atomically once → widgets read `mvc.chatInputStyles` directly inline in `build()` (no `Obx` needed).

**Parsers** (from `appza_engine/.../parser_functions.dart` via `importer.dart`): `parseBoxDecoration`, `parseTextStyle`, `parseImageDecoration`, `parsePadding`, `parseMargin`, `parseColor`, `parseAlignment`, `parseBoxFit`. All accept string-typed JSON values. `parsePadding` / `parseMargin` are **non-nullable** — wrap with null guard.

**Final widget → style mapping (canonical contract):**

| Widget surface | Style key(s) |
|---|---|
| Scaffold body `Container` | `general_decoration` (padding/margin/decoration) — user added at view level (`974c18c6` + `7242c5df` fix) |
| Received bubble container | `box_decoration` |
| Received bubble body text | `primary_text_decoration` |
| Sent bubble container | `primary_button_decoration` (per user direction `b33b8b2b`) |
| Sent bubble body text | `primary_button_text_decoration` |
| Sender name above bubble | `author_text_decoration` |
| Received timestamp | `secondary_text_decoration` |
| Sent timestamp | `text_decoration` (per user direction `68e3485c` — dual binding) |
| Input outer container | `general_decoration` |
| Typed input text | `text_decoration` (primary binding) |
| Input hint placeholder | `hint_text_decoration` |
| Input text-field fill + border | `textbox_licon_decoration` |
| Emoji + photo icons | `icon_decoration` |
| Send button (ready) | `approve_button_decoration` + `approve_button_text_decoration` (per `b9deed75`) |
| Send button (uploading) | `button_decoration` + `button_text_decoration` |
| Reply preview strip (above input) | `textbox_licon_decoration` (`6691a7a9`) |
| Reply bubble (inside a message) | `textbox_licon_decoration` (`6691a7a9`) |
| AppBar avatar | `avatar_decoration` color/alignment/boxfit/radius; size hardcoded 40px (`0d74073f`) |

**Sent-vs-received split principle**: `isMe` reads the "button" variant (primary_button → bubble, text_decoration → timestamp), non-isMe reads the "box/secondary" variants. Two visual languages on one map, no separate sent/received style groups needed.

### Perf — first-paint timing

Stopwatch instrumented in `initializePage` (`8ecf001d`). Baseline:
- `styles fetch: 1020ms`
- `messages fetch: 926ms`
- Sequential await total: **1946ms** (blocks first paint)

Two-step optimization in one commit (`8ecf001d`):
1. **Parallelize** via `Future.wait` — drops to ~1242ms (cold, bounded by slowest fetch).
2. **In-memory cache** for styles — static field `ChatDetailsController._cachedChatInputStyles`. Cold MISS pays the network fetch; warm HIT is ~0ms. Survives controller disposal, cleared on app restart only. User explicitly chose runtime-memory only (not Hive/SharedPreferences persistence) — keeps logic trivial, accepts cold-load cost per app launch.

Cache log lines: `[ChatDetails] styles cache HIT` / `MISS — fetching`. Stopwatch + `total initializePage` colorPrint left in for now — useful for ongoing perf monitoring per the "Use colorPrint for Debug Traces" rule. Can strip when perf is locked in.

### Known gaps / server-side TODOs (do NOT client-side hack around these)

- `avatar_decoration.width` / `height` come back as `"0.3"` (fractional placeholder). `parseImageDecoration` reads them as 0.3 pixels = invisible avatar. **Workaround in `ChatAppBarAvatar`**: ImageDecoration construct manually with `width: 40, height: 40` hardcoded, but pull `color`/`alignment`/`boxfit`/`radius` from the server. Same workaround needed for the 32px in-bubble avatar inside `ChatMessageBubble` — currently still hardcoded entirely, did NOT wire `avatar_decoration` there. Server should fix the fractional values, then we can use `parseImageDecoration` faithfully and drop the workaround.
- Nusrat pinned an `"Image decoration"` comment at (216, 1032) on the figma frame (`1412:2510`) — server has NOT yet added an `image_decoration` key. Once shipped, wire it into `ChatImageGrid` + `ChatImageGridFromMediaItems`. Open server-side TODO.
- `text_decoration.text_decoration: "lineThrough"` is set on the server but parsed faithfully — typed input text shows strikethrough. Probably accidental on the server; leave as-is.

### Figma comments workflow (one-off, ad-hoc this session — not yet automated)

Used `FIGMA_TOKEN` env var to fetch `/v1/files/MtDxxWCRgzanTKcV6qG6Nu/comments`, then matched comments under the parent frame `1412:2510` (Chat page - Details) via descendant containment on `client_meta.node_id` / `stable_path`. 40 pins from Nusrat label each decoration name; cross-referenced with screen layout to produce the widget mapping table above. The most recent comment is mine (`Md Saiful Hossain, 2026-05-13`): "Please Use all of the value statically" — confirms apply-the-map-directly approach (no renderer pipeline).

Proposed (but NOT yet built) automation deferred per user "we will do this later":
1. `lib/presentation/features/<feature>/<feature>.kit.json` per-screen sidecar with `sdui_slug`, `style_class_type`, `figma_file_key`, `figma_frame_node`, and a `decoration_map`. Single source of truth, no re-discovery next session.
2. Tiny `scripts/figma-pins.sh` + `scripts/sdui-styles.sh` to replace the inline curl+python dance.
3. LazyTask description convention — embed slug + figma URL so `tasks show <id>` gives everything.
4. `/component-setup` slash command (only after 3+ component tasks make it pay off).

### Memory rule added

`feedback.md` gained **"Never Download PNG/Image Attachments From LazyTasks"** (`219e57d8`). Hard rule — empirically Claude gets so badly lost downloading LazyTask images that it stops responding. If task context needs a screenshot, ask the user to paste it directly in chat (chat-attached images are safe). Stronger than a preference; treat as inviolable.

### Commits shipped this session (all on `dev`, all pushed)

Refactor pass (8 widget extractions + linter cleanup):
- `a05897bf`, `a6f9d9a9`, `3bf34643`, `6254fcb9`, `01161bd4`, `9874433c`, `3219435e`, `f456d955`, `f01c1218`

Style wiring + perf:
- `219e57d8` — feedback: never download lazytask image attachments
- `61eee142` — feat: fetch FCommunity_ChatInput styles before first paint
- `b28c2c53` — feat: apply box/text styles to ChatMessageBubble
- `fa02c61b` — feat: apply styles to ChatMessageInput
- `b33b8b2b` — feat: use primary_button styles for sent message bubbles
- `6691a7a9` — feat: textbox_licon_decoration on reply preview + reply bubble
- `0d74073f` — feat: apply avatar_decoration to ChatAppBarAvatar (with size override)
- `b9deed75` — feat: rebind send button to approve_button styles
- `68e3485c` — feat: sent timestamps read text_decoration
- `974c18c6` — feat: apply general_decoration to chat body container (user-authored)
- `7242c5df` — fix: correct margin parser + null-safe general_decoration
- `8ecf001d` — perf: parallel fetch + in-memory styles cache

Total: **21 commits this session, all squash-eligible, all on `dev`, all pushed.**

### What's open / next-session pickup

- **MemberCard manual device test** — STILL outstanding (5 sessions now). Critical checks unchanged from previous notes: single snackbar on follow/unfollow, list rebuild via `onRefresh`, admin status update flow, logged-out gating, profile navigation. Carry forward.
- **Server-side gaps** (above): proper `image_decoration` key + fix `avatar_decoration` width/height to pixels (not fractions). Once landed, wire `ChatImageGrid` + drop the avatar-size workaround.
- **Forms-architecture follow-ups from previous session** — still unstarted: validation layer, `show_when`/`hide_when` conditionals, `FormRegistry.clearAll()` lifecycle, inline submit spinner, chat refetch-on-send. Lower priority than completing the V:2.1.5 "New Component" backlog.
- **LazyTask "New Component" batch (V:2.1.5)** — 9 tasks total (3262–3270), 6 High priority. Task 3268 (ChatInput) is now effectively complete pending Nusrat verification; the others (Basic Profile Information, Profile Social Link, Profile Username, Save Button, User Avatar, Post Information, etc.) follow the same pattern: extract widget if needed → wire styles from the appropriate SDUI page slug → device-test.
- **Stopwatch instrumentation in `initializePage`** — kept on per the colorPrint rule. Strip when perf is locked or noise becomes a problem.
- **Comment on LazyTask 3268** — owed to SQA (mandatory hand-off comment per the tasks skill). Sub-issues addressed: styles fetched from `chat-page-details`, applied statically across bubbles/input/replies/avatar with the decoration → widget table above; first-paint perf parallelized + cached. Not yet posted. Do at start of next session before claiming complete.
- **Reassignment of 3268** — wait until app is delivered (build pushed via `shipping_to_nusrat.md` flow). Don't auto-reassign on commit per the Completion ≠ Reassignment rule.

### Untouched LazyTasks (no priority change)

- 2828 (Push Notification), 2830 (Feed link list-1), 2834/2835/2836 (Feed Post-1 UI nits), 3205 (comment reply-to-reply), 3271/3272/3274/3277–3285 (V:2.1.5 New Works backlog beyond New Components).

### Awaiting other devs (don't action)

- 3200 (Hasan vai — Space members loading)
- 3202 (Sohel vai — wrong username status)
- 3203 (plugin dev — wrong password login)
- 3224 server-side: backend dev to honor "everyone" permission on `/members/followers` + `/members/followings` for guest requests.

## Previous session: 2026-05-05 (SDUI forms architecture + chat-experiment + card_views reorg)

Big architectural session. Built a forms layer for SDUI from scratch, used it to wire two experiment pages (login form + cross-card chat), and reorganized the entire `card_views/` tree by page family.

### Forms architecture (NEW — `lib/presentation/core/forms/`)

Five files, ~250 lines total. Treat as the canonical cross-card coordination primitive going forward. **Reuse for filters, multi-step forms, list↔detail interactions, anything that needs shared reactive state across cards.**

- **`form_session.dart`** — `FormSession` with `Rx<Map<String, dynamic>> fields`, `setValue`, `getValue`, `reset`. Reactive: any card that does `Obx(() => session.fields.value['x'])` rebuilds on changes from any other card.
- **`form_registry.dart`** — `FormRegistry.instance.get(formId)` singleton, lazy-creates sessions keyed by `form_id`. **No LRU bound, no auto-clear on landing onInit yet** — sessions persist process-wide. Wire `FormRegistry.instance.clearAll()` into `clearAllCardActionStates()` later if stale-state bites.
- **`form_handlers.dart`** — `FormHandlers.register(name, handler, {onSuccess?})`. Handler is `Future<ApiResponse> Function(values)`. Optional `onSuccess(response, session)` lets a handler clear specific fields, suppress the default snackbar, etc.
- **`form_submitter.dart`** — `FormSubmitter.submit(formId, handlerName)` static. Used by FormSubmit card AND by AppBar actions (proven in playground). Default behavior: success snackbar; if handler has onSuccess, calls that instead.
- **`form_text_field.dart`** — Reusable `StatefulWidget` that owns its `TextEditingController` for the widget's lifetime, seeds initial text from session-or-default, listens for external resets via `session.fields.listen` so onSuccess clears propagate to the controller's text.

**Architectural rules learned/codified:**
- Cards are pure renderers; cross-card state lives in `FormSession`. Cards do `Obx(() => session.fields.value[key])`.
- JSON declares structure (`form_id`, `field_id`, `submit_handler`, `obscure`, `placeholder`, etc.); code declares behavior (registered handlers).
- `submit_handler: "X"` in JSON + `FormHandlers.register('X', ...)` in code is the dispatch contract. Handlers are typically registered in `app_initializer.dart`; for page-specific behavior they can be registered in a controller's `initializePage`.
- `onSuccess` does the right thing for fire-and-forget actions like chat send (clear input, no snackbar). Don't snackbar every chat send.

### Two playground pages built (debug FABs on landing, `kDebugMode` only)

**Playground (purple flask FAB)** — `lib/presentation/pages/playground/`
- 3 form cards stacked: `FormPair` (first/last name), `FormOne` (username), `FormTwo` (password, obscured)
- AppBar Submit + in-page Submit button — both call `FormSubmitter.submit('playground_login', 'login')`
- Real login via `AuthService.login` registered as the `login` handler in `app_initializer.dart`
- Multi-field-in-one-component proven via `FCommunity_FormPair` (declares N field_ids in a single `fields` array, renders them via `FormTextField` each)

**Chat experiment (teal chat-bubble FAB)** — `lib/presentation/pages/chat_experiment/`
- Two cards: `FCommunity_ChatMessageList` + `FCommunity_ChatMessageInput`, sharing `form_id: "chat_experiment"`
- Controller auto-fetches `ChatService.getSpaces()` → first thread → `getMessages(threadId)` → normalizes to `{id, author, text, is_mine}` → seeds `session.fields['messages']`
- List card `Obx`-watches `session.fields['messages']` and renders bubbles. Right-aligned blue bubble for `is_mine == true`, left-aligned grey bubble with avatar + author label for others.
- Tap any message → writes structured `reply_context: {message_id, author, preview}` to session → reply strip in input card appears via Obx
- Cancel-X on strip → clears `reply_context` only (typed text preserved)
- Send → real `ChatService.sendMessage(threadId, text, replyMessageId, replyMessageText)`. The `send_chat_message` handler's `onSuccess` appends the just-sent message to `messages` (optimistic), then clears `message_text` + `reply_context`. **No snackbar.**
- Three independent rebuilds from one `session.fields` change — single source of truth for cross-card coordination, exactly the architectural payoff.

**Known caveats / things deliberately NOT done:**
- Optimistic-appended message is built from local values + `LoggedUser().displayName`; does NOT use `response.data`. Server-side text transforms (auto-link, mentions) won't show until page reload. Easy improvement: parse `response.data` or refetch via `_loadMessages()` from onSuccess (handler in app_initializer doesn't have controller access — would need refactor).
- HTML stripping is naive regex `<[^>]+>`. Mangles literal `<`. OK for experiment.
- No realtime updates / sockets. Refresh = leave & re-enter.
- `FormSession` persists across page exits. Sent messages stay in the in-memory list across navigation. Lifecycle clear deferred.
- Chat-send payload doesn't include `mediaItems` / `last_message_id`. Text + reply only.

### Card-views reorganization (cosmetic but big)

`lib/presentation/core/renderer/card_views/` was 39 flat folders → reorganized into **12 page-family parents** + 3 organizational folders kept flat. All `git mv` (renames preserved blame), then mass sed pass on absolute imports + relative escape paths bumped one ../ deeper. Net: 0 errors, 10 pre-existing warnings untouched.

```
card_views/
├── chat/, comment/, course/, document/, drawer/, feed/,
│   leaderboard/, member/, notification/, playground/,
│   schedule_post/, space/      ← page-family parents
├── _example_new_architecture/, base/, shared/  ← stay flat
└── get_selected_card_view.dart
```

**`home/` was renamed to `feed/`** in a follow-up — cards in there (feed, feed_banner, link, create_post) appear on home, space-details, profile, bookmark, search. Calling it "home" was misleading.

**`card-view` skill (`.claude/skills/card-view/SKILL.md`) updated** to reflect the new structure: page-family list, full paths under `<page_family>/<card_name>/`, page_configs typo fix → `page_josn`, and three improvements ported from the standalone version (commit-only-when-asked rule fix, kApiConverters preference reframed, honest gap-flagging that no kApiConverters exemplar exists in this repo's card_views yet).

### Commits shipped this session (all on `dev`, all pushed)

- `5942a5e` — feat: add debug Playground screen and fix home page JSON type (earlier in session)
- `13cca9e` — feat(playground): add FormOne/FormTwo/FormSubmit card views
- `17bdefe` — fix(forms): own TextEditingController for the widget's lifetime
- `8c745b9` — feat(forms): orchestrate SDUI cards via FormSession + handler registry
- `48d6f35` — refactor(card_views): group families by page (home/space/course/...) [310 files]
- `5e1d6e2` — refactor(card_views): rename home/ to feed/
- `942b168` — docs(card-view skill): align with page-grouped reorg + port standalone fixes
- `f4ac1392` — feat(chat-experiment): cross-card SDUI page with real chat send

### What's open / next session pickup points

**Forms-architecture follow-ups (in priority order):**

1. **Validation layer** (`#2` from the roadmap I proposed, deferred for chat exploration). Add `FormValidators` registry + JSON `validation: { required, min_length, regex }` declarations + `errors` map on session + per-field error rendering in `FormTextField`. ~120 lines.
2. **Layer 2 cross-card visibility** — JSON `show_when` / `hide_when` rules + a `FormConditional` wrapper widget that wraps every card with such rules in `get_selected_card_view.dart`. The 80% case for "trigger another component to rebuild." ~80 lines.
3. **Lifecycle**: wire `FormRegistry.instance.clearAll()` into `clearAllCardActionStates()` so sessions don't leak across logout/relogin or stale chat threads.
4. **`isSubmitting` on session + inline button spinner** — kills the global EasyLoading modal in favor of in-button feedback. Small, polish.
5. **Refetch chat messages on send-success** instead of optimistic append — needs handler→controller bridge (currently impossible because handler is in app_initializer). Either move handler registration into the controller's `initializePage`, or add a `session.refresh_callback` slot the controller registers.
6. **Cross-page interaction patterns** (filter→list reload pattern, deferred). The chat case was easier than filter→list because chat is single-direction (input → submit) — filter→list needs the data source to react to external param changes. Open architectural question.

**Pre-existing carry-forward (still un-actioned across multiple sessions):**

- ⚠️ MemberCard manual device test STILL never happened (4 sessions outstanding now). The new-arch MemberCard is in production but has zero device-verification.
- Card #3 candidate (`notification_card_views` or `drawer_top_link_card_views`) for the new architecture (CardRegistry-based, not BaseCardFactory). Distinct from the form/chat experiment cards which are intentionally legacy `BaseCardFactory` for ergonomics.
- `_example_new_architecture/` folder rename — wait until 3+ cards locked.

**LazyTasks untouched (no priority, no progress):**
- 2828 — Push Notification isn't working
- 2830 — Feed link list-1 properties issue
- 2834 — Create post-1 textbox licon decoration
- 2835 — Feed Post-1 margin/padding
- 2836 — Feed Post-1 decoration

**Awaiting other devs (don't action):**
- 3200 (Hasan vai), 3202 (Sohel vai), 3203 (plugin dev) — all from prior sessions, unchanged.

### Insights worth carrying forward

- **`FormSession` is the universal cross-card primitive.** Forms, chat reply context, multi-field components all use it. Don't build parallel singletons (FilterRegistry, ChatRegistry) without a strong reason — the reactive `Rx<Map>` is the same mechanic regardless.
- **JSON for structure, code for behavior.** `form_id` / `field_id` / `submit_handler` are JSON-declared so the server controls form composition. Handlers + onSuccess live in code so the client owns service-layer dispatch and side effects.
- **`onSuccess` is what unlocks fire-and-forget actions** (chat send, comment post, react). Default success snackbar is wrong for these; per-handler onSuccess is the cleanest opt-out.
- **Reorg blame survives** if every move is `git mv` (or batched as renames at commit time). Lost blame on `form_pair_card_views/` only because it was untracked when the batch ran.
- **Reorg taxonomy: name folders by data domain, not page**. "home/" was wrong because the cards reused on profile, space-details, search, bookmark. "feed/" is right because that's the actual domain.

## Background: Prior architecture thread (`_example_new_architecture/`) — paused

A parallel-track architecture lives at `lib/presentation/core/renderer/card_views/_example_new_architecture/`. **Two cards migrated and shipped** (registered, wired through `app_initializer.dart` → `registerAllCardTypes()` → `get_selected_card_view.dart` → `CardRegistry.build()`):

- `SpaceCard` — first migration. **"Rich recovery" shape**: `_refreshEntity`, `_applyJoinResponse`, `state.data ?? entity` fallback, retry chip on network error. Used because Space's host page (landing) does not rebuild on join; the card is responsible for keeping itself in sync.
- `MemberCard` — second migration. **"Minimal 2-state" shape**: action layer is `idle ↔ loading` only, no recovery layer, no error rendering, no retry chip. Used because Member's host pages call `args['onRefresh']` on success, which rebuilds the list with fresh server entities — the card needs only a loading flag.

The legacy `BaseCardFactory` cards in `space_card_views/` and `member_card_views/` are retained as orphaned fallbacks — not deleted yet, kept until card #3 ships and the new versions are stable in production.

**Key invariants of the new architecture (do not break):**
- Factory parses `item` → typed entity **once** at the boundary; design widgets receive a `final SpaceListEntity entity` / `final MemberListEntity entity` field. No `parse()` getters that re-run `fromJson` on every access.
- `(design, orientation)` switch lives **inside each factory** (no shared `DesignDispatch` helper — that was deleted because it ignored `design`).
- `StyleMap` is typed; `item` is typed via the entity; `args` stays as `Map<String, dynamic>` (callback bag for `onRefresh` etc.).
- `CardRegistry.findByValue(rawString)` is the source of truth for raw-string → CardType mapping. There is intentionally no `CardType.values` list to keep in sync.
- `ScopedState<T>` has an LRU bound (default 100) — never relies on host calling `clear()`.
- **Space invariant** — `_buildErrorButton` is only rendered when `error.shouldRetry == true`. For all other errors (auth/business/parse/unknown), the normal button stays visible — the snackbar communicates the failure. Action layer is allowed to call `_fetchFreshSpace` as recovery (passive entity refresh). It must **not** interpret a rejection as success or auto-navigate. UI renders whatever's true via `current = state.data ?? entity`.
- **Member invariant** — action layer has NO recovery, NO retry, NO error rendering. State machine is `idle → loading → idle` only. `ActionState.data` stays null; the design widget reads only `isLoading`. Errors are snackbar-only and forgotten — next user tap is a fresh attempt. Entity is always the freshest from the most recent `onRefresh`-driven rebuild.
- `clearAllCardActionStates()` is called in `landing_controller.dart` (old) `onInit()`. Currently wires only `SpaceActions.instance.clearAllStates()`. **`MemberActions` is intentionally NOT wired** — Member cards live in many places (member lists, search, leaderboard, profile views), not just landing, so there's no single onInit anchor.
- **Snackbar policy** — action layer must NOT fire `errorSnackBar(response.message)` in the `else` branch. `CommunityApiServiceBase._handleErrorResponse` (`lib/data/services/network/services.dart:187-200`) already auto-fires it for any call with `showSnackBar: true` (the default in `postCommunityData` / `getCommunityData`). The only `errorSnackBar` calls an action layer should make are: (1) the auth pre-check (defensive — base hasn't been called yet) and (2) the `catch` of *destructive* actions (status update, remove) as a generic fallback for legacy parity. Soft actions (follow, unfollow, unblock, join) should be silent in catch — just `colorPrint`.

## Last session: 2026-04-29 (LazyTasks 3224 + 3227 — followers loader + space list sync)

Two high-priority bugs shipped to Nusrat in one build, plus one drive-by fix at session start.

### Code changes shipped (all on `dev`)

- **`b9e453f` — ui(space-members): hide kebab menu for pending members.** Drive-by request before tasks started. `space_member_card_views/.../vertical_view.dart:285` — `_shouldShowPopupMenu` now returns false when `item.status == 'pending'`. Pending rows already expose Approve/Decline buttons, so the "Remove Member" popup was redundant. No LazyTask.
- **`193d12f` — fix(profile): stop infinite loader on followers/following fetch failure.** Task 3224. Controller initialised `followingUserList` / `followersUserList` / `blockedUserList` to `null` and reset them to `null` on any API failure. Dialog UI treats `null` as "still loading" → infinite spinner forever when fetch fails. Failure paths now resolve to `[]` so the empty state renders. Loading state on initial fetch still works (lists start as `null`, only flipped after request resolves).
- **`59cd2d6` — fix(spaces): keep list in sync after join/leave from anywhere.** Task 3227. Three threads:
  1. `LandingController.onInit` now subscribes via `ever(SpaceEventService.to.event, _onSpaceEvent)`. The `SpaceDetailsController` was already firing `notifyJoined`/`notifyLeft` on success — but **no screen listened**, so the spaces list never refreshed when the user backed out of details. Worker disposed in `onClose`. `SpaceEventService.to.reset()` called first to drop any stale event from a prior session.
  2. **`_applyJoinResponse` parse error fixed** (`_example_new_architecture/space_card_views_new/actions/space_actions.dart`). The new-architecture `SpaceListEntity.toJson()` is shallow (no `explicitToJson: true`), so `entity.toJson()` returned a Map with live `SpaceSettings`/`SpaceUser` instances inside. Re-parsing exploded with `type 'SpaceSettings' is not a subtype of Map<String, dynamic>` and the join silently failed (`[SpaceActions] Join succeeded but parse failed for: figlab`). Replaced with `jsonDecode(jsonEncode(entity))` so nested entities are recursively converted to real Maps.
  3. **Awaitable refresh added to `SpaceEventService`** to avoid a circular import. `LandingController` (which imports `register_all_cards.dart` → `space_actions.dart`) couldn't be imported back from `SpaceActions` without a cycle. Solution: `SpaceEventService.awaitableListRefresh` field, registered by `LandingController.onInit` as a callback to `refreshCurrentTab`. `SpaceActions._joinRequest` calls `await SpaceEventService.to.awaitableListRefresh?.call()` before `openSpaceDetails(updatedEntity)` — list refresh completes before navigation, so back-nav lands on a fresh list with no flicker. EasyLoading stays on through the refresh for one continuous spinner.
  4. Other join sites (`space_card_views/shared/space_card_base.dart`, `feed_card_new/shared/feed_card_base.dart`) now also fire `notifyJoined` for consistency. They don't await — they don't need to, because they don't navigate after the action.

### Comments + reassignments

- **3224** — comment id 393 (posted earlier in the session before fixing): documented the server 401 issue (logged-out users get 401 on `/members/followers` and `/members/followings` even when permission is "everyone") and the client UX fix. **User notified backend dev separately** — they'll honor "everyone" permission on the endpoint. Marked **Complete** + reassigned to Nusrat.
- **3227** — comment id 394: explained the three-thread fix above with test steps. Marked **Complete** + reassigned to Nusrat.

### Key insight: 401 fix from `8640522` (prev session) exposed this bug

Before `8640522`, any 401 redirected to login regardless of token presence. After `8640522`, 401s without a token propagate silently as failed responses. Good fix — but it surfaced the latent UX bug in the followers/following dialog, which had been masked because the redirect tore down the dialog before the user saw the loader. Lesson: when changing how an error path propagates, audit the consumers that previously short-circuited on the old behavior.

### Latent bug spotted in standalone (not fixed)

`fc_mobile_stand_alone/lib/presentation/pages/variant_screens/profile_screen/profile_controller.dart:1111` and `:1117` — inside `getFollowingMembers()`, the failure paths assign to `blockedUserList.value = null` instead of `followingUserList.value = null`. Wrong list gets wiped on a failed following fetch. Worth fixing when porting today's null→`[]` change to standalone.

### Standalone parity note

Today's appza_community fixes (`null → []` in profile_controller, landing controller `ever()` listener wiring, awaitable list refresh) all have direct counterparts to port to standalone. Standalone has the same `null = loading` UI bug, the same details controller already firing events with no listener, and the same shallow `toJson` issue if a similar new-architecture path exists there. Defer until SQA flags it on standalone — the fix is mechanical.

### What's open / next

**Tasks still untouched (5 — same as last session):**
- 2828 — Push Notification isn't working
- 2830 — Feed link list-1 properties issue
- 2834 — Create post-1 textbox licon decoration
- 2835 — Feed Post-1 margin/padding
- 2836 — Feed Post-1 decoration

**Awaiting backend (notified, no client action):**
- 3224 server-side: backend dev to honor "everyone" permission on `/members/followers` + `/members/followings` for guest requests. Once shipped, the dialog will populate without any client change.
- 3200 (Hasan vai), 3202 (Sohel vai), 3203 (plugin dev) — all from prev session.

**Carry-forward from prior sessions (still un-verified):**
- ⚠️ MemberCard manual device test still never happened (3 sessions outstanding now).
- Card #3 candidate (`notification_card_views` or `drawer_top_link_card_views`).
- `_example_new_architecture/` folder rename — wait until 3+ cards locked.

**Optional follow-up if SQA reports bandwidth waste:** profile-detail controller still fires auth-required calls when logged out (silent 401s now). Proper fix: gate calls in the controller on `LoggedUser().token != null`.

## Previous session: 2026-04-28 (LazyTasks triage + fixes + shipping pipeline learned)

Day spent triaging the 11-task LazyTask backlog rather than continuing the card migration thread. Three bug fixes shipped, six comments posted, one infra helper added, and the white-label shipping flow finally documented.

### Code changes shipped (all on `dev`, build pipeline ran twice)

- **`8640522` — fix: skip 401 redirect when no token was attached.** Guards `CommunityApiServiceBase.handleUnauthorized` with `LoggedUser().token`. Without this, a logged-out user tapping a profile got bounced to login because any 401 triggered the redirect; now the redirect only fires when we *thought* we had auth and the server rejected it (token expired). Fixes task 3201. **Note:** the profile-detail controller still fires auth-required calls when logged out — they fail silently rather than visibly redirecting, but it's bandwidth-wasteful. Carried into "what's open" below.
- **`df26560` — fix: honour `disable_feed_sort_by` + apply default sort when hidden.** The home-page transform in `appza_pages.dart` only reacted when the flag was `'no'`, so when SQA set it to `'yes'` the server's filters were left intact (dropdown stayed visible) AND no `default_feed_sort_by` was applied to requests. Now the client *always* controls `feedView['filters']`: when disabled, filters are cleared and `order_by_type` is injected as a hidden pagination param. Fixes task 3206. **Behavior note:** the null-case behavior changed (was: leave server filters intact; now: inject the dropdown). Acceptable because the server's filters were already being overwritten when the flag was `'no'`, so client always-overwrite is consistent.
- **`5edf608` — fix: hide settings icon on login page.** Gates the `IconButton` with inline `if (false)` instead of a build-flag const (user explicitly chose direct `false` over a const named `hasServerSelection` after I initially added one). Triggers a `dead_code` analyzer warning by design — the literal acts as an explicit on/off toggle. Fixes task 3199 sub-issue 1.
- **`c7fc9d2` — feat(api): add `getAppzaData` helper.** Mirrors `getCommunityData` but routes through `AppzaApiService` (`/wp-json/appza/api/v1/`). Until now the Appza base only had a POST helper. CLAUDE.md updated to document 4 helpers (was 3). Not wired to any call site yet — added on demand for an upcoming change.

### Comments posted on LazyTasks

- **3201** — comment id ~387 (had a `'''s` for `it's` shell-escaping artifact, left as-is per user — substance accurate). Triggered the move to **Python-based JSON body construction** for all subsequent comment posts to avoid the bug.
- **3203** "wrong password user can login" — handled plugin-side. Comment posted noting the fix arrives with the next plugin update. Not a client fix.
- **3200** "Space members page keep loading" — notified Hasan vai for investigation. Comment id 388.
- **3202** "wrong username error status" — notified Sohel vai (backend dev). Comment id 391.
- **3199** "Login page settings icon & Continue with otp" — initial comment id 389 covered both sub-issues (settings icon fixed, OTP route pending Hasan vai). Initial comment incorrectly referenced a `hasServerSelection` build flag; **follow-up correction comment id 390** clarifies it's inline `if (false)` instead.
- **3206** "Disable sort by isn't showing any effect" — comment summarizing the fix; explicitly notes the same disable logic does NOT yet exist for Spaces or Courses, and that no `disable_space_sort_by` / `disable_course_sort_by` fields exist in the app config model.

### Reassignments (delivery batch)

Three tasks reassigned to Nusrat Jahan Heer (id 11) — the actual client-side fixes shipped in this build:
- 3199 → Nusrat
- 3201 → Nusrat
- 3206 → Nusrat

Tasks **NOT reassigned** because they're awaiting another dev's fix:
- 3200 (Hasan vai), 3202 (Sohel vai), 3203 (plugin dev) — stay with whoever is responsible for the actual fix.

Reassignment was `assigned_to`-only — status left as `Active` per the new SKILL.md "Completion ≠ Reassignment" rule.

### Tooling changes

- **`tasks` SKILL.md ported 3 improvements from fc_mobile_stand_alone** (`b37e5e2`):
  1. Added `tasks/show/${TASK_ID}` endpoint for full task payload (the `by/user` listing is intentionally slim, no `comments` / `commentsAndLogActivities`).
  2. Replaced "Auto-reassign on Complete" with explicit Completion ≠ Reassignment split. Reassignment is now a separate batched step triggered by phrases like "send the app to X" / "we pushed the build".
  3. Added mandatory post-ship comment requirement with structured format (sub-issue numbering, user-visible behavior, what was NOT changed, build mention).
- **`shipping_to_nusrat.md` memory** — captures the white-label build flow:
  ```
  cd ~/projects/appza_community
  git checkout dev && git pull
  supervisorctl-builder restart all
  ```
  Plus the SSH-tunnel + supervisord fallback if the supervisor isn't running. **Key insight:** reassignment to Nusrat happens *immediately* after the worker restart — she runs the build/test on her side using the same pipeline. There's no waiting period for an APK to land. (My initial mental model — "wait for APK, then reassign" — was wrong; corrected the memory mid-session.)
- **Auto-memory MEMORY.md** indexed the new shipping memory.

### Architectural note recorded inline

Confirmed by curl-ing the live app-config endpoint that the server actually sends `disable_feed_sort_by: "yes"` and `default_feed_sort_by: "unanswered"`. The bug was definitively in the client transform, not in the server payload. **Lesson reinforced (already in feedback.md):** verify ground truth at the data boundary before changing the consumer logic. A 30-second curl saved a speculative fix.

### Lessons / heuristics carried forward

1. **Posting to LazyTasks via shell heredoc breaks on apostrophes** — `it's` becomes `'''s`. After hitting it once on task 3201, switched to `python3 <<PYEOF` JSON construction for every subsequent comment. **Don't go back to heredoc-curl for LazyTasks comments.**
2. **`if (false)` over a build-flag const for one-off UI hides** — when the user wants a "kill switch" on a single widget and explicitly says "no need bool const", inline `if (false)` is the correct choice even though it triggers a `dead_code` warning. Don't argue.
3. **Confirm-before-reassigning** — don't infer reassignment from "mark complete". Per the new SKILL.md split, reassignment requires explicit delivery phrasing.

### What's open / next

**Tasks still untouched (5):**
- 2828 — Push Notification isn't working (no priority)
- 2830 — component Feed link list-1 properties issue (no priority)
- 2834 — Create post-1 textbox licon decoration (no priority)
- 2835 — Feed Post-1 margin/padding (no priority)
- 2836 — Feed Post-1 decoration (no priority)

These are all UI/decoration items in the App Bugs section. Not investigated this session.

**Tasks awaiting other devs (don't action — they own the fix):**
- 3200 (Hasan vai — Space members loading)
- 3202 (Sohel vai — wrong username status code)
- 3203 (plugin dev — wrong password login)

**Pre-existing dirty files left untouched** (don't accidentally commit): `.DS_Store`, `.idea/appza_community.iml`, `lib/data/services/network/api_config.dart`, `macos/Podfile`, `macos/Runner.xcodeproj/project.pbxproj`. Were already modified at session start.

**Carry-forward from prior MemberCard session (still un-verified):**
- ⚠️ MemberCard manual device test never happened. Critical checks still outstanding: single snackbar on follow/unfollow, list rebuild via `onRefresh`, admin status update flow, logged-out gating, profile navigation. The card has been in the build for two sessions now without device verification.
- The legacy `member_card_views/` and `space_card_views/` folders are still orphaned but kept as fallbacks.
- Card #3 candidate: a no-action card (`notification_card_views` or `drawer_top_link_card_views`).

**Bandwidth follow-up for 3201:** the profile-detail controller still fires auth-required calls when logged out — silent now (no redirect bounce) but wasteful. The proper fix is controller-level gating: the page knows what it needs and should branch on `LoggedUser().token != null` to skip auth-required calls. Plus the service-layer `requiresAuth: true` backstop discussed earlier. Neither shipped this session — would be a clean follow-up if SQA flags wasted bandwidth or the silent 401s show up in logs.

## Earlier session: 2026-04-09 (afternoon — MemberCard migration as card #2)

### What shipped

Goal: validate the new architecture pattern at scale by porting a card with multiple actions and surfacing what does NOT port 1:1 from SpaceCard.

**Files created (6):**
- `_example_new_architecture/member_card_views_new/models/member_list_entity.dart` — `@JsonSerializable(createToJson: false)` (read-only — Member never round-trips entity → JSON, unlike Space which needs `toJson` for `_applyJoinResponse`). All 4 nested types in one file: `MemberListEntity`, `MemberXProfile`, `MemberLevel`, `MemberMeta` (with `Member` prefix to avoid clashing with the legacy classes still in `member_card_views/models/`).
- `_example_new_architecture/member_card_views_new/actions/member_actions.dart` — singleton (private constructor + static instance). `ScopedState<ActionState<MemberListEntity>>` keyed by `xprofile.userId.toString()`. 5 actions (`follow`, `unfollow`, `unblock`, `updateMemberStatus`, `removeFromSpace`) + `navigateToProfile`. Each action follows a 2-state template: `idle → loading → idle` regardless of success/failure. Snackbar policy doc'd inline at top of class.
- `_example_new_architecture/member_card_views_new/member_card_factory.dart` — parse-once boundary at `MemberListEntity.fromJson(item)`, `(design, orientation)` switch, `_ParseErrorCard`, `_LogAndFallback`. Self-registers via `CardRegistry.register(CardType.member, buildCard)`.
- `_example_new_architecture/member_card_views_new/design_1/vertical.dart` — full port of legacy ~570-line `member_card_views/design_1/vertical.dart`. Action chips wrapped in `Obx` that show `_LoadingChip` while `isLoading`. Enums (`MemberStatus`, `FollowState`) at the bottom.
- `_example_new_architecture/member_card_views_new/design_1/horizontal.dart` — minimal placeholder matching legacy (status text only).
- `_example_new_architecture/member_card_views_new/design_1/grid.dart` — minimal placeholder matching legacy (avatar + display name).

**Files edited (2):**
- `_example_new_architecture/register_all_cards.dart` — added `MemberCardFactory.register()`, added `CardType.member` to assert list. **Did NOT** add `MemberActions.instance.clearAllStates()` to `clearAllCardActionStates()` (intentional — see Finding 2). Rewrote the `clearAllCardActionStates` doc comment to explain why not all action singletons belong there and to record that Member is intentionally absent.
- `get_selected_card_view.dart` — removed legacy `member_card_views/member_card_factory.dart` import, removed the explicit `if (classType == 'FCommunity_Member') { ... }` branch (lines 298-309 in pre-edit version) — it now routes through `CardRegistry` automatically. Updated comment in the legacy switch.

`fvm flutter analyze` clean on all changed files. Build_runner regenerated `member_list_entity.g.dart` successfully.

### Architectural findings (record in PROPOSAL.md eventually — not done this session)

**Finding 1: Cards with `onRefresh`-driven host pages need only a 2-state loading flag.**
Member's host pages call `args['onRefresh']?.call()` on success, which rebuilds the entire list with fresh server entities. The action layer therefore needs only `idle ↔ loading`. Optimistic mutation, `_refreshEntity` recovery, `state.data ?? entity` fallback, retry chips — none of it earns its weight when the host page rebuilds anyway. Two architectural shapes are now visible:
- **Space shape** — host doesn't rebuild on join → action owns local state mutation + retry chip + recovery refetch
- **Member shape** — host rebuilds on success → action is loading flag only, errors snackbar-only

**Finding 2: Action singletons don't all need lifecycle wipes.**
`clearAllCardActionStates()` makes sense for Space because Space cards live primarily on the landing page and the controller's `onInit` is the right anchor. Member cards live in many places, so there's no single anchor. Tying Member's wipe to landing `onInit` would be semantically wrong. Rule: only add a singleton's `clearAllStates()` to `clearAllCardActionStates()` if its cards live primarily on the landing page AND there's a stale-UI scenario the wipe would fix. For Member, the per-action `idle → loading → idle` reset + ScopedState LRU bound + the design widget's auth-error invisibility (no error rendering at all) collectively prevent any visible stale state.

**Finding 3: Action layer must NOT double-fire snackbars on `!response.success`.**
`CommunityApiServiceBase._handleErrorResponse` (`lib/data/services/network/services.dart:187-200`) auto-fires `errorSnackBar(response.message)` for any call with `showSnackBar: true` (the default in `postCommunityData` / `getCommunityData`). The legacy `MemberCard.followMember` etc. were correct to never fire `errorSnackBar` in the `else` branch — they relied on the base service. The new `MemberActions` was initially wrong (added `errorSnackBar(response.message)` to every `else` branch); fixed late in session by removing them. **Codified as a key invariant above.**

### Architectural pattern frame for `MemberActions`

The class is a deliberate **hybrid** that doesn't fit one classical name. Combines:
1. **Singleton** (as namespace, not for state ownership)
2. **Facade** (hides 3 backend services: MemberService, ProfileService, SpaceService)
3. **Observer / Reactive store** (`ScopedState<ActionState<MemberListEntity>>` + `Rx`)
4. **Stateless command method** (entity passed by parameter, never stored as field)

Closest informal labels: **"stateful service singleton"** or **"reactive action service"**. In GetX terms it's a hand-rolled `GetxService` (no lifecycle hooks needed). The distinguishing property — and what makes it different from any classic pattern — is that **the entity is passed by parameter, never owned by the singleton.** Singleton is stateless about data, stateful about per-id loading flags. One singleton serves N card rows because state is keyed by user_id. Memory bounded to ~10KB (100 entries × ~100 bytes) regardless of list length.

### What's open / next

**REQUIRED before declaring this migration done (carry into next session):**
- ⚠️ **Manual test on device.** NOT yet done at session close. Test steps were provided in-session. Critical checks:
  1. **Single snackbar on follow/unfollow** — verify the double-snackbar fix actually holds (this bug was caught at the very end of the session)
  2. **List rebuild via `onRefresh`** — verify the button label flips Follow → Unfollow on success (the entire 2-state design depends on this assumption)
  3. **Admin status update flow** — least-tested code path; popup menu → confirmation → spinner → success
  4. **Logged-out state** — action buttons should still be hidden via `_shouldShowActionButtons` gating
  5. **Profile navigation** — tap outside the action chip should still open the profile screen
- ⚠️ **Commit** after verification. Single squash commit for the whole migration. **NOT yet committed at session close.**

**Card #3 recommendation:**
Pick a small **no-action card** like `notification_card_views` or `drawer_top_link_card_views` to validate the question "is the action singleton optional?". If a card can be just factory + design widgets (no actions file at all), that's a meaningful simplification for the 21 unmigrated cards (most read-only). After #3, **`feed_card_views`** is the right #4 — most actions of any card AND likes need optimistic mutation, would stress-test in the opposite direction from Member.

**Things deliberately NOT done (do NOT do these without explicit reason):**
- ❌ **No helper extraction.** Tempting to factor out a `runAction<T>(...)` template from the 5 near-identical action methods. N=1 abstractions calcify the wrong shape. Wait until N=3+ migrations confirm what actually repeats.
- ❌ **No folder rename of `_example_new_architecture/`.** Wait until 3+ cards are migrated and the pattern is locked.
- ❌ **No legacy folder delete.** `member_card_views/` is now orphaned (no consumers) but kept until card #3 is stable.
- ❌ **No re-entry guard in MemberActions.** EasyLoading covers double-tap. Add only on a real symptom.
- ❌ **No tests.** Singleton actions documented as not unit-test-injectable.
- ❌ **No PROPOSAL.md edits.** The three findings should be folded in eventually, but bundle with a future architecture review.
- ❌ **No fold-back of Member's 2-state pattern into Space.** Different shape on purpose; Space's host doesn't rebuild on join. Keep both patterns.

## Earlier session: 2026-04-09 (morning — senior review of PROPOSAL.md + bug fixes)

### Senior review of `_example_new_architecture/` PROPOSAL.md + fixes shipped

Reviewed the proposal as senior engineer. Found and fixed:

**Blockers:**
- B1 (false bug claim): proposal said `case 'A' || 'B':` was a Dart switch fall-through bug. **Empirically disproven on Dart 3.11.4** — `||` is a valid logical-or pattern in switch cases. Reframed §1.2 of PROPOSAL.md as a corrected note; deleted §9 Appendix.
- B2: `DesignDispatch.resolve()` ignored the `design` parameter (matched `(_, vertical)`). Deleted the helper entirely. Each factory now switches on `(design, orientation)` directly with a logged `_LogAndFallback` for unknown variants.
- B3: PROPOSAL.md said "Status: Draft" but Phase 1 was already in production (`app_initializer.dart:61`). Updated status, marked rows in §7 as 🟢 Live / ⏳ Stub / 📋 Template.

**Major:**
- M1: `args['item']?['privacy']` deep-traversal still pervasive in actions. Fixed by parsing `item` → `SpaceListEntity entity` once at the factory boundary, passing typed entity to design widgets and to all `SpaceActions` methods.
- M2: `model => SpaceActions.instance.parse(item)` getter re-parsed on every access (15+ calls/render). Fixed by storing entity as `final` field on the design widget.
- M3: `ScopedState` had no auto-eviction. Added LRU bound (default 100) via `LinkedHashMap`.
- M4: Singleton testability documented as accepted limitation, with migration recipe to `Get.lazyPut`/`Get.find` if needed later.
- M5: `parse()` silently swallowed errors. Now: parse failure at the factory boundary → `_ParseErrorCard`. No silent nulls reaching design widgets.

**Smaller:**
- Removed `_unused()` analyzer-patch dead code from horizontal/grid stubs.
- Removed manual `CardType.values` list; `CardRegistry.findByValue` is source of truth.
- `card_template/card_template_factory.dart` rewritten as runnable scaffold (no broken commented imports).
- `register_all_cards.dart` runs a debug `assert` at boot — fails fast if a `CardType` is registered but missing from the assert list.
- `get_selected_card_view.dart` no longer wraps `CardRegistry.build` in try/catch (was masking real factory bugs).

### Bug fixes from this session

**Auth state bug:** Auth pre-check failure (`failure(auth)`) was persisted in singleton state, leaving cards stuck in red error UI after the user logged in.

- **Decided NOT to fix at the action layer** (i.e., kept the `state.failure(auth)` write in `buttonAction` and `_showAccessDeniedDialog`).
- **Fix landed at the lifecycle layer:** added `clearAllCardActionStates()` to `landing_controller.dart` (old) `onInit()`, right after `CacheControllerManager.clearAll()` and `SpaceTimestampManager().clearAll()`. State wipes on every fresh landing mount.
- `clearAllCardActionStates()` lives in `register_all_cards.dart` and currently calls `SpaceActions.instance.clearAllStates()`.
- New landing controller (`new_landing_page/new_landing_controller.dart`) intentionally NOT wired — only the old one, per user direction.

**Button-disappears-on-error bug:** Non-retryable errors replaced the button with red text, stranding the user.

- `vertical.dart`: error button branch narrowed to `actionState.hasError && !actionState.hasData && actionState.error!.shouldRetry`. For non-retryable errors (auth/business/parse/unknown), the normal button stays visible.
- `_buildErrorButton`: dead `if (error.shouldRetry)` defensive guards removed (caller now guarantees the precondition).

**"Already a member" / string-parsing fragility:** `_looksLikeAlreadyMember(message).contains('already a member')` was brittle (locale, wording, backend changes).

- Iterated through Option 3 (refetch + auto-navigate interpretation) → user rejected as too clever.
- Final design: **passive refresh, no interpretation.**
- `_recoverViaFreshState` deleted. Replaced with `_refreshEntity({slug, error})` which fetches via `_fetchFreshSpace`, sets `state(data: fresh, error: <given>)`, but **does not interpret success/failure** — the API's verdict stands.
- `success: false` branch → `_refreshEntity(business)` + snackbar.
- `DioException 4xx` branch → `_refreshEntity(business)` + snackbar (no `fromDioException` here — 4xx is rejection, not network).
- `DioException 5xx / timeout / connection` branch → `failure(network)` + cache fallback (existing retry path).
- `_fetchFreshSpace` retained; it's the maximum extra recovery action allowed.
- No more string parsing anywhere in `space_actions.dart`.

### What's open / next

- ✅ **DONE in afternoon session** — "Migrate one more card to validate the pattern at scale" → Member shipped as card #2. See "Last session" above for findings.
- **No tests yet** — singleton actions are accepted as not unit-test injectable. Both `SpaceActions` and `MemberActions` have zero unit coverage. Carried forward.
- **`_example_new_architecture/` folder name** — production landing controllers import from a folder labeled "example". Carried forward to "deliberately NOT done" in afternoon session — wait until 3+ cards locked before renaming.
- **Backend ticket:** add structured `error_code` field to error responses so client can stop trusting prose. Not blocking. Carried forward.

## Previous sessions (BaseCardFactory migrations — the old architecture path)

The legacy `BaseCardFactory<T, C>` system in `lib/presentation/core/renderer/card_views/shared/` continues alongside the new architecture. The proposal's Phase 3 calls for these to be re-migrated to the new pattern eventually. Until then, three systems coexist: (1) the newest `CardRegistry`-based architecture, (2) the intermediate `BaseCardFactory<T, C>` system, (3) the oldest `CardViewFactory` system.

### Card views on the NEWEST architecture (`_example_new_architecture/` + `CardRegistry`) — 2:
- `space_card_views_new/` (SpaceCard — first migration, "rich recovery" shape)
- `member_card_views_new/` (MemberCard — second migration, "minimal 2-state" shape)

The legacy `BaseCardFactory` versions of these two (`space_card_views/` and `member_card_views/`) still exist but are now orphaned — `get_selected_card_view.dart` no longer routes to them. Kept as fallback until the new versions are stable in production.

### Card views migrated to BaseCardFactory (15 total — intermediate generation):
- course_enroll, course_content, course_information, course_instructor
- lesson_video, lesson_details, lesson_information, lesson_documents, lesson_quiz, lesson_discussion
- space_card_views, course_card_views, feed_card_views (as `feed_card_new/`)
- member_card_views, chat_add_member_view

### Card views still on old CardViewFactory system (21):
- chat_card_views
- notification_card_views, comment_card_views
- space_member_card_views
- member_about_card_views, member_social_link_card_views
- document_card_views, space_banner_card_views, feed_banner_card_views
- create_post_card_views, schedule_post_card_views, schedule_post_count_card_views
- drawer_space_card_views, drawer_group_link_card_views, drawer_top_link_card_views, drawer_header_menu_card_views
- link_card_views, leader_board_level_card_views, leader_board_members_card_views, leader_board_profile_card_views

### Pending (older items, not from this session)
- Rename `feed_card_new/` → `feed_card_views/` (old is deleted, name is stale)
- Port BaseCardFactory system to fc_mobile_stand_alone

### Phase 2: Code quality pass (after migration complete)
- Clean up dead code
- Remove commented-out legacy code
- Consistent error handling patterns
- Extract shared widgets
- Remove `VariantBaseController`/`VariantBaseView` once all screens are off it