{"id":50965226,"url":"https://github.com/goldbarth/port-tidewatch","last_synced_at":"2026-06-18T19:32:49.532Z","repository":{"id":363159288,"uuid":"1262015659","full_name":"goldbarth/port-tidewatch","owner":"goldbarth","description":"Water-level ingestion and storm-surge alerting service, modelled on Hamburg's WADI warning system. .NET ingestion pipeline with RabbitMQ, staged threshold evaluation, and a read-only Angular dashboard.","archived":false,"fork":false,"pushed_at":"2026-06-15T11:34:48.000Z","size":7336,"stargazers_count":0,"open_issues_count":3,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-15T11:49:20.638Z","etag":null,"topics":["angular","argocd","aspnet-core","azure-container-apps","clean-architecture","csharp","dotnet","event-driven","kubernetes","microservices","observability","opentelemetry","rabbitmq","testcontainers"],"latest_commit_sha":null,"homepage":"https://zealous-field-0d1829603.7.azurestaticapps.net/","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/goldbarth.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-07T13:12:29.000Z","updated_at":"2026-06-15T11:34:50.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/goldbarth/port-tidewatch","commit_stats":null,"previous_names":["goldbarth/port-tidewatch"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/goldbarth/port-tidewatch","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/goldbarth%2Fport-tidewatch","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/goldbarth%2Fport-tidewatch/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/goldbarth%2Fport-tidewatch/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/goldbarth%2Fport-tidewatch/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/goldbarth","download_url":"https://codeload.github.com/goldbarth/port-tidewatch/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/goldbarth%2Fport-tidewatch/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34505419,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-18T02:00:06.871Z","response_time":128,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["angular","argocd","aspnet-core","azure-container-apps","clean-architecture","csharp","dotnet","event-driven","kubernetes","microservices","observability","opentelemetry","rabbitmq","testcontainers"],"created_at":"2026-06-18T19:32:48.473Z","updated_at":"2026-06-18T19:32:49.513Z","avatar_url":"https://github.com/goldbarth.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/assets/port-tidewatch-logo.svg\" alt=\"Tidewatch\" width=\"96\" height=\"96\" /\u003e\n\u003c/p\u003e\n\n\u003ch1 align=\"center\"\u003eTidewatch\u003c/h1\u003e\n\u003cp align=\"center\"\u003e\n  \u003cstrong\u003eWater-Level Ingestion \u0026amp; Storm-Surge Alerting\u003c/strong\u003e\u003cbr\u003e\n  \u003csub\u003e\u003ccode\u003eport-tidewatch\u003c/code\u003e\u003c/sub\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\nA small, focused ingestion service for port water-level telemetry, with\nthreshold-based storm-surge alerting and a read-only monitoring dashboard.\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n\u003ca href=\"https://github.com/goldbarth/port-tidewatch/releases\"\u003e\u003cimg src=\"https://img.shields.io/github/v/release/goldbarth/port-tidewatch?logo=github\u0026label=release\" alt=\"Release\"\u003e\u003c/a\u003e\n\u003ca href=\"https://github.com/goldbarth/port-tidewatch/actions/workflows/ci.yml\"\u003e\u003cimg src=\"https://github.com/goldbarth/port-tidewatch/actions/workflows/ci.yml/badge.svg\" alt=\"CI\"\u003e\u003c/a\u003e\n\u003ca href=\"https://github.com/goldbarth/port-tidewatch/actions/workflows/deploy-container-apps.yml\"\u003e\u003cimg src=\"https://img.shields.io/github/actions/workflow/status/goldbarth/port-tidewatch/deploy-container-apps.yml?label=deploy\u0026logo=github\" alt=\"Deploy\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"https://img.shields.io/badge/.NET-10-512BD4?logo=dotnet\u0026logoColor=white\" alt=\".NET 10\"\u003e\n\u003cimg src=\"https://img.shields.io/badge/RabbitMQ-FF6600?logo=rabbitmq\u0026logoColor=white\" alt=\"RabbitMQ\"\u003e\n\u003cimg src=\"https://img.shields.io/badge/OpenTelemetry-000000?logo=opentelemetry\u0026logoColor=white\" alt=\"OpenTelemetry\"\u003e\n\u003cimg src=\"https://img.shields.io/badge/Angular-DD0031?logo=angular\u0026logoColor=white\" alt=\"Angular\"\u003e\n\u003cimg src=\"https://img.shields.io/badge/Docker-2496ED?logo=docker\u0026logoColor=white\" alt=\"Docker\"\u003e\n\u003cimg src=\"https://img.shields.io/badge/Kubernetes-326CE5?logo=kubernetes\u0026logoColor=white\" alt=\"Kubernetes\"\u003e\n\u003cimg src=\"https://img.shields.io/badge/Argo_CD-EF7B4D?logo=argo\u0026logoColor=white\" alt=\"Argo CD\"\u003e\n\n\u003e 📖 **Written up on my site:** [the project](https://www.goldbarth.dev/projects/port-tidewatch),\n\u003e and — in German, leicht verdaulich — [the surge-evaluator decision (ADR-004)](https://www.goldbarth.dev/decisions/surge-evaluator-decisions).\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/assets/demo/tidewatch-dashboard-screenshot-4.png\" alt=\"Tidewatch dashboard on the live PEGELONLINE Elbe feed — four Hamburg gauges, all normal\" width=\"860\"\u003e\n\u003c/p\u003e\n\u003cp align=\"center\"\u003e\u003csub\u003eLive dashboard on \u003cstrong\u003ereal WSV/PEGELONLINE data\u003c/strong\u003e — the public Hamburg Elbe gauges (St. Pauli, Zollenspieker, Over, Bunthaus), polled live.\u003c/sub\u003e\u003c/p\u003e\n\n---\n\nThe domain is modelled on the Hamburg storm-surge warning service (WADI):\na warning is raised when an expected surge peak can exceed **4.50 m above\nsea level (NHN)** / 2.40 m above mean high water (MThw). tidewatch ingests\nwater-level readings — from the **live WSV/PEGELONLINE Elbe feed** (the German\nWaterways and Shipping Administration's open gauge data) or a scripted simulator\n— evaluates them against that threshold, and surfaces the result.\n\n\u003e **Scope is intentionally narrow:** one domain, one ingestion path, no write\n\u003e operations from the UI. The goal is a reliable, observable ingestion\n\u003e pipeline end to end.\n\n---\n\n## Why this project\n\nThe Hamburg port runs on reliable, observable, security-relevant\ninfrastructure. tidewatch works the ingestion-and-alerting pattern in a\ndomain I care about, and takes the next steps in my stack — Angular and\nKubernetes/GitOps — on real ground rather than in the abstract.\n\n---\n\n## What it does\n\n- A reading-source host emits water-level readings for a set of gauges —\n  either a scripted simulator or the live PEGELONLINE Elbe feed, chosen by\n  config (same build, no recompile).\n- An ingestion service consumes readings via RabbitMQ (with a dead-letter\n  path for poison messages), evaluates each reading against the WADI\n  threshold, and emits an alert state.\n- A read-only Angular dashboard shows current levels, per-gauge alert status\n  (normal / warning / severe), and a short recent-history trend.\n\n---\n\n## Demo\n\n### Scripted storm surge\n\n\u003ctable\u003e\n\u003ctr\u003e\n\u003ctd width=\"33%\"\u003e\u003cimg src=\"docs/assets/demo/tidewatch-dashboard-screenshot-1.png\" alt=\"Tidewatch dashboard with every gauge in the normal stage\"\u003e\u003c/td\u003e\n\u003ctd width=\"33%\"\u003e\u003cimg src=\"docs/assets/demo/tidewatch-dashboard-screenshot-2.png\" alt=\"Tidewatch dashboard during a storm surge, the CUX gauge in the warning stage\"\u003e\u003c/td\u003e\n\u003ctd width=\"33%\"\u003e\u003cimg src=\"docs/assets/demo/tidewatch-dashboard-screenshot-3.png\" alt=\"Tidewatch dashboard at the surge peak, the CUX gauge in the severe stage\"\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd align=\"center\"\u003e\u003csub\u003eCalm — every gauge \u003ccode\u003enormal\u003c/code\u003e, overall status normal.\u003c/sub\u003e\u003c/td\u003e\n\u003ctd align=\"center\"\u003e\u003csub\u003eSurge rising — \u003ccode\u003eCUX\u003c/code\u003e in \u003ccode\u003ewarning\u003c/code\u003e (5.37 m), overall status warning.\u003c/sub\u003e\u003c/td\u003e\n\u003ctd align=\"center\"\u003e\u003csub\u003eSurge peak — \u003ccode\u003eCUX\u003c/code\u003e crosses \u003ccode\u003esevere\u003c/code\u003e (5.83 m), overall status severe.\u003c/sub\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n\u003cp align=\"center\"\u003e\u003csub\u003e▶ \u003ca href=\"docs/release-notes/v1.1.0.md#demo\"\u003eWatch the ~60-second surge demo\u003c/a\u003e — the full \u003ccode\u003enormal → warning → severe → recede\u003c/code\u003e cascade.\u003c/sub\u003e\u003c/p\u003e\n\n**What you're seeing.** Each card is a gauge, plotted on a fixed **0–6 m NHN**\nscale. The shaded bands *are* the WADI thresholds: **warning at 4.50 m** and\n**severe at 5.50 m** (2.40 m over mean high water). One gauge — `CUX` — runs a\nscripted storm surge and cascades `normal → warning → severe → recede`; the\nothers hold `normal` for contrast. The header summarises gauges per stage, the\nhighest current level, and overall status, while the live / last-updated\nindicator makes stale data obvious — turning the threshold story into a single\nglance.\n\n---\n\n## Architecture\n\n```\n┌─────────────────────────────┐    readings     ┌──────────────────┐  alerts / state    ┌─────────────┐\n│ reading-source host (.NET)  │ ──────────────▶ │ ingestion service│ ─────────────────▶ │  dashboard  │\n│ one source, set by config:  │    RabbitMQ     │      (.NET)      │   REST (polling)   │  (Angular)  │\n│  • simulator (scripted)     │                 └──────────────────┘                    └─────────────┘\n│  • PEGELONLINE Elbe feed    │                           │\n└─────────────────────────────┘                           │ poison messages\n        ReadingSource switch                              ▼\n                                                ┌──────────────────┐\n                                                │ dead-letter queue│\n                                                └──────────────────┘\n```\n\n### Alert-state lifecycle\n\nA gauge moves between stages as its evaluated level crosses the configured\nthresholds (m above NHN). Stages are strictly ordered; the evaluator derives\nthem from the reading window, not from a single spike.\n\n```\n  normal ──(≥ 4.50 m)──▶ warning ──(≥ 5.50 m)──▶ severe\n    ▲                       │                       │\n    └───────────────────────┴───────────────────────┘\n                  (level falls back below stage)\n```\n\nArchitecture decisions are recorded as ADRs under `docs/adrs/`. Open questions\nare tracked there too, so the reasoning is visible even where the\nimplementation is not finished.\n\n| #   | Concern                       | Decision                                                                                          |\n|-----|-------------------------------|---------------------------------------------------------------------------------------------------|\n| 001 | Where threshold logic lives   | [Threshold evaluation lives in the ingestion service](docs/adrs/001-threshold-logic-in-service.md) |\n| 002 | Dashboard client state        | [Angular state structure](docs/adrs/002-angular-state-structure.md)                               |\n| 003 | Deploy target                 | [Azure Container Apps vs. Kubernetes + Argo CD](docs/adrs/003-container-apps-vs-kubernetes.md)     |\n| 004 | Stage-determination algorithm | [Surge evaluator algorithm](docs/adrs/004-surge-evaluator-algorithm.md) · [Blog (DE)](https://www.goldbarth.dev/decisions/surge-evaluator-decisions) |\n\n---\n\n## API\n\nThe ingestion service exposes a deliberately thin, read-only HTTP surface for\nthe dashboard. The reading consumer runs as a hosted service alongside it; all\nstate is live and in-memory.\n\n| Method | Path          | Description                                                                 |\n|--------|---------------|-----------------------------------------------------------------------------|\n| `GET`  | `/healthz`    | Liveness probe — returns `ok`.                                              |\n| `GET`  | `/api/gauges` | Snapshot of every gauge: current level, alert stage, and downsampled trend. |\n\n`/api/gauges` returns one object per gauge:\n\n```jsonc\n{\n  \"gaugeId\": \"st-pauli\",\n  \"level\": 4.62,              // metres above NHN, latest reading (null if none yet)\n  \"stage\": \"warning\",         // normal | warning | severe\n  \"changedAt\": \"2026-06-12T08:41:00Z\",\n  \"trend\": [                  // recent window, downsampled to ≤ 24 points\n    { \"t\": \"2026-06-12T08:30:00Z\", \"v\": 4.41 },\n    { \"t\": \"2026-06-12T08:35:00Z\", \"v\": 4.58 }\n  ],\n  \"rateMetersPerMin\": 0.04,   // least-squares rate-of-change over the window (null if \u003c 2 points)\n  \"timeInStageSeconds\": 180,  // how long the gauge has held its current stage (null if none yet)\n  \"windowMin\": 4.38,          // window extent — lowest reading (null if empty)\n  \"windowMax\": 4.71           // window extent — highest reading (null if empty)\n}\n```\n\n\u003e The derived signals (`rateMetersPerMin`, `timeInStageSeconds`, `windowMin` /\n\u003e `windowMax`) are computed in the API mapper, not held in state — the state\n\u003e holder stays raw (ADR-002).\n\n---\n\n## Tech Stack\n\n| Concern           | Technology                                |\n|-------------------|-------------------------------------------|\n| Service \u0026 API     | .NET 10 / ASP.NET Core                    |\n| Messaging         | RabbitMQ                                  |\n| Observability     | OpenTelemetry                             |\n| Testing           | xUnit · Testcontainers                    |\n| Dashboard         | Angular                                   |\n| Containerisation  | Docker                                    |\n| Baseline deploy   | Azure Container Apps                       |\n| GitOps deploy     | Kubernetes + Argo CD (final phase)        |\n\n---\n\n## Out of scope (deliberately)\n\nThe narrow scope is a design choice, not a backlog. Kept out so the pipeline\nstays the thing that gets done well:\n\n- **No writes from the UI** — the dashboard is read-only by design (ADR-002).\n- **No persistence layer** — gauge state is live, in-memory only; there is no\n  historical store. The point is the ingestion path, not a time-series database.\n- **No auth on the API** — single-tenant showcase; the surface is two read-only\n  endpoints.\n- **One domain, one ingestion path** — no multi-network federation, no\n  multi-tenancy. A real public gauge feed (PEGELONLINE) ships in v1.2 (M7),\n  selectable against the simulator via the `ReadingSource` switch; one source is\n  active per run (they are not run side by side).\n- **Notification delivery** — `ApplyStageChange` now publishes alert events at\n  its single chokepoint (v1.1 / M6), but *acting* on them (email / push, a\n  notification consumer) stays out of scope; the showcase ends at the event.\n\n---\n\n## Status\n\nMilestones M1–M7 are complete — **v1.0 is presentable end to end, v1.1 (demo\n\u0026 polish) and v1.2 (real PEGELONLINE data) have landed.** M8 (v1.3) is the\nremaining planned work. Every intermediate state is built to stay coherent —\nsee the roadmap.\n\n## Roadmap\n\nBuilt in milestones, each an intermediate state that stays coherent. The full\nissue-by-issue breakdown lives in **[`docs/ISSUES.md`](docs/ISSUES.md)**.\n\n| Milestone                                  | Focus | Status      |\n|--------------------------------------------|-------|-------------|\n| **M1 · Foundation**                        | Repo scaffold, contracts, threshold configuration | Done        |\n| **M2 · Ingestion**                         | RabbitMQ transport, consumer + dead-letter, per-gauge state, surge evaluator | Done        |\n| **M3 · Observability \u0026 Tests**             | OpenTelemetry tracing, Testcontainers integration tests | Done        |\n| **M4 · Dashboard**                         | Angular read-only view — levels, status, trend | Done |\n| **M5 · Deploy**                            | Kubernetes + Argo CD (GitOps, primary) and Azure Container Apps (IaC + CI) | Done |\n| **M6 · Demo \u0026 polish (v1.1)**              | Storm-surge scenario, dashboard polish, richer signals, demo assets, alert events | Done |\n| **M7 · Real data (v1.2)**                  | Real PEGELONLINE Elbe feed alongside the simulator, source selection | Done |\n| **M8 · Observability made visible (v1.3)** | Surface the OpenTelemetry path — latency pulse, Jaeger deep-link, optional trace waterfall | Planned |\n\n\u003e **M5 ordering:** Kubernetes + Argo CD is the primary deployment, run on a local\n\u003e cluster to stay at €0; Azure Container Apps ships as IaC + CI, deployed on demand.\n\u003e The reasoning is in [ADR-003](docs/adrs/003-container-apps-vs-kubernetes.md).\n\n---\n\n## Running it\n\n| Stack | Runbook |\n|-------|---------|\n| Local dev (broker + ingestion + simulator + `ng serve`) | [runbook-local-dashboard.md](docs/runbook-local-dashboard.md) |\n| Kubernetes + Argo CD (local kind, GitOps) | [runbook-k8s-argocd.md](docs/runbook-k8s-argocd.md) |\n| Azure Container Apps (azd) | [runbook-container-apps.md](docs/runbook-container-apps.md) |\n\n### Local surfaces\n\nOnce the local stack is up, these are the addresses you'll use:\n\n| Surface | URL | Notes |\n|---------|-----|-------|\n| Dashboard (`ng serve`) | \u003chttp://localhost:4200\u003e | Proxies `/api` to the service. |\n| Ingestion API | \u003chttp://localhost:5080/api/gauges\u003e | Read-only; also `/healthz`. |\n| RabbitMQ management UI | \u003chttp://localhost:15672\u003e | **Dev-only** — default `guest` / `guest`. |\n\n### .NET solution commands\n\nThe solution is a `.slnx` — needs the **.NET 10 SDK**.\n\n```bash\n# Build / restore\ndotnet build port-tidewatch.slnx\ndotnet restore port-tidewatch.slnx\n\n# Run the ingestion service (needs a reachable RabbitMQ — see appsettings RabbitMq section)\ndotnet run --project src/Tidewatch.Ingestion\n\n# Run the reading-source host (publishes Reading messages; RABBITMQ_HOST env var overrides host).\n# Defaults to the scripted simulator; switch to the live Elbe feed with ReadingSource=Pegelonline.\ndotnet run --project src/Tidewatch.Source\nReadingSource=Pegelonline dotnet run --project src/Tidewatch.Source\n```\n\n### Reading source: simulator vs. live feed\n\n`Tidewatch.Source` runs exactly one reading source, chosen at startup by the\n`ReadingSource` config switch (appsettings key or env var) — the same build serves\nthe scripted demo or the real feed without recompiling:\n\n| `ReadingSource` | Source | Notes |\n|-----------------|--------|-------|\n| `Simulator` (default) | Scripted surge | One gauge runs warning → severe → recede; the rest stay normal. |\n| `Pegelonline` | Live WSV/PEGELONLINE Elbe feed | Polls the configured Hamburg Elbe gauges; cm → m and PNP → NHN applied in an explicit mapping layer. |\n\nA missing or unrecognised `ReadingSource` fails at startup (same fail-fast posture\nas the threshold config). Both sources emit the identical `Reading` shape, so the\ningestion path cannot tell them apart.\n\nThe live feed covers four Hamburg Elbe gauges, configured by UUID under the\n`Pegelonline` section: **St. Pauli**, **Bunthaus**, **Over**, **Zollenspieker**.\nPEGELONLINE data is Datenlizenz Deutschland Zero 2.0 (free, no auth). HPA tidal\ngauges expose no `gaugeZero`, so their PNP is set explicitly (Hamburg PNP =\nNHN −5.00 m).\n\n---\n\n## Testing\n\nTwo .NET test projects plus the Angular suite. Integration tests stand up a\nreal broker via Testcontainers, so the message path is exercised end to end —\nno mocked transport.\n\n| Layer | Project / command | Scope |\n|-------|-------------------|-------|\n| Unit | `Tidewatch.Ingestion.UnitTests` | Surge-evaluator stage logic — thresholds, trend, outlier handling. |\n| Integration | `Tidewatch.Ingestion.IntegrationTests` | Full consume path against a Testcontainers RabbitMQ, incl. the dead-letter route for poison messages. |\n| Frontend | `npm test` (in `frontend/`) | Angular component / state tests. |\n\n```bash\n# All .NET tests (Docker daemon must be running — Testcontainers starts a RabbitMQ container)\ndotnet test\n\n# A single test or class\ndotnet test --filter \"FullyQualifiedName~SurgeEvaluatorTests\"\n```\n\n---\n\n## Effort accounting\n\nThis is an AI-assisted build, and I'd rather be transparent about it than coy.\nv1.0 — the full pipeline (RabbitMQ transport with a dead-letter path, per-gauge\nstate, the surge evaluator, OpenTelemetry tracing, Testcontainers tests, the\nAngular dashboard, and *both* a Kubernetes/Argo CD and an Azure Container Apps\ndeploy) — came together over roughly **five focused days**.\n\nWhat got compressed was keystrokes: boilerplate, DTOs, wiring, test scaffolds.\nWhat did **not** get compressed was the thinking. The architectural calls — where\nthreshold logic lives, the queue topology, the evaluator algorithm, the deploy\nordering — are mine, made deliberately and written down as ADRs before the code\nfollowed. The AI is a fast pair of hands; the design ownership stayed with me.\nThat's the honest accounting, and it's why the ADRs exist.\n\n---\n\n## License\n\n[MIT](LICENSE) © Felix Wahl\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgoldbarth%2Fport-tidewatch","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgoldbarth%2Fport-tidewatch","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgoldbarth%2Fport-tidewatch/lists"}