diff --git a/docs/plans/current_spec.md b/docs/plans/current_spec.md index 699da9b9..fe7de0b9 100644 --- a/docs/plans/current_spec.md +++ b/docs/plans/current_spec.md @@ -1649,3 +1649,748 @@ Adds security.crowdsec.starting i18n key to all 5 supported locales. Closes issue 3, closes issue 4 (regression test only, backend fix in PR-1). ``` + +--- + +## PR-5: TCP Monitor UX — Issue 5 + +**Title:** fix(frontend): correct TCP monitor URL placeholder and add per-type UX guidance +**Issue Resolved:** Issue 5 — "monitor tcp port ui can't add" +**Classification:** CONFIRMED BUG +**Dependencies:** None (independent of all other PRs) +**Status:** READY FOR IMPLEMENTATION + +--- + +### 1. Introduction + +A user attempting to create a TCP uptime monitor follows the URL input placeholder text, +which currently reads `"https://example.com or tcp://host:port"`. They enter +`tcp://192.168.1.1:8080` as instructed and submit the form. The creation silently fails +with an HTTP 500 error. The form provides no guidance on why — no inline error, no revised +placeholder, no helper text. + +This plan specifies four layered fixes ranging from the single-line minimum viable fix to +comprehensive per-type UX guidance and client-side validation. All four fixes belong in a +single small PR. + +--- + +### 2. Research Findings + +#### 2.1 Root Cause: `net.SplitHostPort` Chokes on the `tcp://` Scheme + +`net.SplitHostPort` in Go's standard library splits `"host:port"` on the last colon. When the +host portion itself contains a colon (as `"tcp://192.168.1.1"` does after the `://`), Go's +implementation returns `"too many colons in address"`. + +Full trace: + +``` +frontend: placeholder says "tcp://host:port" + ↓ +user enters: tcp://192.168.1.1:8080 + ↓ +POST /api/v1/uptime/monitors { name: "…", url: "tcp://192.168.1.1:8080", type: "tcp", … } + ↓ +uptime_handler.go:42 c.ShouldBindJSON(&req) → OK (all required fields present) + ↓ +uptime_service.go:1107 url.Parse("tcp://192.168.1.1:8080") → OK (valid URL, scheme=tcp) + ↓ +uptime_service.go:1122 net.SplitHostPort("tcp://192.168.1.1:8080") + → host = "tcp://192.168.1.1" ← contains a colon (the : after tcp) + → Go: byteIndex(host, ':') >= 0 → return addrErr(…, "too many colons in address") + ↓ +uptime_service.go:1123 returns fmt.Errorf("TCP URL must be in host:port format: %w", err) + ↓ +uptime_handler.go:51 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + → HTTP 500 {"error":"TCP URL must be in host:port format: address tcp://192.168.1.1:8080: too many colons in address"} + ↓ +frontend onError: toast.error("TCP URL must be in host:port format: …") + → user sees a cryptic toast, form stays open, no inline guidance +``` + +`net.SplitHostPort` accepts **only** the bare `host:port` form. Valid examples: +`192.168.1.1:8080`, `myserver.local:22`, `[::1]:8080` (IPv6). The scheme prefix `tcp://` +is not valid and is never stripped by the backend. + +#### 2.2 The Unit Test Pre-Empted the Fix But It Was Never Applied + +`frontend/src/pages/__tests__/Uptime.test.tsx` at **line 40** already mocks: +```typescript +'uptime.urlPlaceholder': 'https://example.com or host:port', +``` +The test was written with the corrected value in mind. The actual translation file was +never updated to match. This is a direct discrepancy that CI does not catch because the +unit test mocks the i18n layer — it never reads the real translation file. + +#### 2.3 State of All Locale Files + +`frontend/src/locales/en/translation.json` is the **only** locale file that contains the +newer uptime keys added when the Create Monitor modal was built. The other four locales +(de, fr, es, zh) all fall through to the English key values for these keys (i18next +fallback). The `urlPlaceholder` key does not exist in any non-English locale — they +inherit the English value verbatim, which is the broken one. + +Missing keys across **all** non-English locales (de, fr, es, zh): + +``` +uptime.addMonitor uptime.syncWithHosts uptime.createMonitor +uptime.monitorCreated uptime.syncComplete uptime.syncing +uptime.monitorType uptime.monitorUrl uptime.monitorTypeHttp +uptime.monitorTypeTcp uptime.urlPlaceholder uptime.autoRefreshing +uptime.noMonitorsFound uptime.triggerHealthCheck uptime.monitorSettings +uptime.failedToDeleteMonitor uptime.failedToUpdateMonitor +uptime.failedToTriggerCheck uptime.pendingFirstCheck uptime.last60Checks +uptime.unpaused uptime.deleteConfirmation uptime.loadingMonitors +``` + +**PR-5 addresses all new keys added by this PR** (see §5.1 below). The broader backfill of +missing non-English uptime keys is a separate i18n debt task outside PR-5's scope. + +#### 2.4 Component Architecture — Exact Locations in `Uptime.tsx` + +The `CreateMonitorModal` component occupies lines 342–476 of +`frontend/src/pages/Uptime.tsx`. + +Key lines within the component: + +| Line | Code | +|------|------| +| 342 | `const CreateMonitorModal: FC<{ onClose: () => void; t: (key: string) => string }> = ({ onClose, t }) => {` | +| 345 | `const [url, setUrl] = useState('');` | +| 346 | `const [type, setType] = useState<'http' \| 'tcp'>('http');` | +| 362 | `if (!name.trim() \|\| !url.trim()) return;` (`handleSubmit` guard) | +| 363 | `mutation.mutate({ name: name.trim(), url: url.trim(), type, interval, max_retries: maxRetries });` | +| 395–405 | URL `