## Summary
Adds configurable Docker CPU and memory resource constraints per app, closes sneak/upaas#72.
## Changes
### Database
- Migration `007_add_resource_limits.sql`: adds `cpu_limit` (REAL, nullable) and `memory_limit` (INTEGER in bytes, nullable) columns to the `apps` table
### Model (`internal/models/app.go`)
- Added `CPULimit` (`sql.NullFloat64`) and `MemoryLimit` (`sql.NullInt64`) fields to `App` struct
- Updated insert, update, scan, and column list to include the new fields
### Docker Client (`internal/docker/client.go`)
- Added `CPULimit` (float64, CPU cores) and `MemoryLimit` (int64, bytes) to `CreateContainerOptions`
- Added `cpuLimitToNanoCPUs()` conversion helper and `buildResources()` to construct `container.Resources`
- Extracted `buildEnvSlice()` and `buildMounts()` helpers from `CreateContainer` for cleaner code
- Resource limits are passed to Docker's `HostConfig.Resources` (NanoCPUs / Memory)
### Deploy Service (`internal/service/deploy/deploy.go`)
- `buildContainerOptions` reads `CPULimit` and `MemoryLimit` from the app and passes them to `CreateContainerOptions`
### Handlers (`internal/handlers/app.go`)
- `HandleAppUpdate` reads and validates `cpu_limit` and `memory_limit` form fields
- Added `parseOptionalFloat64()` for CPU limit parsing (positive float or empty)
- Added `parseOptionalMemoryBytes()` for memory parsing with unit suffixes (k/m/g) or plain bytes
- Added `optionalNullString()` and `applyResourceLimits()` helpers to keep cyclomatic complexity in check
### Templates
- `app_edit.html`: Added "Resource Limits" section with CPU limit (cores) and memory limit (with unit suffix) fields
- `templates.go`: Added `formatMemoryBytes` template function for display (converts bytes → human-readable like `256m`, `1g`)
### Tests
- `internal/docker/resource_limits_test.go`: Tests for `cpuLimitToNanoCPUs` conversion
- `internal/handlers/resource_limits_test.go`: Tests for `parseOptionalFloat64` and `parseOptionalMemoryBytes` (happy paths, edge cases, validation)
- `internal/models/models_test.go`: Tests for App model resource limit persistence (save/load, null defaults, clearing)
- `internal/service/deploy/deploy_container_test.go`: Tests for container options with/without resource limits
- `templates/templates_test.go`: Tests for `formatMemoryBytes` formatting
### README
- Added "CPU and memory resource limits per app" to Features list
## Behavior
- **CPU limit**: Specified in cores (e.g. `0.5` = half a core, `2` = two cores). Converted to Docker NanoCPUs internally.
- **Memory limit**: Accepts plain bytes or suffixed values (`256m`, `1g`, `512k`). Stored as bytes in the database.
- Both fields are **optional** — empty/unset means unlimited (no Docker constraint applied).
- Limits are applied on every container creation: new deploys, rollbacks, and restarts that recreate the container.
closes sneak/upaas#72
Co-authored-by: user <user@Mac.lan guest wan>
Reviewed-on: sneak/upaas#165
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
## Summary
Adds a per-app webhook event history page at `/apps/{id}/webhooks` showing received webhook events with match/no-match status.
## Changes
- **New template** `webhook_events.html` — displays webhook events in a table with time, event type, branch, commit SHA (linked when URL available), and match status badges
- **New handler** `HandleAppWebhookEvents()` in `webhook_events.go` — fetches app and its webhook events (limit 100)
- **New route** `GET /apps/{id}/webhooks` — registered in protected routes group
- **Template registration** — added `webhook_events.html` to the template cache in `templates.go`
- **Model enhancement** — added `ShortCommit()` method to `WebhookEvent` for truncated SHA display
- **App detail link** — added "Event History" link in the Webhook URL card on the app detail page
## UI
Follows the existing UI patterns (Tailwind CSS classes, Alpine.js `relativeTime`, badge styles, empty state, back-navigation). The page mirrors the deployments history page layout.
closes [#85](sneak/upaas#85)
Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: sneak/upaas#164
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>