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:
@@ -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' }),
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user