feat(logs): date filter + clear log endpoints

- 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 "<날짜> 하루치 삭제".
This commit is contained in:
2026-05-23 17:55:58 +09:00
parent 8312cfe861
commit 7362b45846
4 changed files with 116 additions and 5 deletions

View File

@@ -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' }),
}

View File

@@ -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 (
<div>
@@ -40,6 +80,19 @@ export default function Logs() {
<option value="blocked">차단</option>
<option value="error">에러</option>
</select>
<label className="inline">
날짜
<input
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
/>
</label>
{date && (
<button className="ghost" onClick={() => setDate('')}>
오늘 해제
</button>
)}
<label className="inline">
<input
type="checkbox"
@@ -49,6 +102,10 @@ export default function Logs() {
자동 갱신 (3)
</label>
<span className="muted"> {data.total.toLocaleString()}</span>
<button className="danger" onClick={clearLogs}>
{clearLabel}
</button>
{msg && <span className="ok">{msg}</span>}
</div>
<table className="table">
<thead>