lmaa.space WebApp Project Analysis
A review of the repository for architecture, code quality, redundancy, dead code, dependency hygiene, naming, and documentation gaps. The layering is sound and the backend is well tested. Twelve findings came out of it, of which the sharpest are a like endpoint that requires a fingerprint and then ignores it, five copies of the social media platform list with the promised sync test missing, and four workspace packages that import things they do not declare. A second pass re-checked every concrete reference against the code and confirmed all twelve, with one line number off by two.
Contents
1. Scope and what was run
The analysis used the current code as the primary source. No subagents were used, and no active plan files were present under .codex/plans/open or .claude/plans/open.
| Command | Result |
|---|---|
npm run ci:quality | passed. It covers ESLint, the shared package build, the workspace typechecks, the full workspace build, and the smoke checks. |
npm test -w @lmaa/backend | passed. 42 test files, 442 tests. |
npx --yes knip --reporter compact | exited with findings, and also reported a config-loading error because apps/dashboard/vite.config.ts requires BACKEND_URL when loaded as a serve config. Treat the Knip output as a candidate list rather than as an automatic deletion list. |
2. Architecture summary
The project is a private npm workspace monorepo.
| Workspace | What it is |
|---|---|
apps/backend | Hono API on Node 22, PostgreSQL through Drizzle, migrations in apps/backend/drizzle, bundled with tsup. |
apps/frontend | Astro SSR website with React islands, UnoCSS, and a server-side API client. |
apps/dashboard | React and Vite admin dashboard with TanStack Query, React Router, Recharts, UnoCSS, and shared UI. |
packages/shared | Cross-app domain types, constants, token helpers, API error helpers, and shared CSS tokens. |
packages/contracts | Zod schemas and DTO contracts shared by backend, frontend, and dashboard. |
packages/ui | Shared React components used by frontend islands and by the dashboard. |
The high-level layering is sound. Routes call services, services call repositories, schemas live in contracts, and common DTOs and constants live in the shared packages. The backend has meaningful test coverage and the CI workflow runs quality gates before deployment.
3. What holds up well
The backend boot path is compact and explicit, with global middleware, public routes, admin routes, Swagger and OpenAPI, the health check, background jobs, and graceful shutdown all visible in apps/backend/src/index.ts:23. Admin route grouping applies authentication centrally after the public auth routes in apps/backend/src/routes/admin/index.ts:36, and runtime environment validation is explicit and fails fast through Zod in apps/backend/src/config/env.ts:12.
The database schema definitions include indexes, foreign keys, and check constraints in apps/backend/src/db/schema.ts at lines 42, 583, and 643. The public and dashboard builds are green, and the backend tests are broad enough to cover many route, service, and repository contracts.
4. The twelve findings
4.1 The like endpoint validates fingerprint and never uses it
Severity medium. The frontend generates and sends a browser fingerprint in apps/frontend/src/scripts/actions/like.ts:148, but the backend only destructures liked and token in apps/backend/src/routes/public.ts:288. The schema requires fingerprint at line 272, yet toggleShopLike only validates a stateless shop and timestamp HMAC in apps/backend/src/services/public.ts:141.
The counters are direct increment and decrement operations in apps/backend/src/repositories/public.ts at lines 614 and 629. A valid token can be replayed within its lifetime, bounded mainly by rate limiting. That makes like counts easy to inflate or deflate and means the submitted fingerprint provides no actual protection.
Either remove fingerprint from the API if the feature is intentionally best-effort, or make it part of the server-side contract. The better shape is a persistent shop_likes table keyed by shop_id plus a hashed IP or fingerprint, with idempotent increments and decrements derived from insert and delete semantics. If it stays stateless, bind the HMAC payload to a server-derived visitor key and reject repeated transitions.
4.2 Workspace package manifests do not declare several direct imports
Severity medium. There are direct imports that the importing workspace package does not declare.
| Import | Where | Missing from |
|---|---|---|
date-fns/locale | apps/dashboard/src/components/ui/DateTimePicker.tsx:2 | apps/dashboard/package.json |
@lmaa/contracts | many dashboard files, for example apps/dashboard/src/features/templates/hooks/useSocialMediaPostTemplates.ts:3 | apps/dashboard/package.json, which lists @lmaa/shared and @lmaa/ui only |
zod | packages/shared/src/utils/social-media.ts:1 | packages/shared/package.json, which declares only sqids |
| CodeMirror subpackages | packages/ui/src/JsonEditor.tsx:2 and packages/ui/src/MarkdownEditor.tsx:2 | packages/ui/package.json |
This currently works because the dependencies are hoisted or transitive, but each package is not independently correct. Add every directly imported runtime package to the local manifest, keep type-only packages in devDependencies, and add a dependency hygiene check to CI once Knip is configured for this monorepo.
4.3 The frontend bypasses package boundaries by importing shared UI source over relative paths
Severity medium. apps/frontend/src/components/islands/DynamicForm.tsx reaches directly into packages/ui/src at lines 18, 22, and 28. That bypasses the @lmaa/ui package exports declared in packages/ui/package.json:6, couples the app to the workspace layout, and makes moving or publishing the package harder.
Add narrow subpath exports for the lazily loaded components, such as @lmaa/ui/markdown-editor, @lmaa/ui/region-select, and @lmaa/ui/char-counter, then update the frontend to import those instead of ../../../../../packages/....
4.4 Social media platform constants are duplicated and the documented sync test is missing
Severity medium. The project has five platform lists: packages/shared/src/utils/social-media.ts:4, packages/contracts/src/social-media-accounts.ts:8, packages/contracts/src/admin-social-media-templates.ts:5, packages/ui/src/social-media-platforms.ts:35, and hard-coded database constraints in apps/backend/src/db/schema.ts:613.
packages/contracts/src/social-media-accounts.ts:3 says a runtime sync test exists at apps/backend/src/__tests__/social-media-platforms-sync.test.ts, and a repository search found no such file. That creates drift risk whenever a platform is added or renamed. Pick one source of truth for the platform keys in packages/shared or packages/contracts, derive the UI metadata and the posting subsets from it, and either add the missing test or remove the stale comment.
4.5 DynamicForm is a large mixed-responsibility component and duplicates shared UI behaviour
Severity medium. apps/frontend/src/components/islands/DynamicForm.tsx is 1.589 lines and owns validation, payload building, asynchronous submission, multi-select dropdowns, field rendering, markdown blocks, button actions, success presentation, and duplicate local styling. It defines its own MultiSelectDropdown at line 541 whilst packages/ui/src/MultiSelect.tsx:70 already provides a shared implementation, and it duplicates region options locally at line 52 whilst packages/ui/src/RegionSelect.tsx:47 exposes the canonical defaults.
Split the file into field renderer components, submission and payload helpers, multi-select adapters, button action helpers, and success and error presentation. Reuse the shared MultiSelect, RegionSelect, and the exported region option helpers, and add focused tests for payload construction and required multi-select validation.
4.6 Unused and dead-code candidates should be reviewed
Severity low to medium. Knip reported several unused files and exports. Some are false positives, because Astro script imports are hard for generic tooling to follow: apps/frontend/src/scripts/actions/index.ts:1 is reached from apps/frontend/src/layouts/BaseLayout.astro:127 through apps/frontend/src/scripts/shop-actions.ts:5.
Four candidates look credible from a repository-wide search: apps/dashboard/src/components/layout/SidebarItem.tsx:26, apps/dashboard/src/components/ui/Toolbar.tsx:11, apps/frontend/src/components/ShopCard.astro:1, and apps/dashboard/src/features/content/shops/ShopDeletionInfoOverlay.tsx:64. The last one also keeps apps/dashboard/src/lib/render-markdown-to-react.tsx:138 and marked alive. Section 8 audits all four.
4.7 Generated Astro metadata is tracked and currently dirty
Severity low. git status --short shows modified generated files under apps/frontend/.astro, and git ls-files confirms these files are tracked. The .gitignore starts its list with generated artefacts such as node_modules/ and dist/, and no .astro/ pattern is present. Decide whether Astro generated metadata belongs in Git, which in most Astro apps it does not, and if not, add **/.astro/ and remove the tracked files in a deliberate cleanup commit.
4.8 The documentation standard is not enforced
Severity low. docs/CODE-DOCUMENTATION.md:10 requires TSDoc for every export across the apps and packages, and many exports have none, among them packages/contracts/src/admin-social-media-templates.ts at lines 5, 8, and 11, apps/backend/src/services/public.ts at lines 429 and 463, apps/frontend/src/lib/liked-shops.ts:13, and apps/dashboard/src/features/templates/hooks/useSocialMediaPostTemplates.ts:13.
Either enforce the standard with a lint or documentation check, or reduce the standard to public package exports only. Update the generated reference documentation with npm run docs:code after filling the gaps.
4.9 Frontend and dashboard have no test suites
Severity low to medium. The workflow runs backend tests in .github/workflows/deploy.yml:35, and neither apps/frontend/package.json nor apps/dashboard/package.json defines a test script. A repository search found no frontend or dashboard *.test.* or *.spec.* files. That leaves large UI-heavy areas such as DynamicForm, the form builder, the footer builder, and the dashboard editor flows covered only by typecheck and build.
Add focused component or interaction tests for the high-risk UI state machines, starting with payload generation, form validation edge cases, template assignment, and the critical dashboard mutations. A small Playwright smoke suite over the public shop detail, the suggestion submission, and the dashboard login path would cover the rest.
4.10 Developer and deployment documentation has stale or missing details
Severity low. The root README gives a useful product and architecture overview, and there is no backend README to match the frontend and dashboard ones. zerops.yml:29 lists SESSION_SECRET as a secret whilst the backend environment schema in apps/backend/src/config/env.ts:13 does not define it.
Add apps/backend/README.md covering environment variables, migration commands, local development, test commands, and operational jobs. Align the zerops.yml comments with the actual schema, and document the package boundary rules for @lmaa/shared, @lmaa/contracts, and @lmaa/ui.
4.11 One- and two-character local names reduce readability in non-trivial code
Severity low. Some short names are conventional event parameters, and several appear in logic where a clearer name would help: qc for the query client in apps/dashboard/src/features/templates/hooks/useSocialMediaPostTemplates.ts:34, s for sidebar messages in apps/dashboard/src/components/layout/Sidebar.tsx at lines 179 and 510, m and s for minutes and seconds in apps/dashboard/src/features/analytics/analytics-utils.ts at lines 249 and 250, and s in the form reducer at apps/frontend/src/components/islands/DynamicForm.tsx:919.
Rename these to domain terms such as queryClient, sidebarMessages, minutes, remainingSeconds, and state.
4.12 The in-memory cache is known multi-instance debt
Severity low. apps/backend/src/middleware/cache.ts:15 carries a TODO to replace the in-memory cache with Redis for multi-instance deployments, and the app logs a production warning further down the same file. The TTL is short, so this is not currently a severe consistency issue, and admin mutations only invalidate the cache of the process they ran in.
Keep the current implementation whilst a short stale window is acceptable. If several backend instances become normal, the existing database-backed rate-limit pattern is the precedent for a shared cache or invalidation store.
5. Dependency and tooling notes
Knip reported further candidates that need verification. apps/frontend/package.json includes @hookform/resolvers and qr-border-plugin with no source imports found. packages/ui/package.json includes @radix-ui/react-popover and cmdk with the same result. And socialMediaPostTemplateCreateSchema and socialMediaPostTemplateUpdateSchema both point at the same schema in packages/contracts/src/admin-social-media-templates.ts at lines 31 and 64.
Because Knip could not fully load the dashboard config without BACKEND_URL, these should be checked after adding a project-specific Knip config or setting safe environment defaults for analysis runs.
6. Suggested priority order
- Fix manifest hygiene for direct dependencies, because it is low-risk and prevents workspace drift.
- Decide and fix the like endpoint contract: remove the unused fingerprint or make it meaningful and idempotent.
- Replace the direct
packages/ui/srcimports with package subpath exports. - Consolidate the social media platform constants and add the missing sync test.
- Delete the verified dead files and unused dependencies.
- Split
DynamicForm, once tests cover payload and validation behaviour. - Add frontend and dashboard tests for the critical flows.
- Align the documentation with the actual runtime and deployment contract.
7. Independent verification
A second pass re-checked every concrete claim against the current code. Each file:line reference, every claimed import, every claimed missing or duplicated symbol, and each dead-code candidate was re-checked by reading the relevant file and grepping the workspace. Cited files were read rather than skimmed through symbol overviews, and for the dead-code candidates the search went beyond static imports to dynamic import() and lazy(), path-based string references, index.* re-exports, Storybook stories, Astro page imports, and configuration and Markdown files.
One thing the second pass did not do
npm run ci:quality and npm test -w @lmaa/backend were not re-run. The ci:quality script does exist in package.json and runs lint, the shared build, typecheck, the full build, and ci:smoke, but the "passed" result above is taken from the report author rather than independently re-executed.
| # | Finding | Verification |
|---|---|---|
| 1 | Like endpoint validates fingerprint and never uses it | confirmed The frontend sends {liked, token, fingerprint} around like.ts:150-154; the schema requires it at public.ts:272-276; the handler destructures {liked, token} only at line 288; toggleShopLike validates the HMAC only at services/public.ts:173-189; the raw counter SQL sits at repositories/public.ts:614, 629. |
| 2 | Workspace manifests miss several direct imports | confirmed in all four sub-claims. date-fns and @lmaa/contracts are not declared in the dashboard manifest, zod is not declared in the shared manifest, and packages/ui declares @codemirror/lang-json and @codemirror/lang-markdown but not @codemirror/language, @codemirror/state, or @codemirror/view. |
| 3 | Frontend bypasses @lmaa/ui over relative paths | confirmed Lines 18, 22, and 28 use ../../../../../packages/ui/src/.... The exports map declares only . and ./shop-location-map, so the narrower components cannot be imported by package name. |
| 4 | Platform constants duplicated five times, sync test missing | confirmed All five locations exist. The promised test does not; only admin-social-media-post-templates-routes.test.ts is in that directory. The stale comment is at social-media-accounts.ts:3. |
| 5 | DynamicForm at 1.589 lines, duplicating MultiSelect and regions | confirmed wc -l returns 1589. The local MultiSelectDropdown is at line 541 and the local REGION_OPTIONS at line 52; the canonical createDefaultRegionOptions() is at RegionSelect.tsx:50 and the shared MultiSelect at MultiSelect.tsx:70. |
| 6 | Unused and dead-code candidates | confirmed All four candidates are truly dead and safe to delete. Section 8 has the audit. |
| 7 | Generated Astro metadata tracked and dirty | confirmed git ls-files returns six files under apps/frontend/.astro/. The 31-line .gitignore contains no .astro/ pattern, and git status --short shows data-store.json and settings.json modified. |
| 8 | Documentation standard not enforced | confirmed with one nuance. All sampled exports lack per-export TSDoc. apps/frontend/src/lib/liked-shops.ts has a file-level docstring at lines 1 to 5, whilst the getLikedShopIds export at line 13 still has none, so whether it satisfies the rule depends on how strictly the rule is read. |
| 9 | Frontend and dashboard have no test suites | confirmed Neither manifest defines a test script, neither app contains a test file, and the workflow runs only npm test -w @lmaa/backend. |
| 10 | Backend README missing, SESSION_SECRET mismatch | confirmed apps/backend/README.md does not exist, zerops.yml:29 lists the secret, and the Zod schema does not define it. |
| 11 | Single-letter local names in non-trivial code | confirmed at all five locations. |
| 12 | In-memory cache is multi-instance debt | confirmed The Redis TODO is at cache.ts:15. The production warning is at line 75 rather than the 73 the report gives, so the line number drifted by two whilst the content holds. |
Tooling claims
socialMediaPostTemplateUpdateSchema and the create schema are literally identical, since line 64 contains the assignment of one to the other. @hookform/resolvers and qr-border-plugin are declared in apps/frontend/package.json and imported nowhere under apps/frontend/src/**. @radix-ui/react-popover and cmdk are declared in packages/ui/package.json and imported nowhere under packages/ui/src/**. All three confirmed.
8. Deep verification of the dead-code finding
The original report classified finding 4.6 as low to medium and recommended a follow-up audit. That audit was performed. Each candidate was checked against static import statements, dynamic import() and lazy(), path-based string references in .ts, .tsx, .astro, .md, .mdx, .json, .yml and .yaml, index.* barrel re-exports in the containing directory, Storybook stories, and Astro page imports.
| Candidate | Static | Dynamic | Barrel | Storybook | Astro pages | Config and docs | Result |
|---|---|---|---|---|---|---|---|
apps/dashboard/src/components/layout/SidebarItem.tsx | own definition only | none | no layout/index.* | none in repo | n/a | none | dead |
apps/dashboard/src/components/ui/Toolbar.tsx | own definition only | none | no ui/index.* | none | n/a | none | dead |
apps/frontend/src/components/ShopCard.astro | none | none | no components/index.* | none | no page imports it | none | dead |
apps/dashboard/src/features/content/shops/ShopDeletionInfoOverlay.tsx | own definition only | none | no shops/index.* | none | n/a | none | dead |
Disambiguation against similarly named files
Toolbar.tsx is unrelated to apps/dashboard/src/features/overview/SubmissionToolbar.tsx and to apps/dashboard/src/components/ui/EditorToolbarButton.tsx. Both of those are separate files with different exports and remain in active use. ShopCard.astro is unrelated to apps/frontend/src/components/ShopCardReact.tsx, which is the card actually in use and is imported by three islands: LikedShopsGrid.tsx:6, FilterableSearchResults.tsx:6, and FilterableCategoryShops.tsx:6.
Transitive deletions
Deleting ShopDeletionInfoOverlay.tsx makes apps/dashboard/src/lib/render-markdown-to-react.tsx dead, because its only external consumer is the overlay at line 14. Deleting both makes marked unused in the dashboard, since across that tree it is imported only at ShopDeletionInfoOverlay.tsx:2 and render-markdown-to-react.tsx:1, so it can be dropped from apps/dashboard/package.json once those two files are gone.
The other three drag nothing with them. SidebarItem.tsx uses only react-router, which is used broadly elsewhere. Toolbar.tsx uses only the ReactNode type. ShopCard.astro imports ShopAvatar.astro and shopDomain from @/lib/shop, both of which have other consumers.
Verdict
The original report holds. Every concrete code reference is correct, with one line number in finding 4.12 off by two. The priority order is reasonable, and the dead-code deletions are now verified deeply enough to act on without a further audit.