1 Commits
10 changed files with 165 additions and 42 deletions

No files matched your search

+27
View File
@@ -7,6 +7,33 @@ Version numbers follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html
--- ---
## [0.9.3] — 2026-10-28
### Changed
- **Every time follows the `TZ` of the Inspector stack.** Before, the dashboard,
Events page and alerts were always in US Pacific time while the maintenance window
and logs used `TZ`, so anyone outside Pacific saw times hours off (e.g. a 02:00
Berlin window shown as 5:00 PM). Now the dashboard, the Events page (including
where each day starts), alert times, cooldown times and logs all use `TZ`.
- Alert times include the zone abbreviation (`10-28 09:27 AM CET`), and "Cooldown
until" in alerts, event messages and logs is a local time instead of a UTC
timestamp.
- The active time zone is shown at the bottom of every Settings page, on the Events
page and on the Maintenance page.
- **If you never set `TZ`**, times are now shown in UTC instead of Pacific. Set `TZ`
in the stack (e.g. `America/New_York`). An unrecognized name logs a warning and
falls back to UTC.
### Docs
- INSTALL: `TZ` is now marked as needed, with a "Why the time zone matters" section
(maintenance window, alerts, dashboard, logs), how to find your zone name, and a
troubleshooting row. Guide: a Time zone section and a first-login check. Compose
and README updated.
---
## [0.9.2] — 2026-10-28 ## [0.9.2] — 2026-10-28
Fixes from a code and security review. None of them change how containers are Fixes from a code and security review. None of them change how containers are
+29 -1
View File
@@ -52,12 +52,39 @@ TZ=America/Los_Angeles
|---|---|---| |---|---|---|
| `INSPECTOR_DATA` | Folder on your server where Inspector keeps its settings, logs, and history. **Unraid:** `/mnt/user/appdata/inspector`. **Other Linux:** `/opt/inspector`. Created automatically. | Yes | | `INSPECTOR_DATA` | Folder on your server where Inspector keeps its settings, logs, and history. **Unraid:** `/mnt/user/appdata/inspector`. **Other Linux:** `/opt/inspector`. Created automatically. | Yes |
| `INSPECTOR_PASSWORD` | Password for the dashboard (username `admin`). Used only on first start; change it later in the dashboard. Leave it out and Inspector generates one and prints it in the container logs. | Recommended | | `INSPECTOR_PASSWORD` | Password for the dashboard (username `admin`). Used only on first start; change it later in the dashboard. Leave it out and Inspector generates one and prints it in the container logs. | Recommended |
| `TZ` | Your [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones), so times on the dashboard are local. | Recommended | | `TZ` | **Your time zone**, e.g. `America/New_York` or `Europe/Berlin`. Without it Inspector uses UTC, so the maintenance window, alerts and dashboard times are off by your UTC offset. See [Why the time zone matters](#why-the-time-zone-matters). | **Yes** (default `UTC`) |
| `INSPECTOR_PORT` | Port on your server for the dashboard. Change it if `8080` is taken. | No (default `8080`) | | `INSPECTOR_PORT` | Port on your server for the dashboard. Change it if `8080` is taken. | No (default `8080`) |
| `INSPECTOR_TAG` | Version to run, e.g. `0.8.0`. See [Updating](#4-updating). | No (default `latest`) | | `INSPECTOR_TAG` | Version to run, e.g. `0.8.0`. See [Updating](#4-updating). | No (default `latest`) |
| `PUID` / `PGID` | User and group that own Inspector's files. Unraid users often use `99` / `100` so the files match other appdata. | No (default `1000` / `1000`) | | `PUID` / `PGID` | User and group that own Inspector's files. Unraid users often use `99` / `100` so the files match other appdata. | No (default `1000` / `1000`) |
| `PORTAINER_API_TOKEN` | A Portainer access token. Only needed if you use **Portainer stack redeploy** for VPN-routed apps; see the in-app guide (Help → Portainer). | No | | `PORTAINER_API_TOKEN` | A Portainer access token. Only needed if you use **Portainer stack redeploy** for VPN-routed apps; see the in-app guide (Help → Portainer). | No |
### Why the time zone matters
Inspector uses `TZ` for every time it shows or acts on. If it's wrong, things
still work, but at the wrong hour or with confusing times:
- **The maintenance window runs in this time zone.** A window of `02:00–05:00`
with `TZ` left at UTC runs at 02:00 UTC, which is the middle of the evening in
the Americas or mid-morning in Asia. Inspector would then restart containers
during your nightly backups, and ignore real problems during the day.
- **Alerts** (Gotify / Pushover) show the time of the problem in this zone, with
its abbreviation, e.g. `10-28 03:15 AM CET`.
- **The dashboard and Events page** show times in this zone, and the Events
page's days run midnight to midnight in this zone.
- **Logs** are stamped in this zone.
**How to set it:** use your zone's name from the
[list of time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
("TZ identifier" column), e.g. `America/New_York`, `America/Chicago`,
`Europe/London`, `Europe/Berlin`, `Australia/Sydney`. Abbreviations like `EST` or
`CET` don't work. On Unraid, **Settings → Date and Time** shows the zone your
server uses.
**How to check it:** the bottom of every Settings page shows
`time zone: <name>`. If it says `UTC` and you're not on UTC, set `TZ` in the
stack and click **Update the stack**. If the name you set isn't recognized,
Inspector logs a warning and uses UTC.
## 3. Deploy and sign in ## 3. Deploy and sign in
1. Click **Deploy the stack**. 1. Click **Deploy the stack**.
@@ -116,6 +143,7 @@ starts and restarts the ones you add to it.
| Logs say *Permission denied on /var/run/docker.sock* | Make sure the stack has no `user:` line, then update the stack. Inspector needs to start normally to set up socket access. | | Logs say *Permission denied on /var/run/docker.sock* | Make sure the stack has no `user:` line, then update the stack. Inspector needs to start normally to set up socket access. |
| Red **READ-ONLY** badge at the bottom of Settings | The config folder mount ends in `:ro` or the folder isn't writable. Remove `:ro` and update the stack. | | Red **READ-ONLY** badge at the bottom of Settings | The config folder mount ends in `:ro` or the folder isn't writable. Remove `:ro` and update the stack. |
| Can't sign in | Look for the generated password in the container logs. If you've lost it, open `INSPECTOR_DATA/config/config.yml` (e.g. via your Unraid share), set a new `password:` under `dashboard:`, and restart Inspector. | | Can't sign in | Look for the generated password in the container logs. If you've lost it, open `INSPECTOR_DATA/config/config.yml` (e.g. via your Unraid share), set a new `password:` under `dashboard:`, and restart Inspector. |
| Times are a few hours off, or the maintenance window runs at the wrong time | `TZ` is missing or wrong. Check `time zone:` at the bottom of Settings, set `TZ` to your zone name (e.g. `Europe/London`), and update the stack. |
| A container shows **CONTAINER NOT FOUND** | The name in Inspector doesn't match Docker. Copy the exact name from Portainer → Containers. | | A container shows **CONTAINER NOT FOUND** | The name in Inspector doesn't match Docker. Copy the exact name from Portainer → Containers. |
## Security notes ## Security notes
+2
View File
@@ -35,6 +35,8 @@ restarts them when they break, and tells your phone about it.</p>
- **Stays out of the way when you're working.** Put a container in maintenance or - **Stays out of the way when you're working.** Put a container in maintenance or
mute its alerts with one click, or set a nightly maintenance window for backups mute its alerts with one click, or set a nightly maintenance window for backups
and updates. and updates.
- **Works in your time zone.** Set `TZ` in the stack and the maintenance window,
alerts, dashboard and logs all follow it.
## Install ## Install
+3 -1
View File
@@ -7,7 +7,9 @@
# Unraid: /mnt/user/appdata/inspector Other: /opt/inspector # Unraid: /mnt/user/appdata/inspector Other: /opt/inspector
# INSPECTOR_PASSWORD Dashboard password (username is "admin"). # INSPECTOR_PASSWORD Dashboard password (username is "admin").
# Only used on first start; change it later in Settings. # Only used on first start; change it later in Settings.
# TZ Your timezone, e.g. America/Los_Angeles # TZ Your time zone name, e.g. America/New_York, Europe/Berlin.
# Important: the maintenance window, alerts, dashboard and
# logs all use it. Left unset, everything is in UTC.
# #
# Optional: # Optional:
# INSPECTOR_PORT Port for the dashboard on your server (default 8080). # INSPECTOR_PORT Port for the dashboard on your server (default 8080).
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.9.2" __version__ = "0.9.3"
+21 -3
View File
@@ -103,6 +103,8 @@ dashboard; you never need to touch a config file.</p>
<i>Inspector dashboard login</i> with the username and password.</li> <i>Inspector dashboard login</i> with the username and password.</li>
<li>Open <code>http://&lt;your-server&gt;:8080</code> and sign in.</li> <li>Open <code>http://&lt;your-server&gt;:8080</code> and sign in.</li>
<li>Change the password under <b>Settings → Dashboard</b> if you like.</li> <li>Change the password under <b>Settings → Dashboard</b> if you like.</li>
<li>Check the <b>time zone</b>: at the bottom of any Settings page it says
<code>time zone: …</code>. If that's not your zone, see <a href="#settings">Time zone</a>.</li>
</ol> </ol>
</section> </section>
@@ -392,13 +394,29 @@ maintenance. Use <b>Prev day</b> / <b>Next day</b> to look back.</p>
<tr><td>Dashboard</td><td>Login username and password for this web UI.</td></tr> <tr><td>Dashboard</td><td>Login username and password for this web UI.</td></tr>
<tr><td>Event history</td><td>How many days of events to keep.</td></tr> <tr><td>Event history</td><td>How many days of events to keep.</td></tr>
<tr><td>Health summary</td><td>A periodic digest (e.g. daily) of what happened, sent through your notification channels.</td></tr> <tr><td>Health summary</td><td>A periodic digest (e.g. daily) of what happened, sent through your notification channels.</td></tr>
<tr><td>Maintenance</td><td>A daily quiet window (e.g. 02:00–05:00) so nightly backups or updates don't trigger restarts. Turn on <b>maintenance_window</b> on each container that should follow it.</td></tr> <tr><td>Maintenance</td><td>A daily quiet window (e.g. 02:00–05:00, in your <a href="#settings">time zone</a>) so nightly backups or updates don't trigger restarts. Turn on <b>maintenance_window</b> on each container that should follow it.</td></tr>
<tr><td>Notification mute</td><td>Turns the per-container <b>Mute notifications</b> buttons on or off, and sets how long a mute lasts.</td></tr> <tr><td>Notification mute</td><td>Turns the per-container <b>Mute notifications</b> buttons on or off, and sets how long a mute lasts.</td></tr>
<tr><td>Uptime Kuma</td><td>Connection to your Uptime Kuma status page, used by container reachability checks.</td></tr> <tr><td>Uptime Kuma</td><td>Connection to your Uptime Kuma status page, used by container reachability checks.</td></tr>
<tr><td>Portainer</td><td>Connection to Portainer, used to redeploy stacks. See <a href="#portainer">section 7</a>.</td></tr> <tr><td>Portainer</td><td>Connection to Portainer, used to redeploy stacks. See <a href="#portainer">section 7</a>.</td></tr>
</table> </table>
<p>At the bottom of every settings page is a strip showing where the config file lives and <p>At the bottom of every settings page is a strip showing where the config file lives,
whether Inspector can write to it.</p> whether Inspector can write to it, and which time zone it is using.</p>
<h3>Time zone</h3>
<p>Inspector uses the <code>TZ</code> setting of its Portainer stack for every time it shows or acts on:</p>
<ul>
<li><b>The maintenance window runs in this zone.</b> A 02:00–05:00 window with the wrong zone
runs hours off, so Inspector could restart containers during your backups and ignore
problems at other times.</li>
<li>Alert times (with the zone, e.g. <code>03:15 AM CET</code>), dashboard times, the Events
page's days (midnight to midnight), and logs.</li>
</ul>
<p>To change it: in Portainer open <b>Stacks → inspector → Editor</b>, set the <code>TZ</code>
environment variable to your zone's name, e.g. <code>America/New_York</code>,
<code>Europe/London</code> or <code>Australia/Sydney</code> (names from the
<a href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">list of time zones</a>;
abbreviations like <code>EST</code> don't work), then click <b>Update the stack</b>. Without
<code>TZ</code>, Inspector uses UTC.</p>
</section> </section>
<section id="restart"> <section id="restart">
+18 -11
View File
@@ -30,7 +30,8 @@ from .config import (
valid_hhmm, valid_hhmm,
write_config_raw, write_config_raw,
) )
from .formatting import PACIFIC, format_timestamp from . import formatting
from .formatting import format_timestamp
from .health import CONTAINER_NOT_FOUND, ERROR, HEALTHY, NOT_RUNNING, UNHEALTHY, UNREACHABLE from .health import CONTAINER_NOT_FOUND, ERROR, HEALTHY, NOT_RUNNING, UNHEALTHY, UNREACHABLE
from .maintenance import MaintenanceTracker from .maintenance import MaintenanceTracker
from .notification_mute import NotificationMuteTracker from .notification_mute import NotificationMuteTracker
@@ -180,20 +181,20 @@ def _status_snapshot(
def _parse_date_param(value: str | None) -> date: def _parse_date_param(value: str | None) -> date:
"""Parse a ?date=YYYY-MM-DD query param; falls back to "today" in """Parse a ?date=YYYY-MM-DD query param; falls back to "today" in the
America/Los_Angeles on missing/invalid input.""" configured time zone on missing/invalid input."""
if value: if value:
try: try:
return date.fromisoformat(value) return date.fromisoformat(value)
except ValueError: except ValueError:
pass pass
return datetime.now(PACIFIC).date() return datetime.now(formatting.LOCAL_TZ).date()
def _day_bounds_utc(day: date) -> tuple[datetime, datetime]: def _day_bounds_utc(day: date) -> tuple[datetime, datetime]:
"""UTC [start, end) bounds for a Pacific calendar day, extended one hour """UTC [start, end) bounds for a local calendar day, extended one hour
into the previous day for the lookback-overlap.""" into the previous day for the lookback-overlap."""
midnight = datetime.combine(day, datetime.min.time(), tzinfo=PACIFIC) midnight = datetime.combine(day, datetime.min.time(), tzinfo=formatting.LOCAL_TZ)
start = (midnight - timedelta(hours=1)).astimezone(timezone.utc) start = (midnight - timedelta(hours=1)).astimezone(timezone.utc)
end = (midnight + timedelta(days=1)).astimezone(timezone.utc) end = (midnight + timedelta(days=1)).astimezone(timezone.utc)
return start, end return start, end
@@ -218,8 +219,8 @@ def _read_events_range(db_path: str, start: datetime, end: datetime) -> list[dic
def _hour_markers(day: date) -> list[datetime]: def _hour_markers(day: date) -> list[datetime]:
"""24 Pacific-local hour boundaries for `day`, 00:00..23:00 ascending.""" """24 local hour boundaries for `day`, 00:00..23:00 ascending."""
midnight = datetime.combine(day, datetime.min.time(), tzinfo=PACIFIC) midnight = datetime.combine(day, datetime.min.time(), tzinfo=formatting.LOCAL_TZ)
return [midnight + timedelta(hours=h) for h in range(24)] return [midnight + timedelta(hours=h) for h in range(24)]
@@ -239,7 +240,7 @@ def _group_events_by_hour(
for ev in events: for ev in events:
try: try:
ts = datetime.fromisoformat(ev["ts"]).astimezone(PACIFIC) ts = datetime.fromisoformat(ev["ts"]).astimezone(formatting.LOCAL_TZ)
except (ValueError, KeyError): except (ValueError, KeyError):
continue continue
if prev_hour_start <= ts < markers[0]: if prev_hour_start <= ts < markers[0]:
@@ -1111,12 +1112,15 @@ def _render_events_page(
prev_day = (selected_day - timedelta(days=1)).isoformat() prev_day = (selected_day - timedelta(days=1)).isoformat()
next_day = (selected_day + timedelta(days=1)).isoformat() next_day = (selected_day + timedelta(days=1)).isoformat()
today = datetime.now(PACIFIC).date() today = datetime.now(formatting.LOCAL_TZ).date()
day_label = selected_day.strftime("%A, %B %-d, %Y") day_label = selected_day.strftime("%A, %B %-d, %Y")
parts.append('<div class="events-nav">\n') parts.append('<div class="events-nav">\n')
parts.append(f'<a class="day-nav" href="/events?date={prev_day}">&larr; Prev day</a>\n') parts.append(f'<a class="day-nav" href="/events?date={prev_day}">&larr; Prev day</a>\n')
parts.append(f'<span class="day-label">{_escape(day_label)}</span>\n') parts.append(
f'<span class="day-label">{_escape(day_label)}'
f' <span style="color:var(--muted); font-weight:400; font-size:0.8em;">· times in {_escape(formatting.LOCAL_TZ_NAME)}</span></span>\n'
)
if selected_day >= today: if selected_day >= today:
parts.append('<span class="day-nav disabled">Next day &rarr;</span>\n') parts.append('<span class="day-nav disabled">Next day &rarr;</span>\n')
else: else:
@@ -2062,6 +2066,7 @@ def _render_diag_strip(config_ref: ConfigRef) -> str:
return ( return (
'<div class="diag-strip">' '<div class="diag-strip">'
f'<span>config: <code>{_escape(str(path))}</code></span>' f'<span>config: <code>{_escape(str(path))}</code></span>'
f'<span>time zone: <code>{_escape(formatting.LOCAL_TZ_NAME)}</code></span>'
f"<span>{badge}{hint}</span>" f"<span>{badge}{hint}</span>"
f'<span style="margin-left:auto;">Inspector v{__version__}</span>' f'<span style="margin-left:auto;">Inspector v{__version__}</span>'
"</div>\n" "</div>\n"
@@ -2201,6 +2206,8 @@ def _settings_body_for(section: str, app: AppConfig, csrf_token: str) -> str:
return ( return (
'<h3 class="pane-title">Scheduled maintenance window</h3>' '<h3 class="pane-title">Scheduled maintenance window</h3>'
'<p class="pane-sub">Daily quiet hours. Containers that opt in (<code>maintenance_window: true</code> in their config) are not acted on during the window.</p>' '<p class="pane-sub">Daily quiet hours. Containers that opt in (<code>maintenance_window: true</code> in their config) are not acted on during the window.</p>'
f'<p class="pane-sub" style="margin-top:-1rem;">Times are in <b>{_escape(formatting.LOCAL_TZ_NAME)}</b>, '
"from the <code>TZ</code> setting of the Inspector stack. If that's not your time zone, the window runs at the wrong hours.</p>"
+ _settings_form("maintenance", "Maintenance", body, csrf_token) + _settings_form("maintenance", "Maintenance", body, csrf_token)
) )
+45 -6
View File
@@ -1,14 +1,49 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timezone import logging
from zoneinfo import ZoneInfo import os
from datetime import datetime, timezone, tzinfo
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
PACIFIC = ZoneInfo("America/Los_Angeles") log = logging.getLogger("inspector.formatting")
def format_timestamp(value: datetime | str | None) -> str: def _resolve_local_tz(env: dict | None = None) -> tuple[tzinfo, str]:
"""Render a timestamp in US Pacific time, e.g. '06-14 03:45 PM'. """The zone every displayed time uses, plus its name.
Follows the same rules as the C library (which drives log timestamps and
the maintenance window), so all times Inspector shows agree:
TZ set to a zone name -> that zone; TZ unset -> the container's
/etc/localtime; anything else -> UTC.
"""
env = os.environ if env is None else env
name = (env.get("TZ") or "").strip().lstrip(":")
if name:
try:
return ZoneInfo(name), name
except (ZoneInfoNotFoundError, ValueError):
log.warning(
"TZ=%r is not a known time zone name (e.g. Europe/Berlin); showing times in UTC",
name,
)
return timezone.utc, "UTC"
try:
with open("/etc/localtime", "rb") as fh:
zone = ZoneInfo.from_file(fh, key="localtime")
label = datetime.now(zone).tzname() or "localtime"
return zone, label
except (OSError, ValueError):
return timezone.utc, "UTC"
# Resolved once at import; the container's TZ doesn't change while it runs.
LOCAL_TZ, LOCAL_TZ_NAME = _resolve_local_tz()
def format_timestamp(value: datetime | str | None, *, with_zone: bool = False) -> str:
"""Render a timestamp in the configured local zone, e.g. '06-14 03:45 PM'
(with_zone adds the abbreviation: '06-14 03:45 PM CET').
Accepts a datetime, an ISO 8601 string (as stored by EventStore / Accepts a datetime, an ISO 8601 string (as stored by EventStore /
ContainerState), or None/"" (returns ""). Naive datetimes/strings are ContainerState), or None/"" (returns ""). Naive datetimes/strings are
@@ -24,4 +59,8 @@ def format_timestamp(value: datetime | str | None) -> str:
return value return value
if value.tzinfo is None: if value.tzinfo is None:
value = value.replace(tzinfo=timezone.utc) value = value.replace(tzinfo=timezone.utc)
return value.astimezone(PACIFIC).strftime("%m-%d %I:%M %p") local = value.astimezone(LOCAL_TZ)
text = local.strftime("%m-%d %I:%M %p")
if with_zone:
text += f" {local.tzname()}"
return text
+4 -4
View File
@@ -170,7 +170,7 @@ def _check_one(
message=( message=(
f"**{name}** is back to **HEALTHY** (was *{state.previous_status}*).\n\n" f"**{name}** is back to **HEALTHY** (was *{state.previous_status}*).\n\n"
f"- Errors before recovery: **{state.error_count}**\n" f"- Errors before recovery: **{state.error_count}**\n"
f"- Time: {format_timestamp(now)}" f"- Time: {format_timestamp(now, with_zone=True)}"
), ),
priority=3, priority=3,
) )
@@ -196,7 +196,7 @@ def _check_one(
f"**{name}** is missing from Docker.\n\n" f"**{name}** is missing from Docker.\n\n"
+ (f"- Detail: {result.message}\n" if result.message else "") + (f"- Detail: {result.message}\n" if result.message else "")
+ f"- Last known status: *{state.previous_status or 'unknown'}*\n" + f"- Last known status: *{state.previous_status or 'unknown'}*\n"
f"- Time: {format_timestamp(now)}" f"- Time: {format_timestamp(now, with_zone=True)}"
), ),
priority=7, priority=7,
) )
@@ -240,7 +240,7 @@ def _check_one(
+ ".\n\n" + ".\n\n"
f"- Error count: **{state.error_count}/{container_config.max_error_count}**\n" f"- Error count: **{state.error_count}/{container_config.max_error_count}**\n"
f"- Previous status: *{state.previous_status or 'unknown'}*\n" f"- Previous status: *{state.previous_status or 'unknown'}*\n"
f"- Time: {format_timestamp(now)}" f"- Time: {format_timestamp(now, with_zone=True)}"
), ),
priority=5, priority=5,
) )
@@ -276,7 +276,7 @@ def _check_one(
f"**{container_config.max_restarts_per_hour}** restart(s)/hour.\n\n" f"**{container_config.max_restarts_per_hour}** restart(s)/hour.\n\n"
f"- Current status: **{result.status}**\n" f"- Current status: **{result.status}**\n"
f"- Restarts paused until the hourly window clears\n" f"- Restarts paused until the hourly window clears\n"
f"- Time: {format_timestamp(now)}" f"- Time: {format_timestamp(now, with_zone=True)}"
), ),
priority=9, priority=9,
) )
+15 -15
View File
@@ -228,7 +228,7 @@ def _perform_restart_then_redeploy(
f"**{result.status}** check(s).\n\n" f"**{result.status}** check(s).\n\n"
f"- If it doesn't recover, Inspector escalates to a Portainer stack redeploy\n" f"- If it doesn't recover, Inspector escalates to a Portainer stack redeploy\n"
f"- Cooldown: **{container_config.cooldown_seconds}s** after restart\n" f"- Cooldown: **{container_config.cooldown_seconds}s** after restart\n"
f"- Time: {format_timestamp(now)}" f"- Time: {format_timestamp(now, with_zone=True)}"
), ),
priority=6, priority=6,
) )
@@ -242,7 +242,7 @@ def _perform_restart_then_redeploy(
state.restart_limit_notified = False state.restart_limit_notified = False
log.info( log.info(
"%-*s restart succeeded; cooldown until %s (will redeploy if still failing)", "%-*s restart succeeded; cooldown until %s (will redeploy if still failing)",
width, name, state.cooldown_until.isoformat(), width, name, format_timestamp(state.cooldown_until, with_zone=True),
) )
eventstore.record( eventstore.record(
container=name, container=name,
@@ -250,15 +250,15 @@ def _perform_restart_then_redeploy(
old_status=state.previous_status, old_status=state.previous_status,
new_status=result.status, new_status=result.status,
error_count=state.error_count, error_count=state.error_count,
message=f"restart succeeded; cooldown until {state.cooldown_until.isoformat()}", message=f"restart succeeded; cooldown until {format_timestamp(state.cooldown_until, with_zone=True)}",
) )
notifier.notify( notifier.notify(
title=f"✅ Inspector: {name} restart completed", title=f"✅ Inspector: {name} restart completed",
message=( message=(
f"**{name}** was successfully restarted by Docker.\n\n" f"**{name}** was successfully restarted by Docker.\n\n"
f"- Previous status: *{result.status}*\n" f"- Previous status: *{result.status}*\n"
f"- Cooldown until: {state.cooldown_until.isoformat()}\n" f"- Cooldown until: {format_timestamp(state.cooldown_until, with_zone=True)}\n"
f"- Time: {format_timestamp(now)}" f"- Time: {format_timestamp(now, with_zone=True)}"
), ),
priority=3, priority=3,
) )
@@ -321,7 +321,7 @@ def _perform_docker_action(
f"{verb.capitalize()}ing **{name}** after **{state.error_count}** consecutive " f"{verb.capitalize()}ing **{name}** after **{state.error_count}** consecutive "
f"**{result.status}** check(s).\n\n" f"**{result.status}** check(s).\n\n"
f"- Cooldown: **{container_config.cooldown_seconds}s** after {verb}\n" f"- Cooldown: **{container_config.cooldown_seconds}s** after {verb}\n"
f"- Time: {format_timestamp(now)}" f"- Time: {format_timestamp(now, with_zone=True)}"
), ),
priority=6, priority=6,
) )
@@ -335,22 +335,22 @@ def _perform_docker_action(
state.record_restart(now, container_config.cooldown_seconds) state.record_restart(now, container_config.cooldown_seconds)
state.last_notification_state = "RESTARTED" state.last_notification_state = "RESTARTED"
state.restart_limit_notified = False state.restart_limit_notified = False
log.info("%-*s %s succeeded; cooldown until %s", width, name, verb, state.cooldown_until.isoformat()) log.info("%-*s %s succeeded; cooldown until %s", width, name, verb, format_timestamp(state.cooldown_until, with_zone=True))
eventstore.record( eventstore.record(
container=name, container=name,
event_type=EVENT_RESTART, event_type=EVENT_RESTART,
old_status=state.previous_status, old_status=state.previous_status,
new_status=result.status, new_status=result.status,
error_count=state.error_count, error_count=state.error_count,
message=f"{verb} succeeded; cooldown until {state.cooldown_until.isoformat()}", message=f"{verb} succeeded; cooldown until {format_timestamp(state.cooldown_until, with_zone=True)}",
) )
notifier.notify( notifier.notify(
title=f"✅ Inspector: {name} {verb} completed", title=f"✅ Inspector: {name} {verb} completed",
message=( message=(
f"**{name}** was successfully {verb}ed by Docker.\n\n" f"**{name}** was successfully {verb}ed by Docker.\n\n"
f"- Previous status: *{result.status}*\n" f"- Previous status: *{result.status}*\n"
f"- Cooldown until: {state.cooldown_until.isoformat()}\n" f"- Cooldown until: {format_timestamp(state.cooldown_until, with_zone=True)}\n"
f"- Time: {format_timestamp(now)}" f"- Time: {format_timestamp(now, with_zone=True)}"
), ),
priority=3, priority=3,
) )
@@ -362,7 +362,7 @@ def _perform_docker_action(
f"{verb.capitalize()} of **{name}** failed.\n\n" f"{verb.capitalize()} of **{name}** failed.\n\n"
f"- Status: **{result.status}**\n" f"- Status: **{result.status}**\n"
f"- Error count: **{state.error_count}**\n" f"- Error count: **{state.error_count}**\n"
f"- Time: {format_timestamp(now)}\n\n" f"- Time: {format_timestamp(now, with_zone=True)}\n\n"
f"```\n{err}\n```" f"```\n{err}\n```"
), ),
priority=9, priority=9,
@@ -469,7 +469,7 @@ def _perform_portainer_redeploy(
f"Redeploying Portainer stack **{stack_name}** (environment **{environment}**) " f"Redeploying Portainer stack **{stack_name}** (environment **{environment}**) "
f"to recover **{name}**.\n\n" f"to recover **{name}**.\n\n"
f"- Status: **{result.status}**\n" f"- Status: **{result.status}**\n"
f"- Time: {format_timestamp(now)}" f"- Time: {format_timestamp(now, with_zone=True)}"
), ),
priority=6, priority=6,
) )
@@ -483,7 +483,7 @@ def _perform_portainer_redeploy(
message=( message=(
f"Portainer redeploy of **{stack_name}** (environment **{environment}**) failed.\n\n" f"Portainer redeploy of **{stack_name}** (environment **{environment}**) failed.\n\n"
f"- Triggered by: **{name}** ({result.status})\n" f"- Triggered by: **{name}** ({result.status})\n"
f"- Time: {format_timestamp(now)}\n\n" f"- Time: {format_timestamp(now, with_zone=True)}\n\n"
f"```\n{redeploy_result.message}\n```" f"```\n{redeploy_result.message}\n```"
), ),
priority=9, priority=9,
@@ -516,7 +516,7 @@ def _perform_portainer_redeploy(
f"successfully redeployed to recover **{name}**.\n\n" f"successfully redeployed to recover **{name}**.\n\n"
f"- Triggered by: *{result.status}*\n" f"- Triggered by: *{result.status}*\n"
f"- Post-redeploy wait: {wait_seconds}s\n" f"- Post-redeploy wait: {wait_seconds}s\n"
f"- Time: {format_timestamp(now)}" f"- Time: {format_timestamp(now, with_zone=True)}"
), ),
priority=4, priority=4,
) )
@@ -536,7 +536,7 @@ def _perform_portainer_redeploy(
f"Portainer stack **{stack_name}** was redeployed, but the HTTP recheck " f"Portainer stack **{stack_name}** was redeployed, but the HTTP recheck "
f"for **{name}** still reports a failure.\n\n" f"for **{name}** still reports a failure.\n\n"
f"- Recheck result: *{recheck.message}*\n" f"- Recheck result: *{recheck.message}*\n"
f"- Time: {format_timestamp(now)}" f"- Time: {format_timestamp(now, with_zone=True)}"
), ),
priority=6, priority=6,
) )