Neater abuse table formatting.
This commit is contained in:
@@ -151,7 +151,7 @@ function countryName(code) {
|
||||
<tr v-for="(v, i) in visitRows" :key="i">
|
||||
<td class="trail">
|
||||
<TrailLink v-if="v.refererStep" :step="v.refererStep" @close="$emit('close')" />
|
||||
<span v-if="v.utm && v.utm !== '—'" class="utm-tag" :title="v.utmTitle">{{ v.utm }}</span>
|
||||
<span v-if="v.utm && v.utm !== '—'" class="utm-tag small muted" :title="v.utmTitle">{{ v.utm }}</span>
|
||||
<TrailLink v-for="(s, si) in v.trail" :key="si" :step="s" @close="$emit('close')" />
|
||||
</td>
|
||||
<td class="ip-locale-cell" :class="{ 'host-cell': v.isHost }">
|
||||
@@ -159,10 +159,10 @@ function countryName(code) {
|
||||
<div class="ip-locale-row">
|
||||
<div class="locale-line">
|
||||
<span v-if="flagSvg(v.country)" class="flag" v-html="flagSvg(v.country)" :title="countryName(v.country) || v.country"></span>
|
||||
<template v-if="v.city && v.city !== '—'"><small class="city-name">{{ v.city }}</small></template>
|
||||
<template v-if="v.city && v.city !== '—'"><small class="city-name muted">{{ v.city }}</small></template>
|
||||
<template v-else-if="!flagSvg(v.country)">—</template>
|
||||
</div>
|
||||
<div class="ip-line"><span class="clickable-ip"
|
||||
<div class="ip-line"><span class="clickable-ip small muted"
|
||||
:title="v.ip"
|
||||
@click="copyIp(v.ip, $event)">{{ v.ipDisplay }}</span></div>
|
||||
</div>
|
||||
@@ -172,7 +172,7 @@ function countryName(code) {
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="last-seen"
|
||||
<td class="last-seen muted"
|
||||
:title="v.lastSeenLocal"
|
||||
@click="copyList(v.lastSeenIso, $event)">{{ v.lastSeen }}</td>
|
||||
</tr>
|
||||
@@ -199,12 +199,12 @@ function countryName(code) {
|
||||
<TrailLink v-for="(s, si) in c.pages" :key="si" :step="s" :count="s.count" @close="$emit('close')" />
|
||||
</td>
|
||||
<td class="ip-ua-cell">
|
||||
<div><span class="clickable-ip"
|
||||
<div><span class="clickable-ip small muted"
|
||||
:title="c.ip"
|
||||
@click="copyIp(c.ip, $event)">{{ c.ipDisplay }}</span></div>
|
||||
<div class="ua-line"><small class="muted" :title="c.uaRaw">{{ c.ua }}</small></div>
|
||||
</td>
|
||||
<td class="last-seen"
|
||||
<td class="last-seen muted"
|
||||
:title="c.lastSeenLocal"
|
||||
@click="copyList(c.lastSeenIso, $event)">{{ c.lastSeen }}</td>
|
||||
</tr>
|
||||
@@ -229,30 +229,28 @@ function countryName(code) {
|
||||
<tr v-for="(a, i) in abuseRows" :key="i">
|
||||
<td class="trail abuse-list clickable-list"
|
||||
@click="copyList(a.allPaths, $event)">
|
||||
<div v-for="(p, pi) in a.paths.slice(0, ABUSE_MAX_LINES)" :key="pi"
|
||||
class="list-line">
|
||||
{{ p.path }}
|
||||
</div>
|
||||
<div v-if="a.paths.length > ABUSE_MAX_LINES" class="list-line">
|
||||
<small class="muted">+{{ a.paths.length - ABUSE_MAX_LINES }} more</small>
|
||||
<div class="abuse-items">
|
||||
<span v-for="(p, pi) in a.paths.slice(0, ABUSE_MAX_LINES)" :key="pi"
|
||||
class="inline-item">
|
||||
<small v-if="p.count > 1" class="muted">{{ formatCount(p.count) }}×</small>{{ p.path }}
|
||||
</span>
|
||||
<small v-if="a.paths.length > ABUSE_MAX_LINES" class="muted">+{{ a.paths.length - ABUSE_MAX_LINES }} more</small>
|
||||
</div>
|
||||
</td>
|
||||
<td class="ip-ua-cell">
|
||||
<div><span class="clickable-ip"
|
||||
<div><span class="clickable-ip small muted"
|
||||
:title="a.ip"
|
||||
@click="copyIp(a.ip, $event)">{{ a.ipDisplay }}</span></div>
|
||||
<div class="abuse-uas-list clickable-list"
|
||||
@click="copyList(a.allUas, $event)">
|
||||
<div v-for="(u, ui) in a.uas.slice(0, ABUSE_MAX_LINES)" :key="ui"
|
||||
class="list-line">
|
||||
<span v-for="(u, ui) in a.uas.slice(0, ABUSE_MAX_LINES)" :key="ui"
|
||||
class="inline-item">
|
||||
<small v-if="u.count > 1" class="muted">{{ formatCount(u.count) }}×</small>{{ u.ua }}
|
||||
</div>
|
||||
<div v-if="a.uas.length > ABUSE_MAX_LINES" class="list-line">
|
||||
<small class="muted">+{{ a.uas.length - ABUSE_MAX_LINES }} more</small>
|
||||
</div>
|
||||
</span>
|
||||
<small v-if="a.uas.length > ABUSE_MAX_LINES" class="muted">+{{ a.uas.length - ABUSE_MAX_LINES }} more</small>
|
||||
</div>
|
||||
</td>
|
||||
<td class="last-seen"
|
||||
<td class="last-seen muted"
|
||||
:title="a.lastSeenLocal"
|
||||
@click="copyList(a.lastSeenIso, $event)">{{ a.lastSeen }}</td>
|
||||
</tr>
|
||||
@@ -333,6 +331,15 @@ function countryName(code) {
|
||||
margin-top: 1.8rem;
|
||||
}
|
||||
|
||||
.analytics-view a {
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
}
|
||||
.analytics-view a:hover { color: var(--accent); }
|
||||
|
||||
.analytics-view :deep(.muted) { color: var(--muted); }
|
||||
.analytics-view :deep(.small) { font-size: 0.75em; }
|
||||
|
||||
.totals {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
@@ -372,14 +379,9 @@ function countryName(code) {
|
||||
width: 7.5rem;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.visit-table .last-seen:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.visit-table .trail {
|
||||
max-width: 20rem;
|
||||
overflow-wrap: break-word;
|
||||
@@ -392,14 +394,9 @@ function countryName(code) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.visit-table .trail a:hover,
|
||||
.visit-table .trail-link:hover { color: var(--accent); }
|
||||
|
||||
.visit-table .trail > * + * {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
@@ -410,8 +407,6 @@ function countryName(code) {
|
||||
padding: 0.05rem 0.4rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75em;
|
||||
color: var(--muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -423,29 +418,26 @@ function countryName(code) {
|
||||
max-width: 22rem;
|
||||
}
|
||||
|
||||
.visit-table .clickable-list .list-line {
|
||||
.visit-table .abuse-items,
|
||||
.visit-table .abuse-uas-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.15rem 0.5rem;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.visit-table .abuse-uas-list {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.visit-table .inline-item {
|
||||
max-width: 18rem;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.visit-table .clickable-list .list-line + .list-line {
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
.visit-table .ua.abuse-uas {
|
||||
max-width: 24rem;
|
||||
}
|
||||
|
||||
.visit-table .trail small,
|
||||
.visit-table small.muted {
|
||||
color: var(--muted);
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.visit-table .clickable-ip {
|
||||
font-size: 0.75em;
|
||||
word-break: keep-all;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
.visit-table .clickable-ip,
|
||||
@@ -455,12 +447,6 @@ function countryName(code) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.visit-table .clickable-ip:hover,
|
||||
.visit-table .clickable-list:hover,
|
||||
.visit-table .last-seen:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.visit-table .ip-locale-cell {
|
||||
width: 36ch;
|
||||
max-width: 36ch;
|
||||
@@ -547,9 +533,6 @@ function countryName(code) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.visit-table .abuse-uas-list {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.visit-table .copy-popup {
|
||||
position: absolute;
|
||||
|
||||
@@ -367,43 +367,59 @@ export function formatCrawlerRows(crawlers, pageTree, now = Date.now()) {
|
||||
/**
|
||||
* Group abuse hits by IP (never by UA — scanners randomize theirs to
|
||||
* masquerade as legitimate crawlers) and format each group as a row with
|
||||
* the full paths probed, in access order. Flagged paths (the ones that
|
||||
* triggered abuse classification) are lifted to the top, followed by
|
||||
* other 404s, then document GETs from the abuser. UAs are shown raw,
|
||||
* one per line, with their occurrence counts. Paths are shown verbatim
|
||||
* (query string included), not resolved against the page tree.
|
||||
* the full paths probed. Identical paths are collapsed into one entry
|
||||
* with their hit count. Flagged paths (the ones that triggered abuse
|
||||
* classification) are lifted to the top, followed by other 404s, then
|
||||
* document GETs from the abuser. Within each category paths are sorted by
|
||||
* count descending, then earliest first. UAs are shown raw, one per line,
|
||||
* with their occurrence counts. Paths are shown verbatim (query string
|
||||
* included), not resolved against the page tree.
|
||||
*/
|
||||
export function formatAbuseRows(abuse, now = Date.now()) {
|
||||
const groups = new Map()
|
||||
for (const a of abuse || []) {
|
||||
const g = groups.get(a.ip) || {
|
||||
ip: a.ip || '',
|
||||
pathHits: [],
|
||||
pathCounts: new Map(),
|
||||
rawUas: [],
|
||||
uaCounts: new Map(),
|
||||
lastStart: 0,
|
||||
}
|
||||
const start = new Date(a.start).getTime()
|
||||
if (start > g.lastStart) g.lastStart = start
|
||||
g.pathHits.push({
|
||||
path: a.path || '',
|
||||
start,
|
||||
const path = a.path || ''
|
||||
const existing = g.pathCounts.get(path) || {
|
||||
path,
|
||||
count: 0,
|
||||
firstStart: start,
|
||||
flag: a.flag || false,
|
||||
is_404: a.is_404 || false,
|
||||
})
|
||||
}
|
||||
existing.count += 1
|
||||
if (start < existing.firstStart) existing.firstStart = start
|
||||
if (a.flag) existing.flag = true
|
||||
if (!a.is_404) existing.is_404 = false
|
||||
g.pathCounts.set(path, existing)
|
||||
const ua = a.ua || '(no UA)'
|
||||
g.rawUas.push(ua)
|
||||
g.uaCounts.set(ua, (g.uaCounts.get(ua) || 0) + 1)
|
||||
groups.set(a.ip, g)
|
||||
}
|
||||
const totalHits = (g) => g.pathHits.length
|
||||
const totalHits = (g) => {
|
||||
let n = 0
|
||||
for (const p of g.pathCounts.values()) n += p.count
|
||||
return n
|
||||
}
|
||||
return [...groups.values()]
|
||||
.sort((a, b) => totalHits(b) - totalHits(a) || b.lastStart - a.lastStart)
|
||||
.slice(0, 10)
|
||||
.map((g) => {
|
||||
const pathCategory = (p) => (p.flag ? 0 : p.is_404 ? 1 : 2)
|
||||
const paths = [...g.pathHits].sort(
|
||||
(a, b) => pathCategory(a) - pathCategory(b) || a.start - b.start,
|
||||
const paths = [...g.pathCounts.values()].sort(
|
||||
(a, b) =>
|
||||
pathCategory(a) - pathCategory(b) ||
|
||||
b.count - a.count ||
|
||||
a.firstStart - b.firstStart,
|
||||
)
|
||||
const uas = [...g.uaCounts.entries()]
|
||||
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
||||
@@ -411,8 +427,15 @@ export function formatAbuseRows(abuse, now = Date.now()) {
|
||||
lastSeen: formatWhen(g.lastStart, now),
|
||||
lastSeenIso: formatWhenIso(g.lastStart),
|
||||
lastSeenLocal: formatWhenLocal(g.lastStart),
|
||||
paths: paths.map((p) => ({ path: p.path, flag: p.flag, is_404: p.is_404 })),
|
||||
allPaths: paths.map((p) => p.path).join('\n'),
|
||||
paths: paths.map((p) => ({
|
||||
path: p.path,
|
||||
count: p.count,
|
||||
flag: p.flag,
|
||||
is_404: p.is_404,
|
||||
})),
|
||||
allPaths: paths
|
||||
.map((p) => (p.count > 1 ? `${p.count}× ${p.path}` : p.path))
|
||||
.join('\n'),
|
||||
uas: uas.map(([ua, count]) => ({ ua, count })),
|
||||
allUas: uas
|
||||
.map(([ua, count]) => (count > 1 ? `${count}× ${ua}` : ua))
|
||||
|
||||
Reference in New Issue
Block a user