Admin / Merchant Shell Split Implementation Plan โ
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Restructure apps/admin/ into two role-scoped shells (AdminShell, MerchantShell) with a shared library zone, enforce the boundary at lint + build time, and align the auth API so both shells have well-defined surfaces.
Architecture: Top-level role fork in App.jsx after Firebase auth resolves. /admin/* renders AdminShell, /merchant/:merchantId/* renders MerchantShell. Three directories โ src/admin/, src/merchant/, src/shared/ โ with hard cross-import boundaries enforced by ESLint import/no-restricted-paths (error severity) and a build-time chunk-graph validator. Backend route handlers are extracted to pure functions and dual-mounted under /auth/admin/merchants/:id/* (admin) and /auth/merchant/me/* (merchant-self).
Tech Stack: React 19 + Vite + Vitest + Testing Library + react-router-dom (admin); Express 5 + Firebase Admin (auth API). No new top-level dependencies expected.
Spec: docs/superpowers/specs/2026-04-27-admin-merchant-shell-split-design.md
File Structure (terminal state) โ
New files (admin app) โ
apps/admin/src/admin/AdminShell.jsxโ sidebar + Routes for/admin/*apps/admin/src/admin/__tests__/AdminShell.test.jsxapps/admin/src/merchant/MerchantShell.jsxโ sidebar + Routes for/merchant/:merchantId/*apps/admin/src/merchant/MerchantSwitcher.jsx(moved + adapted)apps/admin/src/merchant/__tests__/MerchantShell.test.jsxapps/admin/src/merchant/profile/MyMerchantProfile.jsxapps/admin/src/shared/lib/merchantApi.jsโ role-aware clientapps/admin/src/shared/lib/__tests__/merchantApi.test.jsapps/admin/src/__tests__/App.test.jsx(or update existing)
New files (auth API) โ
services/api/auth/src/handlers/merchantHandlers.jsโ extracted pure functionsservices/api/auth/src/handlers/__tests__/merchantHandlers.test.jsservices/api/auth/src/routes/merchantSelf.jsโ/memountservices/api/auth/src/routes/__tests__/merchantSelf.test.js
New files (tooling) โ
tooling/scripts/check-shell-isolation.mjsโ bundle validatortooling/scripts/lint-fixture-cross-zone.mjsโ lint-rule fixture test
Renamed/moved โ
- All of
apps/admin/src/components/*is redistributed acrosssrc/shared/components/,src/admin/<sub>/,src/merchant/<sub>/. apps/admin/src/lib/*โapps/admin/src/shared/lib/*- Merchant tabs lose the "Tab" suffix when they move (
MerchantOverviewTabโOverview).
Modified โ
apps/admin/vite.config.mjsโ path aliasesapps/admin/vitest.config.mjsโ path aliases mirrorapps/admin/.eslintrc.jsonโimport/no-restricted-pathsruleapps/admin/src/App.jsxโ role fork + URL-mode handlers + back-compat redirectsservices/api/auth/src/middleware/auth.jsโ addsrequireMerchantservices/api/auth/src/routes/adminMerchants.jsโ becomes thin wiringservices/api/auth/src/index.jsโ mountsmerchantSelfRoutesunder/auth/merchant/metooling/scripts/validate.jsโ wires the new shell + lint-fixture scripts
Deleted โ
apps/admin/src/components/AdminDashboard.jsxapps/admin/src/components/merchants/MerchantDetail.jsxapps/admin/src/components/merchants/MerchantsIndexRedirect.jsxapps/admin/src/components/__tests__/AdminDashboard.test.jsx
Conventions to Follow โ
- Staging discipline (CRITICAL): the working tree on this branch contains unrelated in-flight files (
packages/api-docs-renderer/*, sometimes others). Every commit MUST stage ONLY the files touched by that step. Usegit add <exact paths>. NEVER usegit add -A,git add ., orgit commit -am. Rungit -C /home/mechelle/repos/lantern_app status --shortBEFORE every commit to verify only the intended files are staged. If anything else is staged, unstage it. - Migration pattern: every commit leaves the working tree green. After every step,
npm run build -w lantern-adminandnpm test -w lantern-admin --runmust succeed. Auth API tests:cd services/api/auth && npm run test:run. If any of these fail, that's a blocker โ stop and fix or escalate. - Use
git mvfor renames so git tracks the file move with history preserved. - Path alias usage: once aliases are added in Phase A, any newly written import that crosses zones MUST use the alias (
@admin/...,@merchant/...,@shared/...). Old imports updated in-place can use either relative or alias form, but prefer aliases when the import crosses a directory boundary. - Phase G is audit-driven. Step 30 surfaces the actual list of merchant write paths needing
/memounts. The exact follow-on commits depend on what the audit finds. Plan for 2-6 commits in that phase. - Commit messages:
feat(scope):,refactor(scope):,test(scope):,chore(scope):etc. Keep them grep-able.
Task 1: Sanity check + branch verification โ
Files:
Read:
docs/superpowers/specs/2026-04-27-admin-merchant-shell-split-design.md[ ] Step 1.1: Confirm branch state
git -C /home/mechelle/repos/lantern_app branch --show-current
git -C /home/mechelle/repos/lantern_app status --short
git -C /home/mechelle/repos/lantern_app log --oneline -5Expected: on claude/merchant-integration (or whatever the integration branch is named). Working tree may have unrelated in-flight files in packages/api-docs-renderer/ โ that's fine, leave them alone.
- [ ] Step 1.2: Re-read the spec
Confirm v1 scope, the 9 phases, and the out-of-scope list. Everything in "Out of Scope" stays untouched.
- [ ] Step 1.3: Capture the existing module count for sanity
ls /home/mechelle/repos/lantern_app/apps/admin/src/components/ | wc -l
ls /home/mechelle/repos/lantern_app/apps/admin/src/lib/ | wc -lNote the numbers. By Phase E end, apps/admin/src/components/ should be empty (or near-empty) and apps/admin/src/lib/ should be gone.
Phase A โ Foundation (Tasks 2-5) โ
No behavior change. Adds infrastructure that's inert until later phases use it.
Task 2: Add path aliases to Vite + Vitest configs โ
Files:
Modify:
apps/admin/vite.config.mjsModify:
apps/admin/vitest.config.mjs[ ] Step 2.1: Inspect the current configs
cat /home/mechelle/repos/lantern_app/apps/admin/vite.config.mjs | head -50
cat /home/mechelle/repos/lantern_app/apps/admin/vitest.config.mjsFind the resolve.alias block in each (vitest.config.mjs already has '@' mapped to './src').
- [ ] Step 2.2: Add the three new aliases to
vite.config.mjs
In the resolve.alias block, add:
'@admin': path.resolve(__dirname, 'src/admin'),
'@merchant': path.resolve(__dirname, 'src/merchant'),
'@shared': path.resolve(__dirname, 'src/shared'),Make sure path and __dirname are imported / available at the top of the file (the existing code already uses path.resolve(__dirname, ...) for the existing alias, so they should already be).
- [ ] Step 2.3: Mirror the aliases in
vitest.config.mjs
Same three lines in the same resolve.alias block.
- [ ] Step 2.4: Verify build still succeeds
npm run build -w lantern-admin 2>&1 | tail -10Expected: build succeeds. The aliases point to non-existent directories, but no code uses them yet so no error fires.
- [ ] Step 2.5: Commit
git -C /home/mechelle/repos/lantern_app add apps/admin/vite.config.mjs apps/admin/vitest.config.mjs
git -C /home/mechelle/repos/lantern_app status --short
# Confirm only those two files are staged.
git -C /home/mechelle/repos/lantern_app commit -m "chore(admin): add @admin/@merchant/@shared path aliases
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Task 3: Create empty shell zone directories โ
Files:
Create:
apps/admin/src/shared/.gitkeepCreate:
apps/admin/src/admin/.gitkeepCreate:
apps/admin/src/merchant/.gitkeep[ ] Step 3.1: Create the three directories with
.gitkeepplaceholder files
mkdir -p /home/mechelle/repos/lantern_app/apps/admin/src/shared
mkdir -p /home/mechelle/repos/lantern_app/apps/admin/src/admin
mkdir -p /home/mechelle/repos/lantern_app/apps/admin/src/merchant
touch /home/mechelle/repos/lantern_app/apps/admin/src/shared/.gitkeep
touch /home/mechelle/repos/lantern_app/apps/admin/src/admin/.gitkeep
touch /home/mechelle/repos/lantern_app/apps/admin/src/merchant/.gitkeep- [ ] Step 3.2: Commit
git -C /home/mechelle/repos/lantern_app add apps/admin/src/shared/.gitkeep apps/admin/src/admin/.gitkeep apps/admin/src/merchant/.gitkeep
git -C /home/mechelle/repos/lantern_app commit -m "chore(admin): scaffold shared/admin/merchant shell zones
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Task 4: Add ESLint import/no-restricted-paths rule โ
Files:
Modify:
apps/admin/.eslintrc.json(or whichever ESLint config the workspace uses)[ ] Step 4.1: Inspect the current ESLint setup
ls /home/mechelle/repos/lantern_app/apps/admin/.eslint*
ls /home/mechelle/repos/lantern_app/.eslint*
cat /home/mechelle/repos/lantern_app/apps/admin/.eslintrc.json 2>/dev/null || cat /home/mechelle/repos/lantern_app/.eslintrc.json | head -40Confirm eslint-plugin-import is in the resolved plugin list (search package.json files).
- [ ] Step 4.2: Confirm
eslint-plugin-importis available
grep -l "eslint-plugin-import" /home/mechelle/repos/lantern_app/apps/admin/package.json /home/mechelle/repos/lantern_app/package.json 2>/dev/nullIf it's not in apps/admin/package.json or root package.json, add it:
npm i -D -w lantern-admin eslint-plugin-import(Only run the install if missing โ most React + ESLint setups already have it via a preset.)
- [ ] Step 4.3: Add the zone rule
If apps/admin/.eslintrc.json exists, add the rule. Otherwise create it.
{
"plugins": ["import"],
"rules": {
"import/no-restricted-paths": ["error", {
"zones": [
{
"target": "./src/admin",
"from": "./src/merchant",
"message": "Admin code may not import from src/merchant. Move shared code to src/shared/."
},
{
"target": "./src/merchant",
"from": "./src/admin",
"message": "Merchant code may not import from src/admin. Move shared code to src/shared/."
},
{
"target": "./src/shared",
"from": ["./src/admin", "./src/merchant"],
"message": "src/shared may not depend on shell-specific code. Inline or invert the dependency."
}
]
}]
}
}If a config already exists, merge into its existing structure (preserve other plugins/rules). The key constraint: the rule must apply to files inside apps/admin/src/. If the workspace inherits from a root config that doesn't have eslint-plugin-import, add the plugin import + the rule scoped to this workspace.
- [ ] Step 4.4: Verify lint passes
npm run lint -w lantern-admin 2>&1 | tail -10Expected: no new errors. The zones are still empty (only .gitkeep files), so the rule is trivially satisfied.
If npm run lint doesn't exist on the workspace, this is a known gap โ flag it as DONE_WITH_CONCERNS and note that the rule won't be enforced until a lint script is added. (Currently the workspace runs lint as part of npm run validate, so check there first.)
- [ ] Step 4.5: Commit
git -C /home/mechelle/repos/lantern_app add apps/admin/.eslintrc.json
git -C /home/mechelle/repos/lantern_app commit -m "chore(admin): enforce shell zone boundaries with import/no-restricted-paths
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"(If you also touched root package.json or apps/admin/package.json to add eslint-plugin-import, include those โ verify with git status --short.)
Task 5: Add check-shell-isolation.mjs bundle validator โ
Files:
Create:
tooling/scripts/check-shell-isolation.mjsModify:
tooling/scripts/validate.js(wire it into the validate pipeline)[ ] Step 5.1: Inspect
tooling/scripts/validate.jsto understand the existing pipeline
grep -n "scope\|workspace\|step\|register" /home/mechelle/repos/lantern_app/tooling/scripts/validate.js | head -20
cat /home/mechelle/repos/lantern_app/tooling/scripts/validate.js | head -80Note how existing checks are registered (likely a list of named steps with shell commands). The new check is a Node script that reads Vite's manifest.
- [ ] Step 5.2: Create
tooling/scripts/check-shell-isolation.mjs
#!/usr/bin/env node
/**
* Verifies that the admin and merchant Vite chunks don't bleed into each other.
*
* Reads dist/.vite/manifest.json (produced by the admin app build) and walks the
* import graph from each shell entry. If a chunk reachable from MerchantShell.jsx
* imports anything under src/admin/ (or vice versa), exit non-zero.
*
* Run after `npm run build -w lantern-admin`.
*/
import fs from 'node:fs/promises'
import path from 'node:path'
const REPO_ROOT = path.resolve(new URL('../..', import.meta.url).pathname)
const MANIFEST = path.join(REPO_ROOT, 'apps/admin/dist/.vite/manifest.json')
const ADMIN_PREFIX = 'src/admin/'
const MERCHANT_PREFIX = 'src/merchant/'
async function main() {
let manifest
try {
manifest = JSON.parse(await fs.readFile(MANIFEST, 'utf8'))
} catch (err) {
if (err.code === 'ENOENT') {
console.log('check-shell-isolation: skipping โ admin app not built yet')
process.exit(0)
}
throw err
}
const violations = []
// Walk every entry. For each chunk, list its source modules. Flag if a
// module under src/merchant/ ends up in a chunk reachable from src/admin/
// entries (or vice versa).
for (const [key, entry] of Object.entries(manifest)) {
if (!entry.isEntry) continue
if (key.includes('AdminShell') || key.includes('admin/')) {
const merchantSources = (entry.css || [])
.concat(entry.assets || [])
.concat([entry.file])
.concat(entry.imports || [])
.filter((s) => s && s.includes(MERCHANT_PREFIX))
if (merchantSources.length > 0) {
violations.push(`AdminShell entry pulls in merchant code: ${merchantSources.join(', ')}`)
}
}
if (key.includes('MerchantShell') || key.includes('merchant/')) {
const adminSources = (entry.css || [])
.concat(entry.assets || [])
.concat([entry.file])
.concat(entry.imports || [])
.filter((s) => s && s.includes(ADMIN_PREFIX))
if (adminSources.length > 0) {
violations.push(`MerchantShell entry pulls in admin code: ${adminSources.join(', ')}`)
}
}
}
if (violations.length > 0) {
console.error('check-shell-isolation: cross-shell module imports detected:')
for (const v of violations) console.error(` โข ${v}`)
process.exit(1)
}
console.log('check-shell-isolation: OK โ shells are isolated in the bundle')
}
main().catch((err) => {
console.error('check-shell-isolation: error', err)
process.exit(2)
})NOTE: Vite's manifest format walks differently in practice. The above is a starting heuristic. After Phase D + E lands (when there are real shells to validate against), refine the script if needed. For now, the script being present and runnable is enough โ it gracefully no-ops when there's no built output.
- [ ] Step 5.3: Wire it into
tooling/scripts/validate.js
Locate the validate.js scope/step registry (the structure varies โ read the file to figure out where new steps go). Add a new step that runs node tooling/scripts/check-shell-isolation.mjs. Tag it under the same scope as test or build so it runs as part of npm run validate.
If you can't determine where to plug it in safely, add a top-level npm script in package.json:
{
"scripts": {
"check:shell-isolation": "node tooling/scripts/check-shell-isolation.mjs"
}
}โฆand document in the spec follow-up that it should be wired into validate.js as a follow-up. For now, ensure it's at minimum invokable.
- [ ] Step 5.4: Verify it runs
node /home/mechelle/repos/lantern_app/tooling/scripts/check-shell-isolation.mjsExpected (no admin build yet): check-shell-isolation: skipping โ admin app not built yet. Exit code 0.
npm run build -w lantern-admin 2>&1 | tail -5
node /home/mechelle/repos/lantern_app/tooling/scripts/check-shell-isolation.mjsExpected: check-shell-isolation: OK โ shells are isolated in the bundle (or similar). Exit code 0. (If it errors with a manifest parsing issue, that's a real bug in the script โ fix it.)
- [ ] Step 5.5: Commit
git -C /home/mechelle/repos/lantern_app add tooling/scripts/check-shell-isolation.mjs
# Plus tooling/scripts/validate.js or package.json depending on what you wired
git -C /home/mechelle/repos/lantern_app status --short
git -C /home/mechelle/repos/lantern_app commit -m "chore(tooling): add check-shell-isolation bundle validator
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Phase B โ Move shared infrastructure (Tasks 6-10) โ
Move global chrome and lib code into src/shared/. Each move is a single commit. Update consumers' import paths in the same commit (otherwise the build breaks).
Task 6: Move design tokens / styles.css to src/shared/styles/ โ
Files:
Move:
apps/admin/src/styles.css(or wherever the global stylesheet lives) โapps/admin/src/shared/styles/styles.cssModify: any file that imports the stylesheet (typically
src/main.jsx)[ ] Step 6.1: Locate the global stylesheet and its consumers
ls /home/mechelle/repos/lantern_app/apps/admin/src/*.css 2>/dev/null
grep -rn "styles.css\|index.css\|main.css" /home/mechelle/repos/lantern_app/apps/admin/src/ | head -10Identify the global stylesheet file and every place it's imported.
- [ ] Step 6.2:
git mvit intosrc/shared/styles/
mkdir -p /home/mechelle/repos/lantern_app/apps/admin/src/shared/styles
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/styles.css apps/admin/src/shared/styles/styles.cssAdjust path if the actual file lives elsewhere.
- [ ] Step 6.3: Update consumers' import paths
For each file that imported the old path, update to import from './shared/styles/styles.css' or use the alias @shared/styles/styles.css. Most likely just apps/admin/src/main.jsx:
// before
import './styles.css'
// after
import './shared/styles/styles.css'- [ ] Step 6.4: Verify build
npm run build -w lantern-admin 2>&1 | tail -10Expected: build succeeds. Visual smoke should be unchanged because the stylesheet contents didn't change.
- [ ] Step 6.5: Commit
git -C /home/mechelle/repos/lantern_app add apps/admin/src/main.jsx apps/admin/src/shared/styles/styles.css
# Ensure git status --short shows only the rename + import update
git -C /home/mechelle/repos/lantern_app commit -m "refactor(admin): move global styles to src/shared/styles/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Task 7: Move LanternLogo, LoadingScreen, AccessDenied, AdminMigrationBanner to src/shared/components/ โ
Files:
Move:
apps/admin/src/components/LanternLogo.jsxโapps/admin/src/shared/components/LanternLogo.jsxMove:
apps/admin/src/components/LoadingScreen.jsxโapps/admin/src/shared/components/LoadingScreen.jsxMove:
apps/admin/src/components/AccessDenied.jsxโapps/admin/src/shared/components/AccessDenied.jsxMove:
apps/admin/src/components/AdminMigrationBanner.jsxโapps/admin/src/shared/components/AdminMigrationBanner.jsxModify: every consumer's import paths
[ ] Step 7.1: Find consumers
grep -rn "from '.*LanternLogo'" /home/mechelle/repos/lantern_app/apps/admin/src/ | head -20
grep -rn "from '.*LoadingScreen'" /home/mechelle/repos/lantern_app/apps/admin/src/ | head -20
grep -rn "from '.*AccessDenied'" /home/mechelle/repos/lantern_app/apps/admin/src/ | head -20
grep -rn "from '.*AdminMigrationBanner'" /home/mechelle/repos/lantern_app/apps/admin/src/ | head -20- [ ] Step 7.2: Move each file with
git mv
mkdir -p /home/mechelle/repos/lantern_app/apps/admin/src/shared/components
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/LanternLogo.jsx apps/admin/src/shared/components/LanternLogo.jsx
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/LoadingScreen.jsx apps/admin/src/shared/components/LoadingScreen.jsx
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/AccessDenied.jsx apps/admin/src/shared/components/AccessDenied.jsx
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/AdminMigrationBanner.jsx apps/admin/src/shared/components/AdminMigrationBanner.jsx- [ ] Step 7.3: Update import paths in every consumer
For each consumer file found in 7.1, update the import. Use the @shared/ alias for cross-zone imports; for files that are still in src/components/ (i.e., not yet moved), use a relative path that works.
Example transform:
// before
import LanternLogo from './LanternLogo'
// after (when consumer is in src/components/)
import LanternLogo from '../shared/components/LanternLogo'
// or
import LanternLogo from '@shared/components/LanternLogo'- [ ] Step 7.4: Verify build
npm run build -w lantern-admin 2>&1 | tail -10- [ ] Step 7.5: Verify tests still pass
npm test -w lantern-admin -- --run 2>&1 | tail -10Expected: tests pass. If any test imports the moved component, update its path too.
- [ ] Step 7.6: Commit
git -C /home/mechelle/repos/lantern_app status --short
# Should show only the four moved files + any consumer updates
git -C /home/mechelle/repos/lantern_app add -u # picks up modifications
# (or add specific files if you're worried about other in-flight files)
git -C /home/mechelle/repos/lantern_app status --short
# Verify only the intended files are staged
git -C /home/mechelle/repos/lantern_app commit -m "refactor(admin): move chrome utilities to src/shared/components/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"NOTE on git add -u: this stages modifications to tracked files only (not new files). It can pick up unrelated in-flight edits to tracked files. If status --short shows surprises, use specific file paths instead.
Task 8: Move PageHeader, PageTabs, StyledSelect, Toolbar to src/shared/components/ โ
Files:
Move: each of the four files โ
apps/admin/src/shared/components/Modify: every consumer
[ ] Step 8.1: Move with
git mv(mirror Task 7's pattern)
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/PageHeader.jsx apps/admin/src/shared/components/PageHeader.jsx
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/PageTabs.jsx apps/admin/src/shared/components/PageTabs.jsx
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/StyledSelect.jsx apps/admin/src/shared/components/StyledSelect.jsx
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/Toolbar.jsx apps/admin/src/shared/components/Toolbar.jsx- [ ] Step 8.2: Update consumers
grep -rln "from '.*PageHeader'\|from '.*PageTabs'\|from '.*StyledSelect'\|from '.*Toolbar'" /home/mechelle/repos/lantern_app/apps/admin/src/For each match, update import paths. Prefer @shared/components/X.
- [ ] Step 8.3: Build + test verification
npm run build -w lantern-admin 2>&1 | tail -5
npm test -w lantern-admin -- --run 2>&1 | tail -5- [ ] Step 8.4: Commit
git -C /home/mechelle/repos/lantern_app status --short
git -C /home/mechelle/repos/lantern_app add -u
# Or add specific files if status --short surfaces unrelated changes.
git -C /home/mechelle/repos/lantern_app status --short
git -C /home/mechelle/repos/lantern_app commit -m "refactor(admin): move PageHeader/PageTabs/StyledSelect/Toolbar to src/shared/components/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Task 9: Move auth-flow screens (LoginScreen, Set*Password, ReinitializeLanternModal) to src/shared/components/ โ
Files:
Move:
apps/admin/src/components/LoginScreen.jsxapps/admin/src/components/SetAdminPassword.jsxapps/admin/src/components/SetMerchantPassword.jsxapps/admin/src/components/SetPassword.jsxapps/admin/src/components/SetupAdminPasswordModal.jsxapps/admin/src/components/ReinitializeLanternModal.jsx
Move associated tests (
apps/admin/src/components/__tests__/SetMerchantPassword.test.jsxโapps/admin/src/shared/components/__tests__/SetMerchantPassword.test.jsx)Modify: every consumer
[ ] Step 9.1: Move files with
git mv
mkdir -p /home/mechelle/repos/lantern_app/apps/admin/src/shared/components/__tests__
for f in LoginScreen SetAdminPassword SetMerchantPassword SetPassword SetupAdminPasswordModal ReinitializeLanternModal; do
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/${f}.jsx apps/admin/src/shared/components/${f}.jsx
doneIf a test file exists for SetMerchantPassword, move it too:
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/__tests__/SetMerchantPassword.test.jsx apps/admin/src/shared/components/__tests__/SetMerchantPassword.test.jsx- [ ] Step 9.2: Update consumers + test import paths
grep -rln "from '.*LoginScreen'\|from '.*SetAdminPassword'\|from '.*SetMerchantPassword'\|from '.*SetPassword'\|from '.*SetupAdminPasswordModal'\|from '.*ReinitializeLanternModal'" /home/mechelle/repos/lantern_app/apps/admin/src/Update each consumer to @shared/components/X.
The SetMerchantPassword.test.jsx mock paths also shift โ vi.mock('../../firebase', ...) becomes vi.mock('../../../firebase', ...) (or use the absolute alias @/firebase). Verify the test still passes.
- [ ] Step 9.3: Build + test verification
npm run build -w lantern-admin 2>&1 | tail -5
npm test -w lantern-admin -- --run 2>&1 | tail -5- [ ] Step 9.4: Commit
git -C /home/mechelle/repos/lantern_app status --short
git -C /home/mechelle/repos/lantern_app add -u
git -C /home/mechelle/repos/lantern_app status --short
git -C /home/mechelle/repos/lantern_app commit -m "refactor(admin): move auth-flow screens to src/shared/components/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Task 10: Move apps/admin/src/lib/* to apps/admin/src/shared/lib/ โ
Files:
Move all files in
apps/admin/src/lib/โapps/admin/src/shared/lib/Move
apps/admin/src/lib/__tests__/โapps/admin/src/shared/lib/__tests__/Modify: every consumer
[ ] Step 10.1: Inspect what's there
ls /home/mechelle/repos/lantern_app/apps/admin/src/lib/
ls /home/mechelle/repos/lantern_app/apps/admin/src/lib/__tests__/ 2>/dev/nullYou should see at least: apiClient.js, authApi.js, currentMerchant.js, analyticsApi.js, assistantApi.js, docsApi.js, merchantsApi.js, offerNormalizer.js, plus any tests.
NOTE: analyticsApi.js, assistantApi.js, docsApi.js, merchantsApi.js are arguably admin-side or merchant-side rather than truly shared. Move all of them to src/shared/lib/ for now (preserves the current "everything is in lib/" mental model). Future cleanup can re-zone individual files when their callers are unambiguously one shell.
- [ ] Step 10.2: Move with
git mv
mkdir -p /home/mechelle/repos/lantern_app/apps/admin/src/shared/lib
mkdir -p /home/mechelle/repos/lantern_app/apps/admin/src/shared/lib/__tests__
for f in $(ls /home/mechelle/repos/lantern_app/apps/admin/src/lib/ | grep -v __tests__); do
git -C /home/mechelle/repos/lantern_app mv "apps/admin/src/lib/$f" "apps/admin/src/shared/lib/$f"
done
for f in $(ls /home/mechelle/repos/lantern_app/apps/admin/src/lib/__tests__/ 2>/dev/null); do
git -C /home/mechelle/repos/lantern_app mv "apps/admin/src/lib/__tests__/$f" "apps/admin/src/shared/lib/__tests__/$f"
done
rmdir /home/mechelle/repos/lantern_app/apps/admin/src/lib/__tests__ 2>/dev/null || true
rmdir /home/mechelle/repos/lantern_app/apps/admin/src/lib 2>/dev/null || true- [ ] Step 10.3: Update consumers
grep -rln "from '.*src/lib/\|from '\(\.\./\)*lib/" /home/mechelle/repos/lantern_app/apps/admin/src/For each consumer, update the import to point at @shared/lib/X. Be careful: dynamic imports like import('./lib/authApi') (used inside firebase.js for re-exports) need updating too. Search:
grep -rn "import('./lib" /home/mechelle/repos/lantern_app/apps/admin/src/
grep -rn "import('../lib" /home/mechelle/repos/lantern_app/apps/admin/src/Update each to import('./shared/lib/...') or the alias form.
- [ ] Step 10.4: Build + test verification
npm run build -w lantern-admin 2>&1 | tail -5
npm test -w lantern-admin -- --run 2>&1 | tail -5If any test mocks '../../firebase' or similar relative paths and that mocked module's path shifted, the mock will silently miss โ verify firebase.js itself didn't move (it didn't; it stays at the root) and that mocks targeting '../../firebase' from __tests__ still resolve.
- [ ] Step 10.5: Commit
git -C /home/mechelle/repos/lantern_app status --short
git -C /home/mechelle/repos/lantern_app add -u
git -C /home/mechelle/repos/lantern_app status --short
git -C /home/mechelle/repos/lantern_app commit -m "refactor(admin): move src/lib/ to src/shared/lib/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Phase C โ Build the shell skeletons (Tasks 11-14) โ
Task 11: Create src/admin/AdminShell.jsx (mirroring AdminDashboard's chrome) โ
Files:
Create:
apps/admin/src/admin/AdminShell.jsx[ ] Step 11.1: Read
AdminDashboard.jsxto understand the chrome layout
cat /home/mechelle/repos/lantern_app/apps/admin/src/components/AdminDashboard.jsx | head -120Note:
Sidebar structure (search for
expandedSections, the sidebar nav array, the<aside>or sidebar<div>block)Top bar / page header layout
The main
<Routes>block with all the admin route definitions (~lines 555-630)The
useEffectredirect formerchantOnlyusers (we'll DELETE this in AdminShell โ that's the role fork's job now)[ ] Step 11.2: Create
apps/admin/src/admin/AdminShell.jsx
The new file is roughly a copy of AdminDashboard's chrome + Routes block, with these changes:
- Drop the
merchantOnlyflag and theuseEffectredirect (App.jsx handles role routing now). - All routes are RELATIVE to
/adminโ the parent in App.jsx mounts AdminShell at/admin/*, so<Route path="/" element={<DashboardHome />} />is now/admin/, etc. - Update import paths:
DashboardHome,UserManagement,MerchantsAll, etc., still live inapps/admin/src/components/...for now (they get moved in Phase D). Use their current paths. - Sidebar: same items as AdminDashboard's, EXCEPT remove any merchant-only items (e.g., the
merchantOnlybranch). The sidebar is admin-only now. - Routes for the merchant detail tree (
/merchants/:merchantId/*) are REMOVED from AdminShell โ admins access merchant content via the merchant shell only. Keep/admin/merchants(list) and/admin/merchants/new(create).
Key parts (paraphrased):
import React, { useState, useEffect } from 'react'
import { Routes, Route, Navigate, useNavigate, useLocation, Outlet } from 'react-router-dom'
// ... all the icon imports from AdminDashboard ...
import LanternLogo from '@shared/components/LanternLogo'
import LoadingScreen from '@shared/components/LoadingScreen'
import PageHeader from '@shared/components/PageHeader'
// ... more shared imports ...
// Admin-side pages (still in old locations until Phase D):
import DashboardHome from '../components/DashboardHome' // or wherever
import UserManagement from '../components/UserManagement'
import DocsEditor from '../components/DocsEditor'
import StorybookEmbed from '../components/StorybookEmbed'
import ApiOverview from '../components/ApiOverview'
import ApiReferencePage from '../components/ApiReferencePage'
import ClientSdkPage from '../components/ClientSdkPage'
// ... etc ...
import MerchantsAll from '../components/merchants/MerchantsAll'
import MerchantsCreate from '../components/merchants/MerchantsCreate'
export default function AdminShell({ user, onSignOut, ... }) {
// Sidebar markup (paraphrased โ copy from AdminDashboard)
// No merchantOnly redirect.
// Routes block (relative paths, since mounted at /admin/*):
return (
<div className="admin-dashboard-container">
<aside className="sidebar">
{/* sidebar markup */}
</aside>
<main>
<Routes>
<Route path="/" element={<DashboardHome onNavigate={...} />} />
<Route path="users" element={<UserManagement currentUser={user} />} />
<Route path="merchants" element={<Navigate to="/admin/merchants/all" replace />} />
<Route path="merchants/all" element={<MerchantsAll />} />
<Route path="merchants/new" element={<MerchantsCreate />} />
{/* docs, storybook, api, config, analytics, system, billing, profile, feature-tracker */}
<Route path="*" element={<Navigate to="/admin" replace />} />
</Routes>
</main>
</div>
)
}The exact sidebar markup, navigation, and route entries should mirror what AdminDashboard does today. The key difference is no merchant routes, and paths are admin-relative.
- [ ] Step 11.3: Verify build
AdminShell.jsx is created but not yet wired into App.jsx. Build should still succeed (file just isn't imported anywhere yet).
npm run build -w lantern-admin 2>&1 | tail -5- [ ] Step 11.4: Commit
git -C /home/mechelle/repos/lantern_app add apps/admin/src/admin/AdminShell.jsx
git -C /home/mechelle/repos/lantern_app commit -m "feat(admin): scaffold AdminShell (admin-only routes + sidebar)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Task 12: Create src/merchant/MerchantShell.jsx skeleton (placeholder pages) โ
Files:
Create:
apps/admin/src/merchant/MerchantShell.jsx[ ] Step 12.1: Read
MerchantDetail.jsxfor context
cat /home/mechelle/repos/lantern_app/apps/admin/src/components/merchants/MerchantDetail.jsx | head -120This shows how the merchant detail page composes its tabs today. We'll keep similar data-loading semantics but invert the layout (sidebar instead of horizontal tabs).
- [ ] Step 12.2: Create
apps/admin/src/merchant/MerchantShell.jsxwith placeholder pages
import React, { useEffect, useState } from 'react'
import { Routes, Route, Navigate, useNavigate, useLocation, useParams, Link } from 'react-router-dom'
import {
ClipboardList, Tag, MapPin, StickyNote, Image, Home, User as UserIcon, ChevronLeft, LogOut,
} from 'lucide-react'
import LanternLogo from '@shared/components/LanternLogo'
import LoadingScreen from '@shared/components/LoadingScreen'
const TABS = [
{ id: 'overview', label: 'Overview', icon: ClipboardList },
{ id: 'offers', label: 'Offers', icon: Tag },
{ id: 'venues', label: 'Venues', icon: MapPin },
{ id: 'notes', label: 'Notes', icon: StickyNote },
{ id: 'photos', label: 'Photos', icon: Image },
{ id: 'address', label: 'Address', icon: Home },
{ id: 'profile', label: 'Profile', icon: UserIcon },
]
function Placeholder({ title }) {
return (
<div style={{ padding: 'var(--space-6)' }}>
<h2>{title}</h2>
<p className="text-muted">Migrating from <code>apps/admin/src/components/merchants/tabs/</code> in Phase E.</p>
</div>
)
}
export default function MerchantShell({ user, isAdmin, onSignOut }) {
const { merchantId: urlMerchantId } = useParams()
const navigate = useNavigate()
const location = useLocation()
// Real merchants must be viewing their own merchant. Admins are free.
const ownMerchantId = user?.merchantId || null
if (!isAdmin && ownMerchantId && urlMerchantId !== ownMerchantId) {
return <Navigate to={`/merchant/${ownMerchantId}/overview`} replace />
}
const activeTab = location.pathname.split('/').pop() || 'overview'
return (
<div className="merchant-shell">
<aside className="sidebar">
<div className="sidebar-header">
<LanternLogo size={32} />
<span>Merchant</span>
</div>
<nav>
{TABS.map((t) => (
<Link
key={t.id}
to={`/merchant/${urlMerchantId}/${t.id}`}
className={`sidebar-item ${activeTab === t.id ? 'active' : ''}`}
>
<t.icon size={16} />
<span>{t.label}</span>
</Link>
))}
{isAdmin && (
<Link to="/admin" className="sidebar-item sidebar-item--secondary">
<ChevronLeft size={16} />
<span>Admin</span>
</Link>
)}
<button onClick={onSignOut} className="sidebar-item sidebar-item--button">
<LogOut size={16} />
<span>Sign out</span>
</button>
</nav>
</aside>
<main>
<Routes>
<Route path="/" element={<Navigate to="overview" replace />} />
<Route path="overview" element={<Placeholder title="Overview" />} />
<Route path="offers/*" element={<Placeholder title="Offers" />} />
<Route path="venues" element={<Placeholder title="Venues" />} />
<Route path="notes" element={<Placeholder title="Notes" />} />
<Route path="photos" element={<Placeholder title="Photos" />} />
<Route path="address" element={<Placeholder title="Address" />} />
<Route path="profile" element={<Placeholder title="Profile" />} />
<Route path="*" element={<Navigate to="overview" replace />} />
</Routes>
</main>
</div>
)
}This is a skeleton โ real pages get wired in Phase E. The shell is renderable + role-guarded as of this commit.
- [ ] Step 12.3: Verify build
npm run build -w lantern-admin 2>&1 | tail -5The shell isn't reachable yet (App.jsx doesn't render it), but the file should compile.
- [ ] Step 12.4: Commit
git -C /home/mechelle/repos/lantern_app add apps/admin/src/merchant/MerchantShell.jsx
git -C /home/mechelle/repos/lantern_app commit -m "feat(admin): scaffold MerchantShell with placeholder pages + role guard
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Task 13: Update App.jsx with the role fork + back-compat redirects โ
Files:
- Modify:
apps/admin/src/App.jsx
This is the load-bearing change. After this task, real merchants who sign in see MerchantShell (with placeholder pages); admins see AdminShell.
- [ ] Step 13.1: Read the current App.jsx
cat /home/mechelle/repos/lantern_app/apps/admin/src/App.jsxNote:
Current rendering pattern (likely
<AdminDashboard user={user} ... />)Existing URL-mode handlers (adminReset, merchantReset, resetPassword)
The auth state machine (loading, !user, signed-in)
[ ] Step 13.2: Replace the post-auth render block with the role fork
Imports near the top:
import AdminShell from './admin/AdminShell'
import MerchantShell from './merchant/MerchantShell'Drop the import AdminDashboard from './components/AdminDashboard' (or comment it out for now โ we'll delete the file in Task 25).
In the render section, after the URL-mode handlers and after the loading + !user guards, replace:
return <AdminDashboard user={user} isAdmin={isAdmin} isMerchant={isMerchant} ... />with the role fork:
const role = user?.customClaims?.role || (isAdmin ? 'admin' : isMerchant ? 'merchant' : null)
const ownMerchantId = user?.merchantId || null
// Real merchants without a merchantId โ defensive, shouldn't happen in normal flow
if (role === 'merchant' && !ownMerchantId) {
return <NoMerchantAttached onSignOut={handleSignOut} /> // (or just redirect to login)
}
// Path-based redirects (back-compat + role-based defaults)
const path = location.pathname
// Real merchants visiting /admin/* or root โ bounce to their own merchant
if (role === 'merchant' && (path === '/' || path.startsWith('/admin'))) {
return <Navigate to={`/merchant/${ownMerchantId}/overview`} replace />
}
// Admins visiting / โ admin home
if (role === 'admin' && path === '/') {
return <Navigate to="/admin" replace />
}
// Old admin URLs (back-compat for one release cycle)
const oldUrlMap = [
['/users', '/admin/users'],
['/docs', '/admin/docs'],
['/storybook', '/admin/storybook'],
['/client-sdk', '/admin/api/client-sdk'],
['/system', '/admin/system'],
['/billing', '/admin/billing'],
['/profile', '/admin/profile'],
['/feature-tracker', '/admin/feature-tracker'],
['/api', '/admin/api'],
['/config', '/admin/config'],
['/analytics', '/admin/analytics'],
['/merchants/all', '/admin/merchants/all'],
['/merchants/new', '/admin/merchants/new'],
['/merchants', '/admin/merchants/all'],
]
for (const [oldPrefix, newPrefix] of oldUrlMap) {
if (path === oldPrefix || path.startsWith(oldPrefix + '/')) {
const rest = path.slice(oldPrefix.length)
return <Navigate to={newPrefix + rest} replace />
}
}
// Old /merchants/:merchantId/<tab> โ /merchant/:merchantId/<tab> (pluralโsingular)
const oldMerchantDetailMatch = path.match(/^\/merchants\/([^/]+)(\/.*)?$/)
if (oldMerchantDetailMatch) {
const [, merchantId, rest] = oldMerchantDetailMatch
return <Navigate to={`/merchant/${merchantId}${rest || '/overview'}`} replace />
}
// Render the appropriate shell
return (
<Routes>
<Route path="/admin/*" element={role === 'admin' ? <AdminShell user={user} isAdmin onSignOut={handleSignOut} /> : <Navigate to="/admin" replace />} />
<Route path="/merchant/:merchantId/*" element={<MerchantShell user={user} isAdmin={role === 'admin'} onSignOut={handleSignOut} />} />
<Route path="*" element={<Navigate to={role === 'admin' ? '/admin' : `/merchant/${ownMerchantId}/overview`} replace />} />
</Routes>
)You may need to lift Routes/useLocation imports โ react-router-dom should already be imported. Add useLocation if missing.
The exact code structure depends on the existing App.jsx โ some bits like handleSignInWithEmail, handleSignOut, handlePasswordSetComplete, setCorruptionDetectedAt etc. all stay. The change is replacing the single rendering of AdminDashboard with the role-based shell selection.
- [ ] Step 13.3: Verify build
npm run build -w lantern-admin 2>&1 | tail -5- [ ] Step 13.4: Manual smoke (if you have a running local dev server)
If you can spin up the admin dev server and use a known admin Firebase user:
- Visit
/โ should redirect to/admin. - Visit
/usersโ should redirect to/admin/users. - Sign out and sign back in โ admins land on
/admin/.
For a merchant user (use one of the test accounts created via earlier tasks):
- Sign in โ should land on
/merchant/<theirId>/overview(showing the placeholder page). - Visit
/admin/usersโ should redirect to/merchant/<theirId>/overview.
If you can't run a live server, run automated tests; we'll add a real test in Task 36 anyway.
- [ ] Step 13.5: Commit
git -C /home/mechelle/repos/lantern_app add apps/admin/src/App.jsx
git -C /home/mechelle/repos/lantern_app status --short
git -C /home/mechelle/repos/lantern_app commit -m "feat(admin): role-fork in App.jsx, mounting AdminShell vs MerchantShell
Adds back-compat redirects from old admin URLs to /admin/* and from old
/merchants/:id/* to /merchant/:id/*. Real merchants are bounced to their
own merchant overview if they hit any admin URL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Task 14: Add requireMerchant middleware to the auth API โ
Files:
Modify:
services/api/auth/src/middleware/auth.js[ ] Step 14.1: Inspect existing middleware
cat /home/mechelle/repos/lantern_app/services/api/auth/src/middleware/auth.jsFind requireAdmin. The new requireMerchant is a direct mirror.
- [ ] Step 14.2: Add the middleware
After requireAdmin, append:
export function requireMerchant(req, res, next) {
if ((req.user?.role) !== 'merchant') {
return res.status(403).json({ error: 'FORBIDDEN', message: 'Merchant role required' })
}
next()
}- [ ] Step 14.3: Verify syntax
cd /home/mechelle/repos/lantern_app/services/api/auth && node --check src/middleware/auth.js
cd /home/mechelle/repos/lantern_app/services/api/auth && npm run test:run 2>&1 | tail -5- [ ] Step 14.4: Commit
git -C /home/mechelle/repos/lantern_app add services/api/auth/src/middleware/auth.js
git -C /home/mechelle/repos/lantern_app commit -m "feat(auth-api): add requireMerchant middleware
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Phase D โ Migrate admin pages (Tasks 15-21) โ
Each task moves one admin sub-tree from src/components/... to src/admin/<sub>/. After each, AdminShell's import paths get updated from '../components/...' to '@admin/<sub>/...'. Build + tests verify after each.
Task 15: Move UserManagement, UserDetailPanel, CreateAdminForm, CreateMerchantUserForm, AttachMerchantDialog to src/admin/users/ โ
Files:
Move: 5 component files + their
__tests__toapps/admin/src/admin/users/Modify: AdminShell import paths
[ ] Step 15.1: Move files
mkdir -p /home/mechelle/repos/lantern_app/apps/admin/src/admin/users/__tests__
for f in UserManagement UserDetailPanel CreateAdminForm CreateMerchantUserForm AttachMerchantDialog; do
git -C /home/mechelle/repos/lantern_app mv "apps/admin/src/components/${f}.jsx" "apps/admin/src/admin/users/${f}.jsx"
done
# Tests
for f in $(ls /home/mechelle/repos/lantern_app/apps/admin/src/components/__tests__/ | grep -E "UserManagement|UserDetailPanel|CreateAdminForm|CreateMerchantUserForm|AttachMerchantDialog"); do
git -C /home/mechelle/repos/lantern_app mv "apps/admin/src/components/__tests__/${f}" "apps/admin/src/admin/users/__tests__/${f}"
done- [ ] Step 15.2: Update import paths in moved files
Each moved file imports from './X' (other components in old src/components/) and from '../firebase'. Update:
- Imports of moved siblings within the new
src/admin/users/directory: stay relative (e.g.,import UserDetailPanel from './UserDetailPanel'). - Imports of
../firebase: become'../../firebase'(one extra level up). - Imports of
../shared/...: become'@shared/...'. - Imports of
lucide-react,react, etc.: unchanged.
grep -n "from '" apps/admin/src/admin/users/*.jsx to surface every import.
- [ ] Step 15.3: Update AdminShell import paths
In apps/admin/src/admin/AdminShell.jsx, change:
import UserManagement from '../components/UserManagement'to:
import UserManagement from '@admin/users/UserManagement'(or use relative './users/UserManagement').
- [ ] Step 15.4: Update test mock paths
Test files like UserDetailPanel.test.jsx had vi.mock('../../firebase', ...). Now they're at apps/admin/src/admin/users/__tests__/, so the path to firebase.js is '../../../firebase' (three levels up).
- [ ] Step 15.5: Verify build + tests
npm run build -w lantern-admin 2>&1 | tail -5
npm test -w lantern-admin -- --run 2>&1 | tail -10All tests pass. Build green.
- [ ] Step 15.6: Commit
git -C /home/mechelle/repos/lantern_app status --short
git -C /home/mechelle/repos/lantern_app add -u apps/admin/src/admin/ apps/admin/src/components/
git -C /home/mechelle/repos/lantern_app status --short
git -C /home/mechelle/repos/lantern_app commit -m "refactor(admin): move user management into src/admin/users/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Task 16: Move docs subtree (DocsEditor, MarkdownEditor, DocsEmbed, DocPage, SelfHostedDocsEditor) to src/admin/docs/ โ
Files:
Move: 5 files โ
apps/admin/src/admin/docs/Modify: AdminShell import paths + any cross-references inside the moved files
[ ] Step 16.1: Move files
mkdir -p /home/mechelle/repos/lantern_app/apps/admin/src/admin/docs
for f in DocsEditor MarkdownEditor DocsEmbed DocPage SelfHostedDocsEditor; do
git -C /home/mechelle/repos/lantern_app mv "apps/admin/src/components/${f}.jsx" "apps/admin/src/admin/docs/${f}.jsx"
done- [ ] Step 16.2: Update import paths in moved files
grep -n "from '" /home/mechelle/repos/lantern_app/apps/admin/src/admin/docs/*.jsxFor each from '...' import:
'./<sibling>'(another moved file) โ stays as'./<sibling>'.'../firebase'โ'../../firebase'(one extra level up because we're now 2 deep fromsrc/).'../<shared component>'โ'@shared/components/<X>'(cross-zone โ use the alias).'../<lib>'โ'@shared/lib/<X>'.[ ] Step 16.3: Update AdminShell import paths
In apps/admin/src/admin/AdminShell.jsx, replace '../components/DocsEditor' etc. with '@admin/docs/DocsEditor' (and the four siblings).
- [ ] Step 16.4: Verify build + tests
npm run build -w lantern-admin 2>&1 | tail -5
npm test -w lantern-admin -- --run 2>&1 | tail -5- [ ] Step 16.5: Commit
git -C /home/mechelle/repos/lantern_app status --short
git -C /home/mechelle/repos/lantern_app add -u apps/admin/src/admin/docs/ apps/admin/src/admin/AdminShell.jsx
git -C /home/mechelle/repos/lantern_app status --short
git -C /home/mechelle/repos/lantern_app commit -m "refactor(admin): move docs subtree into src/admin/docs/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Task 17: Move API subtree (ApiOverview, ApiReferencePage, ClientSdkPage, ClientSdkPage.css, ClientSdkEmbed) to src/admin/api/ โ
Files:
Move: 4 jsx files + 1 css file โ
apps/admin/src/admin/api/Modify: AdminShell import paths
[ ] Step 17.1: Move files
mkdir -p /home/mechelle/repos/lantern_app/apps/admin/src/admin/api
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/ApiOverview.jsx apps/admin/src/admin/api/ApiOverview.jsx
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/ApiReferencePage.jsx apps/admin/src/admin/api/ApiReferencePage.jsx
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/ClientSdkPage.jsx apps/admin/src/admin/api/ClientSdkPage.jsx
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/ClientSdkPage.css apps/admin/src/admin/api/ClientSdkPage.css
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/ClientSdkEmbed.jsx apps/admin/src/admin/api/ClientSdkEmbed.jsx- [ ] Step 17.2: Update import paths in moved files
Same rules as Task 16.2: relative siblings stay relative, '../firebase' becomes '../../firebase', cross-zone uses @shared/.... Verify the CSS import path too โ likely import './ClientSdkPage.css' stays unchanged after the move.
- [ ] Step 17.3: Update AdminShell import paths
Replace '../components/ApiOverview' etc. with '@admin/api/ApiOverview' (and siblings).
- [ ] Step 17.4: Verify build + tests + commit
npm run build -w lantern-admin 2>&1 | tail -5
npm test -w lantern-admin -- --run 2>&1 | tail -5
git -C /home/mechelle/repos/lantern_app status --short
git -C /home/mechelle/repos/lantern_app add -u apps/admin/src/admin/api/ apps/admin/src/admin/AdminShell.jsx
git -C /home/mechelle/repos/lantern_app status --short
git -C /home/mechelle/repos/lantern_app commit -m "refactor(admin): move API + Client SDK pages into src/admin/api/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Task 18: Move analytics subtree to src/admin/analytics/ โ
Files:
Move:
apps/admin/src/components/analytics/โapps/admin/src/admin/analytics/Modify: AdminShell import paths
[ ] Step 18.1: Move the directory
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/analytics apps/admin/src/admin/analytics- [ ] Step 18.2: Update import paths
Inside files in apps/admin/src/admin/analytics/:
'../firebase'โ'../../firebase''../<shared component>'โ'@shared/components/<X>''../<lib file>'โ'@shared/lib/<X>'
grep -rn "from '" /home/mechelle/repos/lantern_app/apps/admin/src/admin/analytics/ | head -30In AdminShell.jsx, replace '../components/analytics/X' with '@admin/analytics/X'.
- [ ] Step 18.3: Verify + commit
npm run build -w lantern-admin 2>&1 | tail -5
npm test -w lantern-admin -- --run 2>&1 | tail -5
git -C /home/mechelle/repos/lantern_app status --short
git -C /home/mechelle/repos/lantern_app add -u apps/admin/src/admin/analytics/ apps/admin/src/admin/AdminShell.jsx
git -C /home/mechelle/repos/lantern_app status --short
git -C /home/mechelle/repos/lantern_app commit -m "refactor(admin): move analytics subtree into src/admin/analytics/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Task 19: Move misc admin pages (Billing, SystemHealth, MyAdminProfile, FeatureTracker, StorybookEmbed) โ
Files:
Move 5 components into 5 sibling subdirectories under
src/admin/Modify: AdminShell import paths
[ ] Step 19.1: Create directories + move files
mkdir -p /home/mechelle/repos/lantern_app/apps/admin/src/admin/{billing,system,profile,feature-tracker,storybook}
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/Billing.jsx apps/admin/src/admin/billing/Billing.jsx
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/SystemHealth.jsx apps/admin/src/admin/system/SystemHealth.jsx
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/MyAdminProfile.jsx apps/admin/src/admin/profile/MyAdminProfile.jsx
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/FeatureTracker apps/admin/src/admin/feature-tracker
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/StorybookEmbed.jsx apps/admin/src/admin/storybook/StorybookEmbed.jsx- [ ] Step 19.2: Update import paths in moved files
For each file:
'../firebase'โ'../../firebase''../<shared component>'โ'@shared/components/<X>''../<lib file>'โ'@shared/lib/<X>'
The FeatureTracker/ directory has internal sibling imports โ those stay relative within the directory (e.g., import NewIssueModal from './NewIssueModal' is unchanged).
- [ ] Step 19.3: Update AdminShell
import Billing from '@admin/billing/Billing'
import SystemHealth from '@admin/system/SystemHealth'
import MyAdminProfile from '@admin/profile/MyAdminProfile'
import FeatureTracker from '@admin/feature-tracker/FeatureTracker'
import StorybookEmbed from '@admin/storybook/StorybookEmbed'(Adjust FeatureTracker import to whichever name FeatureTracker's index/main file uses.)
- [ ] Step 19.4: Verify + commit
npm run build -w lantern-admin 2>&1 | tail -5
npm test -w lantern-admin -- --run 2>&1 | tail -5
git -C /home/mechelle/repos/lantern_app status --short
git -C /home/mechelle/repos/lantern_app add -u apps/admin/src/admin/ apps/admin/src/components/
git -C /home/mechelle/repos/lantern_app status --short
git -C /home/mechelle/repos/lantern_app commit -m "refactor(admin): move billing/system/profile/feature-tracker/storybook into src/admin/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Task 20: Move admin merchants management (MerchantsAll, MerchantsCreate, CreateMerchantForm, MerchantSwitcher) to src/admin/merchants/ โ
Note: MerchantSwitcher is admin-only here (admin's switcher in the merchants list and admin chrome) โ actually in our spec the switcher lives in the merchant shell (src/merchant/MerchantSwitcher). Confirm the spec โ it says: "MerchantSwitcher.jsx โ moves into the merchant shell (it's a merchant-shell affordance)." So MerchantSwitcher is NOT moved here; it goes to src/merchant/ in Task 25.
For this task, move:
mkdir -p /home/mechelle/repos/lantern_app/apps/admin/src/admin/merchants
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/merchants/MerchantsAll.jsx apps/admin/src/admin/merchants/MerchantsAll.jsx
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/merchants/MerchantsCreate.jsx apps/admin/src/admin/merchants/MerchantsCreate.jsx
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/CreateMerchantForm.jsx apps/admin/src/admin/merchants/CreateMerchantForm.jsx- [ ] Step 20.1: Add the "Open in merchant mode" affordance to
MerchantsAll
Each row in MerchantsAll.jsx's table currently navigates to /merchants/:merchantId (the old admin merchant detail). Change the click behavior:
Instead of navigate(/merchants/${merchantId}), do:
onClick={() => window.open(`/merchant/${merchantId}/overview`, '_blank')}Or add a dedicated action button in the row labeled "Open in merchant mode" with that behavior. The exact UX should match what feels right โ a button is more discoverable; a row-click is more efficient. Pick one and apply consistently.
- [ ] Step 20.2: Update import paths + AdminShell + verify + commit
"refactor(admin): move merchant management views into src/admin/merchants/
Adds 'Open in merchant mode' affordance to MerchantsAll rows that opens
/merchant/:id/overview in a new browser tab."Task 21: AdminShell paths cleanup pass โ
After Phase D, AdminShell.jsx should import everything from @admin/* or @shared/*. There should be no '../components/...' imports left in AdminShell.
- [ ] Step 21.1: Audit AdminShell imports
grep -n "from '" /home/mechelle/repos/lantern_app/apps/admin/src/admin/AdminShell.jsxExpected: every import resolves to @shared/, @admin/, react, react-router-dom, lucide-react. Any '../components/...' is a leftover that needs fixing.
- [ ] Step 21.2: Fix any leftover paths
Update each leftover to its new location.
- [ ] Step 21.3: Verify build + tests + lint
npm run build -w lantern-admin 2>&1 | tail -5
npm test -w lantern-admin -- --run 2>&1 | tail -5
npm run lint -w lantern-admin 2>&1 | tail -10 # if lint script existsLint should now flag any cross-zone imports inside src/admin/. Fix any that surface.
- [ ] Step 21.4: Commit
git -C /home/mechelle/repos/lantern_app status --short
git -C /home/mechelle/repos/lantern_app add -u
git -C /home/mechelle/repos/lantern_app status --short
git -C /home/mechelle/repos/lantern_app commit -m "refactor(admin): finalize AdminShell imports to @admin/@shared aliases
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Phase E โ Migrate merchant pages (Tasks 22-27) โ
Task 22: Move + rename MerchantOverviewTab โ src/merchant/tabs/Overview.jsx โ
Files:
Move:
apps/admin/src/components/merchants/tabs/MerchantOverviewTab.jsxโapps/admin/src/merchant/tabs/Overview.jsxModify:
MerchantShell.jsxto renderOverview(instead of placeholder)[ ] Step 22.1: Move + rename
mkdir -p /home/mechelle/repos/lantern_app/apps/admin/src/merchant/tabs
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/merchants/tabs/MerchantOverviewTab.jsx apps/admin/src/merchant/tabs/Overview.jsx- [ ] Step 22.2: Update the moved file's component name + imports
Inside Overview.jsx:
- Rename the exported component from
MerchantOverviewTabtoOverview. - Update imports of
'../firebase'โ'../../firebase'(one extra level:merchant/tabsis 2 deep fromsrc). - Update imports of shared components โ
@shared/components/X. - Imports of
useOutletContextfrom react-router-dom: this component currently expects to be a child ofMerchantDetail's<Outlet>. We're flattening that โ the page now consumes data via its own loader (or accepts merchantId from URL params). For now, refactor to:
import { useParams } from 'react-router-dom'
import { useEffect, useState } from 'react'
import { getMerchantData } from '../../firebase' // or @shared/lib/merchantApi (after Task 28)
export default function Overview() {
const { merchantId } = useParams()
const [data, setData] = useState({ merchant: null, owners: [], venues: [] })
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let cancelled = false
;(async () => {
try {
const result = await getMerchantData(merchantId)
if (!cancelled) setData(result)
} catch (err) {
if (!cancelled) setError(err.message)
} finally {
if (!cancelled) setLoading(false)
}
})()
return () => { cancelled = true }
}, [merchantId])
if (loading) return <p>Loading...</p>
if (error) return <div className="form-error">{error}</div>
// Render the existing JSX from MerchantOverviewTab using `data` instead of useOutletContext.
return (...)
}- [ ] Step 22.3: Wire
OverviewintoMerchantShell
Replace the placeholder for path="overview" in MerchantShell.jsx:
import Overview from './tabs/Overview'
// ...
<Route path="overview" element={<Overview />} />- [ ] Step 22.4: Build + verify
npm run build -w lantern-admin 2>&1 | tail -5
npm test -w lantern-admin -- --run 2>&1 | tail -5- [ ] Step 22.5: Commit
"refactor(admin): move MerchantOverviewTab to src/merchant/tabs/Overview.jsx
Component renamed and refactored to read merchantId from URL params
instead of useOutletContext, since MerchantShell flattens the tab tree."Task 23: Move + rename remaining merchant tabs (Offers, Venues, Notes, Photos, Address) โ
For each of MerchantOffersTab, MerchantVenuesTab, MerchantNotesTab, MerchantPhotosTab, MerchantAddressTab, repeat the Task 22 pattern. Prefer one commit per page (5 commits) for traceability.
Files (per page):
Move:
apps/admin/src/components/merchants/tabs/Merchant<X>Tab.jsxโapps/admin/src/merchant/tabs/<X>.jsxModify:
apps/admin/src/merchant/MerchantShell.jsxto render<X>(instead of placeholder)[ ] Step 23.1: Move + rename each file
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/merchants/tabs/MerchantOffersTab.jsx apps/admin/src/merchant/tabs/Offers.jsx
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/merchants/tabs/MerchantVenuesTab.jsx apps/admin/src/merchant/tabs/Venues.jsx
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/merchants/tabs/MerchantNotesTab.jsx apps/admin/src/merchant/tabs/Notes.jsx
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/merchants/tabs/MerchantPhotosTab.jsx apps/admin/src/merchant/tabs/Photos.jsx
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/merchants/tabs/MerchantAddressTab.jsx apps/admin/src/merchant/tabs/Address.jsx- [ ] Step 23.2: Inside each moved file, rename the component + refactor data loading
For each Merchant<X>Tab exported component, rename to <X> (drop the Merchant prefix and Tab suffix). Update the export default line.
Then refactor data loading: the old code used useOutletContext() to receive merchantId + merchant data from MerchantDetail. Now it must read merchantId from URL params and fetch its own data. Apply the Task 22.2 pattern:
import { useParams } from 'react-router-dom'
import { useEffect, useState } from 'react'
import { getMerchantData } from '../../firebase' // or @shared/lib/merchantApi after Task 30
export default function Offers() { // (or Venues, Notes, Photos, Address per file)
const { merchantId } = useParams()
const [data, setData] = useState({ /* shape this tab expects */ })
// ... loadData useEffect, error state, then render existing JSX using `data`
}The exact data shape each tab needs differs. Read the old file to see what useOutletContext() was destructuring; that's what data needs to provide.
Update other imports in the moved file:
'../firebase'โ'../../firebase'(one extra level:merchant/tabs/X.jsxis 2 deep fromsrc/)'../<shared component>'โ'@shared/components/<X>'[ ] Step 23.3: Wire each into MerchantShell
In apps/admin/src/merchant/MerchantShell.jsx:
import Offers from './tabs/Offers'
import Venues from './tabs/Venues'
import Notes from './tabs/Notes'
import Photos from './tabs/Photos'
import Address from './tabs/Address'
// ...
<Route path="offers/*" element={<Offers />} />
<Route path="venues" element={<Venues />} />
<Route path="notes" element={<Notes />} />
<Route path="photos" element={<Photos />} />
<Route path="address" element={<Address />} />(Note: offers/* keeps the wildcard for the upcoming offer subroutes from Task 24.)
After this step, the <Placeholder> helper inside MerchantShell.jsx is no longer referenced โ delete it.
- [ ] Step 23.4: Verify build + tests
npm run build -w lantern-admin 2>&1 | tail -5
npm test -w lantern-admin -- --run 2>&1 | tail -5- [ ] Step 23.5: Commit (one commit per page is preferred for review traceability)
If batching into one commit:
git -C /home/mechelle/repos/lantern_app status --short
git -C /home/mechelle/repos/lantern_app add -u apps/admin/src/merchant/tabs/ apps/admin/src/merchant/MerchantShell.jsx apps/admin/src/components/merchants/tabs/
git -C /home/mechelle/repos/lantern_app status --short
git -C /home/mechelle/repos/lantern_app commit -m "refactor(admin): move merchant tabs (Offers, Venues, Notes, Photos, Address) into src/merchant/tabs/
Tabs renamed (drop the 'Tab' suffix), data loading refactored from
useOutletContext to useParams + per-page fetch. MerchantShell now renders
real pages instead of placeholders.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"If one commit per page (5 commits), use messages like "refactor(admin): move MerchantOffersTab to src/merchant/tabs/Offers.jsx".
Task 24: Move offer flows (OfferDetail, OfferForm, OffersList) to src/merchant/offers/ โ
These are referenced by Offers (the page from Task 23). They live in merchants/tabs/ today.
mkdir -p /home/mechelle/repos/lantern_app/apps/admin/src/merchant/offers
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/merchants/tabs/OfferDetail.jsx apps/admin/src/merchant/offers/OfferDetail.jsx
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/merchants/tabs/OfferForm.jsx apps/admin/src/merchant/offers/OfferForm.jsx
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/merchants/tabs/OffersList.jsx apps/admin/src/merchant/offers/OffersList.jsxUpdate imports inside each file. Update MerchantShell.jsx routes for offer subroutes:
<Route path="offers" element={<OffersList />} />
<Route path="offers/new" element={<OfferForm mode="create" />} />
<Route path="offers/:offerId" element={<OfferDetail />} />
<Route path="offers/:offerId/edit" element={<OfferForm mode="edit" />} />Verify, commit. Commit message: "refactor(admin): move offer flows into src/merchant/offers/".
Task 25: Move MerchantSwitcher to src/merchant/ โ
git -C /home/mechelle/repos/lantern_app mv apps/admin/src/components/merchants/MerchantSwitcher.jsx apps/admin/src/merchant/MerchantSwitcher.jsxUpdate imports inside the file. Wire it into MerchantShell.jsx (visible only when isAdmin):
{isAdmin && <MerchantSwitcher currentMerchantId={urlMerchantId} onPick={(id) => navigate(`/merchant/${id}/overview`)} />}(Add to the sidebar header or appropriate position; match existing styling.)
Verify, commit. Commit message: "refactor(admin): move MerchantSwitcher into src/merchant/, render in shell for admins only".
Task 26: Delete obsolete files โ
apps/admin/src/components/merchants/MerchantDetail.jsxโ superseded by MerchantShellapps/admin/src/components/merchants/MerchantsIndexRedirect.jsxโ superseded by App.jsx role forkapps/admin/src/components/merchants/MerchantDetailFields.jsxโ fold into Overview if still needed; otherwise deleteapps/admin/src/components/merchants/__tests__/โ move/delete based on what's left[ ] Step 26.1: Confirm nothing imports these
grep -rn "MerchantDetail\b\|MerchantsIndexRedirect\b" /home/mechelle/repos/lantern_app/apps/admin/src/Expected: no matches outside the files themselves and any tests. If a test imports MerchantDetail, the test is obsolete โ move/delete it.
- [ ] Step 26.2: Delete with
git rm
git -C /home/mechelle/repos/lantern_app rm apps/admin/src/components/merchants/MerchantDetail.jsx
git -C /home/mechelle/repos/lantern_app rm apps/admin/src/components/merchants/MerchantsIndexRedirect.jsx
git -C /home/mechelle/repos/lantern_app rm apps/admin/src/components/merchants/MerchantDetailFields.jsx 2>/dev/null || true
# Also delete test files referencing deleted components- [ ] Step 26.3: Verify build + tests
npm run build -w lantern-admin 2>&1 | tail -5
npm test -w lantern-admin -- --run 2>&1 | tail -5- [ ] Step 26.4: Commit
"chore(admin): delete obsolete MerchantDetail, MerchantsIndexRedirect, MerchantDetailFields"Task 27: Delete AdminDashboard.jsx and its test โ
AdminDashboard.jsx is still referenced from somewhere if Task 13 didn't fully replace it. After App.jsx forks directly into shells, AdminDashboard is dead.
- [ ] Step 27.1: Confirm no live references
grep -rn "AdminDashboard" /home/mechelle/repos/lantern_app/apps/admin/src/Expected: only the file itself and (possibly) AdminDashboard.test.jsx.
- [ ] Step 27.2: Delete
git -C /home/mechelle/repos/lantern_app rm apps/admin/src/components/AdminDashboard.jsx
git -C /home/mechelle/repos/lantern_app rm apps/admin/src/components/__tests__/AdminDashboard.test.jsx 2>/dev/null || true- [ ] Step 27.3: Verify build + tests
npm run build -w lantern-admin 2>&1 | tail -5
npm test -w lantern-admin -- --run 2>&1 | tail -5- [ ] Step 27.4: Commit
"chore(admin): delete AdminDashboard.jsx โ superseded by App.jsx role fork + AdminShell"Phase F โ Backend dual-mount (Tasks 28-30) โ
Task 28: Extract handlers from routes/adminMerchants.js to handlers/merchantHandlers.js โ
Files:
Create:
services/api/auth/src/handlers/merchantHandlers.jsModify:
services/api/auth/src/routes/adminMerchants.js(becomes thin wiring)[ ] Step 28.1: Create the handlers file
Read routes/adminMerchants.js end-to-end. For each router.method(...) block, extract the (req, res, next) => {...} callback into a named exported function in handlers/merchantHandlers.js.
cat /home/mechelle/repos/lantern_app/services/api/auth/src/routes/adminMerchants.jsThe handlers to extract:
listMerchantsโ wasrouter.get('/', ...)getMerchantDetailโ wasrouter.get('/:merchantId', ...)createMerchantUserForMerchantโ wasrouter.post('/:merchantId/users', ...)detachUserFromMerchantโ wasrouter.delete('/:merchantId/users/:userId', ...)associateVenueWithMerchantโ wasrouter.post('/:merchantId/venues', ...)disassociateVenueFromMerchantโ wasrouter.delete('/:merchantId/venues/:venueId', ...)
Each is moved verbatim into merchantHandlers.js as a named export. Imports follow them (getAuth, getFirestore, FieldValue, zod, randomBytes, sendMerchantInviteEmail).
// services/api/auth/src/handlers/merchantHandlers.js
import { getAuth } from 'firebase-admin/auth'
import { getFirestore, FieldValue } from 'firebase-admin/firestore'
import { z } from 'zod'
import { randomBytes } from 'crypto'
import { sendMerchantInviteEmail } from '../lib/email.js'
// (zod schemas โ copy from adminMerchants.js)
const AssociateVenueBody = z.object({ venueId: z.string().min(1) })
const CreateMerchantUserBody = z.object({ /* ... */ })
export async function listMerchants(req, res, next) {
// copy verbatim from old GET / handler
}
export async function getMerchantDetail(req, res, next) {
// copy verbatim from old GET /:merchantId handler
}
export async function createMerchantUserForMerchant(req, res, next) {
// copy verbatim from old POST /:merchantId/users handler
}
export async function detachUserFromMerchant(req, res, next) {
// copy verbatim from old DELETE /:merchantId/users/:userId handler
}
export async function associateVenueWithMerchant(req, res, next) {
// copy verbatim from old POST /:merchantId/venues handler
}
export async function disassociateVenueFromMerchant(req, res, next) {
// copy verbatim from old DELETE /:merchantId/venues/:venueId handler
}- [ ] Step 28.2: Replace
adminMerchants.jswith thin wiring
// services/api/auth/src/routes/adminMerchants.js
import { Router } from 'express'
import * as h from '../handlers/merchantHandlers.js'
const router = Router()
router.get('/', h.listMerchants)
router.get('/:merchantId', h.getMerchantDetail)
router.post('/:merchantId/users', h.createMerchantUserForMerchant)
router.delete('/:merchantId/users/:userId', h.detachUserFromMerchant)
router.post('/:merchantId/venues', h.associateVenueWithMerchant)
router.delete('/:merchantId/venues/:venueId', h.disassociateVenueFromMerchant)
export default router- [ ] Step 28.3: Verify
cd /home/mechelle/repos/lantern_app/services/api/auth && node --check src/handlers/merchantHandlers.js
cd /home/mechelle/repos/lantern_app/services/api/auth && node --check src/routes/adminMerchants.js
cd /home/mechelle/repos/lantern_app/services/api/auth && npm run test:run 2>&1 | tail -5All tests pass. The behavior is identical โ only the file structure changed.
- [ ] Step 28.4: Commit
git -C /home/mechelle/repos/lantern_app add services/api/auth/src/handlers/merchantHandlers.js services/api/auth/src/routes/adminMerchants.js
git -C /home/mechelle/repos/lantern_app commit -m "refactor(auth-api): extract merchant handlers into pure functions
Same handlers, separated from their wiring. Lets us mount the same
handlers under /auth/admin/merchants and /auth/merchant/me with
different middleware.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Task 29: Add merchantSelf.js route file at /auth/merchant/me โ
Files:
Create:
services/api/auth/src/routes/merchantSelf.jsModify:
services/api/auth/src/index.js(mount the new router)[ ] Step 29.1: Create
merchantSelf.js
// services/api/auth/src/routes/merchantSelf.js
/**
* Merchant self-service routes โ endpoints scoped to the authenticated
* merchant's own merchantId (resolved from their token claim, not the URL).
*/
import { Router } from 'express'
import * as h from '../handlers/merchantHandlers.js'
const router = Router()
// Inject merchantId from the authenticated user's token into req.params
// before any handler runs. The handlers in merchantHandlers.js read
// req.params.merchantId โ they don't care where it came from.
router.use((req, res, next) => {
if (!req.user?.merchantId) {
return res.status(401).json({ error: 'NO_MERCHANT', message: 'No merchant associated with your account' })
}
req.params.merchantId = req.user.merchantId
next()
})
// v1 mounts only getMerchantDetail. Mutations stay admin-only.
router.get('/', h.getMerchantDetail)
export default router- [ ] Step 29.2: Mount it in
index.js
In services/api/auth/src/index.js, find the existing merchantDispatch block (created in earlier merchant auth work). Add:
import merchantSelfRoutes from './routes/merchantSelf.js'
// ... within the merchantDispatch block ...
merchantDispatch.use('/me', verifyFirebaseToken, requireMerchant, merchantSelfRoutes)Make sure requireMerchant is imported from ./middleware/auth.js (Task 14 added it).
- [ ] Step 29.3: Verify syntax + tests
cd /home/mechelle/repos/lantern_app/services/api/auth && node --check src/routes/merchantSelf.js
cd /home/mechelle/repos/lantern_app/services/api/auth && node --check src/index.js
cd /home/mechelle/repos/lantern_app/services/api/auth && npm run test:run 2>&1 | tail -5- [ ] Step 29.4: Commit
git -C /home/mechelle/repos/lantern_app add services/api/auth/src/routes/merchantSelf.js services/api/auth/src/index.js
git -C /home/mechelle/repos/lantern_app commit -m "feat(auth-api): mount /auth/merchant/me routes for merchant self-service
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Task 30: Update shared/lib/merchantApi.js for role-aware URL selection โ
Files:
Create:
apps/admin/src/shared/lib/merchantApi.jsModify: existing
getMerchantDatacallers if any (likely justfirebase.js)[ ] Step 30.1: Create the role-aware client
// apps/admin/src/shared/lib/merchantApi.js
import { authRequest, parseResponse } from './apiClient'
import { auth as firebaseAuth } from '../../firebase'
const API_BASE_URL = import.meta.env.VITE_AUTH_API_URL
/**
* Fetch merchant detail. Role-aware:
* - admin: GET /auth/admin/merchants/:merchantId (any merchant)
* - merchant: GET /auth/merchant/me (own merchant; merchantId arg is ignored
* by the server, kept by the client for symmetry/logging)
*/
export async function getMerchantDetail(merchantId) {
const role = await getCurrentRole()
const url = role === 'admin'
? `${API_BASE_URL}/auth/admin/merchants/${encodeURIComponent(merchantId)}`
: `${API_BASE_URL}/auth/merchant/me`
return parseResponse(await authRequest(url))
}
async function getCurrentRole() {
const user = firebaseAuth.currentUser
if (!user) return null
const tokenResult = await user.getIdTokenResult()
return tokenResult.claims.role || null
}If parseResponse isn't exported from apiClient.js, look for the existing helper that admin code uses (it's likely an internal helper inside authApi.js). Either:
- Export
parseResponsefromapiClient.js(small change, one line). - Or copy the parse logic inline to
merchantApi.js(DRY violation but localized).
Prefer exporting; check if other files would benefit too.
- [ ] Step 30.2: Wire callers to the new function
In firebase.js, find getMerchantData:
grep -n "getMerchantData" /home/mechelle/repos/lantern_app/apps/admin/src/firebase.jsIt currently calls the admin URL directly via authApi. Replace with a call to the new shared client:
export async function getMerchantData(merchantId) {
const { getMerchantDetail } = await import('./shared/lib/merchantApi')
return getMerchantDetail(merchantId)
}- [ ] Step 30.3: Verify build + tests
npm run build -w lantern-admin 2>&1 | tail -5
npm test -w lantern-admin -- --run 2>&1 | tail -5- [ ] Step 30.4: Commit
git -C /home/mechelle/repos/lantern_app add apps/admin/src/shared/lib/merchantApi.js apps/admin/src/firebase.js
# Plus apiClient.js if you exported parseResponse
git -C /home/mechelle/repos/lantern_app commit -m "feat(admin): role-aware merchantApi.getMerchantDetail
When the current user is admin, calls /auth/admin/merchants/:id (any).
When the current user is merchant, calls /auth/merchant/me (own).
firebase.js's getMerchantData delegates to the new shared client.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Phase G โ Wire merchant write paths (Tasks 31-32, may grow) โ
Task 31: Audit merchant-shell write paths โ
Files:
Read: each merchant-shell tab to find its write actions and the API endpoints they call
[ ] Step 31.1: For each merchant tab, document its write endpoints
grep -rn "fetch\|authRequest\|publicRequest\|firebase.*update\|firebase.*set" \
/home/mechelle/repos/lantern_app/apps/admin/src/merchant/tabs/ \
/home/mechelle/repos/lantern_app/apps/admin/src/merchant/offers/Build a table of (tab name, action, current endpoint, current auth model):
| Tab | Action | Endpoint | Auth model | /me mount needed? |
|---|---|---|---|---|
| Overview | Edit business name | ? | ? | ? |
| Offers | CRUD offers | services/api/merchants/ | Firestore rules | No โ already works |
| Venues | (read-only?) | ? | ? | ? |
| Notes | Edit notes | ? | ? | ? |
| Photos | Upload | Firebase Storage | Storage rules | No โ already works |
| Address | Edit address | ? | ? | ? |
- [ ] Step 31.2: Decide per gap
For each "yes โ needs /me mount" row:
If a handler exists in
merchantHandlers.js, add a route tomerchantSelf.js.If no handler exists, create one in
merchantHandlers.js(mirror admin patterns), then mount.If the action is admin-only (e.g., team management), the merchant tab should NOT expose it โ remove the action from the merchant view.
[ ] Step 31.3: Document findings
Append the audit findings to docs/superpowers/specs/2026-04-27-admin-merchant-shell-split-design.md under a new "Audit findings (Phase G)" section, OR write to a fresh doc at docs/superpowers/notes/2026-04-27-merchant-write-audit.md. Either way, the implementer's discoveries are persisted for the reviewer.
- [ ] Step 31.4: Commit the audit doc
git -C /home/mechelle/repos/lantern_app add docs/superpowers/notes/2026-04-27-merchant-write-audit.md
# (or whatever doc you wrote)
git -C /home/mechelle/repos/lantern_app commit -m "docs: audit merchant-shell write paths and document /me mount gaps
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Task 32: Add /me mounts and/or new endpoints for the gaps surfaced in Task 31 โ
Files:
- Modify:
services/api/auth/src/routes/merchantSelf.js - Possibly create new handlers in
services/api/auth/src/handlers/merchantHandlers.js - Possibly modify the merchant frontend (
apps/admin/src/merchant/...) to call the new endpoints
This is shaped by Task 31's findings. For each gap:
- [ ] Step 32.1: For each new handler needed
Example: if "edit business name" needs a handler:
// In merchantHandlers.js
const UpdateBusinessBody = z.object({
businessName: z.string().min(1),
})
export async function updateMerchantBusiness(req, res, next) {
try {
const { merchantId } = req.params
const body = UpdateBusinessBody.parse(req.body)
const callerUid = req.user.uid
const db = getFirestore()
await db.collection('merchants').doc(merchantId).update({
businessName: body.businessName,
updatedAt: FieldValue.serverTimestamp(),
updatedBy: callerUid,
})
db.collection('adminActions').add({
action: 'updateMerchantBusiness',
merchantId, performedBy: callerUid,
performedAt: FieldValue.serverTimestamp(),
}).catch((err) => console.error('failed to log updateMerchantBusiness', err))
return res.json({ success: true, merchantId })
} catch (err) {
next(err)
}
}Then mount it:
- In
routes/adminMerchants.js(admin can edit any):router.patch('/:merchantId/business', h.updateMerchantBusiness) - In
routes/merchantSelf.js:router.patch('/business', h.updateMerchantBusiness)
The frontend call in merchant/tabs/Overview.jsx calls a role-aware client function in shared/lib/merchantApi.js:
export async function updateMerchantBusiness(merchantId, body) {
const role = await getCurrentRole()
const url = role === 'admin'
? `${API_BASE_URL}/auth/admin/merchants/${encodeURIComponent(merchantId)}/business`
: `${API_BASE_URL}/auth/merchant/me/business`
return parseResponse(await authRequest(url, { method: 'PATCH', body: JSON.stringify(body) }))
}Repeat per gap.
- [ ] Step 32.2: One commit per gap
For each gap fixed, commit with a focused message. Example:
"feat(auth-api): support merchant-self updates to business name (PATCH /me/business)""feat(auth-api): support merchant-self notes editing"
The implementer should expect 2-6 commits in this task depending on Task 31's findings. If zero gaps surface (all writes already merchant-accessible via services/api/merchants/ or storage rules), this task is empty โ note that and move on.
- [ ] Step 32.3: Verify build + tests after each commit
cd /home/mechelle/repos/lantern_app/services/api/auth && npm run test:run 2>&1 | tail -5
npm run build -w lantern-admin 2>&1 | tail -5
npm test -w lantern-admin -- --run 2>&1 | tail -5Phase H โ Profile + tests (Tasks 33-37) โ
Task 33: Create MyMerchantProfile.jsx โ
Files:
Create:
apps/admin/src/merchant/profile/MyMerchantProfile.jsxModify:
MerchantShell.jsxto wire it into/profileroute[ ] Step 33.1: Inspect
MyAdminProfile.jsxfor the pattern
cat /home/mechelle/repos/lantern_app/apps/admin/src/admin/profile/MyAdminProfile.jsx | head -80The merchant version is simpler: shows the merchant's own user info + a "change merchant portal password" link that triggers the existing forgot-password flow.
- [ ] Step 33.2: Write
MyMerchantProfile.jsx
import React, { useEffect, useState } from 'react'
import LanternLogo from '@shared/components/LanternLogo'
import { auth, requestMerchantPasswordReset } from '../../firebase'
export default function MyMerchantProfile() {
const [user, setUser] = useState(null)
const [resetSent, setResetSent] = useState(false)
const [resetError, setResetError] = useState(null)
useEffect(() => {
setUser(auth.currentUser)
}, [])
if (!user) return <p>Loading...</p>
const handleResetPassword = async () => {
setResetError(null)
try {
await requestMerchantPasswordReset(user.email)
setResetSent(true)
} catch (err) {
setResetError(err.message || 'Failed to send reset email')
}
}
return (
<div className="profile-container">
<h2>My Merchant Profile</h2>
<div className="form-card">
<h3 className="form-section-title">Account</h3>
<p><strong>Email:</strong> {user.email}</p>
<p><strong>Display name:</strong> {user.displayName || 'โ'}</p>
</div>
<div className="form-card" style={{ marginTop: 'var(--space-3)' }}>
<h3 className="form-section-title">Merchant portal password</h3>
<p className="text-muted">
Reset your merchant portal password. Your Lantern app passphrase is independent and will not be affected.
</p>
{resetSent ? (
<p className="form-success">Reset email sent. Check your inbox.</p>
) : (
<button className="btn btn-secondary" onClick={handleResetPassword}>
Reset portal password
</button>
)}
{resetError && <p className="form-error">{resetError}</p>}
</div>
</div>
)
}- [ ] Step 33.3: Wire into MerchantShell
In MerchantShell.jsx, replace the placeholder profile route:
import MyMerchantProfile from './profile/MyMerchantProfile'
// ...
<Route path="profile" element={<MyMerchantProfile />} />- [ ] Step 33.4: Verify build + tests
npm run build -w lantern-admin 2>&1 | tail -5
npm test -w lantern-admin -- --run 2>&1 | tail -5- [ ] Step 33.5: Commit
git -C /home/mechelle/repos/lantern_app add apps/admin/src/merchant/profile/MyMerchantProfile.jsx apps/admin/src/merchant/MerchantShell.jsx
git -C /home/mechelle/repos/lantern_app commit -m "feat(admin): add MyMerchantProfile page
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Task 34: Component test for MerchantShell โ
Files:
Create:
apps/admin/src/merchant/__tests__/MerchantShell.test.jsx[ ] Step 34.1: Write the test
// eslint-disable-next-line unused-imports/no-unused-imports
import React from 'react'
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import { MemoryRouter, Routes, Route } from 'react-router-dom'
import MerchantShell from '../MerchantShell'
vi.mock('../../firebase', () => ({
auth: { currentUser: null },
}))
function renderShell({ user, isAdmin = false, path = '/merchant/m-1/overview' }) {
return render(
<MemoryRouter initialEntries={[path]}>
<Routes>
<Route
path="/merchant/:merchantId/*"
element={<MerchantShell user={user} isAdmin={isAdmin} onSignOut={() => {}} />}
/>
</Routes>
</MemoryRouter>
)
}
describe('MerchantShell', () => {
it('renders the shell when merchant viewing their own merchant', () => {
renderShell({ user: { merchantId: 'm-1' }, isAdmin: false })
// Sidebar should have the tabs
expect(screen.getByText(/Overview/i)).toBeInTheDocument()
expect(screen.getByText(/Offers/i)).toBeInTheDocument()
})
it('redirects merchant viewing another merchant to their own', () => {
renderShell({ user: { merchantId: 'm-1' }, isAdmin: false, path: '/merchant/m-2/overview' })
// After the redirect resolves, the shell should be at m-1 (verify by URL or content).
// Since MemoryRouter doesn't update document.location, assert by checking the redirect-effect:
// If MerchantShell rendered the placeholder for the wrong merchant, this assertion fails.
expect(screen.queryByText(/m-2/)).not.toBeInTheDocument()
})
it('admin viewing any merchant sees the switcher', () => {
renderShell({ user: { merchantId: null }, isAdmin: true, path: '/merchant/m-7/overview' })
// The switcher should be visible (its placeholder text or aria-label, depending on implementation)
// Adjust this check to whatever MerchantSwitcher actually renders.
expect(screen.queryByText(/Switch merchant|Pick merchant|m-7/i)).toBeInTheDocument()
})
it('real merchant does not see the "Admin" sidebar item', () => {
renderShell({ user: { merchantId: 'm-1' }, isAdmin: false })
expect(screen.queryByText(/^Admin$/)).not.toBeInTheDocument()
})
it('admin sees the "Admin" sidebar item', () => {
renderShell({ user: { merchantId: null }, isAdmin: true, path: '/merchant/m-1/overview' })
expect(screen.getByText(/^Admin$/)).toBeInTheDocument()
})
})These tests are intentionally lightweight โ assertions hit visible text. If MerchantShell's specific wording differs, adjust the regexes.
- [ ] Step 34.2: Run tests
npm test -w lantern-admin -- --run MerchantShell 2>&1 | tail -10If any test fails because of how MerchantShell wires data fetches (e.g., placeholder tries to fetch on mount), mock the data fetches at the top of the test file:
vi.mock('../../shared/lib/merchantApi', () => ({
getMerchantDetail: vi.fn().mockResolvedValue({ merchant: { businessName: 'Test' }, owners: [], venues: [] }),
}))- [ ] Step 34.3: Commit
git -C /home/mechelle/repos/lantern_app add apps/admin/src/merchant/__tests__/MerchantShell.test.jsx
git -C /home/mechelle/repos/lantern_app commit -m "test(admin): MerchantShell renders, redirects on role mismatch, switcher visibility
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Task 35: Component test for AdminShell โ
Files:
Create:
apps/admin/src/admin/__tests__/AdminShell.test.jsx[ ] Step 35.1: Write the test
// eslint-disable-next-line unused-imports/no-unused-imports
import React from 'react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import { MemoryRouter, Routes, Route } from 'react-router-dom'
import AdminShell from '../AdminShell'
vi.mock('../../firebase', () => ({
auth: {},
getAdminProfile: vi.fn().mockResolvedValue({ displayName: 'Test Admin' }),
}))
// Mock all admin-page subimports to keep this focused on the shell:
vi.mock('../users/UserManagement', () => ({
default: () => <div data-testid="users">UserManagement</div>,
}))
vi.mock('../merchants/MerchantsAll', () => ({
default: () => (
<div data-testid="merchants-all">
<button onClick={() => window.open('/merchant/m-1/overview', '_blank')}>Open m-1</button>
</div>
),
}))
// (mock other pages as needed)
function renderShell(initialPath = '/admin') {
return render(
<MemoryRouter initialEntries={[initialPath]}>
<Routes>
<Route path="/admin/*" element={<AdminShell user={{ uid: 'admin-1' }} isAdmin onSignOut={() => {}} />} />
</Routes>
</MemoryRouter>
)
}
describe('AdminShell', () => {
beforeEach(() => vi.clearAllMocks())
it('renders the admin sidebar', async () => {
renderShell('/admin')
expect(await screen.findByText(/Users/i)).toBeInTheDocument()
expect(await screen.findByText(/Merchants/i)).toBeInTheDocument()
expect(await screen.findByText(/Docs/i)).toBeInTheDocument()
})
it('Merchants list "open in merchant mode" calls window.open', async () => {
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
renderShell('/admin/merchants/all')
fireEvent.click(await screen.findByText(/Open m-1/i))
expect(openSpy).toHaveBeenCalledWith('/merchant/m-1/overview', '_blank')
openSpy.mockRestore()
})
})- [ ] Step 35.2: Run + commit
npm test -w lantern-admin -- --run AdminShell 2>&1 | tail -10
git -C /home/mechelle/repos/lantern_app add apps/admin/src/admin/__tests__/AdminShell.test.jsx
git -C /home/mechelle/repos/lantern_app commit -m "test(admin): AdminShell renders sidebar; Merchants row opens new tab
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Task 36: Route-guard tests in App.test.jsx โ
Files:
Create or update:
apps/admin/src/__tests__/App.test.jsx[ ] Step 36.1: Write the test
The tests need to mock firebase.js heavily (auth state, role claims). The test confirms App.jsx's redirects.
Sketch:
// eslint-disable-next-line unused-imports/no-unused-imports
import React from 'react'
import { describe, it, expect, vi } from 'vitest'
import { render, waitFor } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
vi.mock('../firebase', () => ({
auth: {},
onAuthChange: (cb) => { cb({ /* user */ }, { role: 'merchant', merchantId: 'm-1' }); return () => {} },
}))
// (Mock both shells to render a sentinel so we can assert redirection behavior.)
vi.mock('../admin/AdminShell', () => ({ default: () => <div data-testid="admin-shell">admin</div> }))
vi.mock('../merchant/MerchantShell', () => ({ default: () => <div data-testid="merchant-shell">merchant</div> }))
import App from '../App'
describe('App role fork', () => {
it('real merchant landing on /admin/users redirects to their merchant', async () => {
const { container } = render(
<MemoryRouter initialEntries={['/admin/users']}>
<App />
</MemoryRouter>
)
await waitFor(() => {
expect(container.querySelector('[data-testid="merchant-shell"]')).toBeInTheDocument()
expect(container.querySelector('[data-testid="admin-shell"]')).not.toBeInTheDocument()
})
})
it('old /users redirects to /admin/users (which renders for admins)', async () => {
// (re-mock firebase with admin role for this test)
// similar setup; assert admin-shell rendered
})
// ... etc per the spec's manual smoke matrix
})The exact test shape depends on how onAuthChange is structured. Read App.jsx first, mock the appropriate hooks, then write the assertions.
- [ ] Step 36.2: Run + commit
npm test -w lantern-admin -- --run App 2>&1 | tail -10
git -C /home/mechelle/repos/lantern_app add apps/admin/src/__tests__/App.test.jsx
git -C /home/mechelle/repos/lantern_app commit -m "test(admin): App role fork + back-compat redirect tests
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Task 37: merchantApi.test.js โ role-aware URL selection โ
Files:
Create:
apps/admin/src/shared/lib/__tests__/merchantApi.test.js[ ] Step 37.1: Write the test
import { describe, it, expect, vi, beforeEach } from 'vitest'
const fetchMock = vi.fn()
global.fetch = fetchMock
vi.mock('../apiClient', () => ({
authRequest: (url, opts) => fetchMock(url, opts),
parseResponse: async (res) => res.json(),
}))
vi.mock('../../../firebase', () => {
const tokenResultByRole = (role) => ({ claims: { role } })
return {
auth: {
currentUser: { getIdTokenResult: vi.fn() },
},
__setRole(role) {
this.auth.currentUser.getIdTokenResult.mockResolvedValue(tokenResultByRole(role))
},
}
})
import * as fb from '../../../firebase'
import { getMerchantDetail } from '../merchantApi'
describe('merchantApi.getMerchantDetail', () => {
beforeEach(() => {
fetchMock.mockReset()
fetchMock.mockResolvedValue({ json: async () => ({}) })
})
it('admin role โ calls /auth/admin/merchants/:id', async () => {
fb.__setRole('admin')
await getMerchantDetail('m-7')
expect(fetchMock).toHaveBeenCalledWith(
expect.stringMatching(/\/auth\/admin\/merchants\/m-7$/),
expect.anything()
)
})
it('merchant role โ calls /auth/merchant/me (ignores merchantId arg)', async () => {
fb.__setRole('merchant')
await getMerchantDetail('m-anything')
expect(fetchMock).toHaveBeenCalledWith(
expect.stringMatching(/\/auth\/merchant\/me$/),
expect.anything()
)
})
})- [ ] Step 37.2: Run + commit
npm test -w lantern-admin -- --run merchantApi 2>&1 | tail -10
git -C /home/mechelle/repos/lantern_app add apps/admin/src/shared/lib/__tests__/merchantApi.test.js
git -C /home/mechelle/repos/lantern_app commit -m "test(admin): merchantApi role-aware URL selection
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Phase I โ Final validation + PR (Task 38) โ
Task 38: Final validation + manual smoke matrix + PR โ
- [ ] Step 38.1: Run full validation
npm run validate -- --workspace apps/admin 2>&1 | tail -20
cd /home/mechelle/repos/lantern_app/services/api/auth && npm run validate 2>&1 | tail -10Expected: build green, tests green, lint green (modulo the pre-existing audit failure unrelated to this PR).
- [ ] Step 38.2: Run the bundle isolation check
npm run build -w lantern-admin 2>&1 | tail -5
node /home/mechelle/repos/lantern_app/tooling/scripts/check-shell-isolation.mjsExpected: OK โ shells are isolated in the bundle.
- [ ] Step 38.3: Manual end-to-end smoke
Per the spec's smoke matrix. Run admin + auth-API dev servers (npm run dev -w services/api/auth + npm run dev -w lantern-admin) and walk:
| Action | Expected |
|---|---|
| Admin signs in | Lands on /admin/, sees admin sidebar |
| Admin clicks "Merchants" | Goes to /admin/merchants/all (admin list) |
| Admin clicks a merchant row | Opens /merchant/<id>/overview in a NEW BROWSER TAB |
| Admin uses the switcher in merchant mode | URL updates; new merchant's data loads |
| Admin clicks "โ Admin" in merchant mode | Returns to /admin/ |
| Real merchant signs in | Lands on /merchant/<theirId>/overview |
Real merchant types /admin/users in URL bar | Redirected to their own /merchant/<theirId>/overview |
Real merchant types /merchant/<other-id>/overview | Redirected to their own |
Old URL /users | Redirected to /admin/users (which then redirects merchants) |
Old URL /merchants/<id> | Redirected to /merchant/<id>/overview |
| Real merchant edits notes / offers / business name | Writes succeed via /auth/merchant/me/* (or merchants-API) |
| Real merchant clicks "Reset portal password" in profile | Triggers reset email; sets new password; signs in successfully |
- [ ] Step 38.4: Push branch + open PR
git -C /home/mechelle/repos/lantern_app push -u origin claude/merchant-integration
gh pr create --title "feat: merchant integration โ auth decoupling + admin/merchant shell split" \
--body "$(cat <<'EOF'
## Summary
Three integrations land together in this PR:
- **Merchant user create/attach/detach/delete UX** in admin โ replaces the misplaced "Create Merchant" tab with a "Create Merchant User" flow plus attach, detach, and delete actions in the user detail panel.
- **Merchant auth decoupling** โ separate `merchantPasswordHash` on `merchantProfiles`, parallel `/auth/merchant/*` endpoints, three-tier sign-in fall-through (admin โ merchant โ Firebase legacy). Resetting a merchant's portal password no longer destroys their Lantern encryption keys.
- **Admin / merchant shell split** โ `apps/admin/src/` restructured into `src/admin/`, `src/merchant/`, `src/shared/` with hard cross-import boundaries enforced by ESLint and a build-time bundle validator. Two top-level URL trees (`/admin/*`, `/merchant/:id/*`) with a role fork in `App.jsx`. Backend handlers extracted and dual-mounted (`/auth/admin/merchants/:id/*` + `/auth/merchant/me/*`).
## Specs
- `docs/superpowers/specs/2026-04-26-merchant-user-attach-flow-design.md`
- `docs/superpowers/specs/2026-04-27-merchant-auth-decoupling-design.md`
- `docs/superpowers/specs/2026-04-27-admin-merchant-shell-split-design.md`
## Test plan
- [ ] Fresh merchant onboarding (set passphrase + portal password, sign in, land on `/merchant/<id>/overview`)
- [ ] Promoted merchant onboarding (Lantern user attached to merchant, sets only portal password, Lantern encryption preserved)
- [ ] Three-tier sign-in matrix (admin / merchant / Lantern-only / wrong-password each surface correctly)
- [ ] Forgot-password isolation (merchant resets portal password; Lantern encryption canary still readable)
- [ ] Admin "Merchants" sidebar item โ list page โ row click opens merchant mode in a new tab
- [ ] Admin in merchant mode uses the switcher to navigate between merchants
- [ ] Real merchant attempting `/admin/users` is redirected to their own merchant
- [ ] Real merchant attempting `/merchant/<other-id>/...` is redirected to their own
- [ ] Old `/users`, `/docs`, `/merchants/<id>` URLs back-compat redirect
- [ ] `npm run validate` passes (lint zone rule fires on cross-zone import; build green; chunk-isolation check passes)
## Notes
- Some commits on the branch are from parallel work (`api-docs-renderer` package, signup form refinements, storybook stories) and are not part of this integration. They land along with this PR but are functionally orthogonal.
- One commit (`e469b93`) has a misleading message because a pre-commit hook bundled an unrelated styles.css change with the App.jsx three-tier sign-in changes. The code is correct; only the message is off.
- A follow-up agent should be scheduled (~2 weeks post-merge) to remove the back-compat URL redirects once the team has migrated bookmarks.
๐ค Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"Known Gap: Backend Integration Tests โ
The auth API still lacks route-level integration test scaffolding (Express + Firebase emulator + supertest). This PR ships unit tests around the extracted handlers (Phase F's merchantHandlers.test.js) but defers full integration tests to a follow-up issue ("Add route-level integration tests for auth-api admin + merchant endpoints"). Reference all three specs from this PR in that issue's description.
Out of Scope (per spec โ do NOT do in this plan) โ
apps/merchant/separate-app split (own Vite project / own Cloudflare Pages target)@lantern/uishared package extraction- Greenfield merchant dashboard / KPIs / activity feed
- Merchant self-onboarding (signup, billing)
- "View as merchant" admin impersonation
- Multi-role users (single uid that's both admin AND merchant)
- Lockout enforcement on
failedLoginAttempts - Multi-factor auth on the merchant portal
- Email change flow for merchants
- Merchant-side mutation endpoints beyond what surfaces in Phase G's audit
- Visual differentiation between admin and merchant shells
- Removing the back-compat redirects (separate follow-up agent)
If the implementer thinks any of these are needed for the PR to ship, stop and re-confirm with the user.