0d413cfeaf
修复 4 个相关 bug:
1. **缓存目录配置保存后 404 (核心 bug)**
- 原因: _show_config_preview 把 endpoint 名 'config_cache' 存进 session 作
为 return_to,config_preview 把它当 URL 跳到 'config_cache' 报 404
- 修法: _show_config_preview 自动用 url_for() 解析 endpoint 名
2. **日志时间显示 UTC 而不是本地时间 (次生 bug)**
- 原因: log_parser 把 timestamp 解析成 UTC datetime,模板直接
e.timestamp.strftime('%H:%M:%S') 显示 UTC
- 修法: LogEntry 新增 timestamp_local 属性 (astimezone()),
3 个模板改用 e.timestamp_local
3. **DB 同步永远不工作 (次生 bug)**
- 原因: _should_auto_sync 调 get_config('log_sync_interval') ->
_resolve_active_instance -> session.get 在 app_context 里炸,
try/except 静默吞,永远返回 False
- 修法: 新增 _resolve_access_log_path() 不走 session 链,
_should_auto_sync 直接查 AppConfig 表
4. **default_struct 规则引用 Safe_ports/SSL_ports 但 ACL 是 safeports/sslports**
- 大小写不一致导致 squid -k parse 报 FATAL
- 修法: 改成小写统一
测试: cache save 流程全链路跑通 (POST -> preview -> confirm -> /config/cache 200)
所有 config 页面 (main/acls/rules/cache/refresh/auth/peers) 的 preview+confirm
return_to 字段都正确解析为 URL
166 lines
8.5 KiB
HTML
166 lines
8.5 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}日志查询 - Squid Manager{% endblock %}
|
|
{% block content %}
|
|
<div class="page-header">
|
|
<h1>📋 日志查询</h1>
|
|
<div class="page-actions">
|
|
<span class="meta-text">共 {{ total | thousands }} 条匹配</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="sync-bar">
|
|
<div class="sync-info">
|
|
{% if use_persistent %}
|
|
{% if last_sync %}
|
|
<span class="badge badge-green">📥 已启用持久化</span>
|
|
<span class="meta-text">上次同步: {{ last_sync }}{% if last_sync_path %} · 源: <code>{{ last_sync_path }}</code>{% endif %}</span>
|
|
{% else %}
|
|
<span class="badge badge-orange">⚠️ 尚未同步</span>
|
|
<span class="meta-text">点击"立即同步"把当前 access.log 导入到数据库</span>
|
|
{% endif %}
|
|
{% else %}
|
|
<span class="badge badge-gray">📂 文件直读模式</span>
|
|
<span class="meta-text">持久化已关闭 (在 <a href="{{ url_for('config_paths') }}">路径设置</a> 打开后可保留历史)</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="sync-actions">
|
|
<form method="post" action="{{ url_for('logs_sync') }}" style="display:inline">
|
|
<input type="hidden" name="_csrf" value="{{ csrf_token() }}">
|
|
<button type="submit" class="btn btn-primary btn-small">🔄 立即同步</button>
|
|
</form>
|
|
<form method="post" action="{{ url_for('logs_sync') }}?full=1" style="display:inline"
|
|
onsubmit="return confirm('确定要从头扫描整个 access.log?耗时取决于文件大小.');">
|
|
<input type="hidden" name="_csrf" value="{{ csrf_token() }}">
|
|
<input type="hidden" name="full" value="1">
|
|
<button type="submit" class="btn btn-secondary btn-small">📥 全量导入历史</button>
|
|
</form>
|
|
<span class="btn-group" style="display:inline-flex; gap:4px; margin-left:8px;">
|
|
{% set export_args = request.args.to_dict() %}
|
|
{% set _ = export_args.pop('page', None) %}
|
|
<a href="{{ url_for('export_logs_csv', **export_args) }}"
|
|
class="btn btn-secondary btn-small" title="导出当前过滤结果为 CSV">⬇️ CSV</a>
|
|
<a href="{{ url_for('export_logs_json', **export_args) }}"
|
|
class="btn btn-secondary btn-small" title="导出当前过滤结果为 JSON">⬇️ JSON</a>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<form method="get" class="filter-form">
|
|
<div class="filter-grid">
|
|
<div class="form-group">
|
|
<label>客户端 IP</label>
|
|
<input type="text" name="client" value="{{ filters.client }}" class="form-input" placeholder="172.16.12.232">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>HTTP 方法</label>
|
|
<select name="method" class="form-input">
|
|
<option value="">全部</option>
|
|
{% for m in ['GET','POST','PUT','DELETE','CONNECT','HEAD','OPTIONS','TRACE'] %}
|
|
<option value="{{ m }}" {% if filters.method == m %}selected{% endif %}>{{ m }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>结果码</label>
|
|
<input type="text" name="result_code" value="{{ filters.result_code }}" class="form-input" placeholder="TCP_MISS / TCP_TUNNEL / 200 / 404 ...">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>目标主机</label>
|
|
<input type="text" name="host" value="{{ filters.host }}" class="form-input" placeholder="chatgpt.com">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>URL 包含</label>
|
|
<input type="text" name="url" value="{{ filters.url }}" class="form-input" placeholder="metadata">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>响应大小 (字节)</label>
|
|
<div class="range-input">
|
|
<input type="number" name="min_size" value="{{ filters.min_size }}" class="form-input" placeholder="最小">
|
|
<span>~</span>
|
|
<input type="number" name="max_size" value="{{ filters.max_size }}" class="form-input" placeholder="最大">
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>响应时间 (ms)</label>
|
|
<div class="range-input">
|
|
<input type="number" name="min_elapsed" value="{{ filters.min_elapsed }}" class="form-input" placeholder="最小">
|
|
<span>~</span>
|
|
<input type="number" name="max_elapsed" value="{{ filters.max_elapsed }}" class="form-input" placeholder="最大">
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>起始时间 <span class="hint">本地时区</span></label>
|
|
<input type="datetime-local" name="start_time" value="{{ filters.start_time }}" class="form-input">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>结束时间 <span class="hint">本地时区</span></label>
|
|
<input type="datetime-local" name="end_time" value="{{ filters.end_time }}" class="form-input">
|
|
</div>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn btn-primary">🔍 查询</button>
|
|
<a href="{{ url_for('logs_view') }}" class="btn btn-secondary">清空</a>
|
|
{% if filters.start_time or filters.end_time %}
|
|
<span class="meta-text">⏱ 时间范围: {{ filters.start_time or '最早' }} → {{ filters.end_time or '最新' }}</span>
|
|
{% endif %}
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="card">
|
|
{% if entries %}
|
|
<div class="table-scroll">
|
|
<table class="data-table log-table">
|
|
<thead>
|
|
<tr>
|
|
<th>时间</th>
|
|
<th>客户端</th>
|
|
<th>结果码</th>
|
|
<th>HTTP</th>
|
|
<th>方法</th>
|
|
<th>大小</th>
|
|
<th>耗时</th>
|
|
<th>URL</th>
|
|
<th>层级</th>
|
|
<th>Content-Type</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for e in entries %}
|
|
<tr>
|
|
<td class="time-cell">{{ e.timestamp_local.strftime('%Y-%m-%d %H:%M:%S') if e.timestamp_local else e.time }}</td>
|
|
<td><a href="{{ url_for('client_detail', ip=e.client) }}">{{ e.client }}</a></td>
|
|
<td>
|
|
<span class="badge badge-{% if e.category == 'Hit' %}green{% elif e.category == 'Miss' %}blue{% elif e.category == 'Tunnel' %}cyan{% elif e.category == 'Denied' %}red{% elif e.category == 'Aborted' %}orange{% elif e.category == 'Error' %}red{% else %}gray{% endif %}" title="{{ e.result_description }}">{{ e.result_code }}</span>
|
|
</td>
|
|
<td><span class="badge badge-{% if e.http_code and e.http_code|first == '2' %}green{% elif e.http_code and e.http_code|first == '3' %}blue{% elif e.http_code and e.http_code|first == '4' %}orange{% elif e.http_code and e.http_code|first == '5' %}red{% else %}gray{% endif %}">{{ e.http_code or '-' }}</span></td>
|
|
<td>{{ e.method }}</td>
|
|
<td>{{ format_bytes(e.size) }}</td>
|
|
<td>{{ format_duration(e.elapsed_ms) }}</td>
|
|
<td class="url-cell" title="{{ e.url }}"><a href="{{ url_for('logs_view', url=e.url) }}">{{ e.url }}</a></td>
|
|
<td><code>{{ e.hier_code }}</code>{% if e.peer %}<div class="meta-text">{{ e.peer }}</div>{% endif %}</td>
|
|
<td><code>{{ e.content_type }}</code></td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{% if total_pages > 1 %}
|
|
<div class="pagination">
|
|
{% set args = request.args.to_dict() %}
|
|
{% if page > 1 %}{% set _ = args.update({'page': page - 1}) %}<a href="?{{ args | urlencode }}" class="btn btn-small">« 上一页</a>{% endif %}
|
|
<span class="page-info">第 {{ page }} / {{ total_pages }} 页</span>
|
|
{% if page < total_pages %}{% set _ = args.update({'page': page + 1}) %}<a href="?{{ args | urlencode }}" class="btn btn-small">下一页 »</a>{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
{% else %}
|
|
<div class="card-empty">
|
|
<div class="empty-icon">🔍</div>
|
|
<p>未找到匹配的日志</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endblock %}
|