Skip to content

Commit fde96d8

Browse files
authored
Merge pull request #64 from Linked-Liszt/status_page
Status page
2 parents d9ab468 + 99ab7b3 commit fde96d8

6 files changed

Lines changed: 271 additions & 48 deletions

File tree

lau_dash.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import dash_bootstrap_components as dbc
33
from laue_portal.database.session_utils import init_db, get_engine
44
import laue_portal.database.db_utils as db_utils
5+
from laue_portal.processing.redis_utils import init_redis_status
56
import os
67
import config
78
import logging
@@ -29,8 +30,12 @@ def ensure_database_exists():
2930
logging.info(f"Database file '{db_path}' already exists. Running on existing database.")
3031

3132

33+
# Initialize Redis status BEFORE creating the Dash app
34+
# This ensures the status is set before pages (and navbar) are imported
35+
init_redis_status()
36+
3237
app = dash.Dash(__name__,
33-
external_stylesheets=[dbc.themes.FLATLY, dbc_css],
38+
external_stylesheets=[dbc.themes.FLATLY, dbc_css, dbc.icons.BOOTSTRAP],
3439
suppress_callback_exceptions=True,
3540
pages_folder="laue_portal/pages",)
3641

laue_portal/components/navbar.py

Lines changed: 14 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,25 @@
1-
# import dash_bootstrap_components as dbc
2-
3-
# navbar = dbc.NavbarSimple(
4-
# children=[
5-
# dbc.NavItem(dbc.NavLink("Scans", href="/", active="exact")),
6-
# dbc.NavItem(dbc.NavLink("Mask Reconstructions", href="/reconstructions", active="exact")),
7-
# dbc.NavItem(dbc.NavLink("Wire Reconstructions", href="/wire-reconstructions", active="exact")),
8-
# dbc.NavItem(dbc.NavLink("Peak Indexings", href="/peakindexings", active="exact")),
9-
# dbc.NavItem(dbc.NavLink("Run Monitor", href="/run-monitor", active="exact")),
10-
# dbc.DropdownMenu(
11-
# id="manual-entry-dropdown",
12-
# children=[
13-
# dbc.DropdownMenuItem("New Scan", href="/create-scan"),
14-
# dbc.DropdownMenuItem("New CA Reconstruction", href="/create-reconstruction"),
15-
# dbc.DropdownMenuItem("New Wire Reconstruction", href="/create-wire-reconstruction"),
16-
# dbc.DropdownMenuItem("New Peak Indexing", href="/create-peakindexing"),
17-
# ],
18-
# nav=True,
19-
# in_navbar=True,
20-
# label="Manual Entry",
21-
# ),
22-
# ],
23-
# brand="3DMN Portal",
24-
# brand_href="/",
25-
# color="primary",
26-
# className="navbar-lg",
27-
# dark=True,
28-
# style={"max-height": "50px"},
29-
# )
30-
31-
32-
33-
341
import dash_bootstrap_components as dbc
35-
from dash import html, Input, Output, State, callback
2+
from dash import html, Input, Output, State, callback, dcc
3+
from laue_portal.processing.redis_utils import REDIS_CONNECTED_AT_STARTUP
364

375
navbar = dbc.Navbar(
386
dbc.Container(
397
[
40-
dbc.NavbarBrand("3DMN Portal", href="/"),
8+
dbc.NavbarBrand("3DMN Portal", href="/", id="navbar-brand"),
9+
html.Div([
10+
html.I(
11+
className="bi bi-hdd-network",
12+
style={
13+
'fontSize': '1.5rem',
14+
'color': '#18BC9C' if REDIS_CONNECTED_AT_STARTUP else '#FF6B6B'
15+
}
16+
),
17+
], className="d-flex align-items-center ms-2"),
4118
dbc.NavbarToggler(id="nav-toggler"),
4219
dbc.Collapse(
4320
dbc.Nav(
4421
[
45-
dbc.NavItem(dbc.NavLink("Scans", href="/", active="exact")),
22+
dbc.NavItem(dbc.NavLink("Scans", href="/scans", active="exact")),
4623
dbc.NavItem(dbc.NavLink("Mask Reconstructions", href="/reconstructions", active="exact")),
4724
dbc.NavItem(dbc.NavLink("Wire Reconstructions", href="/wire-reconstructions", active="exact")),
4825
dbc.NavItem(dbc.NavLink("Indexations", href="/peakindexings", active="exact")),
@@ -73,12 +50,8 @@
7350
fluid=True,
7451
),
7552
className="py-3",
76-
#brand="3DMN Portal",
77-
#brand_href="/",
7853
color="primary",
7954
dark=True,
80-
#className="navbar-lg",
81-
#style={"max-height": "70px"},
8255
)
8356

8457
@callback(
@@ -88,4 +61,4 @@
8861
prevent_initial_call=True,
8962
)
9063
def toggle_nav(n, is_open):
91-
return not is_open
64+
return not is_open

laue_portal/pages/scans.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from laue_portal.pages.scan import build_technique_strings
1212
import laue_portal.database.session_utils as session_utils
1313

14-
dash.register_page(__name__, path='/')
14+
dash.register_page(__name__, path='/scans')
1515

1616
layout = html.Div([
1717
navbar.navbar,
@@ -212,7 +212,7 @@ def _get_metadatas():
212212
prevent_initial_call=True,
213213
)
214214
def get_metadatas(path):
215-
if path == '/':
215+
if path == '/scans':
216216
cols, metadatas_records = _get_metadatas()
217217
return cols, metadatas_records
218218
else:

laue_portal/pages/status.py

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import dash
2+
from dash import html, dcc
3+
import dash_bootstrap_components as dbc
4+
from dash.dependencies import Input, Output
5+
import laue_portal.components.navbar as navbar
6+
from laue_portal.processing import redis_utils
7+
import config
8+
import os
9+
10+
dash.register_page(__name__, path='/')
11+
12+
# Build the layout
13+
layout = html.Div([
14+
navbar.navbar,
15+
dbc.Container([
16+
# Auto-refresh interval (every 5 seconds)
17+
dcc.Interval(
18+
id='status-refresh-interval',
19+
interval=5*1000, # in milliseconds
20+
n_intervals=0
21+
),
22+
23+
# Row 1: Welcome and Connection Status
24+
dbc.Row([
25+
# Welcome Card
26+
dbc.Col([
27+
dbc.Card([
28+
dbc.CardHeader(html.H4("Welcome to the 3DMN Laue Portal", className="mb-0")),
29+
dbc.CardBody([
30+
html.P([
31+
"The 3DMN Laue Portal is a data processing platform for ",
32+
"Laue X-ray diffraction experiments at beamline 34-ID-E."
33+
], className="mb-3"),
34+
html.H6("Capabilities:", className="mb-2"),
35+
html.Ul([
36+
html.Li("Scan management and tracking"),
37+
html.Li("Wire-based depth reconstruction"),
38+
html.Li("Coded aperture reconstruction"),
39+
html.Li("Automated peak indexing"),
40+
html.Li("Distributed job queue"),
41+
], className="mb-0"),
42+
])
43+
], className="h-100")
44+
], md=6, className="mb-4"),
45+
46+
# Connection Status Card
47+
dbc.Col([
48+
dbc.Card([
49+
dbc.CardHeader(html.H4("Connection Status", className="mb-0")),
50+
dbc.CardBody([
51+
html.Div(id='connection-status-content')
52+
])
53+
], className="h-100")
54+
], md=6, className="mb-4"),
55+
]),
56+
57+
# Row 2: System Resources and Quick Actions
58+
dbc.Row([
59+
# System Resources Card
60+
dbc.Col([
61+
dbc.Card([
62+
dbc.CardHeader(html.H4("System Resources", className="mb-0")),
63+
dbc.CardBody([
64+
html.Div(id='system-resources-content')
65+
])
66+
], className="h-100")
67+
], md=6, className="mb-4"),
68+
69+
# Quick Actions Card
70+
dbc.Col([
71+
dbc.Card([
72+
dbc.CardHeader(html.H4("Quick Actions", className="mb-0")),
73+
dbc.CardBody([
74+
dbc.ListGroup([
75+
dbc.ListGroupItem([
76+
html.I(className="bi bi-list-ul me-2"),
77+
"View All Scans"
78+
], href="/scans", action=True, className="d-flex align-items-center"),
79+
dbc.ListGroupItem([
80+
html.I(className="bi bi-gear me-2"),
81+
"Create Wire Reconstruction"
82+
], href="/create-wire-reconstruction", action=True, className="d-flex align-items-center"),
83+
dbc.ListGroupItem([
84+
html.I(className="bi bi-grid-3x3 me-2"),
85+
"Create Coded Aperture Reconstruction"
86+
], href="/create-reconstruction", action=True, className="d-flex align-items-center"),
87+
dbc.ListGroupItem([
88+
html.I(className="bi bi-search me-2"),
89+
"Create Peak Indexing"
90+
], href="/create-peakindexing", action=True, className="d-flex align-items-center"),
91+
dbc.ListGroupItem([
92+
html.I(className="bi bi-activity me-2"),
93+
"Monitor Job Queue"
94+
], href="/run-monitor", action=True, className="d-flex align-items-center"),
95+
], flush=True)
96+
])
97+
], className="h-100")
98+
], md=6, className="mb-4"),
99+
]),
100+
101+
], fluid=True, className="mt-4")
102+
])
103+
104+
105+
# Callback to update connection status
106+
@dash.callback(
107+
Output('connection-status-content', 'children'),
108+
Input('status-refresh-interval', 'n_intervals')
109+
)
110+
def update_connection_status(n):
111+
"""Update the connection status card with current system information."""
112+
113+
# Check Redis connection
114+
redis_connected = redis_utils.check_redis_connection()
115+
redis_startup_status = redis_utils.REDIS_CONNECTED_AT_STARTUP
116+
117+
# Database path
118+
db_path = config.db_file
119+
db_exists = os.path.exists(db_path)
120+
121+
# Server info
122+
dash_url = f"http://{config.DASH_CONFIG['host']}:{config.DASH_CONFIG['port']}"
123+
redis_url = f"{config.REDIS_CONFIG['host']}:{config.REDIS_CONFIG['port']}"
124+
125+
return [
126+
# Database Status
127+
html.Div([
128+
html.Strong("Database: "),
129+
dbc.Badge(
130+
"Connected" if db_exists else "Not Found",
131+
color="success" if db_exists else "danger",
132+
className="me-2"
133+
),
134+
html.Br(),
135+
html.Small(db_path, className="text-muted"),
136+
], className="mb-3"),
137+
138+
# Dash Server Status
139+
html.Div([
140+
html.Strong("Dash Server: "),
141+
dbc.Badge("Running", color="success", className="me-2"),
142+
html.Br(),
143+
html.Small(dash_url, className="text-muted"),
144+
], className="mb-3"),
145+
146+
# Redis Status
147+
html.Div([
148+
html.Strong("Redis Queue: "),
149+
dbc.Badge(
150+
"Connected" if redis_connected else "Disconnected",
151+
color="success" if redis_connected else "danger",
152+
className="me-2"
153+
),
154+
html.Br(),
155+
html.Small(redis_url, className="text-muted"),
156+
html.Br(),
157+
html.Small(
158+
f"Startup status: {'Connected' if redis_startup_status else 'Disconnected'}"
159+
if redis_startup_status is not None else "Startup status: Unknown",
160+
className="text-muted fst-italic"
161+
),
162+
], className="mb-0"),
163+
]
164+
165+
166+
# Callback to update system resources
167+
@dash.callback(
168+
Output('system-resources-content', 'children'),
169+
Input('status-refresh-interval', 'n_intervals')
170+
)
171+
def update_system_resources(n):
172+
"""Update the system resources card with queue and worker information."""
173+
174+
try:
175+
# Get queue statistics
176+
queue_stats = redis_utils.get_queue_stats()
177+
178+
# Get worker information
179+
workers_info = redis_utils.get_workers_info()
180+
181+
return [
182+
# Queue Statistics
183+
html.H6("Job Queue:", className="mb-2"),
184+
dbc.Row([
185+
dbc.Col([
186+
html.Div([
187+
html.H3(queue_stats.get('queued', 0), className="mb-0 text-primary"),
188+
html.Small("Queued", className="text-muted")
189+
], className="text-center")
190+
], width=6),
191+
dbc.Col([
192+
html.Div([
193+
html.H3(queue_stats.get('started', 0), className="mb-0 text-info"),
194+
html.Small("Running", className="text-muted")
195+
], className="text-center")
196+
], width=6),
197+
], className="mb-3"),
198+
199+
html.Hr(),
200+
201+
# Worker Information
202+
html.H6("Workers:", className="mb-2"),
203+
html.Div([
204+
dbc.Badge(
205+
f"{len(workers_info)} Active" if workers_info else "No Workers",
206+
color="success" if workers_info else "warning",
207+
className="me-2"
208+
),
209+
html.Br() if workers_info else None,
210+
html.Div([
211+
html.Div([
212+
html.Small([
213+
html.Strong(f"{worker['name']}: "),
214+
f"{worker['state']}"
215+
])
216+
], className="mb-1") for worker in workers_info
217+
] if workers_info else [], className="mt-2")
218+
], className="mb-0"),
219+
]
220+
221+
except Exception as e:
222+
return [
223+
dbc.Alert([
224+
html.I(className="bi bi-exclamation-triangle me-2"),
225+
"No system data. Redis is not connected."
226+
], color="warning", className="mb-0")
227+
]

laue_portal/processing/redis_utils.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55

66
from redis import Redis
7+
import redis
78
from rq import Queue, Worker
89
from rq.job import Job, Dependency
910
from rq.registry import StartedJobRegistry, FinishedJobRegistry, FailedJobRegistry
@@ -26,11 +27,28 @@
2627
logger = logging.getLogger(__name__)
2728

2829
# Redis connection
29-
redis_conn = Redis(host='localhost', port=6379, decode_responses=False)
30+
redis_conn = Redis(host='localhost', port=REDIS_CONFIG['port'], decode_responses=False)
3031

3132
# Single queue for all job types
3233
job_queue = Queue('laue_jobs', connection=redis_conn)
3334

35+
# Global variable to store startup status
36+
REDIS_CONNECTED_AT_STARTUP = None
37+
38+
def check_redis_connection():
39+
"""Check if Redis server is accessible and responding."""
40+
try:
41+
return redis_conn.ping()
42+
except (redis.ConnectionError, redis.TimeoutError, Exception):
43+
return False
44+
45+
def init_redis_status():
46+
"""Initialize Redis status check on startup."""
47+
global REDIS_CONNECTED_AT_STARTUP
48+
REDIS_CONNECTED_AT_STARTUP = check_redis_connection()
49+
logger.info(f"Redis connection status at startup: {'Connected' if REDIS_CONNECTED_AT_STARTUP else 'Disconnected'}")
50+
return REDIS_CONNECTED_AT_STARTUP
51+
3452
# Job status mapping
3553
STATUS_MAPPING = {
3654
0: "Queued",

0 commit comments

Comments
 (0)