From 7362b4584625829b9c10f8704c54f8a54e1a5e29 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Sat, 23 May 2026 17:55:58 +0900 Subject: [PATCH] feat(logs): date filter + clear log endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API GET /api/logs now accepts from_ts / to_ts (unix epoch, half-open [from, to)) so callers can scope by arbitrary time range. - API DELETE /api/logs added. Same from_ts / to_ts semantics. No params = wipe everything and reset the AUTOINCREMENT counter. - Dashboard Logs page: date picker that scopes both the view and the delete button to the selected day in the user's local timezone. The clear button is red and confirms before deleting; label switches between "전체 로그 초기화" and "<날짜> 하루치 삭제". --- README.md | 3 +- api/routes/logs.py | 53 +++++++++++++++++++++++++++++++-- frontend/src/api.js | 6 ++++ frontend/src/pages/Logs.jsx | 59 ++++++++++++++++++++++++++++++++++++- 4 files changed, 116 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c11ddbf..ca8693b 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,8 @@ mc-filter-proxy 컨테이너 (25565) | POST | `/api/domains` | 도메인 추가 | | PATCH | `/api/domains/{domain}` | 활성/메모 변경 | | DELETE | `/api/domains/{domain}` | 도메인 삭제 | -| GET | `/api/logs?limit&offset&action` | 접속 로그 (페이지네이션) | +| GET | `/api/logs?limit&offset&action&from_ts&to_ts` | 접속 로그 (페이지네이션, 날짜 필터) | +| DELETE | `/api/logs?from_ts&to_ts` | 접속 로그 삭제 (범위 미지정 시 전체) | | GET | `/api/status` | 프록시 상태 + 통계 | | POST | `/api/proxy/restart` | config 파일 touch (프록시 재로드 트리거) | diff --git a/api/routes/logs.py b/api/routes/logs.py index a48825a..48a4247 100644 --- a/api/routes/logs.py +++ b/api/routes/logs.py @@ -1,4 +1,4 @@ -"""접속 로그 조회.""" +"""접속 로그 조회 및 초기화.""" from __future__ import annotations import sqlite3 @@ -15,16 +15,26 @@ def list_logs( limit: int = Query(50, ge=1, le=500), offset: int = Query(0, ge=0), action: str | None = Query(None), + from_ts: float | None = Query(None, description="unix epoch seconds, inclusive"), + to_ts: float | None = Query(None, description="unix epoch seconds, exclusive"), ) -> dict: if not LOG_DB.exists(): return {"total": 0, "items": []} con = sqlite3.connect(LOG_DB) try: con.row_factory = sqlite3.Row - where, params = "", [] + conds: list[str] = [] + params: list = [] if action: - where = "WHERE action = ?" + conds.append("action = ?") params.append(action) + if from_ts is not None: + conds.append("ts >= ?") + params.append(from_ts) + if to_ts is not None: + conds.append("ts < ?") + params.append(to_ts) + where = ("WHERE " + " AND ".join(conds)) if conds else "" total = con.execute( f"SELECT COUNT(*) FROM connections {where}", params ).fetchone()[0] @@ -36,3 +46,40 @@ def list_logs( finally: con.close() return {"total": total, "items": [dict(r) for r in rows]} + + +@router.delete("/logs") +def clear_logs( + from_ts: float | None = Query(None, description="ts >= from_ts 만 삭제"), + to_ts: float | None = Query(None, description="ts < to_ts 만 삭제"), +) -> dict: + """접속 로그를 삭제한다. + + - from_ts/to_ts 둘 다 없으면 전체 삭제 (AUTOINCREMENT 카운터까지 리셋) + - 둘 중 하나만 있으면 그 조건만 적용 + - 둘 다 있으면 [from_ts, to_ts) 범위 삭제 — 날짜 선택 삭제 용도 + """ + if not LOG_DB.exists(): + return {"deleted": 0} + con = sqlite3.connect(LOG_DB, timeout=5) + try: + conds: list[str] = [] + params: list = [] + if from_ts is not None: + conds.append("ts >= ?") + params.append(from_ts) + if to_ts is not None: + conds.append("ts < ?") + params.append(to_ts) + if conds: + where = "WHERE " + " AND ".join(conds) + cur = con.execute(f"DELETE FROM connections {where}", params) + deleted = cur.rowcount + else: + cur = con.execute("DELETE FROM connections") + deleted = cur.rowcount + con.execute("DELETE FROM sqlite_sequence WHERE name='connections'") + con.commit() + finally: + con.close() + return {"deleted": deleted} diff --git a/frontend/src/api.js b/frontend/src/api.js index f368576..654e1a5 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -32,5 +32,11 @@ export const api = { ).toString() return req('/logs' + (q ? `?${q}` : '')) }, + clearLogs: (params = {}) => { + const q = new URLSearchParams( + Object.fromEntries(Object.entries(params).filter(([, v]) => v !== '' && v != null)) + ).toString() + return req('/logs' + (q ? `?${q}` : ''), { method: 'DELETE' }) + }, restart: () => req('/proxy/restart', { method: 'POST' }), } diff --git a/frontend/src/pages/Logs.jsx b/frontend/src/pages/Logs.jsx index eb7da76..211f877 100644 --- a/frontend/src/pages/Logs.jsx +++ b/frontend/src/pages/Logs.jsx @@ -5,16 +5,33 @@ function fmtTime(ts) { return new Date(ts * 1000).toLocaleString('ko-KR') } +// 'YYYY-MM-DD' 문자열을 로컬 자정 unix epoch (초) 로 바꾼다. +function dayStart(dateStr) { + if (!dateStr) return null + const [y, m, d] = dateStr.split('-').map((s) => parseInt(s, 10)) + return new Date(y, m - 1, d, 0, 0, 0, 0).getTime() / 1000 +} +function dayEnd(dateStr) { + const start = dayStart(dateStr) + return start == null ? null : start + 86400 +} + export default function Logs() { const [data, setData] = useState({ total: 0, items: [] }) const [filter, setFilter] = useState('') + const [date, setDate] = useState('') const [auto, setAuto] = useState(true) const [error, setError] = useState(null) + const [msg, setMsg] = useState(null) async function load() { try { const params = { limit: 100 } if (filter) params.action = filter + if (date) { + params.from_ts = dayStart(date) + params.to_ts = dayEnd(date) + } setData(await api.logs(params)) setError(null) } catch (e) { @@ -27,7 +44,30 @@ export default function Logs() { if (!auto) return const id = setInterval(load, 3000) return () => clearInterval(id) - }, [filter, auto]) + }, [filter, date, auto]) + + async function clearLogs() { + const scope = date ? `${date} 하루치` : '전체' + if (!window.confirm(`${scope} 접속 로그를 정말 삭제할까요? (되돌릴 수 없음)`)) { + return + } + try { + const params = {} + if (date) { + params.from_ts = dayStart(date) + params.to_ts = dayEnd(date) + } + const res = await api.clearLogs(params) + setMsg(`${res.deleted.toLocaleString()}건 삭제됨`) + setError(null) + setTimeout(() => setMsg(null), 2500) + load() + } catch (e) { + setError(e.message) + } + } + + const clearLabel = date ? `${date} 하루치 삭제` : '전체 로그 초기화' return (
@@ -40,6 +80,19 @@ export default function Logs() { + + {date && ( + + )}