Skip to content
Merged
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
9 changes: 5 additions & 4 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,16 @@ jobs:
with:
python-version: "3.11"

- name: Install dependencies
- name: Install pip-audit
run: |
python -m pip install --upgrade pip
pip install pip-audit
# Install only the dependencies, not the local package
pip install PyYAML requests

- name: Run pip-audit
run: pip-audit --strict --progress-spinner off
# Audit only direct project dependencies (requirements files).
# CVE-2026-4539 (pygments) is a transitive dep of coverage/rich with no fix
# version available yet — ignored until a patched release is published.
run: pip-audit --strict --progress-spinner off --no-deps -r requirements.txt -r requirements-dev.txt --ignore-vuln CVE-2026-4539

codeql:
runs-on: ubuntu-latest
Expand Down
14 changes: 8 additions & 6 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,10 +552,12 @@ def test_history_check_fields(self, running_server: ApiServer, db_conn: sqlite3.
assert check_data["response_time_ms"] == 250
assert check_data["error"] == "Service unavailable"

def test_history_limits_to_100(self, running_server: ApiServer, db_conn: sqlite3.Connection) -> None:
"""GET /history/<name> limits results to 100 checks."""
# Insert 110 checks
for i in range(110):
def test_history_limits_to_max(self, running_server: ApiServer, db_conn: sqlite3.Connection) -> None:
"""GET /history/<name> limits results to HISTORY_LIMIT checks."""
from webstatuspi.api import HISTORY_LIMIT

# Insert HISTORY_LIMIT + 10 checks to verify the limit is enforced
for i in range(HISTORY_LIMIT + 10):
check = CheckResult(
url_name="LIMIT_TEST",
url="https://limit.example.com",
Expand All @@ -570,8 +572,8 @@ def test_history_limits_to_100(self, running_server: ApiServer, db_conn: sqlite3
status, body = self._get(running_server, "/history/LIMIT_TEST")

assert status == 200
assert body["count"] == 100
assert len(body["checks"]) == 100
assert body["count"] == HISTORY_LIMIT
assert len(body["checks"]) == HISTORY_LIMIT


class TestResetEndpoint:
Expand Down
20 changes: 12 additions & 8 deletions webstatuspi/_dashboard/static/charts.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
clearContainer(container);

// Filter checks with valid response times
// Note: checks arrive pre-sorted chronologically (oldest→newest) via core.js .reverse()
const validChecks = checks.filter(c => c.response_time_ms !== null && c.response_time_ms !== undefined);

if (validChecks.length === 0) {
Expand All @@ -91,10 +92,12 @@
: validChecks;

// Calculate scales
const times = data.map(c => new Date(c.checked_at).getTime());
// X axis: fixed 24h window ending now, so sparse data doesn't stretch to fill the full width
const now = Date.now();
const windowMs = 24 * 60 * 60 * 1000;
const minTime = now - windowMs;
const maxTime = now;
const values = data.map(c => c.response_time_ms);
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
const maxValue = Math.max(...values, 100); // At least 100ms scale

const xScale = (t) => padding.left + ((t - minTime) / (maxTime - minTime || 1)) * chartWidth;
Expand Down Expand Up @@ -267,11 +270,12 @@
const downtimePeriods = sortedChecks.filter(c => !c.is_up).length;
const ariaLabel = `Uptime: ${uptimePercent}% over the last 24 hours. ${downtimePeriods} downtime period${downtimePeriods !== 1 ? 's' : ''} detected.`;

// Calculate time range
const times = sortedChecks.map(c => new Date(c.checked_at).getTime());
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
const timeRange = maxTime - minTime || 1;
// Fixed 24h window ending now — prevents sparse data from stretching across the full width
const now = Date.now();
const windowMs = 24 * 60 * 60 * 1000;
const minTime = now - windowMs;
const maxTime = now;
const timeRange = windowMs;

const xScale = (t) => padding.left + ((t - minTime) / timeRange) * chartWidth;

Expand Down
5 changes: 3 additions & 2 deletions webstatuspi/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@
RATE_LIMIT_WINDOW_SECONDS = 60

# Maximum number of history records to return per request.
# Balances memory usage vs useful data for 24-hour view at typical intervals.
HISTORY_LIMIT = 100
# At 60s check interval, 1440 records = 24 hours of history for the modal view.
# Pi 1B+ can handle this: each record is ~100 bytes → ~140KB per URL in memory.
HISTORY_LIMIT = 1440


class RateLimiter:
Expand Down
Loading