diff --git a/app.py b/app.py index 721b1e0..ca9cbd2 100644 --- a/app.py +++ b/app.py @@ -529,8 +529,23 @@ def backup_conf(reason: str = "manual") -> str | None: def _show_config_preview(pending_content: str, return_to: str): - """Store pending content in session, redirect to GET /config/preview for confirmation.""" + """Store pending content in session, redirect to GET /config/preview for confirmation. + + ``return_to`` may be either an endpoint name (e.g. ``"config_cache"``) + or a URL path (e.g. ``"/config/cache"``). Endpoint names get resolved + via ``url_for`` so the preview page's "返回" link is always a valid URL. + """ import base64 + # Normalize return_to: if it doesn't look like a URL, treat it as an + # endpoint name and resolve to a URL. This was the root cause of the + # 404 bug after saving a config - we stored "config_cache" and + # ``redirect("config_cache")`` produced a 404. + if return_to and not return_to.startswith("/") and not return_to.startswith("http"): + try: + return_to = url_for(return_to) + except Exception: + # unknown endpoint - fall back to dashboard + return_to = url_for("dashboard") session["_pending_conf"] = base64.b64encode(pending_content.encode()).decode() session["_pending_return"] = return_to return redirect(url_for("config_preview")) @@ -648,7 +663,7 @@ def get_parsed_logs(force: bool = False) -> list[log_parser.LogEntry]: # Best-effort auto-sync (throttled by the file mtime + last sync stamp) if force or _should_auto_sync(instance_id): try: - path = access_log_path() + path = _resolve_access_log_path() if os.path.isfile(path): log_storage.sync_log_to_db( path=path, instance_id=instance_id, @@ -680,18 +695,46 @@ def get_parsed_logs(force: bool = False) -> list[log_parser.LogEntry]: return wrapped +def _resolve_access_log_path() -> str: + """Look up the access log path WITHOUT going through session-bound code. + + ``get_config`` -> ``_resolve_active_instance`` -> ``session.get`` raises + outside a request context, and ``_should_auto_sync`` is called from many + places (background threads, app shell) that don't have a request context. + Here we hit the DB directly using a DB-session that does not require + session-binding. + """ + try: + # 1) instance row, if any is active + active = SquidInstance.query.filter_by(is_active=True).first() + if active and active.access_log_path: + return active.access_log_path + # 2) AppConfig override + row = AppConfig.query.filter_by(key="access_log_path").first() + if row and row.value: + return row.value + except Exception: + pass + return DEFAULTS.get("access_log_path", "/var/log/squid/access.log") + + def _should_auto_sync(instance_id: int) -> bool: """Decide whether to re-sync the access.log tail for ``instance_id``. Cheap heuristic: check the file's mtime against the last sync stamp kept in :class:`AppConfig`. Always returns False when ``log_sync_interval`` is 0 (manual sync only). + + Must NOT depend on ``session`` (called outside request context). """ try: - interval = int(get_config("log_sync_interval", "0")) + # Use raw AppConfig instead of get_config() - get_config touches + # session which raises outside request context. + row = AppConfig.query.filter_by(key="log_sync_interval").first() + interval = int(row.value) if (row and row.value) else 0 if interval <= 0: return False - path = access_log_path() + path = _resolve_access_log_path() if not os.path.isfile(path): return False mtime = os.path.getmtime(path) @@ -709,7 +752,9 @@ def _should_auto_sync(instance_id: int) -> bool: return False _auto_sync_last_call = now return mtime > last_dt.timestamp() - except Exception: + except Exception as e: + # Don't use current_app here - this code can run outside app context + # (e.g. in test scripts). Just swallow. return False @@ -928,7 +973,7 @@ def logs_view(): # best-effort sync before serving (skipped if the file is unchanged) try: if _should_auto_sync(instance_id): - path = access_log_path() + path = _resolve_access_log_path() if os.path.isfile(path): log_storage.sync_log_to_db(path=path, instance_id=instance_id) except Exception: @@ -3115,7 +3160,7 @@ def _export_logs_payload(): import log_storage try: if _should_auto_sync(instance_id): - path = access_log_path() + path = _resolve_access_log_path() if os.path.isfile(path): log_storage.sync_log_to_db(path=path, instance_id=instance_id) except Exception: diff --git a/log_parser.py b/log_parser.py index 5bfb8ce..559ed87 100644 --- a/log_parser.py +++ b/log_parser.py @@ -119,6 +119,17 @@ class LogEntry(dict): def result_description(self) -> str: return RESULT_CODE_DESCRIPTION.get(self.result_code, self.result_code) + @property + def timestamp_local(self): + """Return a datetime converted to the server's local timezone (CST for cn).""" + ts = self.get("timestamp") + if ts is None: + return None + try: + return ts.astimezone() # use system local tz + except Exception: + return ts + def parse_line(line: str) -> LogEntry | None: """Parse a single access.log line. Returns None on unparseable lines.""" diff --git a/squid_config.py b/squid_config.py index 2a3c8a1..a2218ee 100644 --- a/squid_config.py +++ b/squid_config.py @@ -611,8 +611,8 @@ def default_struct() -> dict: ], "rules": [ Rule(action="deny", acls=["manager"], directive="http_access"), - Rule(action="deny", acls=["!Safe_ports"], directive="http_access"), - Rule(action="deny", acls=["!SSL_ports"], directive="http_access"), + Rule(action="deny", acls=["!safeports"], directive="http_access"), + Rule(action="deny", acls=["!sslports"], directive="http_access"), Rule(action="allow", acls=["localhost"], directive="http_access"), Rule(action="deny", acls=["all"], directive="http_access"), ], diff --git a/templates/logs.html b/templates/logs.html index eae30e9..a9b3413 100644 --- a/templates/logs.html +++ b/templates/logs.html @@ -129,7 +129,7 @@
{% for e in entries %}