refactor v2
This commit is contained in:
@@ -5,5 +5,11 @@ ENV PYTHONUNBUFFERED=1
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY . .
|
||||
# Expose dynamic port
|
||||
EXPOSE 8798
|
||||
CMD ["python", "app.py"]
|
||||
|
||||
# Healthcheck
|
||||
HEALTHCHECK CMD python -c "import requests; r=requests.get('http://localhost:${FLASK_PORT:-8798}',timeout=5);exit(0 if r.ok else 1)" || exit 1
|
||||
|
||||
ENV FLASK_PORT=${FLASK_PORT:-8798}
|
||||
CMD ["sh", "-c", "gunicorn -k geventwebsocket.gunicorn.workers.GeventWebSocketWorker -w 1 --threads 16 --bind 0.0.0.0:$FLASK_PORT --timeout 60 app:app"]
|
||||
37
app.py
37
app.py
@@ -17,7 +17,7 @@ app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = config.FLASK_CONFIG['secret_key']
|
||||
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = config.FLASK_CONFIG.get('static_cache_timeout', 60)
|
||||
# Bez Eventlet (deprecated) – prosty tryb wątkowy.
|
||||
socketio = SocketIO(app, cors_allowed_origins="*", async_mode="threading", logger=False)
|
||||
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading', logger=False)
|
||||
|
||||
def get_file_hash(filename):
|
||||
full_path = os.path.join(app.static_folder, filename)
|
||||
@@ -291,11 +291,32 @@ def api_outages(phase_id):
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
clients = 0
|
||||
task = None
|
||||
|
||||
@socketio.on('connect')
|
||||
def handle_connect():
|
||||
global clients, task
|
||||
clients += 1
|
||||
if task is None:
|
||||
task = socketio.start_background_task(background_voltage_update)
|
||||
|
||||
@socketio.on('disconnect')
|
||||
def handle_disconnect():
|
||||
global clients
|
||||
clients = max(0, clients - 1)
|
||||
|
||||
def background_voltage_update():
|
||||
global clients
|
||||
last_refresh = 0
|
||||
refresh_every_s = config.CHART_CONFIG.get("refresh_interval_seconds", 15)
|
||||
interval_s = config.CHART_CONFIG['update_interval'] / 1000.0
|
||||
|
||||
while True:
|
||||
if clients == 0:
|
||||
socketio.sleep(1)
|
||||
continue
|
||||
|
||||
try:
|
||||
voltages = {'timestamp': None}
|
||||
|
||||
@@ -305,29 +326,21 @@ def background_voltage_update():
|
||||
if res['timestamp']:
|
||||
voltages['timestamp'] = res['timestamp']
|
||||
|
||||
# aktualizacja gauge
|
||||
socketio.emit('voltage_update', voltages)
|
||||
|
||||
# cykliczny refresh wykresu + eventów
|
||||
now = time.time()
|
||||
if now - last_refresh >= refresh_every_s:
|
||||
socketio.emit('refresh_timeseries', {'ts': int(now)})
|
||||
last_refresh = now
|
||||
|
||||
except Exception as e:
|
||||
print(f"Worker Error: {e}")
|
||||
app.logger.error(f"Worker Error: {e}")
|
||||
|
||||
#time.sleep(config.CHART_CONFIG['update_interval'] / 1000.0)
|
||||
socketio.sleep(interval_s)
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("\n" + "="*50)
|
||||
print(f"Voltage Monitor API / Port: {config.FLASK_CONFIG['port']}")
|
||||
print("="*50 + "\n")
|
||||
|
||||
socketio.start_background_task(background_voltage_update)
|
||||
socketio.run(
|
||||
app,
|
||||
host="0.0.0.0",
|
||||
port=config.FLASK_CONFIG["port"],
|
||||
allow_unsafe_werkzeug=True
|
||||
)
|
||||
socketio.run(app, host='0.0.0.0', port=config.FLASK_CONFIG['port'], allow_unsafe_werkzeug=True)
|
||||
@@ -82,5 +82,5 @@ FOOTER = {
|
||||
"url": "https://www.linuxiarz.pl"
|
||||
},
|
||||
"project": "Voltage Monitor",
|
||||
"version": "1..0"
|
||||
"version": "2.0"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ services:
|
||||
- PYTHONUNBUFFERED=1
|
||||
- PYTHONDONTWRITEBYTECODE=1
|
||||
- SECRET_KEY=alamakota
|
||||
- FLASK_PORT=${APP_PORT:-8798}
|
||||
command: gunicorn -k geventwebsocket.gunicorn.workers.GeventWebSocketWorker -w 1 --bind 0.0.0.0:${FLASK_PORT:-8798} --timeout 60 app:app
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- monitoring
|
||||
|
||||
@@ -3,4 +3,7 @@ influxdb
|
||||
#influxdb-client
|
||||
simple-websocket
|
||||
python-socketio
|
||||
flask-socketio
|
||||
flask-socketio
|
||||
gunicorn
|
||||
gevent
|
||||
gevent-websocket
|
||||
@@ -1,345 +1,461 @@
|
||||
:root {
|
||||
--bg-dark: #0d1117;
|
||||
--card-bg: #161b22;
|
||||
--border-color: #30363d;
|
||||
--text-main: #c9d1d9;
|
||||
--blue-accent: #58a6ff;
|
||||
:root{
|
||||
--bg-dark:#0d1117;
|
||||
--card-bg:#161b22;
|
||||
--border-color:#30363d;
|
||||
--text-main:#c9d1d9;
|
||||
--blue-accent:#58a6ff;
|
||||
--muted:#8b949e;
|
||||
--shadow:0 8px 28px rgba(0,0,0,.35);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-dark);
|
||||
color: var(--text-main);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
padding: 10px;
|
||||
body{
|
||||
background-color:var(--bg-dark);
|
||||
color:var(--text-main);
|
||||
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;
|
||||
font-size:14px;
|
||||
margin:0;
|
||||
overflow-x:hidden;
|
||||
padding:10px;
|
||||
}
|
||||
|
||||
/* Wskaźniki napięcia */
|
||||
.gauge-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
.border-dotted{border-style:dotted!important}
|
||||
|
||||
.vm-shell{max-width:1120px;margin:0 auto}
|
||||
|
||||
.vm-card{
|
||||
background-color:var(--card-bg);
|
||||
border:1px solid rgba(255,255,255,.06);
|
||||
border-radius:14px;
|
||||
}
|
||||
|
||||
.gauge-card {
|
||||
background-color: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 12px 5px;
|
||||
text-align: center;
|
||||
.vm-topbar{
|
||||
position:sticky;
|
||||
top:0;
|
||||
z-index:50;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:12px;
|
||||
padding:10px 12px;
|
||||
margin-bottom:12px;
|
||||
background:rgba(13,17,23,.82);
|
||||
backdrop-filter:blur(10px);
|
||||
border:1px solid rgba(255,255,255,.06);
|
||||
border-radius:14px;
|
||||
}
|
||||
|
||||
.gauge-canvas-container {
|
||||
max-width: 80px;
|
||||
margin: 0 auto;
|
||||
.vm-topbar-left,
|
||||
.vm-topbar-right{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:10px;
|
||||
}
|
||||
|
||||
.gauge-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--blue-accent);
|
||||
margin-top: 2px;
|
||||
.vm-topbar-text{
|
||||
font-size:.82rem;
|
||||
color:var(--muted);
|
||||
}
|
||||
|
||||
.voltage-value {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
.vm-dot{
|
||||
width:10px;
|
||||
height:10px;
|
||||
border-radius:999px;
|
||||
background:#6c757d;
|
||||
}
|
||||
|
||||
/* Selektor czasu - Desktop (jedna linia) */
|
||||
.time-selector-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 20px;
|
||||
.vm-dot.online{background:#198754}
|
||||
.vm-dot.offline{background:#dc3545}
|
||||
.vm-dot.connecting{background:#ffc107}
|
||||
|
||||
.gauge-grid{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(3,1fr);
|
||||
gap:10px;
|
||||
margin-bottom:15px;
|
||||
}
|
||||
|
||||
.time-btn {
|
||||
flex: 0 1 auto;
|
||||
font-size: 0.75rem !important;
|
||||
height: 32px;
|
||||
min-width: 45px;
|
||||
padding: 2px 10px !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
color: var(--blue-accent) !important;
|
||||
background: transparent;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.gauge-card{
|
||||
background-color:var(--card-bg);
|
||||
border:1px solid var(--border-color);
|
||||
border-radius:12px;
|
||||
padding:12px 5px;
|
||||
text-align:center;
|
||||
box-shadow:var(--shadow);
|
||||
}
|
||||
|
||||
.time-btn.active {
|
||||
background-color: #1f6feb !important;
|
||||
color: white !important;
|
||||
border-color: #1f6feb !important;
|
||||
.gauge-card:hover{
|
||||
transform:translateY(-2px);
|
||||
transition:transform .15s ease;
|
||||
}
|
||||
|
||||
/* Wykres główny */
|
||||
.main-chart-card {
|
||||
background-color: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
height: 50vh;
|
||||
min-height: 320px;
|
||||
margin-bottom: 15px;
|
||||
.gauge-canvas-container{
|
||||
max-width:80px;
|
||||
margin:0 auto;
|
||||
}
|
||||
|
||||
/* Karta logów i nagłówek */
|
||||
.events-card {
|
||||
background-color: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
.gauge-label{
|
||||
font-size:.75rem;
|
||||
font-weight:600;
|
||||
color:var(--blue-accent);
|
||||
margin-top:2px;
|
||||
}
|
||||
|
||||
.events-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
margin-bottom: 12px;
|
||||
.voltage-value{
|
||||
font-size:1.1rem;
|
||||
font-weight:800;
|
||||
color:#fff;
|
||||
}
|
||||
|
||||
#eventRangeLabel {
|
||||
font-size: 0.75rem;
|
||||
color: #8b949e;
|
||||
}
|
||||
/* Kontener zdarzenia */
|
||||
.event-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
.time-selector-wrapper{
|
||||
display:flex;
|
||||
flex-wrap:nowrap;
|
||||
justify-content:center;
|
||||
gap:6px;
|
||||
margin-bottom:20px;
|
||||
}
|
||||
|
||||
.event-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
.time-btn{
|
||||
flex:0 1 auto;
|
||||
font-size:.75rem!important;
|
||||
height:32px;
|
||||
min-width:45px;
|
||||
padding:2px 10px!important;
|
||||
border:1px solid var(--border-color)!important;
|
||||
color:var(--blue-accent)!important;
|
||||
background:transparent;
|
||||
border-radius:6px;
|
||||
cursor:pointer;
|
||||
transition:all .2s;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
}
|
||||
|
||||
.event-badge {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
.time-btn.active{
|
||||
background-color:#1f6feb!important;
|
||||
color:#fff!important;
|
||||
border-color:#1f6feb!important;
|
||||
}
|
||||
|
||||
.event-time {
|
||||
font-family: monospace;
|
||||
font-size: 0.85rem;
|
||||
color: #8b949e;
|
||||
white-space: nowrap;
|
||||
.main-chart-card{
|
||||
background-color:var(--card-bg);
|
||||
border:1px solid var(--border-color);
|
||||
border-radius:12px;
|
||||
padding:15px;
|
||||
height:50vh;
|
||||
min-height:320px;
|
||||
margin-bottom:15px;
|
||||
box-shadow:var(--shadow);
|
||||
}
|
||||
|
||||
.event-desc {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-main);
|
||||
.chart-range-badge{
|
||||
position:absolute;
|
||||
top:10px;
|
||||
right:10px;
|
||||
background:rgba(22,27,34,.9);
|
||||
border:1px solid var(--border-color);
|
||||
border-radius:6px;
|
||||
padding:4px 10px;
|
||||
font-size:.7rem;
|
||||
color:var(--muted);
|
||||
z-index:10;
|
||||
pointer-events:none;
|
||||
backdrop-filter:blur(4px);
|
||||
font-family:monospace;
|
||||
}
|
||||
|
||||
/* Przycisk lupy - Desktop */
|
||||
.zoom-btn-mobile {
|
||||
padding: 5px 10px;
|
||||
background: rgba(88, 166, 255, 0.1);
|
||||
border: 1px solid var(--blue-accent);
|
||||
color: var(--blue-accent);
|
||||
border-radius: 6px;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
margin-left: 15px;
|
||||
.events-card{
|
||||
background-color:var(--card-bg);
|
||||
border:1px solid var(--border-color);
|
||||
border-radius:12px;
|
||||
padding:15px;
|
||||
box-shadow:var(--shadow);
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.event-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.event-content {
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.event-desc {
|
||||
width: 100%;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.time-selector-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr) !important;
|
||||
gap: 10px !important;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.time-btn {
|
||||
width: 80% !important;
|
||||
height: 30px !important;
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
.zoom-btn-mobile {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
padding: 8px 10px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.zoom-btn-mobile:active {
|
||||
background: rgba(88, 166, 255, 0.2);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
.events-header{
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
flex-wrap:wrap;
|
||||
gap:5px;
|
||||
margin-bottom:12px;
|
||||
}
|
||||
|
||||
.border-dotted {
|
||||
border-style: dotted !important;
|
||||
#eventRangeLabel{font-size:.75rem;color:var(--muted)}
|
||||
|
||||
.event-item{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
padding:10px 0;
|
||||
border-bottom:1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* Modal własnego zakresu */
|
||||
#customRangeModal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.event-content{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:12px;
|
||||
flex:1;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--card-bg);
|
||||
padding: 25px;
|
||||
border-radius: 12px;
|
||||
max-width: 420px;
|
||||
width: 90%;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
|
||||
border: 1px solid var(--border-color);
|
||||
.event-badge{
|
||||
width:10px;
|
||||
height:10px;
|
||||
border-radius:50%;
|
||||
flex-shrink:0;
|
||||
}
|
||||
|
||||
.modal-content h3 {
|
||||
margin-top: 0;
|
||||
color: var(--text-main);
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
.event-time{
|
||||
font-family:monospace;
|
||||
font-size:.85rem;
|
||||
color:var(--muted);
|
||||
white-space:nowrap;
|
||||
}
|
||||
|
||||
.modal-form-group {
|
||||
margin-bottom: 15px;
|
||||
.event-desc{
|
||||
font-size:.85rem;
|
||||
color:var(--text-main);
|
||||
}
|
||||
|
||||
.modal-form-group:last-of-type {
|
||||
margin-bottom: 25px;
|
||||
.event-type-icon{
|
||||
width:22px;
|
||||
text-align:center;
|
||||
opacity:.95;
|
||||
}
|
||||
|
||||
.modal-label {
|
||||
display: block;
|
||||
color: #8b949e;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
.zoom-btn-mobile{
|
||||
padding:5px 10px;
|
||||
background:rgba(88,166,255,.1);
|
||||
border:1px solid var(--blue-accent);
|
||||
color:var(--blue-accent);
|
||||
border-radius:6px;
|
||||
font-size:.75rem;
|
||||
cursor:pointer;
|
||||
white-space:nowrap;
|
||||
margin-left:15px;
|
||||
}
|
||||
|
||||
.modal-input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background: var(--bg-dark);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
color: var(--text-main);
|
||||
font-size: 14px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
#customRangeModal{
|
||||
display:none;
|
||||
position:fixed;
|
||||
inset:0;
|
||||
background:rgba(0,0,0,.8);
|
||||
z-index:1000;
|
||||
justify-content:center;
|
||||
align-items:center;
|
||||
}
|
||||
|
||||
.modal-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--blue-accent);
|
||||
.modal-content{
|
||||
background:var(--card-bg);
|
||||
padding:25px;
|
||||
border-radius:12px;
|
||||
max-width:420px;
|
||||
width:90%;
|
||||
box-shadow:0 8px 32px rgba(0,0,0,.6);
|
||||
border:1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.modal-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
.modal-content h3{
|
||||
margin:0 0 20px;
|
||||
color:var(--text-main);
|
||||
font-size:1.25rem;
|
||||
font-weight:600;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
padding: 8px 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
.modal-form-group{margin-bottom:15px}
|
||||
.modal-form-group:last-of-type{margin-bottom:25px}
|
||||
|
||||
.modal-label{
|
||||
display:block;
|
||||
color:var(--muted);
|
||||
margin-bottom:8px;
|
||||
font-size:.875rem;
|
||||
font-weight:500;
|
||||
}
|
||||
|
||||
.modal-btn-cancel {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-main);
|
||||
.modal-input{
|
||||
width:100%;
|
||||
padding:10px;
|
||||
background:var(--bg-dark);
|
||||
border:1px solid var(--border-color);
|
||||
border-radius:6px;
|
||||
color:var(--text-main);
|
||||
font-size:14px;
|
||||
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
|
||||
.modal-btn-cancel:hover {
|
||||
background: rgba(48, 54, 61, 0.5);
|
||||
.modal-input:focus{outline:none;border-color:var(--blue-accent)}
|
||||
.modal-input::-webkit-calendar-picker-indicator{filter:invert(1);cursor:pointer}
|
||||
|
||||
.modal-buttons{
|
||||
display:flex;
|
||||
gap:10px;
|
||||
justify-content:flex-end;
|
||||
}
|
||||
|
||||
.modal-btn-apply {
|
||||
background: #1f6feb;
|
||||
border: 1px solid #1f6feb;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
.modal-btn{
|
||||
padding:8px 20px;
|
||||
border-radius:6px;
|
||||
cursor:pointer;
|
||||
font-size:14px;
|
||||
font-weight:500;
|
||||
transition:all .2s;
|
||||
border:none;
|
||||
}
|
||||
|
||||
.modal-btn-apply:hover {
|
||||
background: #1a5acc;
|
||||
.modal-btn-cancel{
|
||||
background:transparent;
|
||||
border:1px solid var(--border-color);
|
||||
color:var(--text-main);
|
||||
}
|
||||
|
||||
.modal-input::-webkit-calendar-picker-indicator {
|
||||
filter: invert(1);
|
||||
cursor: pointer;
|
||||
.modal-btn-cancel:hover{background:rgba(48,54,61,.5)}
|
||||
|
||||
.modal-btn-apply{
|
||||
background:#1f6feb;
|
||||
border:1px solid #1f6feb;
|
||||
color:#fff;
|
||||
font-weight:600;
|
||||
}
|
||||
|
||||
/* Badge zakresu wykresu */
|
||||
.chart-range-badge {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: rgba(22, 27, 34, 0.9);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 4px 10px;
|
||||
font-size: 0.7rem;
|
||||
color: #8b949e;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
backdrop-filter: blur(4px);
|
||||
font-family: monospace;
|
||||
.modal-btn-apply:hover{background:#1a5acc}
|
||||
|
||||
#apiHelperModal{
|
||||
display:none;
|
||||
position:fixed;
|
||||
inset:0;
|
||||
background:rgba(0,0,0,.82);
|
||||
z-index:1100;
|
||||
justify-content:center;
|
||||
align-items:center;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.chart-range-badge {
|
||||
font-size: 0.65rem;
|
||||
padding: 3px 8px;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
.vm-modal-wide{max-width:860px}
|
||||
|
||||
.vm-modal-head{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:10px;
|
||||
margin-bottom:12px;
|
||||
}
|
||||
|
||||
.vm-icon-btn{
|
||||
background:transparent;
|
||||
border:1px solid rgba(255,255,255,.12);
|
||||
color:var(--text-main);
|
||||
border-radius:8px;
|
||||
padding:6px 10px;
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
.vm-modal-grid{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(2,minmax(0,1fr));
|
||||
gap:12px;
|
||||
margin-bottom:12px;
|
||||
}
|
||||
|
||||
.vm-modal-block{margin-top:10px}
|
||||
|
||||
.vm-inline{
|
||||
display:flex;
|
||||
gap:10px;
|
||||
align-items:center;
|
||||
}
|
||||
|
||||
.vm-mono{
|
||||
font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
|
||||
}
|
||||
|
||||
.vm-modal-actions{
|
||||
display:flex;
|
||||
gap:10px;
|
||||
justify-content:flex-end;
|
||||
margin-top:12px;
|
||||
}
|
||||
|
||||
.vm-pre{
|
||||
background:#0b0f14;
|
||||
border:1px solid rgba(255,255,255,.08);
|
||||
border-radius:10px;
|
||||
padding:12px;
|
||||
color:var(--text-main);
|
||||
max-height:260px;
|
||||
overflow:auto;
|
||||
font-size:.78rem;
|
||||
}
|
||||
|
||||
@media (max-width:768px){
|
||||
.vm-modal-grid{grid-template-columns:1fr}
|
||||
.vm-modal-wide{max-width:420px}
|
||||
}
|
||||
|
||||
@media (max-width:576px){
|
||||
.event-item{flex-direction:column;align-items:flex-start}
|
||||
.event-content{flex-wrap:wrap;gap:6px;width:100%}
|
||||
.event-desc{width:100%;margin-bottom:6px}
|
||||
|
||||
.time-selector-wrapper{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(5,1fr)!important;
|
||||
gap:10px!important;
|
||||
padding:0 10px;
|
||||
}
|
||||
|
||||
.time-btn{
|
||||
width:80%!important;
|
||||
height:30px!important;
|
||||
font-size:.75rem!important;
|
||||
}
|
||||
|
||||
.zoom-btn-mobile{
|
||||
width:100%;
|
||||
margin-left:0;
|
||||
margin-top:8px;
|
||||
text-align:center;
|
||||
padding:8px 10px;
|
||||
font-size:.8rem;
|
||||
}
|
||||
|
||||
.zoom-btn-mobile:active{
|
||||
background:rgba(88,166,255,.2);
|
||||
transform:scale(.98);
|
||||
}
|
||||
|
||||
.chart-range-badge{
|
||||
font-size:.65rem;
|
||||
padding:3px 8px;
|
||||
top:8px;
|
||||
right:8px;
|
||||
}
|
||||
}
|
||||
|
||||
.vm-range-quick{
|
||||
display:flex;
|
||||
gap:6px;
|
||||
flex-wrap:wrap;
|
||||
margin-bottom:6px;
|
||||
}
|
||||
|
||||
.vm-range-btn{
|
||||
padding:4px 10px;
|
||||
font-size:.75rem;
|
||||
border-radius:6px;
|
||||
border:1px solid var(--border-color);
|
||||
background:transparent;
|
||||
color:var(--blue-accent);
|
||||
cursor:pointer;
|
||||
transition:.15s;
|
||||
}
|
||||
|
||||
.vm-range-btn:hover{
|
||||
background:rgba(88,166,255,.1);
|
||||
}
|
||||
|
||||
.vm-range-btn.active{
|
||||
background:#1f6feb;
|
||||
border-color:#1f6feb;
|
||||
color:#fff;
|
||||
}
|
||||
|
||||
94
static/js/apiHelper.js
Normal file
94
static/js/apiHelper.js
Normal file
@@ -0,0 +1,94 @@
|
||||
(function () {
|
||||
const modal = () => document.getElementById('apiHelperModal');
|
||||
|
||||
function open() {
|
||||
const m = modal();
|
||||
if (m) m.style.display = 'flex';
|
||||
generate();
|
||||
}
|
||||
|
||||
function close() {
|
||||
const m = modal();
|
||||
if (m) m.style.display = 'none';
|
||||
}
|
||||
|
||||
function isoFromLocal(value) {
|
||||
if (!value) return null;
|
||||
const d = new Date(value);
|
||||
return isNaN(d.getTime()) ? null : d.toISOString();
|
||||
}
|
||||
|
||||
function generate() {
|
||||
const endpoint = document.getElementById('apiEndpoint')?.value || '/api/events';
|
||||
const range = document.getElementById('apiRange')?.value?.trim();
|
||||
const start = isoFromLocal(document.getElementById('apiStart')?.value);
|
||||
const end = isoFromLocal(document.getElementById('apiEnd')?.value);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (range) params.set('range', range);
|
||||
if (start) params.set('start', start);
|
||||
if (end) params.set('end', end);
|
||||
|
||||
const url = `${window.location.origin}${endpoint}${params.toString() ? '?' + params.toString() : ''}`;
|
||||
|
||||
const outUrl = document.getElementById('apiUrlOutput');
|
||||
const outCurl = document.getElementById('apiCurlOutput');
|
||||
if (outUrl) outUrl.value = url;
|
||||
if (outCurl) outCurl.value = `curl "${url}"`;
|
||||
}
|
||||
|
||||
async function callApi() {
|
||||
generate();
|
||||
const url = document.getElementById('apiUrlOutput')?.value;
|
||||
const pre = document.getElementById('apiResponse');
|
||||
if (!url || !pre) return;
|
||||
|
||||
pre.textContent = 'Loading...';
|
||||
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
pre.textContent = JSON.stringify(data, null, 2);
|
||||
} catch (e) {
|
||||
pre.textContent = 'Error: ' + e;
|
||||
}
|
||||
}
|
||||
|
||||
async function copy(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el || !el.value) return;
|
||||
try { await navigator.clipboard.writeText(el.value); } catch {}
|
||||
}
|
||||
|
||||
function bind() {
|
||||
document.getElementById('openApiHelperBtn')?.addEventListener('click', open);
|
||||
document.getElementById('closeApiHelperBtn')?.addEventListener('click', close);
|
||||
|
||||
document.getElementById('genApiBtn')?.addEventListener('click', generate);
|
||||
document.getElementById('callApiBtn')?.addEventListener('click', callApi);
|
||||
|
||||
document.getElementById('copyApiUrlBtn')?.addEventListener('click', () => copy('apiUrlOutput'));
|
||||
document.getElementById('copyCurlBtn')?.addEventListener('click', () => copy('apiCurlOutput'));
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const m = modal();
|
||||
if (m && e.target === m) close();
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', bind);
|
||||
|
||||
window.apiHelper = { open, close, generate, callApi };
|
||||
})();
|
||||
|
||||
document.querySelectorAll('.vm-range-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.vm-range-btn')
|
||||
.forEach(b => b.classList.remove('active'));
|
||||
|
||||
btn.classList.add('active');
|
||||
|
||||
const input = document.getElementById('apiRange');
|
||||
if (input) input.value = btn.dataset.range;
|
||||
});
|
||||
});
|
||||
134
static/js/chart.js
Normal file
134
static/js/chart.js
Normal file
@@ -0,0 +1,134 @@
|
||||
window.setupMainChart = function setupMainChart() {
|
||||
const ctx = document.getElementById('voltageChart');
|
||||
if (!ctx) return;
|
||||
|
||||
window.voltageChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: { datasets: [] },
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: { mode: 'nearest', axis: 'x', intersect: false },
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
time: {
|
||||
displayFormats: {
|
||||
millisecond: 'HH:mm:ss.SSS',
|
||||
second: 'HH:mm:ss',
|
||||
minute: 'HH:mm',
|
||||
hour: 'HH:mm',
|
||||
day: 'dd LLL',
|
||||
week: 'dd LLL',
|
||||
month: 'LLL yyyy',
|
||||
quarter: 'LLL yyyy',
|
||||
year: 'yyyy'
|
||||
},
|
||||
tooltipFormat: 'dd.MM.yyyy HH:mm:ss'
|
||||
},
|
||||
grid: { color: '#2d3139' },
|
||||
ticks: { color: '#8b949e' }
|
||||
},
|
||||
y: {
|
||||
beginAtZero: false,
|
||||
min: 190,
|
||||
max: 255,
|
||||
grid: { color: '#2d3139' },
|
||||
ticks: { stepSize: 5, color: '#c9d1d9' }
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
zoom: {
|
||||
limits: {
|
||||
x: { min: 'original', max: () => Date.now(), minRange: 60 * 1000 },
|
||||
y: { min: 190, max: 255 }
|
||||
},
|
||||
zoom: {
|
||||
wheel: { enabled: true },
|
||||
pinch: { enabled: true },
|
||||
drag: { enabled: true, backgroundColor: 'rgba(54, 162, 235, 0.3)' },
|
||||
mode: 'x',
|
||||
onZoomComplete: async ({ chart }) => {
|
||||
const now = Date.now();
|
||||
if (chart.scales.x.max > now) {
|
||||
chart.scales.x.max = now;
|
||||
chart.update('none');
|
||||
}
|
||||
|
||||
document.querySelectorAll('.time-btn').forEach(b => b.classList.remove('active'));
|
||||
window.currentTimeRange = 'precise';
|
||||
|
||||
await window.reloadDataForRange(chart.scales.x.min, chart.scales.x.max);
|
||||
}
|
||||
},
|
||||
pan: {
|
||||
enabled: true,
|
||||
mode: 'x',
|
||||
onPanComplete: async ({ chart }) => {
|
||||
const now = Date.now();
|
||||
if (chart.scales.x.max > now) {
|
||||
const rangeWidth = chart.scales.x.max - chart.scales.x.min;
|
||||
chart.scales.x.max = now;
|
||||
chart.scales.x.min = now - rangeWidth;
|
||||
chart.update('none');
|
||||
}
|
||||
|
||||
document.querySelectorAll('.time-btn').forEach(b => b.classList.remove('active'));
|
||||
window.currentTimeRange = 'precise';
|
||||
await window.reloadDataForRange(chart.scales.x.min, chart.scales.x.max);
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
labels: {
|
||||
color: '#c9d1d9',
|
||||
filter: (item) => !['Zanik', 'Powrot', 'Awaria'].some(word => item.text.includes(word))
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
position: 'nearest',
|
||||
callbacks: {
|
||||
label: (ctx) => {
|
||||
const datasetLabel = ctx.dataset.label || '';
|
||||
const raw = ctx.raw;
|
||||
const val = (raw && raw.realV !== undefined) ? raw.realV : ctx.parsed.y;
|
||||
|
||||
if (datasetLabel.includes('Zanik')) return 'ZANIK FAZY';
|
||||
if (datasetLabel.includes('Powrot')) return `POWROT: ${val.toFixed(1)}V`;
|
||||
if (datasetLabel.includes('Awaria')) return null;
|
||||
return `${datasetLabel}: ${val.toFixed(1)}V`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.updateRangeLabel = function updateRangeLabel(min, max) {
|
||||
const start = new Date(min);
|
||||
const end = new Date(max);
|
||||
const optTime = { hour: '2-digit', minute: '2-digit', hour12: false };
|
||||
const optDate = { day: '2-digit', month: '2-digit' };
|
||||
|
||||
let rangeText = '';
|
||||
if (start.toDateString() === end.toDateString()) {
|
||||
rangeText = `Zakres: ${start.toLocaleDateString('pl-PL', optDate)}, ${start.toLocaleTimeString('pl-PL', optTime)} - ${end.toLocaleTimeString('pl-PL', optTime)}`;
|
||||
} else {
|
||||
rangeText = `Zakres: ${start.toLocaleDateString('pl-PL', optDate)} ${start.toLocaleTimeString('pl-PL', optTime)} - ${end.toLocaleDateString('pl-PL', optDate)} ${end.toLocaleTimeString('pl-PL', optTime)}`;
|
||||
}
|
||||
|
||||
const label = document.getElementById('eventRangeLabel');
|
||||
if (label) label.textContent = rangeText;
|
||||
|
||||
const chartDisplay = document.getElementById('chartRangeDisplay');
|
||||
if (chartDisplay) {
|
||||
const optDateShort = { day: '2-digit', month: '2-digit' };
|
||||
if (start.toDateString() === end.toDateString()) {
|
||||
chartDisplay.textContent = `${start.toLocaleDateString('pl-PL', optDateShort)} ${start.toLocaleTimeString('pl-PL', optTime)} - ${end.toLocaleTimeString('pl-PL', optTime)}`;
|
||||
} else {
|
||||
chartDisplay.textContent = `${start.toLocaleDateString('pl-PL', optDateShort)} ${start.toLocaleTimeString('pl-PL', optTime)} - ${end.toLocaleDateString('pl-PL', optDateShort)} ${end.toLocaleTimeString('pl-PL', optTime)}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
121
static/js/data.js
Normal file
121
static/js/data.js
Normal file
@@ -0,0 +1,121 @@
|
||||
window.processPhaseData = function processPhaseData(id, data) {
|
||||
const lineData = [];
|
||||
const outagePoints = [];
|
||||
const recoveryPoints = [];
|
||||
const outageLineData = [];
|
||||
let wasOutage = false;
|
||||
|
||||
data.forEach(p => {
|
||||
const v = p.voltage;
|
||||
const t = new Date(p.time);
|
||||
|
||||
if (v < 180 && v !== null) {
|
||||
const pOut = { x: t, y: 195, realV: v };
|
||||
outagePoints.push(pOut);
|
||||
outageLineData.push(pOut);
|
||||
|
||||
lineData.push({ x: t, y: null });
|
||||
wasOutage = true;
|
||||
} else {
|
||||
if (wasOutage) {
|
||||
const pRec = { x: t, y: v, realV: v };
|
||||
recoveryPoints.push(pRec);
|
||||
outageLineData.push(pRec);
|
||||
wasOutage = false;
|
||||
} else {
|
||||
outageLineData.push({ x: t, y: null });
|
||||
}
|
||||
lineData.push({ x: t, y: v });
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
lineDataset: {
|
||||
label: window.phasesConfig[id].label,
|
||||
data: lineData,
|
||||
borderColor: window.phasesConfig[id].color,
|
||||
backgroundColor: window.phasesConfig[id].color + '15',
|
||||
tension: 0.1,
|
||||
borderWidth: 2,
|
||||
spanGaps: false,
|
||||
pointRadius: 0
|
||||
},
|
||||
outageLine: {
|
||||
label: 'Awaria ' + window.phasesConfig[id].label,
|
||||
data: outageLineData,
|
||||
borderColor: '#ff0000',
|
||||
borderDash: [5, 5],
|
||||
borderWidth: 1,
|
||||
pointRadius: 0,
|
||||
fill: false,
|
||||
spanGaps: false,
|
||||
showLine: true
|
||||
},
|
||||
outageDataset: outagePoints.length ? {
|
||||
label: 'Zanik ' + window.phasesConfig[id].label,
|
||||
data: outagePoints,
|
||||
type: 'scatter',
|
||||
pointRadius: 4,
|
||||
pointBackgroundColor: '#ff0000',
|
||||
pointBorderColor: '#fff',
|
||||
pointBorderWidth: 1,
|
||||
z: 99
|
||||
} : null,
|
||||
recoveryDataset: recoveryPoints.length ? {
|
||||
label: 'Powrot ' + window.phasesConfig[id].label,
|
||||
data: recoveryPoints,
|
||||
type: 'scatter',
|
||||
pointRadius: 4,
|
||||
pointBackgroundColor: '#3fb950',
|
||||
pointBorderColor: '#fff',
|
||||
pointBorderWidth: 1,
|
||||
z: 99
|
||||
} : null
|
||||
};
|
||||
};
|
||||
|
||||
window.reloadDataForRange = async function reloadDataForRange(min, max, rangeName = null) {
|
||||
const now = Date.now();
|
||||
if (max && max > now) max = now;
|
||||
if (min && min > now) min = now - 3600000;
|
||||
|
||||
const urlParams = rangeName
|
||||
? `range=${rangeName}`
|
||||
: `start=${new Date(min).toISOString()}&end=${new Date(max).toISOString()}`;
|
||||
|
||||
const newDatasets = [];
|
||||
|
||||
for (const id of Object.keys(window.phasesConfig)) {
|
||||
try {
|
||||
const raw = await fetch(`/api/timeseries/${id}?${urlParams}`).then(r => r.json());
|
||||
const proc = window.processPhaseData(id, raw);
|
||||
newDatasets.push(proc.lineDataset);
|
||||
if (proc.outageLine) newDatasets.push(proc.outageLine);
|
||||
if (proc.outageDataset) newDatasets.push(proc.outageDataset);
|
||||
if (proc.recoveryDataset) newDatasets.push(proc.recoveryDataset);
|
||||
} catch (e) {
|
||||
console.error("Błąd pobierania fazy " + id, e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const events = await fetch(`/api/events?${urlParams}`).then(r => r.json());
|
||||
window.renderEventLog(events, rangeName || 'precise');
|
||||
} catch (e) {
|
||||
console.error("Błąd pobierania zdarzeń", e);
|
||||
}
|
||||
|
||||
window.voltageChart.data.datasets = newDatasets;
|
||||
|
||||
if (window.disableChartAnimationOnce) {
|
||||
window.voltageChart.update('none');
|
||||
window.disableChartAnimationOnce = false;
|
||||
} else {
|
||||
if (rangeName) window.voltageChart.update();
|
||||
else window.voltageChart.update('none');
|
||||
}
|
||||
|
||||
const finalMin = rangeName ? window.voltageChart.scales.x.min : (min || window.voltageChart.scales.x.min);
|
||||
const finalMax = rangeName ? window.voltageChart.scales.x.max : (max || window.voltageChart.scales.x.max);
|
||||
window.updateRangeLabel(finalMin, finalMax);
|
||||
};
|
||||
74
static/js/events.js
Normal file
74
static/js/events.js
Normal file
@@ -0,0 +1,74 @@
|
||||
window.renderEventLog = function renderEventLog(events, range) {
|
||||
const container = document.getElementById('eventLogContainer');
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
|
||||
if (events && events.error === "range_too_large") {
|
||||
container.innerHTML = `
|
||||
<div style="text-align: center; padding: 30px; color: #ffbb33; border: 1px dashed #ffbb33; border-radius: 12px; margin: 10px;">
|
||||
<div style="font-size: 2rem; margin-bottom: 10px;">⚠️</div>
|
||||
<p style="margin: 0;">${events.message}</p>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (events && events.length > 0) {
|
||||
events.forEach(ev => {
|
||||
const start = new Date(ev.start);
|
||||
const end = new Date(ev.end);
|
||||
const dur = ev.duration;
|
||||
const phase = window.phasesConfig[ev.phase];
|
||||
|
||||
const typeConfig = {
|
||||
'zanik': { label: 'Brak zasilania', color: '#ff4444' },
|
||||
'niskie': { label: 'Zbyt niskie', color: '#ffbb33' },
|
||||
'wysokie': { label: 'Zbyt wysokie', color: '#aa66cc' }
|
||||
};
|
||||
const cfg = typeConfig[ev.type] || { label: 'Problem', color: '#888' };
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'event-item';
|
||||
item.style.display = 'flex';
|
||||
item.style.alignItems = 'center';
|
||||
item.style.justifyContent = 'space-between';
|
||||
|
||||
const timeRangeStr = `${start.toLocaleTimeString('pl-PL', {hour:'2-digit', minute:'2-digit'})} - ${end.toLocaleTimeString('pl-PL', {hour:'2-digit', minute:'2-digit'})}`;
|
||||
|
||||
item.innerHTML = `
|
||||
<div class="event-content">
|
||||
<div class="event-badge" style="background-color: ${cfg.color}; box-shadow: 0 0 0 2px ${phase.color};"></div>
|
||||
<div class="event-time">${start.toLocaleDateString('pl-PL', {day:'2-digit', month:'2-digit'})}, ${timeRangeStr}</div>
|
||||
<div class="event-desc">
|
||||
<span style="color: ${phase.color}; font-weight: bold;">${phase.label}</span>:
|
||||
<strong>${cfg.label}</strong> przez ${dur} min.
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="showEventOnChart('${ev.start}')" class="zoom-btn-mobile">
|
||||
Pokaż na wykresie 🔍
|
||||
</button>
|
||||
`;
|
||||
|
||||
container.appendChild(item);
|
||||
});
|
||||
} else {
|
||||
container.innerHTML = '<div class="no-events">Brak zarejestrowanych zdarzeń w tym zakresie.</div>';
|
||||
}
|
||||
};
|
||||
|
||||
window.showEventOnChart = function showEventOnChart(startTimeStr) {
|
||||
const eventTime = new Date(startTimeStr).getTime();
|
||||
const padding = 3 * 60 * 60 * 1000;
|
||||
|
||||
const min = eventTime - padding;
|
||||
const max = eventTime + padding;
|
||||
|
||||
if (window.voltageChart) {
|
||||
document.querySelectorAll('.time-btn').forEach(b => b.classList.remove('active'));
|
||||
|
||||
window.voltageChart.options.scales.x.min = min;
|
||||
window.voltageChart.options.scales.x.max = max;
|
||||
|
||||
window.currentTimeRange = 'precise';
|
||||
window.reloadDataForRange(min, max);
|
||||
}
|
||||
};
|
||||
25
static/js/index.js
Normal file
25
static/js/index.js
Normal file
@@ -0,0 +1,25 @@
|
||||
window.initMonitor = function initMonitor(phases, defaultRange) {
|
||||
window.phasesConfig = phases;
|
||||
window.currentTimeRange = defaultRange;
|
||||
|
||||
setupGauges();
|
||||
window.setupMainChart();
|
||||
window.bindMonitorSocketHandlers();
|
||||
|
||||
window.changeTimeRange(window.currentTimeRange);
|
||||
};
|
||||
|
||||
window.changeTimeRange = function changeTimeRange(range) {
|
||||
window.currentTimeRange = range;
|
||||
|
||||
if (window.voltageChart) {
|
||||
window.voltageChart.options.scales.x.min = undefined;
|
||||
window.voltageChart.options.scales.x.max = undefined;
|
||||
}
|
||||
|
||||
document.querySelectorAll('.time-btn').forEach(btn => btn.classList.remove('active'));
|
||||
const activeBtn = document.querySelector(`[data-range="${range}"]`);
|
||||
if (activeBtn) activeBtn.classList.add('active');
|
||||
|
||||
window.reloadDataForRange(null, null, range);
|
||||
};
|
||||
@@ -1,11 +1,7 @@
|
||||
/**
|
||||
* Otwiera modal z wyborem własnego zakresu dat
|
||||
*/
|
||||
function openCustomRangePicker() {
|
||||
const modal = document.getElementById('customRangeModal');
|
||||
if (!modal) return;
|
||||
|
||||
// Ustaw domyślne wartości - od 24h temu do teraz
|
||||
const now = new Date();
|
||||
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
|
||||
@@ -15,17 +11,11 @@ function openCustomRangePicker() {
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
/**
|
||||
* Zamyka modal z wyborem zakresu
|
||||
*/
|
||||
function closeCustomRangePicker() {
|
||||
const modal = document.getElementById('customRangeModal');
|
||||
if (modal) modal.style.display = 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Stosuje wybrany własny zakres
|
||||
*/
|
||||
async function applyCustomRange() {
|
||||
const startInput = document.getElementById('customStartDate');
|
||||
const endInput = document.getElementById('customEndDate');
|
||||
@@ -74,9 +64,6 @@ async function applyCustomRange() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatuje datę do formatu datetime-local input
|
||||
*/
|
||||
function formatDateTimeLocal(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
@@ -87,7 +74,6 @@ function formatDateTimeLocal(date) {
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
// Zamknij modal po kliknięciu poza nim
|
||||
document.addEventListener('click', function(event) {
|
||||
const modal = document.getElementById('customRangeModal');
|
||||
if (modal && event.target === modal) {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
const socket = io();
|
||||
window.socket = io();
|
||||
const socket = window.socket;
|
||||
|
||||
let currentTimeRange = '6h';
|
||||
let phasesConfig = {};
|
||||
const gauges = {};
|
||||
|
||||
5
static/js/pageInit.js
Normal file
5
static/js/pageInit.js
Normal file
@@ -0,0 +1,5 @@
|
||||
function initPage(phases, defaultRange) {
|
||||
if (typeof initMonitor === 'function') {
|
||||
initMonitor(phases, defaultRange);
|
||||
}
|
||||
}
|
||||
39
static/js/socket.js
Normal file
39
static/js/socket.js
Normal file
@@ -0,0 +1,39 @@
|
||||
window.bindMonitorSocketHandlers = function bindMonitorSocketHandlers() {
|
||||
const socket = window.socket;
|
||||
if (!socket) return;
|
||||
|
||||
socket.on('voltage_update', (data) => {
|
||||
Object.keys(window.phasesConfig).forEach(id => {
|
||||
const val = data['phase' + id];
|
||||
const textEl = document.getElementById('value' + id);
|
||||
if (val !== undefined && val !== null && textEl) {
|
||||
const num = parseFloat(val);
|
||||
textEl.textContent = num.toFixed(1) + 'V';
|
||||
textEl.style.color = num < 200 ? '#dc3545' : (num > 260 ? '#ffc107' : '#fff');
|
||||
updateGaugeUI(id, num);
|
||||
}
|
||||
});
|
||||
|
||||
if (data.timestamp) {
|
||||
const el = document.getElementById('lastUpdate');
|
||||
if (el) {
|
||||
el.textContent = 'Ostatni odczyt: ' + new Date(data.timestamp).toLocaleTimeString('pl-PL', { hour12: false });
|
||||
}
|
||||
|
||||
const ts = new Date(data.timestamp).getTime();
|
||||
if (!isNaN(ts)) window.lastLiveTs = ts;
|
||||
|
||||
const ms = window.rangeToMs(window.currentTimeRange);
|
||||
if (ms && window.lastLiveTs) {
|
||||
window.updateRangeLabel(window.lastLiveTs - ms, window.lastLiveTs);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('refresh_timeseries', async () => {
|
||||
if (window.currentTimeRange === 'precise') return;
|
||||
|
||||
window.disableChartAnimationOnce = true;
|
||||
await window.reloadDataForRange(null, null, window.currentTimeRange);
|
||||
});
|
||||
};
|
||||
1
static/js/socketClient.js
Normal file
1
static/js/socketClient.js
Normal file
@@ -0,0 +1 @@
|
||||
window.socket = window.socket || io();
|
||||
16
static/js/state.js
Normal file
16
static/js/state.js
Normal file
@@ -0,0 +1,16 @@
|
||||
window.currentTimeRange = window.currentTimeRange || '6h';
|
||||
window.phasesConfig = window.phasesConfig || {};
|
||||
window.gauges = window.gauges || {};
|
||||
window.voltageChart = window.voltageChart || null;
|
||||
window.THRESHOLDS = window.THRESHOLDS || { min: 207, max: 253 };
|
||||
window.disableChartAnimationOnce = window.disableChartAnimationOnce || false;
|
||||
window.lastLiveTs = null;
|
||||
|
||||
window.rangeToMs = function(r){
|
||||
const m = String(r||'').trim().match(/^(\d+)\s*([mhdw])$/i);
|
||||
if(!m) return null;
|
||||
const n = parseInt(m[1],10);
|
||||
const u = m[2].toLowerCase();
|
||||
const mult = { m:60e3, h:3600e3, d:86400e3, w:7*86400e3 }[u];
|
||||
return n * mult;
|
||||
};
|
||||
35
static/js/topbarStatus.js
Normal file
35
static/js/topbarStatus.js
Normal file
@@ -0,0 +1,35 @@
|
||||
(function () {
|
||||
const dot = () => document.getElementById('connDot');
|
||||
const txt = () => document.getElementById('connText');
|
||||
const lastTop = () => document.getElementById('lastUpdateTop');
|
||||
|
||||
function setStatus(state) {
|
||||
const d = dot(), t = txt();
|
||||
if (!d || !t) return;
|
||||
|
||||
d.classList.remove('online', 'offline', 'connecting');
|
||||
if (state === 'online') { d.classList.add('online'); t.textContent = 'Online'; }
|
||||
else if (state === 'offline') { d.classList.add('offline'); t.textContent = 'Offline'; }
|
||||
else { d.classList.add('connecting'); t.textContent = 'Łączenie…'; }
|
||||
}
|
||||
|
||||
function bind() {
|
||||
if (!window.socket) { setStatus('offline'); return; }
|
||||
|
||||
setStatus('connecting');
|
||||
window.socket.on('connect', () => setStatus('online'));
|
||||
window.socket.on('disconnect', () => setStatus('offline'));
|
||||
if (window.socket.io) window.socket.io.on('reconnect_attempt', () => setStatus('connecting'));
|
||||
|
||||
window.socket.on('voltage_update', (data) => {
|
||||
if (!data || !data.timestamp) return;
|
||||
const el = lastTop();
|
||||
if (!el) return;
|
||||
const t = new Date(data.timestamp);
|
||||
const s = t.toLocaleTimeString('pl-PL', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
el.textContent = `Aktualizacja: ${s}`;
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', bind);
|
||||
})();
|
||||
@@ -1,37 +1,61 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pl" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>VoltMonitor</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{{ static_v('css/style.css') }}">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>VoltMonitor</title>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
|
||||
<link rel="stylesheet" href="{{ static_v('css/style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid py-3 px-2">
|
||||
{% block content %}{% endblock %}
|
||||
<footer class="text-center mt-4 py-3">
|
||||
<small class="text-muted">
|
||||
© {{ footer.year }} {{ footer.project }} |
|
||||
Autor:
|
||||
<a href="{{ footer.owner.url }}" target="_blank" class="text-decoration-none">
|
||||
{{ footer.owner.name }}
|
||||
</a>
|
||||
{% if footer.version %}
|
||||
| v{{ footer.version }}
|
||||
{% endif %}
|
||||
</small>
|
||||
</footer>
|
||||
<div class="container-fluid py-3 px-2 vm-shell">
|
||||
|
||||
<!-- TOPBAR -->
|
||||
<div class="vm-topbar">
|
||||
<div class="vm-topbar-left">
|
||||
<span class="vm-dot connecting" id="connDot"></span>
|
||||
<span class="vm-topbar-text" id="connText">Łączenie…</span>
|
||||
</div>
|
||||
|
||||
<div class="vm-topbar-right">
|
||||
<span class="vm-topbar-text d-none d-sm-inline" id="lastUpdateTop">Aktualizacja: —</span>
|
||||
|
||||
<button class="btn btn-sm btn-outline-info" id="openApiHelperBtn" type="button" title="API Helper">
|
||||
<i class="fa-solid fa-code"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" onclick="location.reload()" title="Odśwież">
|
||||
<i class="fa-solid fa-rotate-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Chart.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/date-fns@2.29.3/locale/pl/index.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom"></script>
|
||||
|
||||
<!-- Socket.IO -->
|
||||
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
<footer class="text-center mt-4 py-3">
|
||||
<small class="text-muted">
|
||||
© {{ footer.year }} {{ footer.project }} |
|
||||
Autor:
|
||||
<a href="{{ footer.owner.url }}" target="_blank" class="text-decoration-none">{{ footer.owner.name }}</a>
|
||||
{% if footer.version %}| v{{ footer.version }}{% endif %}
|
||||
</small>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- libs -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/date-fns@2.29.3/locale/pl/index.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom"></script>
|
||||
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
|
||||
|
||||
<!-- app: core -->
|
||||
<script src="{{ static_v('js/socketClient.js') }}"></script>
|
||||
<script src="{{ static_v('js/topbarStatus.js') }}"></script>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,101 +1,218 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<header class="text-center mb-3">
|
||||
<h4 class="mb-0">Sieć Trójfazowa / Rokietnica, Gajowa</h4>
|
||||
</header>
|
||||
<div class="text-center mb-3">
|
||||
<h5>Dane chwilowe:</h5>
|
||||
<span class="badge bg-dark border border-secondary text-muted" id="lastUpdate" style="font-size: 0.7rem;">Ładowanie...</span>
|
||||
|
||||
<!-- HEADER -->
|
||||
<div class="vm-card p-3 mb-3">
|
||||
<div class="d-flex flex-column flex-md-row align-items-md-center justify-content-between gap-2">
|
||||
<div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i class="fa-solid fa-bolt text-warning"></i>
|
||||
<h4 class="mb-0">VoltMonitor</h4>
|
||||
</div>
|
||||
<div class="text-muted small mt-1">Sieć trójfazowa • Rokietnica, Gajowa</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<span class="badge bg-dark border border-secondary text-muted" id="lastUpdate" style="font-size:.72rem;">Ładowanie…</span>
|
||||
<span class="badge bg-dark border border-secondary text-muted" style="font-size:.72rem;">
|
||||
PN-EN 50160: <span class="text-success fw-semibold">207–253V</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gauge Section -->
|
||||
<div class="gauge-grid mb-1">
|
||||
<!-- GAUGES -->
|
||||
<div class="vm-card p-3 mb-3">
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<h6 class="mb-0"><i class="fa-solid fa-gauge-high me-2 text-info"></i>Dane chwilowe</h6>
|
||||
<span class="text-muted small">live</span>
|
||||
</div>
|
||||
|
||||
<div class="gauge-grid mb-0">
|
||||
{% for id, phase in phases.items() %}
|
||||
<div class="gauge-card">
|
||||
<div class="gauge-canvas-container">
|
||||
<canvas id="gauge{{ id }}"></canvas>
|
||||
</div>
|
||||
<div class="gauge-label">{{ phase.label }}</div>
|
||||
<div class="voltage-value" id="value{{ id }}">---</div>
|
||||
<div class="gauge-canvas-container"><canvas id="gauge{{ id }}"></canvas></div>
|
||||
<div class="gauge-label">{{ phase.label }}</div>
|
||||
<div class="voltage-value" id="value{{ id }}">---</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Informacja o normie napięcia -->
|
||||
<div class="text-center mb-3">
|
||||
<span class="badge bg-dark border border-secondary text-muted" style="font-size: 0.65rem; font-weight: 400; opacity: 0.8;">
|
||||
Norma PN-EN 50160: <span class="text-success"><strong>230V ±10% (207V - 253V)</strong></span>
|
||||
</span>
|
||||
</div>
|
||||
<hr class="border-dotted">
|
||||
<!-- CONTROLS -->
|
||||
<div class="vm-card p-3 mb-3">
|
||||
<div class="d-flex flex-column flex-md-row align-items-md-center justify-content-between gap-2 mb-2">
|
||||
<h6 class="mb-0"><i class="fa-solid fa-chart-line me-2 text-primary"></i>Wykres</h6>
|
||||
<div class="text-muted small">Zakres • zoom/pan • przeciągnięcie = precyzyjny wybór</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mb-3">
|
||||
<h5>Wybierz zakres wykresu:</h5>
|
||||
</div>
|
||||
<!-- Time & Reset Selector -->
|
||||
<div class="d-flex justify-content-center align-items-center gap-2 mb-3 flex-wrap">
|
||||
<div class="time-selector-wrapper mb-3">
|
||||
{% for key, r in time_ranges.items() %}
|
||||
<button class="btn btn-sm btn-outline-primary time-btn {% if key == default_range %}active{% endif %}"
|
||||
data-range="{{ key }}" onclick="changeTimeRange('{{ key }}')">
|
||||
{{ key }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
<button class="btn btn-sm btn-outline-primary time-btn"
|
||||
id="customRangeBtn" onclick="openCustomRangePicker()">
|
||||
Własny zakres
|
||||
</button>
|
||||
</div>
|
||||
<div class="time-selector-wrapper mb-0">
|
||||
{% for key, r in time_ranges.items() %}
|
||||
<button class="btn btn-sm btn-outline-primary time-btn {% if key == default_range %}active{% endif %}"
|
||||
data-range="{{ key }}" onclick="changeTimeRange('{{ key }}')">
|
||||
<i class="fa-regular fa-clock me-1"></i>{{ key }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
|
||||
<button class="btn btn-sm btn-outline-primary time-btn" id="customRangeBtn" onclick="openCustomRangePicker()">
|
||||
<i class="fa-solid fa-calendar-days me-1"></i>Własny
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal dla własnego zakresu -->
|
||||
<!-- Modal: custom range -->
|
||||
<div id="customRangeModal">
|
||||
<div class="modal-content">
|
||||
<h3>Wybierz własny zakres</h3>
|
||||
|
||||
<div class="modal-form-group">
|
||||
<label class="modal-label">Data i czas od:</label>
|
||||
<input type="datetime-local" id="customStartDate" class="modal-input">
|
||||
</div>
|
||||
|
||||
<div class="modal-form-group">
|
||||
<label class="modal-label">Data i czas do:</label>
|
||||
<input type="datetime-local" id="customEndDate" class="modal-input">
|
||||
</div>
|
||||
|
||||
<div class="modal-buttons">
|
||||
<button onclick="closeCustomRangePicker()" class="modal-btn modal-btn-cancel">Anuluj</button>
|
||||
<button onclick="applyCustomRange()" class="modal-btn modal-btn-apply">Zastosuj</button>
|
||||
</div>
|
||||
<div class="modal-content">
|
||||
<h3 class="d-flex align-items-center gap-2">
|
||||
<i class="fa-solid fa-calendar-days text-primary"></i>Własny zakres
|
||||
</h3>
|
||||
|
||||
<div class="modal-form-group">
|
||||
<label class="modal-label">Data i czas od:</label>
|
||||
<input type="datetime-local" id="customStartDate" class="modal-input">
|
||||
</div>
|
||||
|
||||
<div class="modal-form-group">
|
||||
<label class="modal-label">Data i czas do:</label>
|
||||
<input type="datetime-local" id="customEndDate" class="modal-input">
|
||||
</div>
|
||||
|
||||
<div class="modal-buttons">
|
||||
<button onclick="closeCustomRangePicker()" class="modal-btn modal-btn-cancel">
|
||||
<i class="fa-solid fa-xmark me-1"></i>Anuluj
|
||||
</button>
|
||||
<button onclick="applyCustomRange()" class="modal-btn modal-btn-apply">
|
||||
<i class="fa-solid fa-check me-1"></i>Zastosuj
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Chart -->
|
||||
<div class="main-chart-card" style="position: relative;">
|
||||
<span id="chartRangeDisplay" class="chart-range-badge"></span>
|
||||
<canvas id="voltageChart"></canvas>
|
||||
<!-- CHART -->
|
||||
<div class="main-chart-card mb-3" style="position:relative;">
|
||||
<span id="chartRangeDisplay" class="chart-range-badge"></span>
|
||||
<canvas id="voltageChart"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- EVENTS -->
|
||||
<div class="events-card mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">Dziennik zdarzeń</h6>
|
||||
<span id="eventRangeLabel" class="text-muted" style="font-size: 0.75rem;">Ładowanie...</span>
|
||||
</div>
|
||||
<div id="eventLogContainer">
|
||||
<div class="no-events">Ładowanie zdarzeń...</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 flex-wrap gap-2">
|
||||
<h6 class="mb-0"><i class="fa-solid fa-list-ul me-2 text-warning"></i>Dziennik zdarzeń</h6>
|
||||
<span id="eventRangeLabel" class="text-muted" style="font-size:.75rem;">Ładowanie…</span>
|
||||
</div>
|
||||
|
||||
<div id="eventLogContainer">
|
||||
<div class="no-events"><i class="fa-solid fa-spinner fa-spin me-2"></i>Ładowanie zdarzeń…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: API Helper -->
|
||||
<div id="apiHelperModal">
|
||||
<div class="modal-content vm-modal-wide">
|
||||
<div class="vm-modal-head">
|
||||
<h3 class="m-0 d-flex align-items-center gap-2">
|
||||
<i class="fa-solid fa-code text-info"></i>API Helper
|
||||
</h3>
|
||||
<button class="vm-icon-btn" type="button" id="closeApiHelperBtn" title="Zamknij">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="vm-modal-grid">
|
||||
<div>
|
||||
<label class="modal-label">Endpoint</label>
|
||||
<select id="apiEndpoint" class="modal-input">
|
||||
<option value="/api/timeseries/1">/api/timeseries/1</option>
|
||||
<option value="/api/timeseries/2">/api/timeseries/2</option>
|
||||
<option value="/api/timeseries/3">/api/timeseries/3</option>
|
||||
<option value="/api/events">/api/events</option>
|
||||
<option value="/api/outages/1">/api/outages/1</option>
|
||||
<option value="/api/outages/2">/api/outages/2</option>
|
||||
<option value="/api/outages/3">/api/outages/3</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="modal-label">range (opcjonalnie)</label>
|
||||
|
||||
<div class="vm-range-quick">
|
||||
<button type="button" class="vm-range-btn" data-range="6h">6h</button>
|
||||
<button type="button" class="vm-range-btn" data-range="12h">12h</button>
|
||||
<button type="button" class="vm-range-btn" data-range="24h">24h</button>
|
||||
<button type="button" class="vm-range-btn" data-range="7d">7d</button>
|
||||
<button type="button" class="vm-range-btn" data-range="30d">30d</button>
|
||||
</div>
|
||||
|
||||
<input id="apiRange" class="modal-input mt-2" placeholder="np. 24h / 7d">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="modal-label">start (opcjonalnie)</label>
|
||||
<input id="apiStart" type="datetime-local" class="modal-input">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="modal-label">end (opcjonalnie)</label>
|
||||
<input id="apiEnd" type="datetime-local" class="modal-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vm-modal-block">
|
||||
<label class="modal-label">URL</label>
|
||||
<div class="vm-inline">
|
||||
<input id="apiUrlOutput" class="modal-input vm-mono" readonly>
|
||||
<button class="modal-btn modal-btn-apply" type="button" id="copyApiUrlBtn">
|
||||
<i class="fa-regular fa-copy me-1"></i>Kopiuj
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vm-modal-block">
|
||||
<label class="modal-label">curl</label>
|
||||
<div class="vm-inline">
|
||||
<input id="apiCurlOutput" class="modal-input vm-mono" readonly>
|
||||
<button class="modal-btn modal-btn-cancel" type="button" id="copyCurlBtn">
|
||||
<i class="fa-regular fa-copy me-1"></i>Kopiuj
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vm-modal-actions">
|
||||
<button class="modal-btn modal-btn-cancel" type="button" id="genApiBtn">
|
||||
<i class="fa-solid fa-link me-1"></i>Generuj
|
||||
</button>
|
||||
<button class="modal-btn modal-btn-apply" type="button" id="callApiBtn">
|
||||
<i class="fa-solid fa-play me-1"></i>Wyślij
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="vm-modal-block">
|
||||
<label class="modal-label">Response</label>
|
||||
<pre id="apiResponse" class="vm-pre">(pusto)</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ static_v('js/gauge.js') }}"></script>
|
||||
<script src="{{ static_v('js/monitor.js') }}"></script>
|
||||
|
||||
<script src="{{ static_v('js/state.js') }}"></script>
|
||||
<script src="{{ static_v('js/chart.js') }}"></script>
|
||||
<script src="{{ static_v('js//events.js') }}"></script>
|
||||
<script src="{{ static_v('js/data.js') }}"></script>
|
||||
<script src="{{ static_v('js/socket.js') }}"></script>
|
||||
<script src="{{ static_v('js/index.js') }}"></script>
|
||||
|
||||
<script src="{{ static_v('js/modal.js') }}"></script>
|
||||
<script src="{{ static_v('js/apiHelper.js') }}"></script>
|
||||
<script src="{{ static_v('js/pageInit.js') }}"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initMonitor({{ phases|tojson }}, '{{ default_range }}');
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initPage({{ phases|tojson }}, '{{ default_range }}');
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user