Skip to content

Commit db51961

Browse files
authored
webserver: Upload and choose log configuration (#404)
* webserver: Upload and choose log configuration * Silence mypy
1 parent 11c7573 commit db51961

18 files changed

Lines changed: 344 additions & 118 deletions

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# Active log configuration symlink (auto-created at startup)
2+
python/pifinder_logconf.json
3+
python/logconf_*.json
4+
15
# Byte-compiled / optimized / DLL files
26
__pycache__/
37
*.py[cod]

python/PiFinder/catalog_base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ def assign_virtual_object_ids(catalog, low_id: int) -> int:
171171
class TimerMixin:
172172
"""Provides timer functionality via composition"""
173173

174-
def __init__(self):
174+
def __init__(self) -> None:
175175
self.timer: Optional[threading.Timer] = None
176176
self.is_running: bool = False
177177
self.time_delay_seconds: Union[int, Callable[[], int]] = (

python/PiFinder/catalog_imports/database.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@
44
This module provides centralized access to database objects for all catalog loaders.
55
"""
66

7+
from typing import Optional
8+
9+
from PiFinder.db.objects_db import ObjectsDatabase
10+
from PiFinder.db.observations_db import ObservationsDatabase
11+
712
from .catalog_import_utils import init_databases
813

914
# Global database objects shared across all catalog loaders
10-
objects_db = None
11-
observations_db = None
15+
objects_db: Optional[ObjectsDatabase] = None
16+
observations_db: Optional[ObservationsDatabase] = None
1217

1318

1419
def init_shared_database():

python/PiFinder/catalog_imports/harris_loader.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,10 +381,13 @@ def create_cluster_object(entry: npt.NDArray, seq: int) -> Dict[str, Any]:
381381
return result
382382

383383

384-
def load_harris():
384+
def load_harris() -> None:
385385
logging.info("Loading Harris Globular Cluster catalog")
386386
catalog: str = "Har"
387387
obj_type: str = "Gb" # Globular Cluster
388+
389+
if objects_db is None:
390+
raise RuntimeError("Database not initialized. Call init_shared_database() first.")
388391
conn, _ = objects_db.get_conn_cursor()
389392

390393
# Enable bulk mode to prevent commits during insert operations

python/PiFinder/main.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,11 @@ def main(
870870
if __name__ == "__main__":
871871
import sys
872872

873+
# Ensure the active log config symlink exists, defaulting to logconf_default.json
874+
_logconf_link = Path("pifinder_logconf.json")
875+
if not _logconf_link.exists():
876+
_logconf_link.symlink_to("logconf_default.json")
877+
873878
debug_no_file_logs = "--debug-no-file-logs" in sys.argv
874879
if debug_no_file_logs:
875880
os.environ["PIFINDER_DEBUG_NO_FILE_LOGS"] = "1"

python/PiFinder/server.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,83 @@ def get_component_levels():
844844
logging.error(f"Error reading log configuration: {e}")
845845
return {"status": "error", "message": str(e)}
846846

847+
@app.route("/logs/configs")
848+
@auth_required
849+
def list_log_configs():
850+
"""Return all available logconf_*.json files with display names."""
851+
import glob
852+
853+
configs = []
854+
active = (
855+
os.path.realpath("pifinder_logconf.json")
856+
if os.path.exists("pifinder_logconf.json")
857+
else None
858+
)
859+
for path in sorted(glob.glob("logconf_*.json")):
860+
stem = path[len("logconf_") : -len(".json")]
861+
display = stem.replace("_", " ").title()
862+
configs.append(
863+
{
864+
"file": path,
865+
"name": display,
866+
"active": os.path.realpath(path) == active,
867+
}
868+
)
869+
return {"configs": configs}
870+
871+
@app.route("/logs/switch_config", method="post")
872+
@auth_required
873+
def switch_log_config():
874+
"""Atomically repoint pifinder_logconf.json to the chosen config, then restart."""
875+
logconf_file = request.forms.get("logconf_file", "").strip()
876+
if (
877+
not logconf_file
878+
or not logconf_file.startswith("logconf_")
879+
or not logconf_file.endswith(".json")
880+
):
881+
return {"status": "error", "message": "Invalid log config file name"}
882+
if not os.path.exists(logconf_file):
883+
return {
884+
"status": "error",
885+
"message": f"Log config file not found: {logconf_file}",
886+
}
887+
try:
888+
link = "pifinder_logconf.json"
889+
tmp = link + ".tmp"
890+
os.symlink(logconf_file, tmp)
891+
os.replace(tmp, link)
892+
logger.info("Switched log config to %s", logconf_file)
893+
except Exception as e:
894+
logger.error("Failed to switch log config: %s", e)
895+
return {"status": "error", "message": str(e)}
896+
return template("restart_pifinder")
897+
898+
@app.route("/logs/upload_config", method="post")
899+
@auth_required
900+
def upload_log_config():
901+
"""Upload a new logconf_*.json file."""
902+
upload = request.files.get("config_file")
903+
if not upload:
904+
return {"status": "error", "message": "No file provided"}
905+
filename = upload.filename
906+
if not filename.startswith("logconf_") or not filename.endswith(".json"):
907+
return {
908+
"status": "error",
909+
"message": "File must be named logconf_<name>.json",
910+
}
911+
if os.path.exists(filename):
912+
return {
913+
"status": "error",
914+
"message": f"File already exists: {filename}",
915+
}
916+
try:
917+
upload.save(filename, overwrite=False)
918+
logger.info("Uploaded log config: %s", filename)
919+
return {"status": "ok", "file": filename}
920+
except Exception as e:
921+
logger.error("Failed to save uploaded log config: %s", e)
922+
return {"status": "error", "message": str(e)}
923+
847924
@app.route("/logs/download")
848925
@auth_required
849926
def download_logs():

python/PiFinder/sqm/save_sweep_metadata.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ def save_sweep_metadata(
8080
# Noise floor estimation details (from NoiseFloorEstimator)
8181
if noise_floor_details is not None:
8282
metadata["noise_floor_estimator"] = {
83-
k: v for k, v in noise_floor_details.items()
83+
k: v
84+
for k, v in noise_floor_details.items()
8485
if k != "request_zero_sec_sample" # Exclude internal flags
8586
}
8687
if camera_type is not None:

python/PiFinder/sqm/sqm.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ def _load_calibration(self) -> bool:
6565
)
6666

6767
if not calibration_file.exists():
68-
logger.debug(f"No calibration file found at {calibration_file}, using defaults")
68+
logger.debug(
69+
f"No calibration file found at {calibration_file}, using defaults"
70+
)
6971
return False
7072

7173
try:
@@ -193,7 +195,9 @@ def _measure_star_flux_with_local_background(
193195

194196
# Check for saturation in aperture
195197
aperture_pixels = image_patch[aperture_mask]
196-
max_aperture_pixel = np.max(aperture_pixels) if len(aperture_pixels) > 0 else 0
198+
max_aperture_pixel = (
199+
np.max(aperture_pixels) if len(aperture_pixels) > 0 else 0
200+
)
197201

198202
if max_aperture_pixel >= saturation_threshold:
199203
# Mark saturated star with flux=-1 to be excluded from mzero calculation
@@ -202,7 +206,6 @@ def _measure_star_flux_with_local_background(
202206
n_saturated += 1
203207
continue
204208

205-
206209
# Total flux in aperture (includes background)
207210
total_flux = np.sum(aperture_pixels)
208211

@@ -263,9 +266,7 @@ def _calculate_mzero(
263266
# Flux-weighted mean: brighter stars contribute more
264267
valid_mzeros_arr = np.array(valid_mzeros)
265268
valid_fluxes_arr = np.array(valid_fluxes)
266-
weighted_mzero = float(
267-
np.average(valid_mzeros_arr, weights=valid_fluxes_arr)
268-
)
269+
weighted_mzero = float(np.average(valid_mzeros_arr, weights=valid_fluxes_arr))
269270

270271
return weighted_mzero, mzeros
271272

@@ -540,7 +541,9 @@ def calculate(
540541
# Following ASTAP: zenith is reference point where extinction = 0
541542
# Only ADDITIONAL extinction below zenith is added: 0.28 * (airmass - 1)
542543
# This allows comparing measurements at different altitudes
543-
extinction_for_altitude = self._atmospheric_extinction(altitude_deg) # 0.28*(airmass-1)
544+
extinction_for_altitude = self._atmospheric_extinction(
545+
altitude_deg
546+
) # 0.28*(airmass-1)
544547

545548
# Main SQM value: no extinction correction (raw measurement)
546549
sqm_final = sqm_uncorrected

python/PiFinder/state.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ def from_json(cls, json_str):
238238

239239

240240
class SharedStateObj:
241-
def __init__(self):
241+
def __init__(self) -> None:
242242
self.__power_state = 1 # 0 = sleep state, 1 = awake state
243243
# self.__solve_state
244244
# None = No solve attempted yet

python/PiFinder/ui/radec_entry.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,7 @@ class LayoutConfig:
586586
class UIRADecEntry(UIModule):
587587
__title__ = _("RA/DEC Entry")
588588

589-
def __init__(self, *args, **kwargs):
589+
def __init__(self, *args, **kwargs) -> None:
590590
super().__init__(*args, **kwargs)
591591

592592
self.callback = self.item_definition.get("callback")

0 commit comments

Comments
 (0)