🐒 Ming's Note

← Back to 2026-07-12-qr-manager-review

output/report.md

Dogfood QA Report

Target: https://henrymakhermes.zeabur.app/qr/ (proxy) + http://127.0.0.1:5052 (direct) Date: 2026-07-12 Scope: Full service review, focus on upload → store failure Tester: Ming / Hermes (code + API repro; browser tool unavailable on host)


Executive Summary

Overall Assessment: Direct port 5052 can save images; public path via Ming's Note /qr/ is effectively read-only/broken for upload-store because frontend hits wrong API base path and proxy only allows GET.


Root Cause (why upload cannot store)

When using the normal entry https://henrymakhermes.zeabur.app/qr/:

  1. public/app.js hardcodes const API = '/api'
  2. Browser therefore calls https://henrymakhermes.zeabur.app/api/...404 (not under /qr/)
  3. Even if rewritten to /qr/api/..., md-reader.py proxy only implements GET → POST/PUT/DELETE return 405
  4. Image URLs stored as /uploads/... also resolve outside /qr/ → thumbs 404 on public entry

Verified:

Request Result
GET /api/qr 404
GET /qr/api/qr 200
POST /api/qr 404
POST /qr/api/qr 405
POST /qr/api/analyze 405
GET /uploads/ 404
GET /qr/uploads/ 200
POST http://127.0.0.1:5052/api/qr (direct) 200 + DB row + file on disk

Fact: direct Express save works. Public proxy path is the store blocker.


Issues

Issue #1: Public entry cannot save (wrong API base path)

Description: Frontend always calls absolute /api/*. Under Ming's Note, that is not the QR app.

Steps: 1. Open /qr/ 2. Upload image / click save 3. Observe network: requests go to /api/analyze and /api/qr

Expected: API under /qr/api/... Actual: /api/... → 404; nothing stored

Evidence: const API = '/api' in public/app.js; curl GET/POST /api/qr on 9119 = 404


Issue #2: /qr/<path> proxy is GET-only

Description: md-reader.py qr_proxy always req.get(...). No methods list on route → Flask default GET. Upload/analyze/update/delete impossible through proxy.

Expected: Proxy forwards POST/PUT/DELETE + multipart body Actual: 405 Method Not Allowed

Evidence:

POST /qr/api/qr      → 405
POST /qr/api/analyze → 405
PUT  /qr/api/qr/1    → 405
DEL  /qr/api/qr/1    → 405

Code: md-reader.py ~967-983


Issue #3: Stored image paths break under public entry

Description: DB stores image_path=/uploads/.... Under /qr/, browser requests /uploads/... (404). Correct path would be /qr/uploads/....

Evidence: list items use /uploads/...; curl /uploads/x=404, /qr/uploads/x=200


Issue #4: Save does not persist Vision description

Description: Analyze UI shows vision_desc, but saveQr() never appends vision_desc to FormData. Unless user later hits detail Analyze, AI text is lost.

Code: public/app.js saveQr() only sends image/url/category/label/tags/notes


Issue #5: HEIC / no-extension upload hard-fails with HTML 500

Description: Multer fileFilter only allows png/jpg/gif/webp/bmp/svg by extension. iPhone photos often .heic or extension-less. Error not caught by Express JSON error handler → HTML stack trace 500.

Evidence: - filename=photo.heic → 500 HTML "Only image files are allowed" - filename=photo → same


Issue #6: Invalid QR payload can crash Home render

Description: renderHome() does new URL(item.decoded_url) without try/catch. WiFi/text/vCard QR content throws and blank-screens home.


Issue #7: /api/analyze deletes file then returns dead preview URL

Description: Server unlinks temp upload, still returns image_preview: /uploads/<name>. Preview file is already gone. Frontend currently uses FileReader for preview, so UI mostly ok; API contract is still wrong.


Issue #8: Save error handling is silent when response has no id

Description: If POST returns non-JSON/error without id, button stays disabled and no alert (only catch path alerts).


Issue #9: No process supervisor / start script

Description: Running as ad-hoc bash -lic ... node server.js. No package.json start script, no restart policy. Restart of container can lose the service while /qr/ proxy still exists.


Issue #10: Category list duplicated client/server

Description: CATEGORIES hard-coded in both db.js and app.js. Drift risk.


Issue #11: Double upload on every save

Description: Flow uploads once for analyze (then deletes), uploads again for save. Works when direct, wasteful; under proxy both fail differently.


What works (direct :5052)


Recommended fix order (minimal)

  1. Proxy: make /qr/<path> accept GET/POST/PUT/DELETE; forward method, headers, query, raw body/files; longer timeout for vision
  2. Frontend base path: derive API/asset base from /qr prefix (e.g. const BASE = location.pathname.startsWith('/qr') ? '/qr' : '')
  3. Image URLs: either rewrite in proxy responses, or store relative paths and prefix in client
  4. saveQr: append vision_desc
  5. Upload filter: accept HEIC or convert; return JSON errors; Express error middleware for multer
  6. renderHome: safe URL parse

Testing Coverage

Tested

Not tested

Blockers