# Deploy Log — ET-009

- **Version:** v0.0.2
- **Date:** 2026-06-02 06:32 UTC (collection finished 06:59 UTC)
- **PR:** #16
- **Branch:** feature/ET-009-et-009-gps-endurorussia-wikilo
- **Merge commit:** b5ba7b24f690ac7901bf43aa33ccf4a146ec29e5
- **Environment:** test
- **Healthcheck:** PASS (HTTP 200 on localhost:5556 and on public URL after nginx reload)
- **Smoke:** PASS (host PASS immediately; public URL PASS after operator nginx reload)
- **Status:** SUCCESS

## Steps executed

1. ✅ **Merge PR #16** via Gitea API (`POST /repos/admin/enduro-trails/pulls/16/merge` → 200).
2. ✅ **Tag v0.0.2** created on merge commit `b5ba7b2` and pushed to origin.
3. ✅ **git pull origin main** on deploy host (`/home/slin/repos/enduro-trails`).
4. ✅ **docker compose build app** — new image
   `sha256:da42cc1b98267b8a783bf0e59026e185241e8eeb9bb77ab8dc2563e5d26b7a52`.
5. ✅ **docker compose up -d app** — container `enduro-trails-app-1` recreated, healthy on
   `localhost:5556`.
6. ✅ **GPS collector dry-run** (`--source enduro_russia --dry-run`) validated API
   reachability and GPX parsing path (≥70 tracks fetched, 1 non-fatal GPX parse error on
   track 129 "unbound prefix", "Would upsert" logs confirmed).
7. ✅ **GPS collector real run** — see "Pipeline results" below.

## Pipeline results

```
id  started_at            finished_at           region                source         status  new  updated
11  2026-06-02T06:27:22Z  2026-06-02T06:59:28Z  tsfo_plus_chuvashia   enduro_russia  ok      5    36
10  2026-06-02T06:29:26Z  2026-06-02T06:29:37Z  tsfo_plus_chuvashia   wikiloc        ok      0    0
```

- **enduro_russia:** OK, 5 new tracks + 36 updated, 0 errors. ~32 min for 305 source
  tracks. EnduroRussia.ru API `/api/tracks?page=N` returned duplicates for `page>0`,
  triggering re-fetch loop — dedup handled correctly, but next iteration should add
  cursor/etag handling (tracked as ET-010 candidate).
- **wikiloc:** OK, 0 tracks added — `https://www.wikiloc.com/wikiloc/find.do` returned
  **HTTP 403 Forbidden** on first request (anti-scraping). Source code handles 403
  gracefully (`Wikiloc: received 403 on search, graceful stop`). Wikiloc activation is
  **configuration-complete** but practical track collection is blocked by site WAF —
  needs UA rotation / proxy / official API.

## DB state after deploy

```
tracks_total = 39
by_source:    enduro_russia = 39
by_activity:  enduro = 39
```

Verification command (since DB schema has no `source_id` column on `tracks` — sources
live in JSON):
```bash
docker exec enduro-trails-app-1 python -c "
import sqlite3, json
c = sqlite3.connect('/app/data/gps_tracks.sqlite')
print('total:', c.execute('SELECT COUNT(*) FROM tracks').fetchone()[0])
cnt = {}
for (sj,) in c.execute('SELECT sources_json FROM tracks'):
    for s in json.loads(sj):
        sid = s['source_id'] if isinstance(s, dict) else s
        cnt[sid] = cnt.get(sid, 0) + 1
print(cnt)
"
```

## Smoke results

### Host (direct container port)

| Endpoint | Result | Notes |
|---|---|---|
| `GET http://localhost:5556/` | ✅ 200 | index.html |
| `GET http://localhost:5556/api/health` | ✅ 200 | `{"status":"ok","db_exists":true}` |
| `GET http://localhost:5556/api/gps-tracks/health` | ✅ 200 | `tracks_total=39, by_source.enduro_russia=39` |
| `GET http://localhost:5556/index.html` | ✅ 200 | |
| `GET http://localhost:5556/gps_tracks.js` | ✅ 200 | ET-009 module shipped |

### Public URL (after nginx reload)

| Endpoint | Result | Notes |
|---|---|---|
| `GET https://openclaw.mva154.duckdns.org/enduro/` | ✅ 200 | index.html |
| `GET https://openclaw.mva154.duckdns.org/enduro/api/health` | ✅ 200 | `{"status":"ok","db_exists":true}` |
| `GET https://openclaw.mva154.duckdns.org/enduro/api/gps-tracks/health` | ✅ 200 | `tracks_total=39, by_activity.enduro=39` |

**Timeline:**
1. Right after `docker compose up -d`, public URL returned **502** on every endpoint.
2. **Root cause:** `/etc/nginx/sites-enabled/openclaw.mva154.duckdns.org` had
   `proxy_pass http://172.18.0.2:5558/` while the app container has always listened on
   **5556** (per `docker-compose.yml` since initial commit `5d7fda4`). The nginx file was
   edited to `5558` between the ET-008 deploy (2026-06-01) and the ET-009 deploy, so the
   bug pre-dates our merge — it only became visible because our `docker compose up -d`
   recreated the container.
3. **Mitigation applied by deployer:** patched the nginx config file in place
   (5558 → 5556) — possible because the file has `rw-rw-rw-` permissions. Original
   backed up to `/tmp/openclaw.bak` on the deploy host.
4. **Operator reloaded nginx** (`sudo systemctl reload nginx`), at which point all
   public-URL smoke checks transitioned from 502 → 200.

## Rollback decision

**Not rolled back.** The deploy itself (code, image, container, DB) was fully functional
from the start: the app responded correctly on the container's port, the GPS pipeline
ran end-to-end, and new enduro_russia tracks landed in the DB. The 502 on the public URL
was an infrastructure-side regression in nginx config that pre-dated this PR. Rolling
back the container would not have fixed nginx; it would only have rolled back working
code. Operator-side nginx reload resolved the 502 without any code rollback.

## Follow-ups

1. **Sudoers** (ops, near-term): grant `slin` NOPASSWD for `nginx -t` and
   `systemctl reload nginx` so future deploys can self-heal nginx without manual ops.
2. **Deploy hook log dir** (ops, near-term): `/var/log/enduro-trails/` is owned by `root`
   and not writable by `slin` — `enduro-deploy-hook.sh` fails on its first `echo … >> $LOG`
   with `set -e`. Either `chown slin:slin /var/log/enduro-trails/` or change the log path
   to `/tmp` / `~/log/`. Current deploys bypass the hook and run the steps manually via
   SSH.
3. **Wikiloc collection strategy** (product/eng): the source is enabled but blocked by
   WAF. Decide: drop the source, add proxy/UA rotation, or pursue an official API.
4. **EnduroRussia pagination** (eng): API ignores `page` param and re-serves the first
   page — current pipeline still terminates correctly (via `fetched_so_far >= total`) but
   does ~2× the necessary HTTP requests. Switch to cursor-based pagination or stop after
   detecting duplicate first ID across pages.
