diff --git a/app/ui.py b/app/ui.py index 8628616..9fb832b 100644 --- a/app/ui.py +++ b/app/ui.py @@ -4041,6 +4041,117 @@ def get_peer_sync_stats(site_id: str): return jsonify(stats) +@ui_bp.get("/system") +def system_dashboard(): + principal = _current_principal() + try: + _iam().authorize(principal, None, "iam:*") + except IamError: + flash("Access denied: System page requires admin permissions", "danger") + return redirect(url_for("ui.buckets_overview")) + + import platform as _platform + import sys + from app.version import APP_VERSION + + try: + import myfsio_core as _rc + has_rust = True + except ImportError: + has_rust = False + + gc = current_app.extensions.get("gc") + gc_status = gc.get_status() if gc else {"enabled": False} + gc_history_records = [] + if gc: + raw = gc.get_history(limit=10, offset=0) + for rec in raw: + r = rec.get("result", {}) + total_freed = r.get("temp_bytes_freed", 0) + r.get("multipart_bytes_freed", 0) + r.get("orphaned_version_bytes_freed", 0) + rec["bytes_freed_display"] = _format_bytes(total_freed) + rec["timestamp_display"] = datetime.fromtimestamp(rec["timestamp"], tz=dt_timezone.utc).strftime("%Y-%m-%d %H:%M UTC") + gc_history_records.append(rec) + + checker = current_app.extensions.get("integrity") + integrity_status = checker.get_status() if checker else {"enabled": False} + integrity_history_records = [] + if checker: + raw = checker.get_history(limit=10, offset=0) + for rec in raw: + rec["timestamp_display"] = datetime.fromtimestamp(rec["timestamp"], tz=dt_timezone.utc).strftime("%Y-%m-%d %H:%M UTC") + integrity_history_records.append(rec) + + features = [ + {"label": "Encryption (SSE-S3)", "enabled": current_app.config.get("ENCRYPTION_ENABLED", False)}, + {"label": "KMS", "enabled": current_app.config.get("KMS_ENABLED", False)}, + {"label": "Versioning Lifecycle", "enabled": current_app.config.get("LIFECYCLE_ENABLED", False)}, + {"label": "Metrics History", "enabled": current_app.config.get("METRICS_HISTORY_ENABLED", False)}, + {"label": "Operation Metrics", "enabled": current_app.config.get("OPERATION_METRICS_ENABLED", False)}, + {"label": "Site Sync", "enabled": current_app.config.get("SITE_SYNC_ENABLED", False)}, + {"label": "Website Hosting", "enabled": current_app.config.get("WEBSITE_HOSTING_ENABLED", False)}, + {"label": "Garbage Collection", "enabled": current_app.config.get("GC_ENABLED", False)}, + {"label": "Integrity Scanner", "enabled": current_app.config.get("INTEGRITY_ENABLED", False)}, + ] + + return render_template( + "system.html", + principal=principal, + app_version=APP_VERSION, + storage_root=current_app.config.get("STORAGE_ROOT", "./data"), + platform=_platform.platform(), + python_version=sys.version.split()[0], + has_rust=has_rust, + features=features, + gc_status=gc_status, + gc_history=gc_history_records, + integrity_status=integrity_status, + integrity_history=integrity_history_records, + ) + + +@ui_bp.post("/system/gc/run") +def system_gc_run(): + principal = _current_principal() + try: + _iam().authorize(principal, None, "iam:*") + except IamError: + return jsonify({"error": "Access denied"}), 403 + + gc = current_app.extensions.get("gc") + if not gc: + return jsonify({"error": "GC is not enabled"}), 400 + + payload = request.get_json(silent=True) or {} + original_dry_run = gc.dry_run + if "dry_run" in payload: + gc.dry_run = bool(payload["dry_run"]) + try: + result = gc.run_now() + finally: + gc.dry_run = original_dry_run + return jsonify(result.to_dict()) + + +@ui_bp.post("/system/integrity/run") +def system_integrity_run(): + principal = _current_principal() + try: + _iam().authorize(principal, None, "iam:*") + except IamError: + return jsonify({"error": "Access denied"}), 403 + + checker = current_app.extensions.get("integrity") + if not checker: + return jsonify({"error": "Integrity checker is not enabled"}), 400 + + payload = request.get_json(silent=True) or {} + result = checker.run_now( + auto_heal=payload.get("auto_heal"), + dry_run=payload.get("dry_run"), + ) + return jsonify(result.to_dict()) + + @ui_bp.app_errorhandler(404) def ui_not_found(error): # type: ignore[override] prefix = ui_bp.url_prefix or "" diff --git a/app/version.py b/app/version.py index d429237..77fc8ae 100644 --- a/app/version.py +++ b/app/version.py @@ -1,6 +1,6 @@ from __future__ import annotations -APP_VERSION = "0.3.9" +APP_VERSION = "0.4.0" def get_version() -> str: diff --git a/templates/base.html b/templates/base.html index e9e8fb4..445b799 100644 --- a/templates/base.html +++ b/templates/base.html @@ -110,6 +110,14 @@ Domains {% endif %} + {% if can_manage_iam %} + + + + + System + + {% endif %} -
- v{{ app.version }} -
diff --git a/templates/system.html b/templates/system.html new file mode 100644 index 0000000..39158d9 --- /dev/null +++ b/templates/system.html @@ -0,0 +1,502 @@ +{% extends "base.html" %} + +{% block title %}System - MyFSIO Console{% endblock %} + +{% block content %} + + +
+
+
+
+
+ + + + Server Information +
+

Runtime environment and configuration

+
+
+ + + + + + + + +
Version{{ app_version }}
Storage Root{{ storage_root }}
Platform{{ platform }}
Python{{ python_version }}
Rust Extension + {% if has_rust %} + Loaded + {% else %} + Not loaded + {% endif %} +
+
+
+
+ +
+
+
+
+ + + + Feature Flags +
+

Features configured via environment variables

+
+
+ + + {% for feat in features %} + + + + + {% endfor %} + +
{{ feat.label }} + {% if feat.enabled %} + Enabled + {% else %} + Disabled + {% endif %} +
+
+
+
+
+ +
+
+
+
+
+
+
+ + + + Garbage Collection +
+

Clean up temporary files, orphaned uploads, and stale locks

+
+
+ {% if gc_status.enabled %} + Active + {% else %} + Disabled + {% endif %} +
+
+
+
+ {% if gc_status.enabled %} +
+ + +
+ +
+
+
+
+ +
+
+
+
+ +
+
+ + + + Configuration +
+
+
Interval: {{ gc_status.interval_hours }}h
+
Dry run: {{ "Yes" if gc_status.dry_run else "No" }}
+
Temp max age: {{ gc_status.temp_file_max_age_hours }}h
+
Lock max age: {{ gc_status.lock_file_max_age_hours }}h
+
Multipart max age: {{ gc_status.multipart_max_age_days }}d
+
+
+ + {% if gc_history %} +
+ + + + + + Recent Executions +
+
+ + + + + + + + + + + {% for exec in gc_history %} + + + + + + + {% endfor %} + +
TimeCleanedFreedMode
{{ exec.timestamp_display }} + {% set r = exec.result %} + {{ (r.temp_files_deleted|d(0)) + (r.multipart_uploads_deleted|d(0)) + (r.lock_files_deleted|d(0)) + (r.orphaned_metadata_deleted|d(0)) + (r.orphaned_versions_deleted|d(0)) + (r.empty_dirs_removed|d(0)) }} + {{ exec.bytes_freed_display }} + {% if exec.dry_run %} + Dry run + {% else %} + Live + {% endif %} +
+
+ {% else %} +
+

No executions recorded yet.

+
+ {% endif %} + + {% else %} +
+ + + +

Garbage collection is not enabled.

+

Set GC_ENABLED=true to enable automatic cleanup.

+
+ {% endif %} +
+
+
+ +
+
+
+
+
+
+ + + + + Integrity Scanner +
+

Detect and heal corrupted objects, orphaned files, and metadata drift

+
+
+ {% if integrity_status.enabled %} + Active + {% else %} + Disabled + {% endif %} +
+
+
+
+ {% if integrity_status.enabled %} +
+ + + +
+ +
+
+
+
+ +
+
+
+
+ +
+
+ + + + Configuration +
+
+
Interval: {{ integrity_status.interval_hours }}h
+
Dry run: {{ "Yes" if integrity_status.dry_run else "No" }}
+
Batch size: {{ integrity_status.batch_size }}
+
Auto-heal: {{ "Yes" if integrity_status.auto_heal else "No" }}
+
+
+ + {% if integrity_history %} +
+ + + + + + Recent Scans +
+
+ + + + + + + + + + + + {% for exec in integrity_history %} + + + + + + + + {% endfor %} + +
TimeScannedIssuesHealedMode
{{ exec.timestamp_display }}{{ exec.result.objects_scanned|d(0) }} + {% set total_issues = (exec.result.corrupted_objects|d(0)) + (exec.result.orphaned_objects|d(0)) + (exec.result.phantom_metadata|d(0)) + (exec.result.stale_versions|d(0)) + (exec.result.etag_cache_inconsistencies|d(0)) + (exec.result.legacy_metadata_drifts|d(0)) %} + {% if total_issues > 0 %} + {{ total_issues }} + {% else %} + 0 + {% endif %} + {{ exec.result.issues_healed|d(0) }} + {% if exec.dry_run %} + Dry + {% elif exec.auto_heal %} + Heal + {% else %} + Scan + {% endif %} +
+
+ {% else %} +
+

No scans recorded yet.

+
+ {% endif %} + + {% else %} +
+ + + + +

Integrity scanner is not enabled.

+

Set INTEGRITY_ENABLED=true to enable automatic scanning.

+
+ {% endif %} +
+
+
+
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %}