fix: 对齐监控概览双通道口径文案与诊断抽样
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -171,6 +171,10 @@ func main() {
|
|||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
|
fmt.Println("=== 对账抽样(近 30 日非 bot)===")
|
||||||
|
printGeoReconcileSamples(db, have, since)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
fmt.Println("=== 样例行(近 30 日,最多 10 条)===")
|
fmt.Println("=== 样例行(近 30 日,最多 10 条)===")
|
||||||
cols := []string{"id", "created_at", "path", "is_bot"}
|
cols := []string{"id", "created_at", "path", "is_bot"}
|
||||||
for _, c := range []string{"ip", "country", "region", "region_iso", "city", "asn", "as_org"} {
|
for _, c := range []string{"ip", "country", "region", "region_iso", "city", "asn", "as_org"} {
|
||||||
@@ -217,10 +221,103 @@ func main() {
|
|||||||
}
|
}
|
||||||
fmt.Fprintln(os.Stderr, "请确认数据目录已放置 IP2LOCATION-LITE-DB3.BIN 与 GeoLite2-ASN.mmdb,且有公网访问产生的 pageview。")
|
fmt.Fprintln(os.Stderr, "请确认数据目录已放置 IP2LOCATION-LITE-DB3.BIN 与 GeoLite2-ASN.mmdb,且有公网访问产生的 pageview。")
|
||||||
fmt.Fprintln(os.Stderr, "本机/私网 IP 通常解不出省,属正常。")
|
fmt.Fprintln(os.Stderr, "本机/私网 IP 通常解不出省,属正常。")
|
||||||
|
fmt.Fprintln(os.Stderr, "注意: 请求日志有地理 ≠ 概览地图/排行有数据(后者只读 page_views)。")
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func printGeoReconcileSamples(db *gorm.DB, have map[string]bool, since time.Time) {
|
||||||
|
type sample struct {
|
||||||
|
ID uint
|
||||||
|
CreatedAt time.Time
|
||||||
|
IP string
|
||||||
|
Country string
|
||||||
|
Region string
|
||||||
|
RegionISO string `gorm:"column:region_iso"`
|
||||||
|
City string
|
||||||
|
ASN uint
|
||||||
|
ASOrg string `gorm:"column:as_org"`
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
selCols := []string{"id", "created_at", "path"}
|
||||||
|
for _, c := range []string{"ip", "country", "region", "region_iso", "city", "asn", "as_org"} {
|
||||||
|
if have[c] {
|
||||||
|
selCols = append(selCols, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sel := strings.Join(selCols, ", ")
|
||||||
|
|
||||||
|
printBlock := func(title string, n int64, rows []sample) {
|
||||||
|
fmt.Printf("%s: %d\n", title, n)
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, s := range rows {
|
||||||
|
fmt.Printf(" #%d %s ip=%s country=%q region=%q iso=%q city=%q asn=%d org=%q path=%s\n",
|
||||||
|
s.ID, s.CreatedAt.Format("2006-01-02 15:04:05"), s.IP,
|
||||||
|
s.Country, s.Region, s.RegionISO, s.City, s.ASN, s.ASOrg, s.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if have["city"] {
|
||||||
|
var n int64
|
||||||
|
_ = db.Table("page_views").
|
||||||
|
Where("created_at >= ? AND is_bot = ? AND city <> '' AND city GLOB '[A-Za-z]*'", since, false).
|
||||||
|
Count(&n).Error
|
||||||
|
var rows []sample
|
||||||
|
_ = db.Table("page_views").
|
||||||
|
Select(sel).
|
||||||
|
Where("created_at >= ? AND is_bot = ? AND city <> '' AND city GLOB '[A-Za-z]*'", since, false).
|
||||||
|
Order("id DESC").
|
||||||
|
Limit(5).
|
||||||
|
Scan(&rows).Error
|
||||||
|
printBlock("英文城市名(可能未中文化)", n, rows)
|
||||||
|
} else {
|
||||||
|
fmt.Println("英文城市名: (city 列缺失)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if have["region"] || have["region_iso"] {
|
||||||
|
cond := "created_at >= ? AND is_bot = ? AND country <> ''"
|
||||||
|
emptyParts := []string{}
|
||||||
|
if have["region"] {
|
||||||
|
emptyParts = append(emptyParts, "(region = '' OR region IS NULL OR region = '-')")
|
||||||
|
}
|
||||||
|
if have["region_iso"] {
|
||||||
|
emptyParts = append(emptyParts, "(region_iso = '' OR region_iso IS NULL)")
|
||||||
|
}
|
||||||
|
where := cond + " AND (" + strings.Join(emptyParts, " AND ") + ")"
|
||||||
|
var n int64
|
||||||
|
_ = db.Table("page_views").Where(where, since, false).Count(&n).Error
|
||||||
|
var rows []sample
|
||||||
|
_ = db.Table("page_views").
|
||||||
|
Select(sel).
|
||||||
|
Where(where, since, false).
|
||||||
|
Order("id DESC").
|
||||||
|
Limit(5).
|
||||||
|
Scan(&rows).Error
|
||||||
|
printBlock("有国家但省为空", n, rows)
|
||||||
|
} else {
|
||||||
|
fmt.Println("有国家但省为空: (省列缺失)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if have["asn"] {
|
||||||
|
var n int64
|
||||||
|
_ = db.Table("page_views").
|
||||||
|
Where("created_at >= ? AND is_bot = ? AND (asn = 0 OR asn IS NULL)", since, false).
|
||||||
|
Count(&n).Error
|
||||||
|
var rows []sample
|
||||||
|
_ = db.Table("page_views").
|
||||||
|
Select(sel).
|
||||||
|
Where("created_at >= ? AND is_bot = ? AND (asn = 0 OR asn IS NULL)", since, false).
|
||||||
|
Order("id DESC").
|
||||||
|
Limit(5).
|
||||||
|
Scan(&rows).Error
|
||||||
|
printBlock("asn=0(无运营商)", n, rows)
|
||||||
|
} else {
|
||||||
|
fmt.Println("asn=0: (asn 列缺失)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func printGeoFiles(dataDir string, suite *service.GeoIPService) {
|
func printGeoFiles(dataDir string, suite *service.GeoIPService) {
|
||||||
fmt.Println("=== Geo 数据文件 ===")
|
fmt.Println("=== Geo 数据文件 ===")
|
||||||
v4, v6, asn, country := suite.Paths()
|
v4, v6, asn, country := suite.Paths()
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ CDN 头(如 `CF-IPCountry`)仅在本地库无国家码时补全。不落 Lat
|
|||||||
|
|
||||||
写入时做本地中文映射(省 / 中国城市按 IP2Location 英文名 / 运营商);同音城市(如苏州/宿州)按省份歧义。`country` 存 ISO2,展示用中文名。历史 `page_views` / 请求日志不会回写,仅影响新写入。
|
写入时做本地中文映射(省 / 中国城市按 IP2Location 英文名 / 运营商);同音城市(如苏州/宿州)按省份歧义。`country` 存 ISO2,展示用中文名。历史 `page_views` / 请求日志不会回写,仅影响新写入。
|
||||||
|
|
||||||
|
**口径提示:** 访客地图与城市/运营商排行只聚合 `page_views`(前台浏览);请求日志里的地理来自 access JSONL。仅有请求日志、没有前台浏览时,地图与排行可为空——属预期。
|
||||||
|
|
||||||
## 写入路径(性能)
|
## 写入路径(性能)
|
||||||
|
|
||||||
1. 中间件:监控关闭或命中排除规则则直接放行;否则 `c.Next()` 后**仅入队**轻量字段(不查 BIN/ASN、不写盘)。
|
1. 中间件:监控关闭或命中排除规则则直接放行;否则 `c.Next()` 后**仅入队**轻量字段(不查 BIN/ASN、不写盘)。
|
||||||
@@ -70,6 +72,7 @@ CDN 头(如 `CF-IPCountry`)仅在本地库无国家码时补全。不落 Lat
|
|||||||
go run ./cmd/monitor-geo-check -db data/monitor.db -ip 14.109.35.246
|
go run ./cmd/monitor-geo-check -db data/monitor.db -ip 14.109.35.246
|
||||||
```
|
```
|
||||||
|
|
||||||
|
会输出 Geo 文件状态、近 30 日省级 Top,以及对账抽样(英文城市名、有国家但省为空、`asn=0`)。
|
||||||
## 明确不做
|
## 明确不做
|
||||||
|
|
||||||
- 不用 CIDR;不把请求日志写入 SQLite;不把 page_views 写入主库
|
- 不用 CIDR;不把请求日志写入 SQLite;不把 page_views 写入主库
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import china from '@svg-maps/china';
|
import china from '@svg-maps/china';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { buildChinaRegionCountMap, heatFill, emptyFill } from './monitorMapUtils';
|
import { buildChinaRegionCountMap, heatFill } from './monitorMapUtils';
|
||||||
|
|
||||||
type Loc = { id: string; name: string; path: string };
|
type Loc = { id: string; name: string; path: string };
|
||||||
|
|
||||||
@@ -15,10 +15,19 @@ type RegionStat = {
|
|||||||
type Props = {
|
type Props = {
|
||||||
regions: RegionStat[];
|
regions: RegionStat[];
|
||||||
className?: string;
|
className?: string;
|
||||||
|
/** 近 30 日是否已有国家级浏览量 */
|
||||||
|
hasCountryHits?: boolean;
|
||||||
|
/** 近 30 日是否已有省级浏览量 */
|
||||||
|
hasRegionHits?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 中国省区地图:按 BIN 解析出的省/区填色 */
|
/** 中国省区地图:按 BIN 解析出的省/区填色 */
|
||||||
export default function MonitorChinaMap({ regions, className }: Props) {
|
export default function MonitorChinaMap({
|
||||||
|
regions,
|
||||||
|
className,
|
||||||
|
hasCountryHits = false,
|
||||||
|
hasRegionHits = false,
|
||||||
|
}: Props) {
|
||||||
const [hover, setHover] = useState<{ name: string; count: number } | null>(null);
|
const [hover, setHover] = useState<{ name: string; count: number } | null>(null);
|
||||||
const counts = useMemo(() => buildChinaRegionCountMap(regions), [regions]);
|
const counts = useMemo(() => buildChinaRegionCountMap(regions), [regions]);
|
||||||
const max = useMemo(() => Math.max(0, ...Object.values(counts)), [counts]);
|
const max = useMemo(() => Math.max(0, ...Object.values(counts)), [counts]);
|
||||||
@@ -26,6 +35,16 @@ export default function MonitorChinaMap({ regions, className }: Props) {
|
|||||||
const locations = (china as { locations: Loc[]; viewBox: string }).locations;
|
const locations = (china as { locations: Loc[]; viewBox: string }).locations;
|
||||||
const viewBox = (china as { viewBox: string }).viewBox;
|
const viewBox = (china as { viewBox: string }).viewBox;
|
||||||
|
|
||||||
|
let emptyTitle = '暂无省级访问数据';
|
||||||
|
let emptyHint = '放置 IP2LOCATION-LITE-DB3.BIN 后按省/区填色';
|
||||||
|
if (hasCountryHits && !hasRegionHits) {
|
||||||
|
emptyTitle = '有国家浏览量,尚无省级数据';
|
||||||
|
emptyHint = '需前台路由产生带省字段的浏览量;仅请求日志不会点亮中国地图。请确认已放置 IP2Location DB3 BIN。';
|
||||||
|
} else if (!hasCountryHits) {
|
||||||
|
emptyTitle = '暂无浏览量地理数据';
|
||||||
|
emptyHint = '需前台路由产生浏览量后才会按省填色;仅请求日志不会点亮此处。';
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('admin-monitor-svg-wrap', className)}>
|
<div className={cn('admin-monitor-svg-wrap', className)}>
|
||||||
<svg
|
<svg
|
||||||
@@ -63,8 +82,8 @@ export default function MonitorChinaMap({ regions, className }: Props) {
|
|||||||
)}
|
)}
|
||||||
{!hasData && (
|
{!hasData && (
|
||||||
<div className="admin-monitor-map-empty-overlay">
|
<div className="admin-monitor-map-empty-overlay">
|
||||||
<p>暂无省级访问数据</p>
|
<p>{emptyTitle}</p>
|
||||||
<p>放置 IP2LOCATION-LITE-DB3.BIN 后按省/区填色</p>
|
<p>{emptyHint}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -79,23 +79,29 @@ function GeoRankTable({
|
|||||||
cities,
|
cities,
|
||||||
asns,
|
asns,
|
||||||
mode,
|
mode,
|
||||||
mapMode,
|
|
||||||
rankMode,
|
rankMode,
|
||||||
onRankMode,
|
onRankMode,
|
||||||
|
hasCountryHits,
|
||||||
}: {
|
}: {
|
||||||
cities: MonitorCityItem[];
|
cities: MonitorCityItem[];
|
||||||
asns: MonitorASNItem[];
|
asns: MonitorASNItem[];
|
||||||
mode: MapMode;
|
mode: MapMode;
|
||||||
rankMode: RankMode;
|
rankMode: RankMode;
|
||||||
onRankMode: (m: RankMode) => void;
|
onRankMode: (m: RankMode) => void;
|
||||||
|
/** 近 30 日是否已有国家级浏览量(用于区分「无 PV 地理」与缺库) */
|
||||||
|
hasCountryHits: boolean;
|
||||||
}) {
|
}) {
|
||||||
const cityRows = mode === 'china'
|
const cityRows = mode === 'china'
|
||||||
? cities.filter((i) => CHINA_CODES.has((i.country || '').toUpperCase()))
|
? cities.filter((i) => CHINA_CODES.has((i.country || '').toUpperCase()))
|
||||||
: cities;
|
: cities;
|
||||||
const rows = rankMode === 'city' ? cityRows : asns;
|
const rows = rankMode === 'city' ? cityRows : asns;
|
||||||
const emptyHint = rankMode === 'city'
|
const emptyHint = rankMode === 'city'
|
||||||
? '放置 IP2Location DB3 BIN 后可见城市排行'
|
? (hasCountryHits
|
||||||
: '放置 GeoLite2-ASN.mmdb 后可见运营商排行';
|
? '需前台路由产生带城市的浏览量;仅请求日志不会进入此排行。请确认已放置 IP2Location DB3 BIN。'
|
||||||
|
: '需前台路由产生浏览量后才会有城市排行;仅请求日志不会点亮此处。')
|
||||||
|
: (hasCountryHits
|
||||||
|
? '需浏览量写入 ASN;请确认已放置 GeoLite2-ASN.mmdb。仅请求日志不会进入此排行。'
|
||||||
|
: '需前台路由产生浏览量后才会有运营商排行;仅请求日志不会点亮此处。');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="admin-monitor-rank">
|
<div className="admin-monitor-rank">
|
||||||
@@ -117,7 +123,7 @@ function GeoRankTable({
|
|||||||
</div>
|
</div>
|
||||||
{rows.length === 0 ? (
|
{rows.length === 0 ? (
|
||||||
<div className="admin-monitor-geo-empty">
|
<div className="admin-monitor-geo-empty">
|
||||||
<p>暂无来源数据</p>
|
<p>暂无浏览量来源数据</p>
|
||||||
<p className="admin-monitor-muted">{emptyHint}</p>
|
<p className="admin-monitor-muted">{emptyHint}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -282,18 +288,24 @@ export default function AdminMonitorPage() {
|
|||||||
|
|
||||||
const metrics = useMemo(() => {
|
const metrics = useMemo(() => {
|
||||||
if (!overview) return [];
|
if (!overview) return [];
|
||||||
|
const pvHint = '前台浏览';
|
||||||
|
const accessHintShort = '服务端请求';
|
||||||
|
const accessHintFull = '服务端请求(含 API,重启后重计)';
|
||||||
return [
|
return [
|
||||||
{ label: '浏览量', value: formatNum(overview.pageviews) },
|
{ label: '浏览量', hint: pvHint, title: pvHint, value: formatNum(overview.pageviews) },
|
||||||
{ label: '访客数', value: formatNum(overview.visitors) },
|
{ label: '访客数', hint: pvHint, title: pvHint, value: formatNum(overview.visitors) },
|
||||||
{ label: '独立 IP', value: formatNum(overview.unique_ips) },
|
{ label: '独立 IP', hint: accessHintShort, title: accessHintFull, value: formatNum(overview.unique_ips) },
|
||||||
{ label: '流量', value: formatBytes(overview.traffic) },
|
{ label: '流量', hint: accessHintShort, title: accessHintFull, value: formatBytes(overview.traffic) },
|
||||||
{ label: '蜘蛛', value: formatNum(overview.bots) },
|
{ label: '蜘蛛', hint: accessHintShort, title: accessHintFull, value: formatNum(overview.bots) },
|
||||||
{ label: '请求数', value: formatNum(overview.requests) },
|
{ label: '请求数', hint: accessHintShort, title: accessHintFull, value: formatNum(overview.requests) },
|
||||||
{ label: '4xx', value: formatNum(overview.status_4xx) },
|
{ label: '4xx', hint: accessHintShort, title: accessHintFull, value: formatNum(overview.status_4xx) },
|
||||||
{ label: '5xx', value: formatNum(overview.status_5xx) },
|
{ label: '5xx', hint: accessHintShort, title: accessHintFull, value: formatNum(overview.status_5xx) },
|
||||||
];
|
];
|
||||||
}, [overview]);
|
}, [overview]);
|
||||||
|
|
||||||
|
const hasCountryHits = (geo?.countries?.length || 0) > 0;
|
||||||
|
const hasRegionHits = (geo?.regions?.length || 0) > 0;
|
||||||
|
|
||||||
const saveSettings = async () => {
|
const saveSettings = async () => {
|
||||||
if (!settings) return;
|
if (!settings) return;
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
@@ -352,7 +364,7 @@ export default function AdminMonitorPage() {
|
|||||||
网站监控
|
网站监控
|
||||||
</h1>
|
</h1>
|
||||||
<p className="admin-page-desc">
|
<p className="admin-page-desc">
|
||||||
浏览量/访客来自前台路由 pageview;请求数与日志来自服务端访问采集(含 /api)
|
浏览量/访客与访客地图来自前台路由 pageview;请求数、流量与请求日志来自服务端访问采集(含 /api,重启后请求类今日指标重计)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -391,8 +403,11 @@ export default function AdminMonitorPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="admin-monitor-today-grid">
|
<div className="admin-monitor-today-grid">
|
||||||
{metrics.map((m) => (
|
{metrics.map((m) => (
|
||||||
<div key={m.label} className="admin-monitor-today-item">
|
<div key={m.label} className="admin-monitor-today-item" title={m.title}>
|
||||||
<div className="admin-monitor-today-label">{m.label}</div>
|
<div className="admin-monitor-today-label">
|
||||||
|
{m.label}
|
||||||
|
<span className="admin-monitor-today-hint">{m.hint}</span>
|
||||||
|
</div>
|
||||||
<div className="admin-monitor-today-value">{m.value}</div>
|
<div className="admin-monitor-today-value">{m.value}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -404,7 +419,13 @@ export default function AdminMonitorPage() {
|
|||||||
<div className="admin-card-head admin-monitor-map-head">
|
<div className="admin-card-head admin-monitor-map-head">
|
||||||
<div className="admin-monitor-today-head" style={{ marginBottom: 0 }}>
|
<div className="admin-monitor-today-head" style={{ marginBottom: 0 }}>
|
||||||
<span className="admin-monitor-section-bar" aria-hidden />
|
<span className="admin-monitor-section-bar" aria-hidden />
|
||||||
<h2 className="admin-monitor-section-title">访客地图(30 日)</h2>
|
<div>
|
||||||
|
<h2 className="admin-monitor-section-title">访客地图</h2>
|
||||||
|
<p className="admin-monitor-map-sub">
|
||||||
|
基于前台浏览 · 近 30 日
|
||||||
|
{mapMode === 'china' ? ' · 按省/区填色' : ' · 按国家填色'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-monitor-seg" role="group" aria-label="地图范围">
|
<div className="admin-monitor-seg" role="group" aria-label="地图范围">
|
||||||
<button
|
<button
|
||||||
@@ -428,7 +449,11 @@ export default function AdminMonitorPage() {
|
|||||||
{mapMode === 'world' ? (
|
{mapMode === 'world' ? (
|
||||||
<MonitorWorldMap items={geoCountries(geo)} />
|
<MonitorWorldMap items={geoCountries(geo)} />
|
||||||
) : (
|
) : (
|
||||||
<MonitorChinaMap regions={geo?.regions || []} />
|
<MonitorChinaMap
|
||||||
|
regions={geo?.regions || []}
|
||||||
|
hasCountryHits={hasCountryHits}
|
||||||
|
hasRegionHits={hasRegionHits}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-monitor-map-side">
|
<div className="admin-monitor-map-side">
|
||||||
@@ -438,6 +463,7 @@ export default function AdminMonitorPage() {
|
|||||||
mode={mapMode}
|
mode={mapMode}
|
||||||
rankMode={rankMode}
|
rankMode={rankMode}
|
||||||
onRankMode={setRankMode}
|
onRankMode={setRankMode}
|
||||||
|
hasCountryHits={hasCountryHits}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -554,10 +580,10 @@ export default function AdminMonitorPage() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{logs.map((row) => {
|
{logs.map((row) => {
|
||||||
|
const placeBits = [row.region, row.city, row.as_org].filter(Boolean);
|
||||||
const geoBits = [
|
const geoBits = [
|
||||||
row.country ? countryLabel(row.country) : '',
|
row.country ? countryLabel(row.country) : '',
|
||||||
row.city || '',
|
...placeBits,
|
||||||
row.as_org || '',
|
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
return (
|
return (
|
||||||
<tr key={row.id}>
|
<tr key={row.id}>
|
||||||
@@ -568,9 +594,9 @@ export default function AdminMonitorPage() {
|
|||||||
<td className="admin-table-mono">
|
<td className="admin-table-mono">
|
||||||
{row.ip || '—'}
|
{row.ip || '—'}
|
||||||
{row.is_bot ? ' · bot' : ''}
|
{row.is_bot ? ' · bot' : ''}
|
||||||
{(row.city || row.as_org) ? (
|
{placeBits.length > 0 ? (
|
||||||
<div className="admin-monitor-muted" style={{ fontSize: 12 }}>
|
<div className="admin-monitor-muted" style={{ fontSize: 12 }}>
|
||||||
{[row.city, row.as_org].filter(Boolean).join(' · ')}
|
{placeBits.join(' · ')}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -15356,6 +15356,18 @@ button.post-poll__option,
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: hsl(var(--muted-foreground));
|
color: hsl(var(--muted-foreground));
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
.admin-monitor-today-hint {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.3;
|
||||||
|
opacity: 0.85;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
.admin-monitor-today-value {
|
.admin-monitor-today-value {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
@@ -15365,6 +15377,13 @@ button.post-poll__option,
|
|||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
.admin-monitor-map-sub {
|
||||||
|
margin: 2px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-monitor-main {
|
.admin-monitor-main {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
Reference in New Issue
Block a user