1270 lines
47 KiB
Python
1270 lines
47 KiB
Python
import os
|
|
import requests
|
|
import subprocess
|
|
from flask import Flask, render_template_string, jsonify, request, send_from_directory
|
|
from socket import gethostbyname_ex
|
|
from datetime import datetime
|
|
|
|
app = Flask(__name__)
|
|
|
|
# Config
|
|
REPORTER_SERVICE = os.environ.get("REPORTER_SERVICE", "ntp-reporter-svc.default.svc.cluster.local")
|
|
BASE_URL = os.environ.get("BASE_URL", "https://time.dws.rip")
|
|
|
|
# Tracking table config
|
|
TRACKING_METRICS_ORDER = [
|
|
"Reference ID", "Ref Source IP", "Stratum", "Ref time (UTC)", "System time",
|
|
"Last offset", "RMS offset", "Frequency", "Residual freq", "Skew",
|
|
"Root delay", "Root dispersion", "Update interval", "Leap status"
|
|
]
|
|
|
|
# Sources table config
|
|
SOURCES_COLUMNS_ORDER = [
|
|
"DWS PEER", "ModeState", "Name/IP address", "Stratum", "Poll", "Reach",
|
|
"LastRx", "Last sample", "Std Dev"
|
|
]
|
|
|
|
# Metric Definitions
|
|
TRACKING_METRICS_DEFS = {
|
|
"Reference ID": "Identifier of current time source (IP or refclock ID)",
|
|
"Ref Source IP": "IP address of the reference time source",
|
|
"Stratum": "Distance from primary time source (lower is better, 1-16)",
|
|
"Ref time (UTC)": "Last time the reference was updated",
|
|
"System time": "Offset between system clock and reference time (seconds)",
|
|
"Last offset": "Offset of last clock update (seconds)",
|
|
"RMS offset": "Root mean square of recent offset values (long-term average)",
|
|
"Frequency": "Rate of system clock drift (ppm - parts per million)",
|
|
"Residual freq": "Residual frequency error not yet corrected",
|
|
"Skew": "Estimated error bound of frequency (accuracy metric)",
|
|
"Root delay": "Total network delay to stratum-1 server (seconds)",
|
|
"Root dispersion": "Total dispersion accumulated to stratum-1 server",
|
|
"Update interval": "Time between clock updates (seconds)",
|
|
"Leap status": "Leap second indicator (Normal, Insert, Delete, or Not synced)"
|
|
}
|
|
|
|
SOURCES_METRICS_DEFS = {
|
|
"DWS PEER": "Node identifier for this NTP daemon instance",
|
|
"ModeState": "Source mode (^=server, ==peer) & state (*=current sync)",
|
|
"Name/IP address": "Hostname or IP address of the NTP source",
|
|
"Stratum": "Stratum level of the source (1=primary reference)",
|
|
"Poll": "Polling interval to source (log2 seconds, e.g., 6 = 64s)",
|
|
"Reach": "Reachability register (377 octal = all 8 recent polls OK)",
|
|
"LastRx": "Time since last successful response from source",
|
|
"Last sample": "Offset measurement from last valid sample (seconds)",
|
|
"Std Dev": "Standard deviation of offset (jitter measurement)"
|
|
}
|
|
|
|
#
|
|
# HTML Template - DWS Design System Compliant
|
|
#
|
|
HTML_TEMPLATE = """<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>DWS LLC NTP STATUS</title>
|
|
<style>
|
|
/* ===================================================================
|
|
DWS DESIGN SYSTEM - PRAGMATIC FUTURISM
|
|
Color Palette, Typography, Grid System
|
|
=================================================================== */
|
|
|
|
/* Font Faces - DWS Two-Typeface System */
|
|
|
|
/* Primary Typeface: Inter (Workhorse Grotesk) */
|
|
/* Using system Inter or CDN fallback - for production, serve from /static/fonts/ */
|
|
@import url('https://rsms.me/inter/inter.css');
|
|
|
|
/* Secondary Typeface: Berkeley Mono (Code Monospace) */
|
|
@font-face {
|
|
font-family: 'BerkeleyMono';
|
|
src: url('/static/fonts/BerkeleyMono-Regular.woff2') format('woff2');
|
|
font-weight: 400;
|
|
font-style: normal;
|
|
font-display: swap;
|
|
}
|
|
@font-face {
|
|
font-family: 'BerkeleyMono';
|
|
src: url('/static/fonts/BerkeleyMono-Bold.woff2') format('woff2');
|
|
font-weight: 700;
|
|
font-style: normal;
|
|
font-display: swap;
|
|
}
|
|
|
|
/* ===================================================================
|
|
DWS COLOR SYSTEM - CATHODE MODE (Dark)
|
|
Exact specifications from DWS Table 1
|
|
=================================================================== */
|
|
:root[data-mode="cathode"], :root {
|
|
/* Base Colors - Cathode Mode (Dark) */
|
|
--cathode-black: #101418;
|
|
--cathode-surface: #1A2026;
|
|
--cathode-primary: #E0E6EB;
|
|
--cathode-secondary: #98A3AF;
|
|
|
|
/* Functional Colors - Cathode Mode */
|
|
--signal-amber: #FFA800;
|
|
--engineering-orange: #F97316;
|
|
--warning-red: #EF4444;
|
|
--vector-green: #34D399;
|
|
--systems-blue: #3B82F6;
|
|
|
|
/* Semantic Mappings - Cathode Mode */
|
|
--dws-bg-primary: var(--cathode-black);
|
|
--dws-bg-secondary: var(--cathode-black);
|
|
--dws-bg-elevated: var(--cathode-surface);
|
|
--dws-text-primary: var(--cathode-primary);
|
|
--dws-text-secondary: var(--cathode-secondary);
|
|
--dws-text-tertiary: var(--cathode-secondary);
|
|
--dws-border-primary: var(--cathode-surface);
|
|
--dws-border-secondary: var(--cathode-secondary);
|
|
--dws-accent-primary: var(--signal-amber);
|
|
--dws-accent-cta: var(--engineering-orange);
|
|
--dws-success: var(--vector-green);
|
|
--dws-error: var(--warning-red);
|
|
--dws-info: var(--systems-blue);
|
|
}
|
|
|
|
/* ===================================================================
|
|
DWS COLOR SYSTEM - VELLUM MODE (Light)
|
|
Exact specifications from DWS Table 1
|
|
=================================================================== */
|
|
:root[data-mode="vellum"] {
|
|
/* Base Colors - Vellum Mode (Light) */
|
|
--vellum-white: #FAF8F5;
|
|
--vellum-surface: #F2F0ED;
|
|
--vellum-primary: #2D2A26;
|
|
--vellum-secondary: #78716A;
|
|
|
|
/* Functional Colors - Vellum Mode (same as Cathode) */
|
|
--signal-amber: #FFA800;
|
|
--engineering-orange: #F97316;
|
|
--warning-red: #EF4444;
|
|
--vector-green: #34D399;
|
|
--systems-blue: #3B82F6;
|
|
|
|
/* Semantic Mappings - Vellum Mode */
|
|
--dws-bg-primary: var(--vellum-white);
|
|
--dws-bg-secondary: var(--vellum-white);
|
|
--dws-bg-elevated: var(--vellum-surface);
|
|
--dws-text-primary: var(--vellum-primary);
|
|
--dws-text-secondary: var(--vellum-secondary);
|
|
--dws-text-tertiary: var(--vellum-secondary);
|
|
--dws-border-primary: var(--vellum-surface);
|
|
--dws-border-secondary: var(--vellum-secondary);
|
|
--dws-accent-primary: var(--signal-amber);
|
|
--dws-accent-cta: var(--engineering-orange);
|
|
--dws-success: var(--vector-green);
|
|
--dws-error: var(--warning-red);
|
|
--dws-info: var(--systems-blue);
|
|
}
|
|
|
|
/* ===================================================================
|
|
DWS SPACING SYSTEM - 8px Base Unit
|
|
=================================================================== */
|
|
:root {
|
|
--space-xxs: 4px; /* 0.5 unit */
|
|
--space-xs: 8px; /* 1 unit */
|
|
--space-sm: 16px; /* 2 units */
|
|
--space-md: 24px; /* 3 units */
|
|
--space-lg: 32px; /* 4 units */
|
|
--space-xl: 48px; /* 6 units */
|
|
--space-xxl: 64px; /* 8 units */
|
|
}
|
|
|
|
/* ===================================================================
|
|
DWS TYPOGRAPHY SCALE - Two-Typeface System
|
|
Primary: Inter (neo-grotesk) | Secondary: Berkeley Mono (monospace)
|
|
Based on DWS Table 2 - 1.25 Major Third scale, 16px base
|
|
=================================================================== */
|
|
|
|
/* Typography Variable Definitions */
|
|
:root {
|
|
/* Primary Typeface (Workhorse Grotesk) */
|
|
--font-primary: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
/* Secondary Typeface (Code Monospace) */
|
|
--font-mono: 'BerkeleyMono', 'IBM Plex Mono', 'Courier New', monospace;
|
|
|
|
/* Display & Headings */
|
|
--type-display-size: 4.883rem; /* 78.13px */
|
|
--type-display-height: 80px;
|
|
--type-h1-size: 3.906rem; /* 62.50px */
|
|
--type-h1-height: 64px;
|
|
--type-h2-size: 3.125rem; /* 50.00px */
|
|
--type-h2-height: 56px;
|
|
--type-h3-size: 2.5rem; /* 40.00px */
|
|
--type-h3-height: 48px;
|
|
--type-h4-size: 2rem; /* 32.00px */
|
|
--type-h4-height: 40px;
|
|
--type-h5-size: 1.6rem; /* 25.60px */
|
|
--type-h5-height: 32px;
|
|
|
|
/* Body Text */
|
|
--type-body-lg-size: 1.25rem; /* 20.00px */
|
|
--type-body-lg-height: 32px;
|
|
--type-body-size: 1rem; /* 16.00px */
|
|
--type-body-height: 24px;
|
|
--type-body-sm-size: 0.8rem; /* 12.80px */
|
|
--type-body-sm-height: 24px;
|
|
|
|
/* Labels (All Caps) */
|
|
--type-label-lg-size: 1rem; /* 16.00px */
|
|
--type-label-lg-height: 24px;
|
|
--type-label-size: 0.8rem; /* 12.80px */
|
|
--type-label-height: 16px;
|
|
--type-label-sm-size: 0.64rem; /* 10.24px */
|
|
--type-label-sm-height: 16px;
|
|
|
|
/* Code/Data */
|
|
--type-code-size: 0.875rem; /* 14.00px */
|
|
--type-code-height: 24px;
|
|
--type-code-sm-size: 0.75rem; /* 12.00px */
|
|
--type-code-sm-height: 16px;
|
|
}
|
|
|
|
/* Base Styles */
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
html {
|
|
font-size: 16px; /* Base size for rem calculations */
|
|
background-color: var(--dws-bg-primary);
|
|
transition: background-color 0.3s ease;
|
|
}
|
|
|
|
body {
|
|
font-family: var(--font-primary);
|
|
font-weight: 400;
|
|
font-size: var(--type-body-size);
|
|
line-height: var(--type-body-height);
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
background-color: var(--dws-bg-primary);
|
|
color: var(--dws-text-primary);
|
|
margin: 0;
|
|
overflow-x: auto;
|
|
transition: background-color 0.3s ease, color 0.3s ease;
|
|
}
|
|
|
|
/* Typography Classes */
|
|
.display {
|
|
font-family: var(--font-primary);
|
|
font-size: var(--type-display-size);
|
|
line-height: var(--type-display-height);
|
|
font-weight: 700;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
h1, .heading-01 {
|
|
font-family: var(--font-primary);
|
|
font-size: var(--type-h1-size);
|
|
line-height: var(--type-h1-height);
|
|
font-weight: 700;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
h2, .heading-02 {
|
|
font-family: var(--font-primary);
|
|
font-size: var(--type-h2-size);
|
|
line-height: var(--type-h2-height);
|
|
font-weight: 600;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
h3, .heading-03 {
|
|
font-family: var(--font-primary);
|
|
font-size: var(--type-h3-size);
|
|
line-height: var(--type-h3-height);
|
|
font-weight: 600;
|
|
}
|
|
|
|
h4, .heading-04 {
|
|
font-family: var(--font-primary);
|
|
font-size: var(--type-h4-size);
|
|
line-height: var(--type-h4-height);
|
|
font-weight: 600;
|
|
}
|
|
|
|
h5, .heading-05 {
|
|
font-family: var(--font-primary);
|
|
font-size: var(--type-h5-size);
|
|
line-height: var(--type-h5-height);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.body-large {
|
|
font-size: var(--type-body-lg-size);
|
|
line-height: var(--type-body-lg-height);
|
|
}
|
|
|
|
.body-small {
|
|
font-size: var(--type-body-sm-size);
|
|
line-height: var(--type-body-sm-height);
|
|
}
|
|
|
|
.label-large, .label-lg {
|
|
font-family: var(--font-primary);
|
|
font-size: var(--type-label-lg-size);
|
|
line-height: var(--type-label-lg-height);
|
|
font-weight: 500;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.label-default, .label {
|
|
font-family: var(--font-primary);
|
|
font-size: var(--type-label-size);
|
|
line-height: var(--type-label-height);
|
|
font-weight: 500;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.label-small, .label-sm {
|
|
font-family: var(--font-primary);
|
|
font-size: var(--type-label-sm-size);
|
|
line-height: var(--type-label-sm-height);
|
|
font-weight: 500;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.code-default, code {
|
|
font-family: var(--font-mono);
|
|
font-size: var(--type-code-size);
|
|
line-height: var(--type-code-height);
|
|
font-weight: 400;
|
|
}
|
|
|
|
.code-small {
|
|
font-family: var(--font-mono);
|
|
font-size: var(--type-code-sm-size);
|
|
line-height: var(--type-code-sm-height);
|
|
font-weight: 400;
|
|
}
|
|
|
|
pre {
|
|
margin: 0;
|
|
padding: 0;
|
|
white-space: pre;
|
|
font-family: var(--font-mono);
|
|
font-size: var(--type-code-size);
|
|
line-height: var(--type-code-height);
|
|
color: var(--dws-text-primary);
|
|
}
|
|
|
|
/* Section Headers - Using DWS label styles */
|
|
b, .section-header {
|
|
font-family: var(--font-primary);
|
|
font-weight: 700;
|
|
font-size: var(--type-label-size);
|
|
line-height: var(--type-label-height);
|
|
color: var(--dws-accent-primary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
/* Dynamic Clock Spans */
|
|
#clock-time {
|
|
font-family: var(--font-mono);
|
|
color: var(--dws-text-primary);
|
|
font-weight: 700;
|
|
font-size: var(--type-body-lg-size);
|
|
line-height: var(--type-body-lg-height);
|
|
}
|
|
|
|
#clock-date {
|
|
font-family: var(--font-mono);
|
|
color: var(--dws-text-secondary);
|
|
font-size: var(--type-code-size);
|
|
line-height: var(--type-code-height);
|
|
}
|
|
|
|
#clock-status {
|
|
font-family: var(--font-mono);
|
|
color: var(--dws-info);
|
|
font-weight: 700;
|
|
}
|
|
|
|
#clock-status.synced {
|
|
color: var(--dws-success);
|
|
}
|
|
|
|
#clock-status.error {
|
|
color: var(--dws-error);
|
|
}
|
|
|
|
#clock-offset {
|
|
font-family: var(--font-mono);
|
|
color: var(--dws-text-secondary);
|
|
}
|
|
|
|
/* Semantic Color Classes */
|
|
.status-normal { color: var(--dws-success); }
|
|
.status-warning { color: var(--dws-accent-primary); }
|
|
.status-error { color: var(--dws-error); }
|
|
.status-info { color: var(--dws-info); }
|
|
|
|
/* ===================================================================
|
|
MODE TOGGLE COMPONENT - DWS Style
|
|
=================================================================== */
|
|
.mode-toggle-container {
|
|
position: fixed;
|
|
top: var(--space-md);
|
|
right: var(--space-md);
|
|
z-index: 1000;
|
|
}
|
|
|
|
.mode-toggle {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-xs);
|
|
background: var(--dws-bg-elevated);
|
|
border: 1px solid var(--dws-border-primary);
|
|
border-radius: 0; /* DWS: 0px border-radius */
|
|
padding: var(--space-xs) var(--space-sm);
|
|
cursor: pointer;
|
|
transition: border-color 0.2s ease, background-color 0.2s ease;
|
|
user-select: none;
|
|
}
|
|
|
|
.mode-toggle:hover {
|
|
border-color: var(--dws-accent-primary);
|
|
background: var(--dws-bg-secondary);
|
|
}
|
|
|
|
.mode-toggle:active {
|
|
border-color: var(--dws-accent-cta);
|
|
}
|
|
|
|
.mode-toggle-label {
|
|
font-size: 12px;
|
|
line-height: 16px;
|
|
color: var(--dws-text-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.mode-toggle-switch {
|
|
position: relative;
|
|
width: 48px;
|
|
height: 24px;
|
|
background: var(--dws-bg-secondary);
|
|
border: 1px solid var(--dws-border-primary);
|
|
border-radius: 0; /* DWS: 0px border-radius */
|
|
transition: background-color 0.3s ease;
|
|
}
|
|
|
|
.mode-toggle-switch::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 2px;
|
|
left: 2px;
|
|
width: 18px;
|
|
height: 18px;
|
|
background: var(--dws-accent-primary);
|
|
border-radius: 0; /* DWS: 0px border-radius */
|
|
transition: transform 0.3s ease, background-color 0.3s ease;
|
|
}
|
|
|
|
.mode-toggle.active .mode-toggle-switch {
|
|
background: var(--dws-bg-elevated);
|
|
}
|
|
|
|
.mode-toggle.active .mode-toggle-switch::after {
|
|
transform: translateX(24px);
|
|
background: var(--dws-accent-cta);
|
|
}
|
|
|
|
/* ===================================================================
|
|
SEPARATOR LINES - Hairline Borders
|
|
=================================================================== */
|
|
.separator {
|
|
border: 0;
|
|
border-top: 1px solid var(--dws-border-primary);
|
|
margin: var(--space-md) 0;
|
|
}
|
|
|
|
/* ===================================================================
|
|
DWS 12-COLUMN GRID SYSTEM - Section V
|
|
=================================================================== */
|
|
.dws-container {
|
|
width: 100%;
|
|
max-width: 1440px;
|
|
margin: 0 auto;
|
|
padding: 0 var(--space-xxl); /* 64px page margins */
|
|
}
|
|
|
|
.dws-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(12, 1fr);
|
|
gap: 16px; /* 16px gutters */
|
|
margin-bottom: var(--space-xl);
|
|
}
|
|
|
|
/* Column Span Classes */
|
|
.col-1 { grid-column: span 1; }
|
|
.col-2 { grid-column: span 2; }
|
|
.col-3 { grid-column: span 3; }
|
|
.col-4 { grid-column: span 4; }
|
|
.col-5 { grid-column: span 5; }
|
|
.col-6 { grid-column: span 6; }
|
|
.col-7 { grid-column: span 7; }
|
|
.col-8 { grid-column: span 8; }
|
|
.col-9 { grid-column: span 9; }
|
|
.col-10 { grid-column: span 10; }
|
|
.col-11 { grid-column: span 11; }
|
|
.col-12 { grid-column: span 12; }
|
|
|
|
/* ===================================================================
|
|
DWS PANEL COMPONENT - Section VI
|
|
=================================================================== */
|
|
.dws-panel {
|
|
background: var(--dws-bg-elevated);
|
|
border: 1px solid var(--dws-border-primary);
|
|
border-radius: 0; /* DWS: 0px border-radius */
|
|
padding: var(--space-lg);
|
|
margin-bottom: var(--space-xl);
|
|
transition: background-color 0.3s ease, border-color 0.3s ease;
|
|
}
|
|
|
|
.dws-panel-header {
|
|
font-family: var(--font-primary);
|
|
font-size: var(--type-label-lg-size);
|
|
line-height: var(--type-label-lg-height);
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: var(--dws-accent-primary);
|
|
margin-bottom: var(--space-md);
|
|
padding-bottom: var(--space-sm);
|
|
border-bottom: 1px solid var(--dws-border-secondary);
|
|
}
|
|
|
|
.dws-panel-body {
|
|
color: var(--dws-text-primary);
|
|
}
|
|
|
|
/* ===================================================================
|
|
DWS TABLE COMPONENT - Section 6.3
|
|
=================================================================== */
|
|
.dws-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-family: var(--font-mono);
|
|
font-size: var(--type-code-size);
|
|
line-height: var(--type-code-height);
|
|
margin-bottom: var(--space-lg);
|
|
}
|
|
|
|
.dws-table thead {
|
|
border-bottom: 1px solid var(--dws-border-primary);
|
|
}
|
|
|
|
.dws-table th {
|
|
font-family: var(--font-primary);
|
|
font-size: var(--type-label-size);
|
|
line-height: var(--type-label-height);
|
|
font-weight: 500;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
text-align: left;
|
|
padding: var(--space-sm) var(--space-xs);
|
|
color: var(--dws-text-secondary);
|
|
border-bottom: 1px solid var(--dws-border-primary);
|
|
}
|
|
|
|
.dws-table td {
|
|
padding: var(--space-sm) var(--space-xs);
|
|
border-bottom: 1px solid var(--dws-border-secondary);
|
|
color: var(--dws-text-primary);
|
|
}
|
|
|
|
.dws-table tbody tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
|
|
/* Table cell alignment per DWS spec */
|
|
.dws-table td:first-child,
|
|
.dws-table th:first-child {
|
|
text-align: left; /* Text data: left-aligned */
|
|
}
|
|
|
|
.dws-table td.numeric,
|
|
.dws-table th.numeric {
|
|
text-align: right; /* Numerical data: right-aligned */
|
|
}
|
|
|
|
.dws-table tr:hover {
|
|
background: var(--dws-bg-elevated);
|
|
}
|
|
|
|
/* ===================================================================
|
|
DWS DEFINITION LIST STYLING
|
|
=================================================================== */
|
|
dl {
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
dt {
|
|
font-family: var(--font-primary);
|
|
font-size: var(--type-label-sm-size);
|
|
line-height: var(--type-label-sm-height);
|
|
font-weight: 500;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: var(--dws-text-secondary);
|
|
margin-top: var(--space-sm);
|
|
margin-bottom: var(--space-xxs);
|
|
}
|
|
|
|
dt:first-child {
|
|
margin-top: 0;
|
|
}
|
|
|
|
dd {
|
|
font-family: var(--font-primary);
|
|
font-size: var(--type-body-sm-size);
|
|
line-height: var(--type-body-sm-height);
|
|
color: var(--dws-text-primary);
|
|
margin: 0;
|
|
margin-left: 0;
|
|
padding-left: var(--space-md);
|
|
border-left: 2px solid var(--dws-border-primary);
|
|
}
|
|
|
|
/* ===================================================================
|
|
DWS BUTTON COMPONENT LIBRARY - Section 6.2
|
|
=================================================================== */
|
|
.dws-button {
|
|
font-family: var(--font-primary);
|
|
font-size: var(--type-label-size);
|
|
line-height: var(--type-label-height);
|
|
font-weight: 500;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
padding: var(--space-sm) var(--space-lg);
|
|
border: 1px solid;
|
|
border-radius: 0; /* DWS: 0px border-radius */
|
|
cursor: pointer;
|
|
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
|
|
display: inline-block;
|
|
text-decoration: none;
|
|
user-select: none;
|
|
}
|
|
|
|
.dws-button-primary {
|
|
background: var(--dws-accent-cta);
|
|
border-color: var(--dws-accent-cta);
|
|
color: var(--cathode-black);
|
|
}
|
|
|
|
.dws-button-primary:hover {
|
|
background: var(--dws-accent-primary);
|
|
border-color: var(--dws-accent-primary);
|
|
}
|
|
|
|
.dws-button-primary:active {
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.dws-button-secondary {
|
|
background: transparent;
|
|
border-color: var(--dws-accent-primary);
|
|
color: var(--dws-accent-primary);
|
|
}
|
|
|
|
.dws-button-secondary:hover {
|
|
background: var(--dws-bg-elevated);
|
|
border-color: var(--dws-accent-cta);
|
|
color: var(--dws-accent-cta);
|
|
}
|
|
|
|
.dws-button-secondary:active {
|
|
background: var(--dws-bg-secondary);
|
|
}
|
|
|
|
.dws-button-tertiary {
|
|
background: transparent;
|
|
border-color: transparent;
|
|
color: var(--dws-text-secondary);
|
|
}
|
|
|
|
.dws-button-tertiary:hover {
|
|
color: var(--dws-accent-primary);
|
|
border-color: var(--dws-border-primary);
|
|
}
|
|
|
|
.dws-button-tertiary:active {
|
|
color: var(--dws-accent-cta);
|
|
}
|
|
|
|
/* ===================================================================
|
|
RESPONSIVE - Mobile Adjustments
|
|
=================================================================== */
|
|
@media (max-width: 1024px) {
|
|
.dws-container {
|
|
padding: 0 var(--space-lg); /* 32px margins on tablet */
|
|
}
|
|
|
|
.dws-grid {
|
|
gap: 8px; /* 8px gutters on mobile per DWS spec */
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
html {
|
|
font-size: 14px; /* Slightly smaller base for mobile */
|
|
}
|
|
|
|
.dws-container {
|
|
padding: 0 var(--space-sm); /* 16px margins on mobile */
|
|
}
|
|
|
|
.dws-grid {
|
|
grid-template-columns: 1fr; /* Single column on mobile */
|
|
}
|
|
|
|
.dws-panel {
|
|
padding: var(--space-md);
|
|
}
|
|
|
|
.mode-toggle-container {
|
|
top: var(--space-sm);
|
|
right: var(--space-sm);
|
|
}
|
|
|
|
.mode-toggle {
|
|
padding: var(--space-xxs) var(--space-xs);
|
|
}
|
|
|
|
.mode-toggle-label {
|
|
font-size: 10px;
|
|
}
|
|
}
|
|
|
|
/* ===================================================================
|
|
PRINT STYLES - Optimized for documentation
|
|
=================================================================== */
|
|
@media print {
|
|
.mode-toggle-container {
|
|
display: none;
|
|
}
|
|
|
|
body {
|
|
background: white;
|
|
color: black;
|
|
}
|
|
}
|
|
</style>
|
|
<meta name="description" content="{{ meta_description }}">
|
|
</head>
|
|
<body>
|
|
<!-- Mode Toggle Component -->
|
|
<div class="mode-toggle-container">
|
|
<div class="mode-toggle" id="modeToggle" role="button" aria-label="Toggle dark/light mode" tabindex="0">
|
|
<span class="mode-toggle-label" id="modeLabel">CATHODE</span>
|
|
<div class="mode-toggle-switch"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- DWS Container with Grid System -->
|
|
<div class="dws-container">
|
|
<!-- Report Header -->
|
|
<header style="margin: var(--space-xl) 0;">
|
|
<h1 class="heading-02" style="color: var(--dws-accent-primary); margin-bottom: var(--space-xs);">DWS LLC NTP STATUS</h1>
|
|
<div class="code-small" style="color: var(--dws-text-secondary);">
|
|
<span class="label-sm">GENERATED:</span> <span class="code-small">{{ gen_time_utc }}</span>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Section 1: Current Time Synchronization -->
|
|
<div class="dws-panel">
|
|
<div class="dws-panel-header">Section 1: Current Time Synchronization</div>
|
|
<div class="dws-panel-body">
|
|
<div style="display: grid; gap: var(--space-sm);">
|
|
<div>
|
|
<span class="label-sm" style="color: var(--dws-text-secondary);">TIME:</span>
|
|
<span id="clock-time" class="code-default" style="margin-left: var(--space-sm);">--:--:--</span>
|
|
</div>
|
|
<div>
|
|
<span class="label-sm" style="color: var(--dws-text-secondary);">DATE:</span>
|
|
<span id="clock-date" class="code-default" style="margin-left: var(--space-sm);">----------</span>
|
|
</div>
|
|
<div>
|
|
<span class="label-sm" style="color: var(--dws-text-secondary);">STATUS:</span>
|
|
<span id="clock-status" class="code-default" style="margin-left: var(--space-sm);">Syncing...</span>
|
|
</div>
|
|
<div>
|
|
<span class="label-sm" style="color: var(--dws-text-secondary);">CLOCK OFFSET:</span>
|
|
<span id="clock-offset" class="code-default" style="margin-left: var(--space-sm);">---</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Section 2: Node Tracking Status Metrics -->
|
|
<div class="dws-panel">
|
|
<div class="dws-panel-header">Section 2: Node Tracking Status Metrics</div>
|
|
<div class="dws-panel-body">
|
|
{{ tracking_table_html | safe }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Section 3: Upstream NTP Sources -->
|
|
<div class="dws-panel">
|
|
<div class="dws-panel-header">Section 3: Upstream NTP Sources</div>
|
|
<div class="dws-panel-body">
|
|
{{ sources_table_html | safe }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Section 4: Metric Definitions & Developer Information -->
|
|
<div class="dws-panel">
|
|
<div class="dws-panel-header">Section 4: Metric Definitions & Developer Information</div>
|
|
<div class="dws-panel-body">
|
|
<h4 class="heading-05" style="color: var(--dws-accent-primary); margin-bottom: var(--space-sm);">Tracking Metrics Glossary</h4>
|
|
<dl style="margin-bottom: var(--space-lg);">
|
|
{{ tracking_glossary_html | safe }}
|
|
</dl>
|
|
|
|
<h4 class="heading-05" style="color: var(--dws-accent-primary); margin-bottom: var(--space-sm);">Sources Metrics Glossary</h4>
|
|
<dl style="margin-bottom: var(--space-lg);">
|
|
{{ sources_glossary_html | safe }}
|
|
</dl>
|
|
|
|
<h4 class="heading-05" style="color: var(--dws-accent-primary); margin-bottom: var(--space-sm);">Usage Information</h4>
|
|
<p class="body-default" style="margin-bottom: var(--space-sm);">
|
|
Use DWS as your NTP pool by setting <code class="code-default" style="color: var(--dws-accent-cta);">time.dws.rip</code> as your NTP source.
|
|
</p>
|
|
|
|
<div class="body-small" style="color: var(--dws-text-secondary); margin-top: var(--space-lg);">
|
|
<div>DWS LLC // "IT'S YOUR INTERNET, TAKE IT BACK" // https://dws.rip</div>
|
|
<div>DWS LLC // UNITED STATES OF AMERICA // 2025</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Report Footer -->
|
|
<footer style="margin: var(--space-xl) 0; padding-top: var(--space-lg); border-top: 1px solid var(--dws-border-primary);">
|
|
<div class="label-sm" style="color: var(--dws-text-tertiary); text-align: left;">
|
|
REPORT GENERATION COMPLETE {{ gen_time_utc }}<br>
|
|
END OF REPORT
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
<!-- End DWS Container -->
|
|
|
|
<script>
|
|
// =====================================================================
|
|
// MODE TOGGLE LOGIC - DWS Design System
|
|
// =====================================================================
|
|
const html = document.documentElement;
|
|
const modeToggle = document.getElementById('modeToggle');
|
|
const modeLabel = document.getElementById('modeLabel');
|
|
|
|
// Initialize mode from localStorage or default to cathode
|
|
const savedMode = localStorage.getItem('dws-mode') || 'cathode';
|
|
html.setAttribute('data-mode', savedMode);
|
|
updateToggleUI(savedMode);
|
|
|
|
modeToggle.addEventListener('click', toggleMode);
|
|
modeToggle.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault();
|
|
toggleMode();
|
|
}
|
|
});
|
|
|
|
function toggleMode() {
|
|
const currentMode = html.getAttribute('data-mode');
|
|
const newMode = currentMode === 'cathode' ? 'vellum' : 'cathode';
|
|
html.setAttribute('data-mode', newMode);
|
|
localStorage.setItem('dws-mode', newMode);
|
|
updateToggleUI(newMode);
|
|
}
|
|
|
|
function updateToggleUI(mode) {
|
|
if (mode === 'vellum') {
|
|
modeToggle.classList.add('active');
|
|
modeLabel.textContent = 'VELLUM';
|
|
} else {
|
|
modeToggle.classList.remove('active');
|
|
modeLabel.textContent = 'CATHODE';
|
|
}
|
|
}
|
|
|
|
// =====================================================================
|
|
// CLOCK SYNC LOGIC
|
|
// =====================================================================
|
|
const clockTimeSpan = document.getElementById('clock-time');
|
|
const clockDateSpan = document.getElementById('clock-date');
|
|
const clockStatusSpan = document.getElementById('clock-status');
|
|
const clockOffsetSpan = document.getElementById('clock-offset');
|
|
const isHistorical = {{ 'true' if is_historical else 'false' }};
|
|
let serverTimeOffsetMs = null;
|
|
let clockUpdateInterval = null;
|
|
let syncInterval = null;
|
|
|
|
function updateClock() {
|
|
if (serverTimeOffsetMs === null) return;
|
|
const now = new Date(new Date().getTime() + serverTimeOffsetMs);
|
|
|
|
const hours = String(now.getUTCHours()).padStart(2, '0');
|
|
const minutes = String(now.getUTCMinutes()).padStart(2, '0');
|
|
const seconds = String(now.getUTCSeconds()).padStart(2, '0');
|
|
const timeString = `${hours}:${minutes}:${seconds}`;
|
|
|
|
const dateString = now.toLocaleDateString('en-US', {
|
|
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC'
|
|
}) + " (UTC)";
|
|
|
|
clockTimeSpan.textContent = timeString;
|
|
clockDateSpan.textContent = dateString;
|
|
}
|
|
|
|
async function syncClockAndOffset() {
|
|
clockStatusSpan.textContent = "Syncing...";
|
|
clockStatusSpan.className = "";
|
|
clockOffsetSpan.textContent = "---";
|
|
try {
|
|
const timeResponse = await fetch('/api/time');
|
|
if (!timeResponse.ok) throw new Error(`Time API Error ${timeResponse.status}`);
|
|
const timeData = await timeResponse.json();
|
|
const serverTime = new Date(timeData.time_utc).getTime();
|
|
const clientTime = new Date().getTime();
|
|
serverTimeOffsetMs = serverTime - clientTime;
|
|
|
|
clockStatusSpan.textContent = "Synced";
|
|
clockStatusSpan.className = "synced";
|
|
clockOffsetSpan.textContent = `${Math.abs(serverTimeOffsetMs)}ms ${serverTimeOffsetMs >= 0 ? 'ahead' : 'behind'}`;
|
|
|
|
if (!clockUpdateInterval) {
|
|
updateClock();
|
|
clockUpdateInterval = setInterval(updateClock, 1000);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error syncing time/offset:', error);
|
|
clockStatusSpan.textContent = `Sync Error`;
|
|
clockStatusSpan.className = "error";
|
|
clockOffsetSpan.textContent = `---`;
|
|
serverTimeOffsetMs = 0;
|
|
if (!clockUpdateInterval) {
|
|
updateClock();
|
|
clockUpdateInterval = setInterval(updateClock, 1000);
|
|
}
|
|
}
|
|
}
|
|
|
|
function displayHistoricalTime(timestampStr) {
|
|
try {
|
|
const timestamp = new Date(timestampStr);
|
|
|
|
const hours = String(timestamp.getUTCHours()).padStart(2, '0');
|
|
const minutes = String(timestamp.getUTCMinutes()).padStart(2, '0');
|
|
const seconds = String(timestamp.getUTCSeconds()).padStart(2, '0');
|
|
const timeString = `${hours}:${minutes}:${seconds}`;
|
|
|
|
const dateString = timestamp.toLocaleDateString('en-US', {
|
|
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC'
|
|
}) + " (UTC)";
|
|
|
|
clockTimeSpan.textContent = timeString;
|
|
clockDateSpan.textContent = dateString;
|
|
clockStatusSpan.textContent = "Historical Snapshot";
|
|
clockStatusSpan.className = "status-info";
|
|
clockOffsetSpan.textContent = "N/A";
|
|
} catch (error) {
|
|
console.error('Error parsing historical timestamp:', error);
|
|
clockTimeSpan.textContent = timestampStr;
|
|
clockDateSpan.textContent = "Historical";
|
|
clockStatusSpan.textContent = "Snapshot";
|
|
clockStatusSpan.className = "status-info";
|
|
clockOffsetSpan.textContent = "N/A";
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
if (isHistorical) {
|
|
displayHistoricalTime("{{ gen_time_utc }}");
|
|
} else {
|
|
syncClockAndOffset();
|
|
syncInterval = setInterval(syncClockAndOffset, 60 * 1000);
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
def get_reporter_ips(service_name):
|
|
try: _, _, ips = gethostbyname_ex(service_name); return ips
|
|
except Exception as e: print(f"Error resolving service IPs: {e}"); return []
|
|
|
|
# --- NEW: Helper to convert Ref time (UTC) ---
|
|
def format_ref_time(timestamp_str):
|
|
try:
|
|
ts = float(timestamp_str)
|
|
dt = datetime.utcfromtimestamp(ts)
|
|
return dt.strftime('%a %b %d %H:%M:%S %Y') + " (UTC)"
|
|
except:
|
|
return timestamp_str
|
|
|
|
# --- NEW: Helper to format floats nicely ---
|
|
def format_float(value_str, precision=3):
|
|
try:
|
|
f_val = float(value_str)
|
|
return f"{f_val:.{precision}f}"
|
|
except:
|
|
return value_str
|
|
|
|
|
|
|
|
# --- Flask Static Files Route ---
|
|
@app.route('/static/fonts/<path:filename>')
|
|
def serve_font(filename):
|
|
"""Serve font files from the fonts directory."""
|
|
return send_from_directory('fonts', filename)
|
|
|
|
@app.route('/api/time')
|
|
def get_server_time():
|
|
return jsonify({"time_utc": datetime.utcnow().isoformat() + "Z"})
|
|
|
|
@app.route('/api/fragments')
|
|
def get_fragments_json():
|
|
fragments = []
|
|
ips = get_reporter_ips(REPORTER_SERVICE)
|
|
for ip in ips:
|
|
try:
|
|
res = requests.get(f"http://{ip}:9898/fragment.json", timeout=1)
|
|
if res.status_code == 200: fragments.append(res.json())
|
|
except: pass
|
|
fragments.sort(key=lambda x: x.get("node_id", "z"))
|
|
return jsonify(fragments)
|
|
|
|
|
|
def format_value(value, max_len=25):
|
|
"""Truncates long values for table display."""
|
|
if value is None: return "N/A"
|
|
s_val = str(value)
|
|
if len(s_val) > max_len:
|
|
return s_val[:max_len-3] + "..."
|
|
return s_val
|
|
|
|
def format_glossary(metrics_defs):
|
|
"""Format metric definitions as terminal-style glossary."""
|
|
lines = []
|
|
for metric, definition in metrics_defs.items():
|
|
lines.append(f" {metric.ljust(20)} - {definition}")
|
|
return "\n".join(lines)
|
|
|
|
def format_glossary_html(metrics_defs):
|
|
"""Format metric definitions as HTML definition list."""
|
|
html_parts = []
|
|
for metric, definition in metrics_defs.items():
|
|
html_parts.append(f"<dt>{metric}</dt>")
|
|
html_parts.append(f"<dd>{definition}</dd>")
|
|
return "\n".join(html_parts)
|
|
|
|
def generate_tracking_table_html(fragments, nodes_list):
|
|
"""Generate HTML table for tracking metrics."""
|
|
if not fragments:
|
|
return '<p class="body-default" style="color: var(--dws-error);">ERROR: Could not fetch data from any reporter pods.</p>'
|
|
|
|
html = ['<table class="dws-table">']
|
|
|
|
# Table header
|
|
html.append('<thead><tr>')
|
|
html.append('<th>Metric</th>')
|
|
for node_id in nodes_list:
|
|
html.append(f'<th class="numeric">{node_id}</th>')
|
|
html.append('</tr></thead>')
|
|
|
|
# Table body
|
|
html.append('<tbody>')
|
|
for metric in TRACKING_METRICS_ORDER:
|
|
html.append('<tr>')
|
|
html.append(f'<td>{metric}</td>')
|
|
|
|
for node_id in nodes_list:
|
|
node_data = next((f for f in fragments if f.get("node_id") == node_id), None)
|
|
value = "N/A"
|
|
if node_data and isinstance(node_data.get("tracking"), dict):
|
|
raw_value = node_data["tracking"].get(metric, "N/A")
|
|
if metric == "Ref time (UTC)":
|
|
value = format_ref_time(raw_value)
|
|
elif metric in ["System time", "Last offset", "RMS offset", "Residual freq", "Skew", "Root delay", "Root dispersion"]:
|
|
value = format_float(raw_value, 6)
|
|
elif metric == "Frequency":
|
|
value = format_float(raw_value, 3)
|
|
elif metric == "Update interval":
|
|
value = format_float(raw_value, 1)
|
|
else:
|
|
value = format_value(raw_value)
|
|
|
|
html.append(f'<td class="numeric">{value}</td>')
|
|
html.append('</tr>')
|
|
|
|
html.append('</tbody>')
|
|
|
|
# Table footer with summary
|
|
html.append('<tfoot>')
|
|
html.append('<tr>')
|
|
html.append(f'<td colspan="{len(nodes_list) + 1}" style="text-align: left; padding-top: var(--space-sm); border-top: 2px solid var(--dws-border-primary);">')
|
|
html.append(f'<span class="label-sm">TOTAL NODES:</span> <span class="code-default">{len(nodes_list)}</span>')
|
|
html.append('</td>')
|
|
html.append('</tr>')
|
|
html.append('</tfoot>')
|
|
|
|
html.append('</table>')
|
|
return '\n'.join(html)
|
|
|
|
def generate_sources_table_html(fragments):
|
|
"""Generate HTML table for NTP sources."""
|
|
if not fragments:
|
|
return '<p class="body-default" style="color: var(--dws-error);">ERROR: Could not fetch data from any reporter pods.</p>'
|
|
|
|
html = ['<table class="dws-table">']
|
|
|
|
# Table header
|
|
html.append('<thead><tr>')
|
|
for col in SOURCES_COLUMNS_ORDER:
|
|
if col in ["Stratum", "Poll", "Reach", "LastRx", "Last sample", "Std Dev"]:
|
|
html.append(f'<th class="numeric">{col}</th>')
|
|
else:
|
|
html.append(f'<th>{col}</th>')
|
|
html.append('</tr></thead>')
|
|
|
|
# Table body
|
|
html.append('<tbody>')
|
|
|
|
node_source_counts = {}
|
|
for f in fragments:
|
|
node_id = f.get("node_id", "unknown")
|
|
sources = f.get("sources", [])
|
|
node_source_counts[node_id] = len(sources) if sources else 0
|
|
|
|
if not sources:
|
|
html.append('<tr>')
|
|
html.append(f'<td>{node_id}</td>')
|
|
html.append('<td>N/A</td>')
|
|
html.append('<td colspan="7">No sources reported</td>')
|
|
html.append('</tr>')
|
|
else:
|
|
for source in sources:
|
|
html.append('<tr>')
|
|
html.append(f'<td>{format_value(node_id, 24)}</td>')
|
|
html.append(f'<td>{source.get("Mode", "?")}{source.get("State", "?")}</td>')
|
|
html.append(f'<td>{format_value(source.get("Name/IP address", "N/A"), 32)}</td>')
|
|
html.append(f'<td class="numeric">{format_value(source.get("Stratum", "N/A"))}</td>')
|
|
html.append(f'<td class="numeric">{format_value(source.get("Poll", "N/A"))}</td>')
|
|
html.append(f'<td class="numeric">{format_value(source.get("Reach", "N/A"))}</td>')
|
|
html.append(f'<td class="numeric">{format_value(source.get("LastRx", "N/A"))}</td>')
|
|
html.append(f'<td class="numeric">{format_float(source.get("Last sample", "N/A"), 6)}</td>')
|
|
html.append(f'<td class="numeric">{format_float(source.get("Std Dev", "N/A"), 3)}</td>')
|
|
html.append('</tr>')
|
|
|
|
html.append('</tbody>')
|
|
|
|
# Table footer with summary
|
|
total_sources = sum(node_source_counts.values())
|
|
html.append('<tfoot>')
|
|
html.append('<tr>')
|
|
html.append(f'<td colspan="{len(SOURCES_COLUMNS_ORDER)}" style="text-align: left; padding-top: var(--space-sm); border-top: 2px solid var(--dws-border-primary);">')
|
|
html.append(f'<span class="label-sm">TOTAL SOURCES:</span> <span class="code-default">{total_sources}</span> | ')
|
|
html.append(f'<span class="label-sm">NODES REPORTING:</span> <span class="code-default">{len(node_source_counts)}</span>')
|
|
html.append('</td>')
|
|
html.append('</tr>')
|
|
html.append('</tfoot>')
|
|
|
|
html.append('</table>')
|
|
return '\n'.join(html)
|
|
|
|
|
|
def render_report(fragments, gen_time, is_historical=False):
|
|
"""Render NTP report from fragments data."""
|
|
meta_offset_ms = "N/A"
|
|
meta_leap_status = "Unknown"
|
|
|
|
nodes_list = [f.get("node_id", "unknown") for f in fragments]
|
|
|
|
# Calculate metadata for meta description
|
|
total_offset_seconds = 0.0
|
|
valid_offset_count = 0
|
|
leap_statuses = set()
|
|
if fragments:
|
|
for frag in fragments:
|
|
tracking = frag.get("tracking", {})
|
|
if isinstance(tracking, dict) and "Error" not in tracking:
|
|
leap = tracking.get("Leap status")
|
|
if leap:
|
|
leap_statuses.add(leap)
|
|
|
|
offset_str = tracking.get("Last offset", 0.1)
|
|
try:
|
|
offset_seconds = float(offset_str)
|
|
total_offset_seconds += offset_seconds
|
|
valid_offset_count += 1
|
|
except (TypeError, ValueError):
|
|
pass
|
|
|
|
if valid_offset_count > 0:
|
|
avg_offset_seconds = total_offset_seconds / valid_offset_count
|
|
meta_offset_ms = f"~{(avg_offset_seconds * 1000):.1f}ms"
|
|
|
|
if len(leap_statuses) == 1:
|
|
meta_leap_status = leap_statuses.pop()
|
|
elif len(leap_statuses) > 1:
|
|
meta_leap_status = "Mixed"
|
|
|
|
# Generate HTML tables
|
|
tracking_table_html = generate_tracking_table_html(fragments, nodes_list)
|
|
sources_table_html = generate_sources_table_html(fragments)
|
|
|
|
# Generate HTML glossaries
|
|
tracking_glossary_html = format_glossary_html(TRACKING_METRICS_DEFS)
|
|
sources_glossary_html = format_glossary_html(SOURCES_METRICS_DEFS)
|
|
|
|
return render_template_string(
|
|
HTML_TEMPLATE,
|
|
gen_time_utc=gen_time,
|
|
tracking_table_html=tracking_table_html,
|
|
sources_table_html=sources_table_html,
|
|
tracking_glossary_html=tracking_glossary_html,
|
|
sources_glossary_html=sources_glossary_html,
|
|
is_historical=is_historical,
|
|
meta_description=f"DWS NTP Pool: {meta_leap_status}. Avg Offset: {meta_offset_ms}."
|
|
)
|
|
|
|
@app.route('/')
|
|
def homepage():
|
|
"""Live NTP status - fetches current data from all nodes."""
|
|
fragments = []
|
|
ips = get_reporter_ips(REPORTER_SERVICE)
|
|
|
|
for ip in ips:
|
|
try:
|
|
res = requests.get(f"http://{ip}:9898/fragment.json", timeout=2)
|
|
if res.status_code == 200:
|
|
fragments.append(res.json())
|
|
else:
|
|
print(f"Failed fetch from {ip}: Status {res.status_code}")
|
|
except Exception as e:
|
|
print(f"Failed connect to {ip}: {e}")
|
|
|
|
fragments.sort(key=lambda x: x.get("node_id", "z"))
|
|
|
|
gen_time = subprocess.run(["date", "-u", "+%Y-%m-%dT%H:%M:%SZ"], capture_output=True, text=True).stdout.strip()
|
|
|
|
return render_report(fragments, gen_time)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
app.run(host='0.0.0.0', port=8080)
|