better formatting
This commit is contained in:
@ -2,7 +2,7 @@
|
|||||||
FROM python:3.10-slim
|
FROM python:3.10-slim
|
||||||
|
|
||||||
# Install Python dependencies
|
# Install Python dependencies
|
||||||
RUN pip install Flask requests tabulate texttable
|
RUN pip install Flask requests tabulate
|
||||||
|
|
||||||
WORKDIR /app/static
|
WORKDIR /app/static
|
||||||
RUN mkdir fonts
|
RUN mkdir fonts
|
||||||
|
|||||||
122
frontend/main.py
122
frontend/main.py
@ -5,8 +5,9 @@ import json
|
|||||||
from flask import Flask, render_template_string, jsonify
|
from flask import Flask, render_template_string, jsonify
|
||||||
from socket import gethostbyname_ex
|
from socket import gethostbyname_ex
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from texttable import Texttable # For ASCII tables
|
from tabulate import tabulate # For ASCII tables
|
||||||
import time # For timestamp conversion
|
import time # For timestamp conversion
|
||||||
|
import uuid # For report IDs
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
@ -99,37 +100,50 @@ HTML_TEMPLATE = """<!DOCTYPE html>
|
|||||||
<body>
|
<body>
|
||||||
<pre>
|
<pre>
|
||||||
$> ./dws_ntp_report
|
$> ./dws_ntp_report
|
||||||
|
<b>**INFO**</b>: INITIALIZING DWS NTP MONITORING SYSTEM
|
||||||
<b>**INFO**</b>: COLLECTING DWS NTP POOL INFORMATION
|
<b>**INFO**</b>: COLLECTING DWS NTP POOL INFORMATION
|
||||||
CURRENT TIME
|
|
||||||
=================================
|
{{ report_header }}
|
||||||
|
|
||||||
|
<b>SECTION 1: CURRENT TIME SYNCHRONIZATION</b>
|
||||||
|
════════════════════════════════════════════════════════════════════════════════
|
||||||
TIME: <span id="clock-time">--:--:--</span>
|
TIME: <span id="clock-time">--:--:--</span>
|
||||||
DATE: <span id="clock-date">----------</span>
|
DATE: <span id="clock-date">----------</span>
|
||||||
STATUS: <span id="clock-status">Syncing...</span>
|
STATUS: <span id="clock-status">Syncing...</span>
|
||||||
CLOCK OFFSET: <span id="clock-offset">---</span>
|
CLOCK OFFSET: <span id="clock-offset">---</span>
|
||||||
|
════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
|
||||||
<b>**INFO**</b>: DETAILED METRICS:
|
<b>SECTION 2: NODE TRACKING STATUS METRICS</b>
|
||||||
<b>**INFO**</b>: COLLECTING TRACKING STATUS METRICS:
|
════════════════════════════════════════════════════════════════════════════════
|
||||||
|
<b>**INFO**</b>: COLLECTING TRACKING STATUS METRICS FROM ALL NODES
|
||||||
|
|
||||||
|
|
||||||
<b>TRACKING STATUS</b>
|
|
||||||
{{ tracking_table_ascii }}
|
{{ tracking_table_ascii }}
|
||||||
|
|
||||||
<b>**INFO**</b>: COLLECTING UPSTREAM SOURCES METRICS:
|
════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
|
||||||
|
<b>SECTION 3: UPSTREAM NTP SOURCES</b>
|
||||||
|
════════════════════════════════════════════════════════════════════════════════
|
||||||
|
<b>**INFO**</b>: COLLECTING UPSTREAM SOURCES METRICS FROM ALL NODES
|
||||||
|
|
||||||
<b>UPSTREAM SOURCES</b>
|
|
||||||
{{ sources_table_ascii }}
|
{{ sources_table_ascii }}
|
||||||
|
|
||||||
<b>**INFO**</b>: REPORT COMPLETE
|
════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
<b>**INFO**</b>: DEVELOPER INFO
|
|
||||||
|
<b>SECTION 4: DEVELOPER INFORMATION</b>
|
||||||
|
════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
USE DWS AS YOUR NTP POOL BY SETTING time.dws.rip AS YOUR NTP SOURCE
|
USE DWS AS YOUR NTP POOL BY SETTING time.dws.rip AS YOUR NTP SOURCE
|
||||||
|
|
||||||
|
|
||||||
<b>**INFO**</b>: DWS LLC // "IT'S YOUR INTERNET, TAKE IT BACK" // https://dws.rip
|
<b>**INFO**</b>: DWS LLC // "IT'S YOUR INTERNET, TAKE IT BACK" // https://dws.rip
|
||||||
<b>**INFO**</b>: DWS LLC // UNITED STATES OF AMERICA // 2025
|
<b>**INFO**</b>: DWS LLC // UNITED STATES OF AMERICA // 2025
|
||||||
<b>**INFO**</b>: DWS NTP REPORT COMPLETE {{ gen_time_utc }}
|
|
||||||
|
════════════════════════════════════════════════════════════════════════════════
|
||||||
|
<b>**INFO**</b>: REPORT GENERATION COMPLETE {{ gen_time_utc }}
|
||||||
|
<b>**INFO**</b>: END OF REPORT
|
||||||
|
════════════════════════════════════════════════════════════════════════════════
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -220,6 +234,23 @@ USE DWS AS YOUR NTP POOL BY SETTING time.dws.rip AS YOUR NTP SOURCE
|
|||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# --- US Graphics Company Style Helpers ---
|
||||||
|
def form_feed_separator(width=80):
|
||||||
|
"""Generate a form feed separator line like vintage computer printouts."""
|
||||||
|
return "═" * width
|
||||||
|
|
||||||
|
def report_header(report_id, timestamp):
|
||||||
|
"""Generate a vintage-style report header."""
|
||||||
|
header = []
|
||||||
|
header.append(form_feed_separator(80))
|
||||||
|
header.append(f"REPORT ID: {report_id}".ljust(40) + f"GENERATED: {timestamp}".rjust(40))
|
||||||
|
header.append(form_feed_separator(80))
|
||||||
|
return "\n".join(header)
|
||||||
|
|
||||||
|
def section_separator(title, width=80):
|
||||||
|
"""Generate a section separator with title."""
|
||||||
|
return f"\n{title}\n{form_feed_separator(width)}\n"
|
||||||
|
|
||||||
def get_reporter_ips(service_name):
|
def get_reporter_ips(service_name):
|
||||||
try: _, _, ips = gethostbyname_ex(service_name); return ips
|
try: _, _, ips = gethostbyname_ex(service_name); return ips
|
||||||
except Exception as e: print(f"Error resolving service IPs: {e}"); return []
|
except Exception as e: print(f"Error resolving service IPs: {e}"); return []
|
||||||
@ -290,14 +321,8 @@ def homepage():
|
|||||||
fragments.sort(key=lambda x: x.get("node_id", "z"))
|
fragments.sort(key=lambda x: x.get("node_id", "z"))
|
||||||
nodes_list = [f.get("node_id", "unknown") for f in fragments]
|
nodes_list = [f.get("node_id", "unknown") for f in fragments]
|
||||||
|
|
||||||
# 2. Generate ASCII Tracking Table
|
# 2. Generate ASCII Tracking Table with tabulate
|
||||||
track_table = Texttable(max_width=0)
|
tracking_rows = []
|
||||||
track_table.set_deco(Texttable.BORDER | Texttable.HEADER | Texttable.VLINES)
|
|
||||||
track_table.set_chars(['─', '│', '┬', '═', '─', '┼', '│', '┌', '┐', '└', '┘'])
|
|
||||||
track_table.set_cols_width(TRACKING_COL_WIDTHS[:len(nodes_list)+1]) # Dynamic width based on nodes found
|
|
||||||
track_table.header(["Metric"] + nodes_list)
|
|
||||||
track_table.set_cols_align(["l"] + ["r"] * len(nodes_list))
|
|
||||||
|
|
||||||
for metric in TRACKING_METRICS_ORDER:
|
for metric in TRACKING_METRICS_ORDER:
|
||||||
row = [metric]
|
row = [metric]
|
||||||
for node_id in nodes_list:
|
for node_id in nodes_list:
|
||||||
@ -312,8 +337,19 @@ def homepage():
|
|||||||
elif metric == "Update interval": value = format_float(raw_value, 1)
|
elif metric == "Update interval": value = format_float(raw_value, 1)
|
||||||
else: value = format_value(raw_value) # Use generic formatter
|
else: value = format_value(raw_value) # Use generic formatter
|
||||||
row.append(value)
|
row.append(value)
|
||||||
track_table.add_row(row)
|
tracking_rows.append(row)
|
||||||
tracking_table_ascii = track_table.draw()
|
|
||||||
|
# Add summary row
|
||||||
|
tracking_rows.append([form_feed_separator(18)] + [form_feed_separator(24)] * len(nodes_list))
|
||||||
|
tracking_rows.append([f"TOTAL NODES: {len(nodes_list)}"] + [""] * len(nodes_list))
|
||||||
|
|
||||||
|
tracking_table_ascii = tabulate(
|
||||||
|
tracking_rows,
|
||||||
|
headers=["Metric"] + nodes_list,
|
||||||
|
tablefmt="fancy_grid",
|
||||||
|
stralign="left",
|
||||||
|
numalign="right"
|
||||||
|
)
|
||||||
|
|
||||||
total_offset_seconds = 0.0
|
total_offset_seconds = 0.0
|
||||||
valid_offset_count = 0
|
valid_offset_count = 0
|
||||||
@ -347,22 +383,20 @@ def homepage():
|
|||||||
meta_leap_status = "Mixed"
|
meta_leap_status = "Mixed"
|
||||||
# else remains "Unknown" if no valid status found
|
# else remains "Unknown" if no valid status found
|
||||||
|
|
||||||
# 3. Generate ASCII Sources Table (Rotated/Concatenated)
|
# 3. Generate ASCII Sources Table with tabulate
|
||||||
source_table = Texttable(max_width=0)
|
|
||||||
source_table.set_deco(Texttable.BORDER | Texttable.HEADER | Texttable.VLINES)
|
|
||||||
source_table.set_chars(['─', '│', '┬', '═', '─', '┼', '│', '┌', '┐', '└', '┘'])
|
|
||||||
source_table.set_cols_width(SOURCES_COL_WIDTHS)
|
|
||||||
source_table.header(SOURCES_COLUMNS_ORDER)
|
|
||||||
source_table.set_cols_align(["l"] * 3 + ["r"] * 6) # Left align text, right align numbers
|
|
||||||
|
|
||||||
if not fragments:
|
if not fragments:
|
||||||
sources_table_ascii = "ERROR: Could not fetch data from any reporter pods."
|
sources_table_ascii = "ERROR: Could not fetch data from any reporter pods."
|
||||||
else:
|
else:
|
||||||
|
sources_rows = []
|
||||||
|
node_source_counts = {}
|
||||||
|
|
||||||
for f in fragments:
|
for f in fragments:
|
||||||
node_id = f.get("node_id", "unknown")
|
node_id = f.get("node_id", "unknown")
|
||||||
sources = f.get("sources", [])
|
sources = f.get("sources", [])
|
||||||
|
node_source_counts[node_id] = len(sources) if sources else 0
|
||||||
|
|
||||||
if not sources:
|
if not sources:
|
||||||
source_table.add_row([node_id, "N/A", "No sources reported", "N/A", "N/A", "N/A", "N/A", "N/A", "N/A"])
|
sources_rows.append([node_id, "N/A", "No sources reported", "N/A", "N/A", "N/A", "N/A", "N/A", "N/A"])
|
||||||
else:
|
else:
|
||||||
for source in sources:
|
for source in sources:
|
||||||
row = [
|
row = [
|
||||||
@ -376,14 +410,32 @@ def homepage():
|
|||||||
format_float(source.get("Last sample", "N/A"), 6), # Format sample offset
|
format_float(source.get("Last sample", "N/A"), 6), # Format sample offset
|
||||||
format_float(source.get("Std Dev", "N/A"), 3) # Format Std Dev/Jitter
|
format_float(source.get("Std Dev", "N/A"), 3) # Format Std Dev/Jitter
|
||||||
]
|
]
|
||||||
source_table.add_row(row)
|
sources_rows.append(row)
|
||||||
sources_table_ascii = source_table.draw()
|
|
||||||
|
# Add summary section
|
||||||
|
sources_rows.append([form_feed_separator(SOURCES_COL_WIDTHS[0]-2)] + [form_feed_separator(w-2) for w in SOURCES_COL_WIDTHS[1:]])
|
||||||
|
total_sources = sum(node_source_counts.values())
|
||||||
|
summary_text = f"TOTAL SOURCES: {total_sources} | NODES REPORTING: {len(node_source_counts)}"
|
||||||
|
sources_rows.append([summary_text] + [""] * (len(SOURCES_COLUMNS_ORDER) - 1))
|
||||||
|
|
||||||
|
sources_table_ascii = tabulate(
|
||||||
|
sources_rows,
|
||||||
|
headers=SOURCES_COLUMNS_ORDER,
|
||||||
|
tablefmt="fancy_grid",
|
||||||
|
stralign="left",
|
||||||
|
numalign="right"
|
||||||
|
)
|
||||||
|
|
||||||
gen_time = subprocess.run(["date", "-u", "+%Y-%m-%dT%H:%M:%SZ"], capture_output=True, text=True).stdout.strip()
|
gen_time = subprocess.run(["date", "-u", "+%Y-%m-%dT%H:%M:%SZ"], capture_output=True, text=True).stdout.strip()
|
||||||
|
|
||||||
|
# Generate report header
|
||||||
|
report_id = str(uuid.uuid4())[:8].upper()
|
||||||
|
report_header_text = report_header(report_id, gen_time)
|
||||||
|
|
||||||
return render_template_string(
|
return render_template_string(
|
||||||
HTML_TEMPLATE,
|
HTML_TEMPLATE,
|
||||||
gen_time_utc=gen_time,
|
gen_time_utc=gen_time,
|
||||||
|
report_header=report_header_text,
|
||||||
tracking_table_ascii=tracking_table_ascii,
|
tracking_table_ascii=tracking_table_ascii,
|
||||||
sources_table_ascii=sources_table_ascii,
|
sources_table_ascii=sources_table_ascii,
|
||||||
meta_description=f"DWS NTP Pool: {meta_leap_status}. Avg Offset: {meta_offset_ms}.",
|
meta_description=f"DWS NTP Pool: {meta_leap_status}. Avg Offset: {meta_offset_ms}.",
|
||||||
|
|||||||
Reference in New Issue
Block a user