Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ body:
attributes:
label: ChannelWatch version
description: In Settings > Updates, copy Application version. Include Container image version if the problem involves updating.
placeholder: "e.g. 1.1.2"
placeholder: "e.g. 1.2.0"
validations:
required: true

Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/question.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ body:
attributes:
label: ChannelWatch version
description: In Settings > Updates, copy Application version. Include Container image version if the problem involves updating.
placeholder: "e.g. 1.1.2"
placeholder: "e.g. 1.2.0"
validations:
required: true

Expand Down
4 changes: 2 additions & 2 deletions .github/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ Security fixes are provided for the current stable release line.

| Version line | Supported |
|---|---|
| 1.1.x | Yes |
| 1.0.x and earlier | No |
| 1.2.x | Yes |
| 1.1.x and earlier | No |

If you are still on an older build, upgrade before reporting a security issue unless the issue itself blocks upgrade.

Expand Down
55 changes: 28 additions & 27 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ env:
DOCKERHUB_IMAGE: coderluii/channelwatch
GHCR_IMAGE: ghcr.io/coderluii/channelwatch
DOCKER_PLATFORMS: linux/amd64,linux/arm64
SKOPEO_IMAGE: quay.io/skopeo/stable@sha256:db4108427c05acbadd1447316caa9b5f097a9a737d897d2297443baedff37ded # v1.22.2
SKOPEO_IMAGE: quay.io/skopeo/stable@sha256:6cc6b5002dc40f2adf2b3b32e775c9d50c3cf63e15100f15581be6971ab0c9e5 # v1.22.3
RELEASE_TAG: ${{ github.ref_name }}
RELEASE_SHA: ${{ github.sha }}

Expand All @@ -40,7 +40,7 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'
- name: Install dependencies
Expand All @@ -63,7 +63,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
package-manager-cache: false
Expand All @@ -87,18 +87,19 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
package-manager-cache: false
- run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm exec playwright install --with-deps chromium firefox webkit
- run: pnpm build
- name: Run browser, responsive, visual, keyboard, and accessibility checks
run: pnpm exec playwright test
- name: Retain browser diagnostics, including first failed attempts
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: pr-browser-${{ github.sha }}
path: |
Expand All @@ -117,7 +118,7 @@ jobs:
with:
ref: ${{ github.sha }}
persist-credentials: false
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
package-manager-cache: false
Expand All @@ -139,7 +140,7 @@ jobs:
with:
ref: ${{ github.sha }}
persist-credentials: false
- uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
- uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
- name: Lint and render managed-local mode
run: |
helm lint deploy/helm/channelwatch
Expand All @@ -163,8 +164,8 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
file: deploy/docker/Dockerfile
Expand All @@ -183,7 +184,7 @@ jobs:
fetch-depth: 0
persist-credentials: false
- name: Install Trivy
uses: aquasecurity/setup-trivy@3fb12ec12f41e471780db15c232d5dd185dcb514 # v0.2.6
uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1
with:
version: v0.74.0
cache: true
Expand Down Expand Up @@ -341,21 +342,21 @@ jobs:
fi

- name: Set up pinned Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'

- name: Set up pinned Node.js
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
package-manager-cache: false

- name: Set up QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1

- name: Verify pinned publication helper is available
run: |
Expand Down Expand Up @@ -395,7 +396,7 @@ jobs:
# Browser checks use disposable API fixtures and run before signing secrets.
- name: Retain browser diagnostics, including first failed attempts
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-browser-${{ github.sha }}
path: |
Expand Down Expand Up @@ -473,7 +474,7 @@ jobs:
echo "Building exact candidate image from normalized source timestamp ${build_date}."

- name: Build first exact multi-architecture image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
file: deploy/docker/Dockerfile
Expand All @@ -490,7 +491,7 @@ jobs:
SOURCE_DATE_EPOCH=${{ env.CANDIDATE_SOURCE_EPOCH }}

- name: Build second exact multi-architecture image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
file: deploy/docker/Dockerfile
Expand Down Expand Up @@ -542,7 +543,7 @@ jobs:
cmp --silent "${archive}" "${second_archive}"

- name: Load exact-source AMD64 image for historical recovery canaries
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
file: deploy/docker/Dockerfile
Expand All @@ -558,7 +559,7 @@ jobs:
SOURCE_DATE_EPOCH=${{ env.CANDIDATE_SOURCE_EPOCH }}

- name: Install Trivy
uses: aquasecurity/setup-trivy@3fb12ec12f41e471780db15c232d5dd185dcb514 # v0.2.6
uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1
with:
version: v0.74.0
cache: true
Expand Down Expand Up @@ -877,7 +878,7 @@ jobs:

- name: Retain sealed nonpublishing candidate evidence
id: candidate_upload
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: channelwatch-release-candidate-${{ github.sha }}
path: dist/candidate-sealed/*.sealed
Expand Down Expand Up @@ -913,12 +914,12 @@ jobs:
persist-credentials: false

- name: Set up pinned Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'

- name: Set up pinned Node.js
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
package-manager-cache: false
Expand Down Expand Up @@ -1403,7 +1404,7 @@ jobs:
echo "The exact private candidate image is ready for repeat scanning and publication."

- name: Install Trivy
uses: aquasecurity/setup-trivy@3fb12ec12f41e471780db15c232d5dd185dcb514 # v0.2.6
uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1
with:
version: v0.74.0
cache: true
Expand All @@ -1424,7 +1425,7 @@ jobs:

- name: Install pinned Syft
id: syft
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
uses: anchore/sbom-action/download-syft@006b7ce8314066bdf1765b4500370d40fa6917a3 # v0.24.2
with:
syft-version: v1.51.0

Expand Down Expand Up @@ -1784,7 +1785,7 @@ jobs:
echo "version=${version}" >> "${GITHUB_OUTPUT}"

- name: Set up pinned Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'

Expand Down Expand Up @@ -2141,7 +2142,7 @@ jobs:
persist-credentials: false

- name: Set up pinned Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'

Expand Down Expand Up @@ -2425,7 +2426,7 @@ jobs:
persist-credentials: false

- name: Set up pinned Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,9 @@ The Helm chart is single-replica by design because ChannelWatch uses writable ap

## Updating ChannelWatch

Use `coderluii/channelwatch:1.1.0` for the current image milestone, or `1.1`/`latest` for the newest image on that line. Preserve `/config` and any external storage key configuration when recreating the container.
Use `coderluii/channelwatch:1.2.0` for the current image milestone, or `latest` for the newest stable image. Preserve `/config` and any external storage key configuration when recreating the container.

ChannelWatch v1.1.2 is a signed in-app update for the v1.1.0 image line. Open **Settings > Updates** to install it; an operational v1.1.0 installation does not need a newer image. This release prevents a successful recording from also being reported as interrupted when the DVR recording response omits its job identity. Recording completion and reconciliation now recover that identity from matching DVR file metadata.
ChannelWatch v1.2.0 requires a container image update. Pull the new image and recreate the container with the same `/config` volume. It prevents false missed-start alerts for recordings that completed normally, adds a Program image switch for Recording Outcomes alerts, and refreshes compatible dependencies. **Settings > Updates** will identify this as an image update rather than installing it in-app.

Open **Settings > Updates** to review the automatic update policy, check the official signed stable channel, apply an update immediately, postpone it, retry a failed attempt, or roll back a compatible app bundle. Automatic compatible updates default to the local 03:00–05:00 maintenance window; notify-only mode is available.

Expand Down
2 changes: 1 addition & 1 deletion app/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import os
from pathlib import Path

__version__ = "1.1.2"
__version__ = "1.2.0"
__app_name__ = "ChannelWatch"


Expand Down
3 changes: 3 additions & 0 deletions app/core/alerts/recording_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,9 @@ def _resolve_recording_image_url(
def _format_recording_alert(
self, *, default_message: str, image_url: Optional[str], context: Dict[str, Any]
) -> Dict[str, Any]:
if not getattr(self.settings, "rd_image", True):
image_url = None
context = {**context, "image_url": ""}
return self.alert_formatter.format_templated_alert(
alert_type="recording_events",
default_title=self.ALERT_TITLE,
Expand Down
44 changes: 36 additions & 8 deletions app/core/alerts/recording_outcomes.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,29 +355,38 @@ def acknowledge(self, outcome: RecordingOutcome) -> None:
def pending_outcome(self, job_id: str) -> RecordingOutcome | None:
return next((item for item in self.pending_outcomes() if item.job_id == job_id), None)

def started_jobs_missing(self, jobs: Iterable[dict[str, Any]]) -> bool:
"""Return whether a fresh recordings lookup is needed.

A missing active job alone is not enough to infer interruption. The
caller performs a separate fresh completed-recordings read only when
this method reports that one is needed.
"""
def terminal_evidence_needed(self, jobs: Iterable[dict[str, Any]]) -> bool:
"""Return whether missing jobs require a fresh recordings lookup."""

observed = {
identifier
for job in jobs
if isinstance(job, dict)
and (identifier := _job_identifier(job))
}
now = self.now()
with self._lock:
return any(
isinstance(entry, dict)
and entry.get("started")
and not entry.get("terminal_outcome")
and job_id not in observed
and (
entry.get("started")
or (
(start_time := _number(
(entry.get("snapshot") or {}).get("start_time")
))
and now >= start_time + MISSED_GRACE_SECONDS
)
)
for job_id, entry in self._state["jobs"].items()
)

def started_jobs_missing(self, jobs: Iterable[dict[str, Any]]) -> bool:
"""Compatibility wrapper for the former lookup predicate."""

return self.terminal_evidence_needed(jobs)

def was_started(self, job_id: str) -> bool:
"""Return whether this lifecycle was durably observed as started."""

Expand Down Expand Up @@ -497,6 +506,25 @@ def reconcile(
start_time = _number(snapshot.get("start_time"))
if not start_time or now < start_time + MISSED_GRACE_SECONDS:
continue
# A missing job is not proof that it failed to start. Channels
# removes successfully completed jobs from the jobs snapshot,
# and the start event can be missed during a reconnect or
# restart. Require a successful recordings read before a
# missed outcome, and prefer exact completed-recording evidence.
if recordings is None:
continue
recording = recordings_by_job.get(job_id)
if recording is not None:
outcome = classify_recording_payload(
recording,
completion_event=True,
)
if outcome and self._set_terminal(entry, outcome, now):
changed = True
outcomes.append(
RecordingOutcome(job_id, outcome, dict(snapshot))
)
continue
last_negative = _number(entry.get("last_negative_at"))
confirmations = int(entry.get("missing_confirmations") or 0)
if confirmations and now - last_negative < NEGATIVE_CONFIRMATION_SECONDS:
Expand Down
1 change: 1 addition & 0 deletions app/core/docker-entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
"dvr_health_alert_delay_seconds": 120,
"notification_preferences_version": 1,
"rd_program_name": True,
"rd_image": True,
"rd_program_desc": True,
"rd_duration": True,
"rd_channel_name": True,
Expand Down
1 change: 1 addition & 0 deletions app/core/helpers/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ class CoreSettings:
dvr_health_alert_delay_seconds: int = 120
notification_preferences_version: int = 0
rd_program_name: bool = True
rd_image: bool = True
rd_program_desc: bool = True
rd_duration: bool = True
rd_channel_name: bool = True
Expand Down
Loading
Loading