# Codebase Concerns

**Analysis Date:** 2026-05-07

## Tech Debt

### Hardcoded Test MAC Bypass (Security Risk)
- **Issue:** Test MAC `AA:BB:CC:DD:EE:FF` is hardcoded to skip "device already bound" validation
- **Files:** 
  - `device_interaction/serializers.py:125`
  - `device_interaction/views.py:702`
- **Code Pattern:**
  ```python
  if value != 'AA:BB:CC:DD:EE:FF' and UserDevice.objects.filter(device=device).exists():
      raise serializers.ValidationError("设备已被其他用户绑定")
  ```
- **Impact:** In production, any request with this MAC bypasses binding validation entirely. If MAC is leaked or discovered, attacker can bypass device permission controls.
- **Fix approach:** Remove hardcoded bypass before production scale-out. Replace with environment-based test mode flag (`ALLOW_TEST_MAC=false` in prod, check at startup). Document test procedures that don't require code branches.
- **Severity:** HIGH — Production-blocking if scale-out planned

### Device Binding Semantics ("Last-Bind-Wins") Implicit in Ordering
- **Issue:** CLAUDE.md (line 233) documents "后绑的挤掉先绑的" (last binder controls device) via `UserDevice.Meta.ordering = ['-bound_at']`. Control resolution in `views.py` uses `.first()` implicitly depending on this ordering, with no explicit comment at call sites.
- **Files:**
  - `device_interaction/models.py` — `UserDevice` model ordering
  - `device_interaction/views.py:702` (bind_status), `views.py:1120` (rtc-token/get_by_mac) — implicit `.first()` usage
  - `userapp/views.py` — MAC login endpoint (explicit `.order_by('-bound_at').first()`)
- **Impact:** New developers may not understand that multiple `UserDevice` rows for same device are intentional, and may try to auto-delete old records thinking they're bugs. Silent control hand-off between users without explicit notification.
- **Fix approach:** Add explicit docstring to `UserDevice.Meta` explaining ordering rationale. Add comments at control-resolution sites: "Implicit: `.first()` depends on Meta.ordering = ['-bound_at']". Implement user notification when control is lost (older user's device suddenly stops responding).
- **Severity:** MEDIUM — Operational surprise risk

### is_primary Ambiguity (Not "Primary Controller")
- **Issue:** CLAUDE.md (line 237) notes `is_primary` is "user view of primary device" (each user has ≤1), NOT "device view of primary user". Same physical device can have multiple `is_primary=True` rows.
- **Files:**
  - `device_interaction/models.py` — `UserDevice.is_primary` field
  - `device_interaction/views.py:746` — `set_primary` action
- **Impact:** Code querying `UserDevice.objects.filter(device=device, is_primary=True)` will return multiple rows; calling `.first()` silently picks arbitrary one. Data migrations may assume primary uniqueness per device and fail.
- **Fix approach:** Rename field to `is_user_primary` to clarify scope. Add unique constraint on `(user, is_primary=True)` at DB level (add migration). Audit all code touching is_primary to ensure correct semantics.
- **Severity:** MEDIUM — Data consistency risk in multi-user scenarios

### Affinity System Field Backward Compatibility
- **Issue:** `AffinityRule` and `AffinityLevel` have old fields (`points`, `daily_limit`, `is_active` for Rule; `required_points`, `rewards` for Level) marked "已弃用" (deprecated) in comments, pending deletion next version.
- **Files:**
  - `userapp/models.py` — field definitions with deprecation comments
  - `userapp/migrations/0005_affinitysetting_affinitylevel_is_deleted_and_more.py` — schema
  - `docs/修改记录.md:47` — explicit note: "保留作为兼容字段，注释标记 '已弃用'，下个版本删除"
- **Impact:** Two parallel field sets in model creates confusion. Serializers/views must avoid referencing old fields. Data migrations targeting old fields will silently succeed but risk ignored values if migration rolls back.
- **Fix approach:** Plan deprecation timeline: 1) mark version when old fields will be deleted (e.g., "v2.0"), 2) add migration warning log at deploy time listing affected rows still using old values, 3) drop fields in scheduled release with clear release notes.
- **Severity:** MEDIUM — Code maintainability; low runtime risk if new field set is complete

### ParadiseUser.favorability Not Yet Deleted After Migration
- **Issue:** Data migrated from `ParadiseUser.favorability` to `UserDevice.favorability` (migration 0006), but old field on ParadiseUser is NOT deleted. Docs (修改记录.md:116) explicitly state "旧 ParadiseUser.favorability 字段保留不删，由后续版本统一清理".
- **Files:**
  - `userapp/models.py` — `ParadiseUser.favorability` field still exists
  - `userapp/migrations/0006_migrate_favorability_to_userdevice.py:11` — migration note
- **Impact:** Source-of-truth split: old field may be modified by legacy code path or manual admin actions; new device-level field won't see updates. Risks silent inconsistency.
- **Fix approach:** Create follow-up migration to drop `ParadiseUser.favorability` once P2 service layer is validated. Until then, add application-level guard: `AffinityService` should only read/write `UserDevice.favorability`, and log warning if `ParadiseUser.favorability > 0` and doesn't match primary device value.
- **Severity:** LOW-MEDIUM — Handled by migration discipline; plan removal in next minor release

## Known Bugs

### Missing TODO in Achievement Conditions Check
- **Issue:** `achievement_app/views.py:139` has explicit TODO comment: "# TODO: 根据成就的conditions字段和前端传递的check_data检查是否满足条件" (achievement condition validation not implemented)
- **Files:** `achievement_app/views.py:139`
- **Impact:** Achievement unlock endpoint does not validate if conditions are met; always grants achievement if user requests it. Any client can claim arbitrary achievements.
- **Trigger:** Call POST `/api/achievement/check/` with any `check_data`
- **Workaround:** None; feature is stub-only
- **Fix approach:** Implement `AchievementService.check_conditions(achievement, check_data)` with per-condition validator registry. Add unit tests for each condition type.
- **Severity:** HIGH — Functional gap in released feature

### Missing Conversation Source Field in WebSocket Callback
- **Issue:** `device_interaction/views.py:1427` has TODO: "# TODO 决策点 #3 落定后填充 'phone' / 'device'" — callback routing doesn't tag which client sent conversation (phone app vs device endpoint).
- **Files:** `device_interaction/views.py:1427` (in `conversation_status` action)
- **Impact:** All WebSocket message routing broadcasts assume single source; can't distinguish if conversation came from phone client or device endpoint. Breaks analytics, replay, and client-side dedup logic that relies on source.
- **Trigger:** Ongoing RTC chat with device and phone both active
- **Workaround:** Clients must infer source from message timing/content patterns
- **Fix approach:** Add `'source': 'phone' | 'device'` field in WebSocket payload. Update all consumers and message routers. Coordinate with client teams to unify source interpretation.
- **Severity:** MEDIUM — Missing feature; blocks analytics correctness

### SMS Verification Code Rate Limiting Not Enforced
- **Issue:** `userapp/views.py:297-318` (SendVerifyCodeView) has no rate limit, throttle, or per-phone backoff on SMS sending.
- **Files:** `userapp/views.py:297-318`
- **Code:**
  ```python
  def post(self, request):
      phone_number = request.data.get('phone_number')
      # ... no rate limit check ...
      code = str(random.randint(100000, 999999))
      response = send_sms(phone_number, code)  # Directly calls Aliyun SMS
      cache.set(phone_number, code, timeout=600)  # Stores code in Redis
  ```
- **Impact:** SMS spam risk: attacker can hammer endpoint with same phone number to exhaust SMS quota or bankrupt via Aliyun charges (SMS is pay-per-send). No cooldown enforced.
- **Trigger:** `while True: POST /api/user/send_verify_code/ {"phone_number": "13800000000"}`
- **Workaround:** IP-level blocking or firewall; not app-level protection
- **Fix approach:** Implement rate limiting: 1) Add Redis key `sms_sent:{phone}:{YYYYMMDD}` with counter, max 5 per day per phone, 2) Add `sms_cooldown:{phone}` with 60s TTL to block rapid retries, 3) Use DRF throttle class or custom decorator for IP-level limits (5 SMS per minute per IP).
- **Severity:** HIGH — Production DoS vector in authentication endpoint

## Security Considerations

### DEBUG Mode Hardcoded to True in Production Settings
- **Issue:** `qy_lty/settings.py:31` has `DEBUG = True  # 开发环境下设置为True`
- **Files:** `qy_lty/settings.py:31`
- **Impact:** DEBUG=True in production exposes full stack traces, local variables, SQL queries, and installed packages to any error page. Leaks internal URLs, file paths, and API keys from traceback context.
- **Current mitigation:** Comment implies intent for dev-only, but no env var check
- **Recommendations:** Change to `DEBUG = config('DEBUG', default=False, cast=bool)` so prod .env can override. Add pre-deploy check in CI to fail if DEBUG=True in prod build.
- **Severity:** HIGH — Information disclosure

### CORS Allows All Origins in Production
- **Issue:** `qy_lty/settings.py:98` sets `CORS_ALLOW_ALL_ORIGINS = True  # 允许所有来源的跨域请求，适用于开发环境`
- **Files:** `qy_lty/settings.py:98`
- **Impact:** Any website can make authenticated requests to the API on behalf of users (e.g., attacker site makes POST requests using victim's stored token). Defeats CORS origin validation.
- **Current mitigation:** Comment indicates dev-only intent; commented-out whitelist provided
- **Recommendations:** Load whitelist from env var: `CORS_ALLOWED_ORIGINS = config('CORS_ALLOWED_ORIGINS', default='http://localhost:3000').split(',')`. Restrict to known Unity client domains + web admin domain + localhost in dev.
- **Severity:** HIGH — CSRF-adjacent vulnerability if session auth used

### Token-in-URL WebSocket Auth Logged in Access Logs
- **Issue:** CLAUDE.md (line 117) documents WebSocket URL-based auth: `ws://domain/ws/device/token/{token}/`
- **Files:** 
  - `device_interaction/consumers.py:54-71` — token extraction from URL
  - CLAUDE.md line 117 — design documentation
- **Impact:** Token appears in nginx/reverse-proxy access logs, browser history, referrer headers, CloudFront logs. Any log aggregation breach exposes all active tokens.
- **Current mitigation:** Header-based auth also supported (`ws://domain/ws/device/`)
- **Recommendations:** Deprecate URL-based token auth (mark as legacy). Document that tokens should only be passed in Headers (`Authorization: Bearer {token}`). For legacy WebSocket clients that can't set custom headers, recommend binding device MAC early and using MAC-login instead.
- **Severity:** HIGH — Token exposure in logs

### Credentials Handling in Audio Service Config
- **Issue:** Audio service provider credentials (Aliyun, Tencent, VolcEngine) are stored in .env and loaded via `config()` calls throughout codebase. If .env is accidentally committed or exposed, all external API keys are compromised.
- **Files:**
  - `qy_lty/settings.py` — loads `ALIYUN_*`, `VOLCENGINE_*` from env
  - `userapp/utils.py:8-30` — uses `ALIYUN_SMS_ACCESS_KEY_*` directly
  - Multiple other service files
- **Current mitigation:** .env is in .gitignore (assumed)
- **Recommendations:** 1) Add .env.example template with dummy values to repo, 2) Pre-deploy check to verify SECRET_KEY, DEBUG, and all *_KEY env vars are set (fail if default), 3) Rotate credentials if any .env branch is accidentally pushed, 4) Use cloud IAM (Aliyun RAM roles) for server-to-service auth instead of long-lived keys where possible.
- **Severity:** HIGH — If .env leaked, all integrations compromised

### CSRF Middleware Disabled
- **Issue:** `qy_lty/settings.py:88` has CSRF middleware commented out: `# 'django.middleware.csrf.CsrfViewMiddleware',`
- **Files:** `qy_lty/settings.py:88`
- **Impact:** POST/PUT/DELETE endpoints accept requests without CSRF tokens, enabling form-based CSRF attacks from third-party sites.
- **Current mitigation:** Token-based REST API auth is used instead of session cookies
- **Recommendations:** If using token auth (Redis tokens, not session cookies), CSRF can be relaxed, but should have: 1) explicit comment explaining trade-off, 2) CSRF exemption decorator `@csrf_exempt` only on REST endpoints, not HTML pages, 3) Consider re-enabling for browser-based admin endpoints (if any).
- **Severity:** MEDIUM — Mitigated by token auth, but could surprise new developers

### File Upload Without Type/Size Validation
- **Issue:** `common/views.py:66-114` (upload_file endpoint) accepts any file type and has no maximum size limit
- **Files:** `common/views.py:66-114`
- **Code:**
  ```python
  file_obj = request.FILES['file']
  file_size = file_obj.size  # Logged but not validated
  content_type = getattr(file_obj, 'content_type', 'unknown')  # Logged but not checked
  result = uploader.upload_file(file_obj, is_permanent, filename)  # No size/type check
  ```
- **Impact:** Users can upload arbitrary files (executables, archives, etc.) to OSS, consuming storage quota and potentially enabling supply-chain attacks (if files are later distributed). No DoS protection against huge uploads.
- **Trigger:** `POST /api/upload/` with `multipart/form-data` containing 5GB file
- **Workaround:** None at app level; depends on OSS bucket policy
- **Recommendations:** Add validation: 1) Max file size (default 100MB, configurable), 2) Whitelist MIME types (audio, image, document only), 3) File extension check, 4) Virus scan if possible (ClamAV integration), 5) Add Content-Length header check before parsing.
- **Severity:** MEDIUM-HIGH — Storage exhaustion + potential malware vector

## Performance Bottlenecks

### N+1 Query Risk in Device/Card List Endpoints
- **Issue:** `device_interaction/views.py:77`, `device_interaction/views.py:193`, `card/views.py:262` define viewsets with plain `queryset = Model.objects.all()` without `select_related()` or `prefetch_related()`.
- **Files:**
  - `device_interaction/views.py:77` — `DeviceTypeViewSet`
  - `device_interaction/views.py:135` — `DeviceBatchViewSet`
  - `device_interaction/views.py:193` — `DeviceViewSet`
  - `card/views.py:262` — `CardViewSet`
- **Patterns:** Serializers reference related fields (`device_type_name = ReadOnlyField(source='device_type.name')`), so each list item triggers separate query for related object.
- **Impact:** Listing 100 devices results in 101 queries (1 for list + 100 for device_type lookup). Scales linearly with result count. Noticeable on slow networks.
- **Trigger:** `GET /api/device/` with large device list
- **Fix approach:** Override `get_queryset()` in each ViewSet to add prefetch_related. Example:
  ```python
  def get_queryset(self):
      qs = super().get_queryset()
      if self.action == 'list':
          qs = qs.select_related('device_type', 'batch')
      return qs
  ```
  Add Django Debug Toolbar in dev to verify queries.
- **Severity:** MEDIUM — Affects list performance as data grows

### WebSocket Consumer Synchronous DB Calls
- **Issue:** `device_interaction/consumers.py:106-131` uses `@database_sync_to_async` decorator on `update_device_status()` and `mark_device_offline()`, which is correct, but the decorator wraps individual methods, not the whole consumer. If `receive()` calls multiple sync methods in sequence, they're not batched.
- **Files:** `device_interaction/consumers.py:106-144`
- **Pattern:**
  ```python
  @database_sync_to_async
  def update_device_status(self, mac_address, device_data):
      device = Device.objects.filter(mac_address=mac_address).first()
      device.status = 'connected'
      device.save()
      cache.set(...)
  ```
- **Impact:** Each WebSocket message triggers separate DB transaction. If device sends 100 messages/sec, that's 100 DB round-trips/sec per device × device count = potential connection pool exhaustion.
- **Trigger:** Device rapidly sending device_info messages
- **Fix approach:** Batch writes using bulk_update() for multiple devices. Use Redis for frequent updates (heartbeat), sync to DB on schedule (5-min batch job). Consider using `select_for_update()` to prevent race conditions during concurrent updates.
- **Severity:** MEDIUM — Affects high-frequency scenarios (concurrent devices)

### Redis Heartbeat TTL Race Condition
- **Issue:** CLAUDE.md (line 230) documents: "设备心跳：每次收消息时刷新 `device:last_seen:{mac}`（TTL 5 分钟），断连时把 `Device.status` 标记为 `disconnected`"
- **Files:** 
  - `device_interaction/consumers.py:147-151` — `refresh_device_heartbeat()` refreshes Redis
  - `device_interaction/consumers.py:153-169` — `disconnect()` marks DB offline
- **Pattern:** Heartbeat is Redis-only (fast); offline mark is DB-only. Gap between Redis expiry and disconnect handler creates brief window where Redis says offline but DB says online.
- **Impact:** Race condition: if device loses connection and immediately reconnects within 5 minutes, Redis has fresh key but DB still marked disconnected. List endpoints show device as offline even though it's online.
- **Trigger:** Device disconnects and reconnects within 5 minutes
- **Fix approach:** On reconnect (in `connect()` method), check Redis and sync DB status: if Redis key exists, update Device.status to 'connected'. Add sync_task that periodically reconciles Redis heartbeat state with DB.
- **Severity:** LOW-MEDIUM — Offline view is slightly stale, but eventually consistent

### Channel Layer Single Redis Instance
- **Issue:** CLAUDE.md (line 116) mentions "基于 Redis 的通道层用于实时消息传递" but Docker compose and settings don't explicitly configure channel layer sharding/replication.
- **Files:**
  - `docker-compose.yml` — no Redis cluster config
  - `qy_lty/settings.py` — CHANNEL_LAYERS likely not set (uses default)
- **Impact:** Single Redis instance is single point of failure for WebSocket message routing. If Redis crashes, all device-to-phone communication drops until restart.
- **Current mitigation:** Docker `restart: always` policy restarts container on Redis crash
- **Recommendations:** For production: 1) Add Redis Sentinel or Redis Cluster for failover, 2) Explicitly configure CHANNEL_LAYERS in settings, 3) Add monitoring/alerting on Redis health, 4) Document failover procedure.
- **Severity:** MEDIUM — High availability concern for production

## Test Coverage Gaps

### No Unit or Integration Tests Found
- **Issue:** Codebase has 168 Python files but zero test files (`find . -name "test_*.py" -o -name "*.test.py"` returns nothing)
- **Files:** Entire codebase
- **Risk Areas (no test coverage):**
  - `device_interaction/consumers.py` (WebSocket consumer logic, token auth, message routing)
  - `device_interaction/views.py` (device binding, RTC token generation, message sending)
  - `aiapp/views.py` (chat API, audio service integration)
  - `card/views.py` (batch generation, QR code handling)
  - `userapp/views.py` (login endpoints, device MAC binding)
  - `userapp/utils.py` (token generation, SMS sending)
- **Impact:** No regression detection for bug fixes. Refactoring is high-risk. New developers can't verify behavior. Critical paths (auth, device control, chat) have zero test coverage.
- **Priority:** HIGH — This is the single largest gap. Recommend: 1) Set up pytest + pytest-django, 2) Implement tests for all auth endpoints (10-15 tests), 3) Test device binding logic (5-10 tests), 4) WebSocket consumer tests (unit, 10-15 tests), 5) Gradually expand to 70%+ coverage.
- **Severity:** HIGH — Operational risk due to zero safety net

## Fragile Areas

### Device Interaction Views (1,867 lines)
- **Files:** `device_interaction/views.py`
- **Why fragile:** Single mega-file contains 30+ action methods for device management, message routing, RTC token generation, WebSocket callbacks, admin endpoints. Changes to one subsystem affect entire file's import dependencies.
- **Safe modification:** Use code folding tools (IDE regions). Create feature branches. Add tests for each action before refactoring. Extract cohesive groups into separate views (e.g., `DeviceRTCViewSet`, `MessageDispatchViewSet`, `VolcEngineCallbackViewSet`).
- **Test coverage:** Zero. Recommend starting with 10 happy-path + 5 error case tests.

### Card System Batch Generation (`card/views.py:262-364`)
- **Files:** `card/views.py` batch_create action (~100 lines)
- **Why fragile:** Batch generation calls external services (QR generation), creates multiple DB records in transaction, generates Excel exports. If any step fails mid-batch, partial state is left.
- **Safe modification:** Use `@transaction.atomic` (already present) but verify rollback covers all side effects. Add dry-run mode. Test with batch size = 1000+.
- **Test coverage:** Zero.

### WebSocket Token Authentication (`device_interaction/consumers.py:78-104`)
- **Files:** `device_interaction/consumers.py`
- **Why fragile:** Token extraction from URL (`get_token_from_url()`) has two fallback paths (url_route kwargs, manual path parsing). Order matters; if first path silently fails, second might not fire.
- **Safe modification:** Add explicit logging at each path. Unit test both extraction methods independently.
- **Test coverage:** Zero. Recommend: 2-3 token auth scenario tests.

## Migration & Data Debt

### Favorability Data Migration Dependency Chain
- **Issue:** Affinity system (P1 phase, commit 2d82b2e) depends on three sequential migrations:
  1. `device_interaction/migrations/0003_userdevice_affinity_*.py` — schema changes
  2. `userapp/migrations/0005_affinitysetting_*.py` — schema + new tables
  3. `userapp/migrations/0006_migrate_favorability_to_userdevice.py` — data migration (RunPython)
- **Files:** All three migration files
- **Impact:** If migration #3 runs before #2, FK constraint fails. If rolled back out-of-order, data loss. Migration file 0006 is irreversible if both forward and backward have been applied (ParadiseUser.favorability loses history).
- **Fix approach:** Document exact migration sequence in CLAUDE.md deploy section. Add migration dependency comments. Test migration path on staging before prod deploy.
- **Severity:** MEDIUM — One-time risk during affinity system rollout

### Schema Churn Around UserDevice
- **Issue:** Recent commits added 4 new columns to UserDevice (favorability, affinity_level, last_active_at, is_active). This is 5th major change to UserDevice in recent history (based on migrations count).
- **Files:** `device_interaction/models.py`, `device_interaction/migrations/0003_*`
- **Impact:** Existing deployments will have migration backlog. Large tables see downtime during migration if index creation locks table.
- **Recommendations:** For future schema changes: 1) Use zero-downtime migration strategy (add column with default, backfill async, add constraint later), 2) Test migration time on prod-sized DB before deploy, 3) Plan maintenance window if >10GB table affected.
- **Severity:** LOW — Manageable if migrations are small; communicate with ops team

## Internationalization Concerns

### i18n Setup Incomplete (en / zh_HAns Only)
- **Issue:** CLAUDE.md (line 85) documents: `django-admin makemessages -l en` and `-l zh_HAns`, but only two language files present. No translation workflow defined (who translates, quality assurance, etc.).
- **Files:** Locale directories (if present)
- **Impact:** English and Simplified Chinese only. Any text from Chinese source code will show untranslated when language=en. Admin UI (SimpleUI) may have partial translations.
- **Recommendations:** 1) Run makemessages and compilemessages at deploy time (add to Docker CMD), 2) Define translation workflow (assign translator, review process), 3) Consider using gettext_lazy for all user-facing strings, 4) Add i18n test to CI (verify no missing keys).
- **Severity:** LOW — UI concern; functional impact minimal if default language is Chinese

## Known TODOs in Codebase

1. **`achievement_app/views.py:139`** — "TODO: 根据成就的conditions字段和前端传递的check_data检查是否满足条件" (achievement condition validation missing)
2. **`device_interaction/views.py:1427`** — "TODO 决策点 #3 落定后填充 'phone' / 'device'" (conversation source field in WebSocket callback)
3. **`subscription_app/apps.py:17`** — "TODO:: 测试环境不启动调度器" (scheduler disabled in test env, needs decision)

## Deployment Concerns

### Docker Image Uses Python 3.8 (EOL)
- **Issue:** `Dockerfile:2` uses `python:3.8` which reached EOL in October 2024. No security patches available.
- **Files:** `Dockerfile:2`
- **Impact:** CVE fixes in Python runtime are no longer released. New dependencies may drop Python 3.8 support.
- **Recommendations:** Upgrade to Python 3.11 or 3.12. Update requirements.txt with compatible versions (test in staging first).
- **Severity:** MEDIUM — Security risk for production

### Single Daphne Process in Docker (No Workers)
- **Issue:** `Dockerfile:25` and `run.sh:2` start single `daphne` instance with no worker scaling. No load balancing, no graceful shutdown.
- **Files:** 
  - `Dockerfile:20-26`
  - `run.sh:2`
- **Impact:** Single process is bottleneck. Restart drops all active WebSocket connections. No way to scale without manual container replication.
- **Recommendations:** 1) Use Docker Compose or Kubernetes to run multiple daphne replicas, 2) Add reverse proxy (nginx) to load balance, 3) Implement graceful shutdown handler (drain connections before exit), 4) Add health check endpoint (Django health_check app).
- **Severity:** MEDIUM — High availability concern

### No Database Healthcheck
- **Issue:** `docker-compose.yml` has no healthcheck for postgres or redis. If DB is down, container still shows "running".
- **Files:** `docker-compose.yml:1-33`
- **Impact:** Deployment tools can't detect DB failures. Restart policies won't trigger on DB outage.
- **Recommendations:** Add healthcheck to docker-compose:
  ```yaml
  healthcheck:
    test: ["CMD", "python", "-c", "import django; django.setup(); from django.db import connection; connection.ensure_connection()"]
    interval: 30s
    timeout: 5s
    retries: 3
  ```
- **Severity:** LOW — Operational tool concern

### Missing Database Migrations in Dockerfile Startup
- **Issue:** `Dockerfile:25` startup script (start.sh) runs daphne directly without calling `python manage.py migrate`. If migrations are pending, runtime errors occur.
- **Files:** `Dockerfile:20-26`
- **Impact:** Fresh deploy or rolling update fails if new migrations exist. Manual SSH to container required to fix.
- **Recommendations:** Modify start.sh:
  ```bash
  python manage.py migrate
  daphne -b 0.0.0.0 -p 8000 qy_lty.asgi:application
  ```
- **Severity:** MEDIUM — Deployment failure risk on schema changes

---

*Concerns audit: 2026-05-07*
