@hatk/hatk 0.0.1-alpha.11 → 0.0.1-alpha.111
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter.d.ts +19 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +113 -0
- package/dist/backfill.d.ts +36 -2
- package/dist/backfill.d.ts.map +1 -1
- package/dist/backfill.js +153 -37
- package/dist/car.js +1 -1
- package/dist/classifiers.d.ts +228 -0
- package/dist/classifiers.d.ts.map +1 -0
- package/dist/classifiers.js +688 -0
- package/dist/cli.js +248 -1043
- package/dist/config.d.ts +179 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +78 -8
- package/dist/database/adapter-factory.d.ts +6 -0
- package/dist/database/adapter-factory.d.ts.map +1 -0
- package/dist/database/adapter-factory.js +20 -0
- package/dist/database/adapters/duckdb-search.d.ts +12 -0
- package/dist/database/adapters/duckdb-search.d.ts.map +1 -0
- package/dist/database/adapters/duckdb-search.js +27 -0
- package/dist/database/adapters/duckdb.d.ts +25 -0
- package/dist/database/adapters/duckdb.d.ts.map +1 -0
- package/dist/database/adapters/duckdb.js +161 -0
- package/dist/database/adapters/sqlite-search.d.ts +16 -0
- package/dist/database/adapters/sqlite-search.d.ts.map +1 -0
- package/dist/database/adapters/sqlite-search.js +76 -0
- package/dist/database/adapters/sqlite.d.ts +18 -0
- package/dist/database/adapters/sqlite.d.ts.map +1 -0
- package/dist/database/adapters/sqlite.js +88 -0
- package/dist/database/db.d.ts +290 -0
- package/dist/database/db.d.ts.map +1 -0
- package/dist/database/db.js +2073 -0
- package/dist/database/dialect.d.ts +45 -0
- package/dist/database/dialect.d.ts.map +1 -0
- package/dist/database/dialect.js +72 -0
- package/dist/{fts.d.ts → database/fts.d.ts} +7 -0
- package/dist/database/fts.d.ts.map +1 -0
- package/dist/{fts.js → database/fts.js} +122 -34
- package/dist/database/index.d.ts +7 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +6 -0
- package/dist/database/ports.d.ts +50 -0
- package/dist/database/ports.d.ts.map +1 -0
- package/dist/database/ports.js +1 -0
- package/dist/database/schema.d.ts +91 -0
- package/dist/database/schema.d.ts.map +1 -0
- package/dist/{schema.js → database/schema.js} +153 -54
- package/dist/dev-entry.d.ts +8 -0
- package/dist/dev-entry.d.ts.map +1 -0
- package/dist/dev-entry.js +131 -0
- package/dist/feeds.d.ts +26 -8
- package/dist/feeds.d.ts.map +1 -1
- package/dist/feeds.js +72 -16
- package/dist/hooks.d.ts +102 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +186 -0
- package/dist/hydrate.d.ts +55 -6
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js +21 -20
- package/dist/indexer.d.ts +194 -3
- package/dist/indexer.d.ts.map +1 -1
- package/dist/indexer.js +627 -92
- package/dist/jetstream.d.ts +118 -0
- package/dist/jetstream.d.ts.map +1 -0
- package/dist/jetstream.js +238 -0
- package/dist/labels.d.ts +36 -0
- package/dist/labels.d.ts.map +1 -1
- package/dist/labels.js +71 -6
- package/dist/lex-types.d.ts +2 -0
- package/dist/lex-types.d.ts.map +1 -1
- package/dist/lexicon-resolve.d.ts.map +1 -1
- package/dist/lexicon-resolve.js +27 -112
- package/dist/lexicons/com/atproto/label/defs.json +75 -0
- package/dist/lexicons/com/atproto/moderation/defs.json +30 -0
- package/dist/lexicons/com/atproto/repo/strongRef.json +21 -0
- package/dist/lexicons/dev/hatk/applyWrites.json +87 -0
- package/dist/lexicons/dev/hatk/createRecord.json +40 -0
- package/dist/lexicons/dev/hatk/createReport.json +48 -0
- package/dist/lexicons/dev/hatk/deleteRecord.json +25 -0
- package/dist/lexicons/dev/hatk/describeCollections.json +41 -0
- package/dist/lexicons/dev/hatk/describeFeeds.json +29 -0
- package/dist/lexicons/dev/hatk/describeLabels.json +45 -0
- package/dist/lexicons/dev/hatk/getFeed.json +30 -0
- package/dist/lexicons/dev/hatk/getPreferences.json +19 -0
- package/dist/lexicons/dev/hatk/getRecord.json +26 -0
- package/dist/lexicons/dev/hatk/getRecords.json +32 -0
- package/dist/lexicons/dev/hatk/putPreference.json +28 -0
- package/dist/lexicons/dev/hatk/putRecord.json +41 -0
- package/dist/lexicons/dev/hatk/searchRecords.json +32 -0
- package/dist/lexicons/dev/hatk/uploadBlob.json +23 -0
- package/dist/logger.d.ts +29 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +29 -0
- package/dist/main.js +212 -75
- package/dist/mst.d.ts +15 -0
- package/dist/mst.d.ts.map +1 -1
- package/dist/mst.js +13 -0
- package/dist/oauth/db.d.ts +27 -1
- package/dist/oauth/db.d.ts.map +1 -1
- package/dist/oauth/db.js +80 -19
- package/dist/oauth/discovery.d.ts +10 -0
- package/dist/oauth/discovery.d.ts.map +1 -1
- package/dist/oauth/discovery.js +54 -2
- package/dist/oauth/server.d.ts +131 -1
- package/dist/oauth/server.d.ts.map +1 -1
- package/dist/oauth/server.js +504 -39
- package/dist/oauth/service-describe.d.ts +42 -0
- package/dist/oauth/service-describe.d.ts.map +1 -0
- package/dist/oauth/service-describe.js +108 -0
- package/dist/oauth/session.d.ts +32 -0
- package/dist/oauth/session.d.ts.map +1 -0
- package/dist/oauth/session.js +131 -0
- package/dist/opengraph.d.ts +10 -0
- package/dist/opengraph.d.ts.map +1 -1
- package/dist/opengraph.js +94 -42
- package/dist/pds-proxy.d.ts +92 -0
- package/dist/pds-proxy.d.ts.map +1 -0
- package/dist/pds-proxy.js +396 -0
- package/dist/private-collections.d.ts +13 -0
- package/dist/private-collections.d.ts.map +1 -0
- package/dist/private-collections.js +27 -0
- package/dist/push.d.ts +54 -0
- package/dist/push.d.ts.map +1 -0
- package/dist/push.js +367 -0
- package/dist/renderer.d.ts +27 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +46 -0
- package/dist/resolve-hatk.d.ts +6 -0
- package/dist/resolve-hatk.d.ts.map +1 -0
- package/dist/resolve-hatk.js +20 -0
- package/dist/response.d.ts +16 -0
- package/dist/response.d.ts.map +1 -0
- package/dist/response.js +69 -0
- package/dist/scanner.d.ts +22 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +92 -0
- package/dist/seed.d.ts +19 -0
- package/dist/seed.d.ts.map +1 -1
- package/dist/seed.js +43 -4
- package/dist/server-init.d.ts +8 -0
- package/dist/server-init.d.ts.map +1 -0
- package/dist/server-init.js +75 -0
- package/dist/server.d.ts +46 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1137 -649
- package/dist/setup.d.ts +28 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +50 -3
- package/dist/spaces/blob.d.ts +112 -0
- package/dist/spaces/blob.d.ts.map +1 -0
- package/dist/spaces/blob.js +460 -0
- package/dist/spaces/credential.d.ts +82 -0
- package/dist/spaces/credential.d.ts.map +1 -0
- package/dist/spaces/credential.js +220 -0
- package/dist/spaces/engine.d.ts +97 -0
- package/dist/spaces/engine.d.ts.map +1 -0
- package/dist/spaces/engine.js +563 -0
- package/dist/spaces/guard.d.ts +41 -0
- package/dist/spaces/guard.d.ts.map +1 -0
- package/dist/spaces/guard.js +79 -0
- package/dist/spaces/identity.d.ts +37 -0
- package/dist/spaces/identity.d.ts.map +1 -0
- package/dist/spaces/identity.js +93 -0
- package/dist/spaces/index.d.ts +56 -0
- package/dist/spaces/index.d.ts.map +1 -0
- package/dist/spaces/index.js +134 -0
- package/dist/spaces/notify.d.ts +60 -0
- package/dist/spaces/notify.d.ts.map +1 -0
- package/dist/spaces/notify.js +128 -0
- package/dist/spaces/records.d.ts +37 -0
- package/dist/spaces/records.d.ts.map +1 -0
- package/dist/spaces/records.js +63 -0
- package/dist/spaces/signature.d.ts +36 -0
- package/dist/spaces/signature.d.ts.map +1 -0
- package/dist/spaces/signature.js +83 -0
- package/dist/spaces/store.d.ts +51 -0
- package/dist/spaces/store.d.ts.map +1 -0
- package/dist/spaces/store.js +99 -0
- package/dist/spaces/uri.d.ts +60 -0
- package/dist/spaces/uri.d.ts.map +1 -0
- package/dist/spaces/uri.js +97 -0
- package/dist/spaces/verify.d.ts +46 -0
- package/dist/spaces/verify.d.ts.map +1 -0
- package/dist/spaces/verify.js +94 -0
- package/dist/spaces/viewer.d.ts +51 -0
- package/dist/spaces/viewer.d.ts.map +1 -0
- package/dist/spaces/viewer.js +167 -0
- package/dist/spaces/visibility.d.ts +57 -0
- package/dist/spaces/visibility.d.ts.map +1 -0
- package/dist/spaces/visibility.js +81 -0
- package/dist/templates/feed.tpl +18 -0
- package/dist/templates/hook.tpl +5 -0
- package/dist/templates/label.tpl +15 -0
- package/dist/templates/og.tpl +17 -0
- package/dist/templates/seed.tpl +11 -0
- package/dist/templates/setup.tpl +5 -0
- package/dist/templates/test-feed.tpl +19 -0
- package/dist/templates/test-xrpc.tpl +19 -0
- package/dist/templates/xrpc.tpl +53 -0
- package/dist/test.d.ts +3 -1
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +60 -36
- package/dist/views.js +1 -1
- package/dist/vite-plugin.d.ts +1 -1
- package/dist/vite-plugin.d.ts.map +1 -1
- package/dist/vite-plugin.js +254 -66
- package/dist/xrpc.d.ts +143 -12
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +282 -55
- package/package.json +29 -9
- package/public/admin-auth.js +271 -306
- package/public/admin.html +652 -61
- package/dist/db.d.ts +0 -135
- package/dist/db.d.ts.map +0 -1
- package/dist/db.js +0 -1365
- package/dist/fts.d.ts.map +0 -1
- package/dist/oauth/hooks.d.ts +0 -10
- package/dist/oauth/hooks.d.ts.map +0 -1
- package/dist/oauth/hooks.js +0 -40
- package/dist/schema.d.ts +0 -51
- package/dist/schema.d.ts.map +0 -1
- package/dist/test-browser.d.ts +0 -14
- package/dist/test-browser.d.ts.map +0 -1
- package/dist/test-browser.js +0 -26
package/public/admin.html
CHANGED
|
@@ -6,10 +6,20 @@
|
|
|
6
6
|
<title>Admin</title>
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<!-- Loaded off the critical path: media="print" keeps it from blocking the
|
|
10
|
+
first render, and the system-ui/Menlo fallbacks carry until it swaps in. -->
|
|
9
11
|
<link
|
|
10
12
|
href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Figtree:wght@400;500;600;700&display=swap"
|
|
11
13
|
rel="stylesheet"
|
|
14
|
+
media="print"
|
|
15
|
+
onload="this.media = 'all'"
|
|
12
16
|
/>
|
|
17
|
+
<noscript>
|
|
18
|
+
<link
|
|
19
|
+
href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Figtree:wght@400;500;600;700&display=swap"
|
|
20
|
+
rel="stylesheet"
|
|
21
|
+
/>
|
|
22
|
+
</noscript>
|
|
13
23
|
<style>
|
|
14
24
|
*,
|
|
15
25
|
*::before,
|
|
@@ -783,23 +793,6 @@
|
|
|
783
793
|
font-size: 1rem;
|
|
784
794
|
}
|
|
785
795
|
|
|
786
|
-
/* ── Schema ── */
|
|
787
|
-
.schema-pre {
|
|
788
|
-
font-family: var(--mono);
|
|
789
|
-
font-size: 0.8rem;
|
|
790
|
-
line-height: 1.6;
|
|
791
|
-
padding: 1rem;
|
|
792
|
-
margin: 0;
|
|
793
|
-
background: var(--bg-recessed);
|
|
794
|
-
border-radius: 0 0 6px 6px;
|
|
795
|
-
white-space: pre-wrap;
|
|
796
|
-
word-break: break-word;
|
|
797
|
-
color: var(--text);
|
|
798
|
-
overflow-x: auto;
|
|
799
|
-
}
|
|
800
|
-
.schema-section {
|
|
801
|
-
margin-bottom: 1.5rem;
|
|
802
|
-
}
|
|
803
796
|
.loading {
|
|
804
797
|
color: var(--text-3);
|
|
805
798
|
font-size: 0.9375rem;
|
|
@@ -942,6 +935,132 @@
|
|
|
942
935
|
border-radius: 10px;
|
|
943
936
|
background: var(--bg);
|
|
944
937
|
}
|
|
938
|
+
/* ── Review queue ── */
|
|
939
|
+
.review-card {
|
|
940
|
+
border-bottom: 1px solid var(--border);
|
|
941
|
+
padding: 0.9rem 1rem 0.9rem 0.75rem;
|
|
942
|
+
border-left: 3px solid transparent;
|
|
943
|
+
transition:
|
|
944
|
+
background 0.1s,
|
|
945
|
+
border-color 0.1s;
|
|
946
|
+
}
|
|
947
|
+
.review-card:last-child {
|
|
948
|
+
border-bottom: none;
|
|
949
|
+
}
|
|
950
|
+
.review-card.selected {
|
|
951
|
+
background: #faf8f6;
|
|
952
|
+
border-left-color: var(--accent);
|
|
953
|
+
}
|
|
954
|
+
.review-top {
|
|
955
|
+
display: flex;
|
|
956
|
+
align-items: flex-start;
|
|
957
|
+
justify-content: space-between;
|
|
958
|
+
gap: 0.75rem;
|
|
959
|
+
}
|
|
960
|
+
.review-handle {
|
|
961
|
+
font-weight: 650;
|
|
962
|
+
font-size: 0.95rem;
|
|
963
|
+
color: var(--text);
|
|
964
|
+
}
|
|
965
|
+
.review-score {
|
|
966
|
+
font-family: var(--mono);
|
|
967
|
+
font-size: 0.8rem;
|
|
968
|
+
font-weight: 700;
|
|
969
|
+
padding: 0.15rem 0.45rem;
|
|
970
|
+
border-radius: 6px;
|
|
971
|
+
flex-shrink: 0;
|
|
972
|
+
background: var(--bg);
|
|
973
|
+
color: var(--text-2);
|
|
974
|
+
}
|
|
975
|
+
.review-score.high {
|
|
976
|
+
background: #fdeaea;
|
|
977
|
+
color: #b4242a;
|
|
978
|
+
}
|
|
979
|
+
.review-score.mid {
|
|
980
|
+
background: #fdf3e3;
|
|
981
|
+
color: #9a6412;
|
|
982
|
+
}
|
|
983
|
+
.signal-row {
|
|
984
|
+
display: flex;
|
|
985
|
+
gap: 0.3rem;
|
|
986
|
+
flex-wrap: wrap;
|
|
987
|
+
margin-top: 0.45rem;
|
|
988
|
+
}
|
|
989
|
+
.signal-chip {
|
|
990
|
+
font-family: var(--mono);
|
|
991
|
+
font-size: 0.7rem;
|
|
992
|
+
padding: 0.1rem 0.4rem;
|
|
993
|
+
border-radius: 5px;
|
|
994
|
+
background: var(--bg);
|
|
995
|
+
color: var(--text-3);
|
|
996
|
+
border: 1px solid var(--border);
|
|
997
|
+
}
|
|
998
|
+
.signal-chip.crossed {
|
|
999
|
+
background: #fdeaea;
|
|
1000
|
+
color: #b4242a;
|
|
1001
|
+
border-color: #f3c9c9;
|
|
1002
|
+
}
|
|
1003
|
+
.review-state {
|
|
1004
|
+
font-size: 0.85rem;
|
|
1005
|
+
color: var(--text-2);
|
|
1006
|
+
margin-top: 0.45rem;
|
|
1007
|
+
white-space: pre-wrap;
|
|
1008
|
+
word-break: break-word;
|
|
1009
|
+
}
|
|
1010
|
+
.review-state .k {
|
|
1011
|
+
font-family: var(--mono);
|
|
1012
|
+
font-size: 0.72rem;
|
|
1013
|
+
color: var(--text-3);
|
|
1014
|
+
}
|
|
1015
|
+
.review-thumbs {
|
|
1016
|
+
display: flex;
|
|
1017
|
+
gap: 0.35rem;
|
|
1018
|
+
flex-wrap: wrap;
|
|
1019
|
+
margin-top: 0.55rem;
|
|
1020
|
+
}
|
|
1021
|
+
.review-thumbs img {
|
|
1022
|
+
width: 72px;
|
|
1023
|
+
height: 72px;
|
|
1024
|
+
object-fit: cover;
|
|
1025
|
+
border-radius: 8px;
|
|
1026
|
+
background: var(--bg);
|
|
1027
|
+
}
|
|
1028
|
+
.review-actions {
|
|
1029
|
+
display: flex;
|
|
1030
|
+
gap: 0.3rem;
|
|
1031
|
+
align-items: center;
|
|
1032
|
+
margin-top: 0.6rem;
|
|
1033
|
+
flex-wrap: wrap;
|
|
1034
|
+
}
|
|
1035
|
+
.scan-bar {
|
|
1036
|
+
display: flex;
|
|
1037
|
+
align-items: center;
|
|
1038
|
+
gap: 0.6rem;
|
|
1039
|
+
flex-wrap: wrap;
|
|
1040
|
+
padding: 0.6rem 1rem;
|
|
1041
|
+
border-bottom: 1px solid var(--border);
|
|
1042
|
+
font-size: 0.82rem;
|
|
1043
|
+
color: var(--text-2);
|
|
1044
|
+
}
|
|
1045
|
+
.kbd {
|
|
1046
|
+
font-family: var(--mono);
|
|
1047
|
+
font-size: 0.68rem;
|
|
1048
|
+
padding: 0.05rem 0.3rem;
|
|
1049
|
+
border: 1px solid var(--border);
|
|
1050
|
+
border-bottom-width: 2px;
|
|
1051
|
+
border-radius: 4px;
|
|
1052
|
+
color: var(--text-3);
|
|
1053
|
+
background: #fff;
|
|
1054
|
+
}
|
|
1055
|
+
.review-hint {
|
|
1056
|
+
font-size: 0.75rem;
|
|
1057
|
+
color: var(--text-3);
|
|
1058
|
+
padding: 0.5rem 1rem;
|
|
1059
|
+
display: flex;
|
|
1060
|
+
gap: 0.7rem;
|
|
1061
|
+
flex-wrap: wrap;
|
|
1062
|
+
align-items: center;
|
|
1063
|
+
}
|
|
945
1064
|
.blob-link {
|
|
946
1065
|
font-family: var(--mono);
|
|
947
1066
|
font-size: 0.72rem;
|
|
@@ -1220,7 +1339,8 @@
|
|
|
1220
1339
|
<button class="tab active" data-tab="overview">Overview</button>
|
|
1221
1340
|
<button class="tab" data-tab="repos">Repos</button>
|
|
1222
1341
|
<button class="tab" data-tab="content">Content</button>
|
|
1223
|
-
<button class="tab" data-tab="
|
|
1342
|
+
<button class="tab" data-tab="review">Review</button>
|
|
1343
|
+
<button class="tab" data-tab="reports">Reports</button>
|
|
1224
1344
|
</nav>
|
|
1225
1345
|
|
|
1226
1346
|
<!-- Overview -->
|
|
@@ -1277,11 +1397,6 @@
|
|
|
1277
1397
|
<div id="repos-results"><div class="loading">Loading</div></div>
|
|
1278
1398
|
</div>
|
|
1279
1399
|
|
|
1280
|
-
<!-- Schema -->
|
|
1281
|
-
<div class="tab-panel" id="panel-schema">
|
|
1282
|
-
<div id="schema-results"><div class="loading">Loading</div></div>
|
|
1283
|
-
</div>
|
|
1284
|
-
|
|
1285
1400
|
<!-- Content -->
|
|
1286
1401
|
<div class="tab-panel" id="panel-content">
|
|
1287
1402
|
<div class="search-bar">
|
|
@@ -1299,6 +1414,43 @@
|
|
|
1299
1414
|
<div class="loading">Loading</div>
|
|
1300
1415
|
</div>
|
|
1301
1416
|
</div>
|
|
1417
|
+
|
|
1418
|
+
<!-- Reports -->
|
|
1419
|
+
<div class="tab-panel" id="panel-review">
|
|
1420
|
+
<div class="search-bar">
|
|
1421
|
+
<select class="search-input" id="review-classifier" style="max-width: 200px">
|
|
1422
|
+
<option value="">All classifiers</option>
|
|
1423
|
+
</select>
|
|
1424
|
+
<select class="search-input" id="review-state" style="max-width: 180px">
|
|
1425
|
+
<option value="pending">Needs review</option>
|
|
1426
|
+
<option value="actioned">Actioned</option>
|
|
1427
|
+
<option value="all">All scored</option>
|
|
1428
|
+
</select>
|
|
1429
|
+
<select class="search-input" id="review-min" style="max-width: 160px">
|
|
1430
|
+
<option value="0.5">Score ≥ 0.50</option>
|
|
1431
|
+
<option value="0.6">Score ≥ 0.60</option>
|
|
1432
|
+
<option value="0.7">Score ≥ 0.70</option>
|
|
1433
|
+
<option value="0.8">Score ≥ 0.80</option>
|
|
1434
|
+
<option value="0">Any score</option>
|
|
1435
|
+
</select>
|
|
1436
|
+
<button class="btn btn-sm" id="review-scan-btn">Scan</button>
|
|
1437
|
+
</div>
|
|
1438
|
+
<div id="review-results"></div>
|
|
1439
|
+
</div>
|
|
1440
|
+
|
|
1441
|
+
<div class="tab-panel" id="panel-reports">
|
|
1442
|
+
<div class="search-bar">
|
|
1443
|
+
<select class="search-input" id="reports-status" style="max-width: 200px">
|
|
1444
|
+
<option value="open">Open</option>
|
|
1445
|
+
<option value="resolved">Resolved</option>
|
|
1446
|
+
<option value="dismissed">Dismissed</option>
|
|
1447
|
+
</select>
|
|
1448
|
+
<select class="search-input" id="reports-label-filter" style="max-width: 200px">
|
|
1449
|
+
<option value="">All labels</option>
|
|
1450
|
+
</select>
|
|
1451
|
+
</div>
|
|
1452
|
+
<div id="reports-results"></div>
|
|
1453
|
+
</div>
|
|
1302
1454
|
</div>
|
|
1303
1455
|
|
|
1304
1456
|
<!-- Bottom nav (mobile) -->
|
|
@@ -1307,7 +1459,8 @@
|
|
|
1307
1459
|
<button class="bnav-btn active" data-tab="overview">Overview</button>
|
|
1308
1460
|
<button class="bnav-btn" data-tab="repos">Repos</button>
|
|
1309
1461
|
<button class="bnav-btn" data-tab="content">Content</button>
|
|
1310
|
-
<button class="bnav-btn" data-tab="
|
|
1462
|
+
<button class="bnav-btn" data-tab="review">Review</button>
|
|
1463
|
+
<button class="bnav-btn" data-tab="reports">Reports</button>
|
|
1311
1464
|
</div>
|
|
1312
1465
|
</div>
|
|
1313
1466
|
</div>
|
|
@@ -1325,6 +1478,8 @@
|
|
|
1325
1478
|
|
|
1326
1479
|
window.auth = auth
|
|
1327
1480
|
let labelDefinitions = []
|
|
1481
|
+
// Started during boot so the overview's data is in flight while we auth
|
|
1482
|
+
let overviewPrefetch = null
|
|
1328
1483
|
|
|
1329
1484
|
function toast(msg, type = '') {
|
|
1330
1485
|
const el = document.createElement('div')
|
|
@@ -1404,8 +1559,17 @@
|
|
|
1404
1559
|
}
|
|
1405
1560
|
loginScreen.style.display = 'none'
|
|
1406
1561
|
|
|
1562
|
+
// Fire these together — awaiting them one at a time puts three round
|
|
1563
|
+
// trips in front of the first render.
|
|
1564
|
+
const whoamiPromise = api('/admin/whoami')
|
|
1565
|
+
const definitionsPromise = api('/admin/labels/definitions').catch(() => null)
|
|
1566
|
+
if (getURLState().tab === 'overview') {
|
|
1567
|
+
overviewPrefetch = api('/admin/info')
|
|
1568
|
+
overviewPrefetch.catch(() => {}) // loadOverview reports the failure
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1407
1571
|
try {
|
|
1408
|
-
const whoami = await
|
|
1572
|
+
const whoami = await whoamiPromise
|
|
1409
1573
|
document.getElementById('viewer-did').textContent = whoami.did
|
|
1410
1574
|
} catch (e) {
|
|
1411
1575
|
document.getElementById('error-msg').textContent =
|
|
@@ -1416,10 +1580,7 @@
|
|
|
1416
1580
|
return
|
|
1417
1581
|
}
|
|
1418
1582
|
|
|
1419
|
-
|
|
1420
|
-
const result = await api('/admin/labels/definitions')
|
|
1421
|
-
labelDefinitions = result.definitions || []
|
|
1422
|
-
} catch {}
|
|
1583
|
+
labelDefinitions = (await definitionsPromise)?.definitions || []
|
|
1423
1584
|
|
|
1424
1585
|
appEl.classList.add('visible')
|
|
1425
1586
|
restoreFromURL()
|
|
@@ -1490,8 +1651,9 @@
|
|
|
1490
1651
|
document.getElementById(`panel-${tab}`).classList.add('active')
|
|
1491
1652
|
if (tab === 'overview') loadOverview()
|
|
1492
1653
|
if (tab === 'repos') loadRepos()
|
|
1493
|
-
if (tab === 'schema') loadSchema()
|
|
1494
1654
|
if (tab === 'content') loadContent()
|
|
1655
|
+
if (tab === 'review') loadReview()
|
|
1656
|
+
if (tab === 'reports') loadReports()
|
|
1495
1657
|
if (push) pushURL({ tab, status: '', q: '', offset: 0, cq: '' })
|
|
1496
1658
|
}
|
|
1497
1659
|
|
|
@@ -1506,7 +1668,9 @@
|
|
|
1506
1668
|
|
|
1507
1669
|
async function loadOverview() {
|
|
1508
1670
|
try {
|
|
1509
|
-
const
|
|
1671
|
+
const pending = overviewPrefetch
|
|
1672
|
+
overviewPrefetch = null
|
|
1673
|
+
const info = await (pending || api('/admin/info'))
|
|
1510
1674
|
const statusCards = document.getElementById('status-cards')
|
|
1511
1675
|
const repoStatuses = info.repos || {}
|
|
1512
1676
|
const total = Object.values(repoStatuses).reduce((a, b) => a + b, 0)
|
|
@@ -1518,6 +1682,7 @@
|
|
|
1518
1682
|
<div class="stat-card"><div class="stat-label">Pending</div><div class="stat-value yellow">${fmt(repoStatuses.pending)}</div></div>
|
|
1519
1683
|
<div class="stat-card"><div class="stat-label">Failed</div><div class="stat-value red">${fmt(repoStatuses.failed)}</div></div>
|
|
1520
1684
|
<div class="stat-card"><div class="stat-label">Taken Down</div><div class="stat-value red">${fmt(repoStatuses.takendown)}</div></div>
|
|
1685
|
+
${info.openReports > 0 ? `<div class="stat-card" style="cursor:pointer" onclick="activateTab('reports')"><div class="stat-label">Open Reports</div><div class="stat-value yellow">${fmt(info.openReports)}</div></div>` : ''}
|
|
1521
1686
|
`
|
|
1522
1687
|
|
|
1523
1688
|
const collectionCards = document.getElementById('collection-cards')
|
|
@@ -1599,36 +1764,6 @@
|
|
|
1599
1764
|
}
|
|
1600
1765
|
})
|
|
1601
1766
|
|
|
1602
|
-
// ── Schema ──
|
|
1603
|
-
|
|
1604
|
-
async function loadSchema() {
|
|
1605
|
-
const container = document.getElementById('schema-results')
|
|
1606
|
-
try {
|
|
1607
|
-
const data = await api('/admin/schema')
|
|
1608
|
-
let html = ''
|
|
1609
|
-
|
|
1610
|
-
// Lexicons section
|
|
1611
|
-
if (data.lexicons && data.lexicons.length) {
|
|
1612
|
-
html += '<div class="schema-section"><div class="section-label">Lexicons</div>'
|
|
1613
|
-
for (const lex of data.lexicons) {
|
|
1614
|
-
html += `<div class="card" style="margin-bottom:0.5rem;"><div style="font-family:var(--mono);font-size:0.8rem;font-weight:600;padding:0.5rem 0.75rem;border-bottom:1px solid var(--border);">${escapeHtml(lex.nsid)}</div><pre class="schema-pre">${escapeHtml(JSON.stringify(lex.lexicon, null, 2))}</pre></div>`
|
|
1615
|
-
}
|
|
1616
|
-
html += '</div>'
|
|
1617
|
-
}
|
|
1618
|
-
|
|
1619
|
-
// DDL section
|
|
1620
|
-
if (data.ddl) {
|
|
1621
|
-
html += '<div class="schema-section"><div class="section-label">Tables (DuckDB DDL)</div>'
|
|
1622
|
-
html += `<div class="card"><pre class="schema-pre">${escapeHtml(data.ddl)}</pre></div>`
|
|
1623
|
-
html += '</div>'
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
container.innerHTML = html || '<div class="empty-state">No schema found</div>'
|
|
1627
|
-
} catch (e) {
|
|
1628
|
-
container.innerHTML = `<div class="empty-state">${escapeHtml(e.message)}</div>`
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
1767
|
// ── Repos ──
|
|
1633
1768
|
|
|
1634
1769
|
let reposLoaded = false
|
|
@@ -2161,6 +2296,462 @@
|
|
|
2161
2296
|
})
|
|
2162
2297
|
})
|
|
2163
2298
|
}
|
|
2299
|
+
|
|
2300
|
+
// ── Reports ──
|
|
2301
|
+
|
|
2302
|
+
const reportsPage = { limit: 50, offset: 0 }
|
|
2303
|
+
|
|
2304
|
+
function populateReportsLabelFilter() {
|
|
2305
|
+
const select = document.getElementById('reports-label-filter')
|
|
2306
|
+
const current = select.value
|
|
2307
|
+
select.innerHTML =
|
|
2308
|
+
'<option value="">All labels</option>' +
|
|
2309
|
+
labelDefinitions.map((d) => `<option value="${d.identifier}">${d.identifier}</option>`).join('')
|
|
2310
|
+
select.value = current
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
document.getElementById('reports-status').addEventListener('change', () => {
|
|
2314
|
+
reportsPage.offset = 0
|
|
2315
|
+
loadReports()
|
|
2316
|
+
})
|
|
2317
|
+
document.getElementById('reports-label-filter').addEventListener('change', () => {
|
|
2318
|
+
reportsPage.offset = 0
|
|
2319
|
+
loadReports()
|
|
2320
|
+
})
|
|
2321
|
+
|
|
2322
|
+
// ── Review queue ──
|
|
2323
|
+
//
|
|
2324
|
+
// The queue exists so nobody has to look at every account. Each card
|
|
2325
|
+
// carries the state the model was shown and the score it returned, so a
|
|
2326
|
+
// decision can be made from the list without opening anything.
|
|
2327
|
+
|
|
2328
|
+
const review = { items: [], index: 0, total: 0, offset: 0, limit: 25, thumbs: new Map() }
|
|
2329
|
+
let reviewClassifiersLoaded = false
|
|
2330
|
+
let scanPoll = null
|
|
2331
|
+
|
|
2332
|
+
function scoreClass(v) {
|
|
2333
|
+
return v >= 0.8 ? 'high' : v >= 0.6 ? 'mid' : ''
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
async function loadReview() {
|
|
2337
|
+
const container = document.getElementById('review-results')
|
|
2338
|
+
if (!reviewClassifiersLoaded) {
|
|
2339
|
+
try {
|
|
2340
|
+
const stats = await api('/admin/review/stats')
|
|
2341
|
+
const sel = document.getElementById('review-classifier')
|
|
2342
|
+
for (const name of stats.classifiers || []) {
|
|
2343
|
+
const o = document.createElement('option')
|
|
2344
|
+
o.value = o.textContent = name
|
|
2345
|
+
sel.appendChild(o)
|
|
2346
|
+
}
|
|
2347
|
+
reviewClassifiersLoaded = true
|
|
2348
|
+
} catch (e) {
|
|
2349
|
+
container.innerHTML = `<div class="empty-state">${escapeHtml(e.message)}</div>`
|
|
2350
|
+
return
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
const classifier = document.getElementById('review-classifier').value
|
|
2354
|
+
const state = document.getElementById('review-state').value
|
|
2355
|
+
const minScore = document.getElementById('review-min').value
|
|
2356
|
+
container.innerHTML = '<div class="loading">Loading</div>'
|
|
2357
|
+
try {
|
|
2358
|
+
let url = `/admin/review?state=${state}&minScore=${minScore}&limit=${review.limit}&offset=${review.offset}`
|
|
2359
|
+
if (classifier) url += `&classifier=${encodeURIComponent(classifier)}`
|
|
2360
|
+
const result = await api(url)
|
|
2361
|
+
review.items = result.rows || []
|
|
2362
|
+
review.total = result.total
|
|
2363
|
+
review.index = 0
|
|
2364
|
+
renderReview()
|
|
2365
|
+
} catch (e) {
|
|
2366
|
+
container.innerHTML = `<div class="empty-state">${escapeHtml(e.message)}</div>`
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
function renderScanBar(stats) {
|
|
2371
|
+
const el = document.getElementById('review-scan-status')
|
|
2372
|
+
if (!el) return
|
|
2373
|
+
if (!stats.configured) {
|
|
2374
|
+
el.textContent = 'Clef not configured — set `clef` in hatk.config.ts'
|
|
2375
|
+
return
|
|
2376
|
+
}
|
|
2377
|
+
const scan = stats.scan
|
|
2378
|
+
if (scan?.running) {
|
|
2379
|
+
el.textContent = `Scanning… ${scan.scored} scored, ${scan.skipped} skipped, ${scan.filed} filed`
|
|
2380
|
+
} else if (scan) {
|
|
2381
|
+
const cost = ((scan.inputTokens / 1e6) * 0.042).toFixed(3)
|
|
2382
|
+
el.textContent = `Last scan: ${scan.scored} scored, ${scan.skipped} unchanged, ${scan.filed} filed, $${cost}`
|
|
2383
|
+
} else {
|
|
2384
|
+
el.textContent = `${stats.total} scored · ${stats.pending} awaiting review`
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
// Render the model's state as short "key: value" lines. Long values are
|
|
2389
|
+
// clipped rather than dropped: the reason an account looks wrong is often
|
|
2390
|
+
// the first few words of its bio.
|
|
2391
|
+
function renderState(state) {
|
|
2392
|
+
if (!state || typeof state !== 'object') return ''
|
|
2393
|
+
const parts = []
|
|
2394
|
+
for (const [k, v] of Object.entries(state)) {
|
|
2395
|
+
if (v == null || v === '') continue
|
|
2396
|
+
let text
|
|
2397
|
+
if (typeof v === 'string') text = v
|
|
2398
|
+
else if (Array.isArray(v)) {
|
|
2399
|
+
text = v
|
|
2400
|
+
.map((x) => (typeof x === 'string' ? x : x?.title || x?.text || JSON.stringify(x)))
|
|
2401
|
+
.filter(Boolean)
|
|
2402
|
+
.slice(0, 3)
|
|
2403
|
+
.join(' · ')
|
|
2404
|
+
} else if (typeof v === 'object') {
|
|
2405
|
+
text = Object.entries(v)
|
|
2406
|
+
.filter(([, n]) => n)
|
|
2407
|
+
.map(([n, val]) => `${n} ${val}`)
|
|
2408
|
+
.join(', ')
|
|
2409
|
+
} else text = String(v)
|
|
2410
|
+
if (!text) continue
|
|
2411
|
+
if (text.length > 180) text = text.slice(0, 180) + '…'
|
|
2412
|
+
parts.push(`<span class="k">${escapeHtml(k)}</span> ${escapeHtml(text)}`)
|
|
2413
|
+
}
|
|
2414
|
+
return parts.join('<br>')
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
function renderReview() {
|
|
2418
|
+
const container = document.getElementById('review-results')
|
|
2419
|
+
const labelOptions = labelDefinitions
|
|
2420
|
+
.map((d) => `<option value="${d.identifier}">${d.identifier}</option>`)
|
|
2421
|
+
.join('')
|
|
2422
|
+
|
|
2423
|
+
if (!review.items.length) {
|
|
2424
|
+
container.innerHTML = `
|
|
2425
|
+
<div class="scan-bar"><span id="review-scan-status"></span></div>
|
|
2426
|
+
<div class="empty-state">Nothing awaiting review</div>`
|
|
2427
|
+
api('/admin/review/stats')
|
|
2428
|
+
.then(renderScanBar)
|
|
2429
|
+
.catch(() => {})
|
|
2430
|
+
return
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
const showPagination = review.total > review.limit
|
|
2434
|
+
container.innerHTML = `
|
|
2435
|
+
<div class="card">
|
|
2436
|
+
<div class="scan-bar"><span id="review-scan-status"></span></div>
|
|
2437
|
+
<div class="review-hint">
|
|
2438
|
+
<span><span class="kbd">j</span><span class="kbd">k</span> move</span>
|
|
2439
|
+
<span><span class="kbd">t</span> takedown</span>
|
|
2440
|
+
<span><span class="kbd">l</span> label</span>
|
|
2441
|
+
<span><span class="kbd">d</span> dismiss</span>
|
|
2442
|
+
</div>
|
|
2443
|
+
${review.items
|
|
2444
|
+
.map((r, i) => {
|
|
2445
|
+
const signals = Object.entries(r.signals || {}).sort((a, b) => b[1].score - a[1].score)
|
|
2446
|
+
const top = Number(r.top_score || 0)
|
|
2447
|
+
const handle = r.handle ? '@' + escapeHtml(r.handle) : escapeHtml(r.subject_did)
|
|
2448
|
+
const takendown = r.repo_status === 'takendown'
|
|
2449
|
+
return `<div class="review-card${i === review.index ? ' selected' : ''}" data-review-index="${i}">
|
|
2450
|
+
<div class="review-top">
|
|
2451
|
+
<div style="min-width:0">
|
|
2452
|
+
<div class="review-handle">${handle}${takendown ? ' <span class="label-tag">taken down</span>' : ''}</div>
|
|
2453
|
+
<div class="record-uri" title="${escapeHtml(r.subject_uri)}">${escapeHtml(r.subject_uri)}</div>
|
|
2454
|
+
</div>
|
|
2455
|
+
<div class="review-score ${scoreClass(top)}">${top.toFixed(2)}</div>
|
|
2456
|
+
</div>
|
|
2457
|
+
<div class="signal-row">
|
|
2458
|
+
${signals
|
|
2459
|
+
.map(
|
|
2460
|
+
([name, sig]) =>
|
|
2461
|
+
`<span class="signal-chip${sig.score >= 0.5 ? ' crossed' : ''}" title="${escapeHtml(sig.classifier || '')}">${escapeHtml(name)} ${sig.score.toFixed(2)}${
|
|
2462
|
+
sig.choice ? ' · ' + escapeHtml(String(sig.choice)) : ''
|
|
2463
|
+
}</span>`,
|
|
2464
|
+
)
|
|
2465
|
+
.join('')}
|
|
2466
|
+
</div>
|
|
2467
|
+
<div class="review-state">${renderState(r.state)}</div>
|
|
2468
|
+
<div class="review-thumbs" data-thumbs-for="${escapeHtml(r.subject_did)}"></div>
|
|
2469
|
+
<div class="review-actions">
|
|
2470
|
+
<button class="btn btn-sm" data-review-act="takedown" data-i="${i}" style="background:#b4242a;color:white">Takedown</button>
|
|
2471
|
+
<select class="search-input" data-review-label="${i}" style="max-width:150px;font-size:0.8rem;padding:0.25rem 0.5rem">
|
|
2472
|
+
${labelOptions}
|
|
2473
|
+
</select>
|
|
2474
|
+
<button class="btn btn-sm" data-review-act="label" data-i="${i}">Apply label</button>
|
|
2475
|
+
<button class="btn btn-sm" data-review-act="dismiss" data-i="${i}">Dismiss</button>
|
|
2476
|
+
<a class="btn btn-sm" href="/admin?tab=repos&q=${encodeURIComponent(r.subject_did)}">Open in Repos</a>
|
|
2477
|
+
</div>
|
|
2478
|
+
</div>`
|
|
2479
|
+
})
|
|
2480
|
+
.join('')}
|
|
2481
|
+
${
|
|
2482
|
+
showPagination
|
|
2483
|
+
? `<div class="pagination">
|
|
2484
|
+
<span>${review.offset + 1}–${Math.min(review.offset + review.limit, review.total)} of ${review.total.toLocaleString()}</span>
|
|
2485
|
+
<div class="pagination-buttons">
|
|
2486
|
+
<button class="btn btn-sm" data-review-page="prev" ${review.offset === 0 ? 'disabled' : ''}>Prev</button>
|
|
2487
|
+
<button class="btn btn-sm" data-review-page="next" ${review.offset + review.limit >= review.total ? 'disabled' : ''}>Next</button>
|
|
2488
|
+
</div>
|
|
2489
|
+
</div>`
|
|
2490
|
+
: ''
|
|
2491
|
+
}
|
|
2492
|
+
</div>`
|
|
2493
|
+
|
|
2494
|
+
api('/admin/review/stats')
|
|
2495
|
+
.then(renderScanBar)
|
|
2496
|
+
.catch(() => {})
|
|
2497
|
+
wireReview()
|
|
2498
|
+
loadReviewThumbs()
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
// Thumbnails come from a second request per subject, so they are fetched
|
|
2502
|
+
// after the cards are on screen and cached — the queue stays useful
|
|
2503
|
+
// whether or not they arrive.
|
|
2504
|
+
async function loadReviewThumbs() {
|
|
2505
|
+
for (const r of review.items) {
|
|
2506
|
+
const holder = document.querySelector(`[data-thumbs-for="${CSS.escape(r.subject_did)}"]`)
|
|
2507
|
+
if (!holder || holder.dataset.done) continue
|
|
2508
|
+
holder.dataset.done = '1'
|
|
2509
|
+
try {
|
|
2510
|
+
let ctx = review.thumbs.get(r.subject_did)
|
|
2511
|
+
if (!ctx) {
|
|
2512
|
+
ctx = await api(`/admin/review/subject/${encodeURIComponent(r.subject_did)}`)
|
|
2513
|
+
review.thumbs.set(r.subject_did, ctx)
|
|
2514
|
+
}
|
|
2515
|
+
const blobs = []
|
|
2516
|
+
for (const records of Object.values(ctx.records || {})) {
|
|
2517
|
+
for (const rec of records) {
|
|
2518
|
+
for (const b of findBlobs(rec.value || rec)) {
|
|
2519
|
+
if (b.cid && (b.mimeType || '').startsWith('image/')) blobs.push(b)
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
holder.innerHTML = blobs
|
|
2524
|
+
.slice(0, 6)
|
|
2525
|
+
.map((b) => `<img src="${blobUrl(r.subject_did, b.cid)}" loading="lazy" alt="">`)
|
|
2526
|
+
.join('')
|
|
2527
|
+
} catch {
|
|
2528
|
+
/* context is a nicety; the score and state already carry the decision */
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
function selectReview(i) {
|
|
2534
|
+
if (!review.items.length) return
|
|
2535
|
+
review.index = Math.max(0, Math.min(review.items.length - 1, i))
|
|
2536
|
+
document.querySelectorAll('.review-card').forEach((el, n) => {
|
|
2537
|
+
el.classList.toggle('selected', n === review.index)
|
|
2538
|
+
if (n === review.index) el.scrollIntoView({ block: 'nearest' })
|
|
2539
|
+
})
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
async function actOnReview(i, action) {
|
|
2543
|
+
const item = review.items[i]
|
|
2544
|
+
if (!item) return
|
|
2545
|
+
const body = { did: item.subject_did, uri: item.subject_uri, action }
|
|
2546
|
+
if (action === 'label') {
|
|
2547
|
+
const sel = document.querySelector(`[data-review-label="${i}"]`)
|
|
2548
|
+
body.label = sel?.value
|
|
2549
|
+
if (!body.label) return toast('Pick a label first', 'error')
|
|
2550
|
+
}
|
|
2551
|
+
try {
|
|
2552
|
+
await api('/admin/review/act', { method: 'POST', body: JSON.stringify(body) })
|
|
2553
|
+
toast(action === 'dismiss' ? 'Dismissed' : action === 'takedown' ? 'Taken down' : 'Label applied')
|
|
2554
|
+
review.items.splice(i, 1)
|
|
2555
|
+
review.total = Math.max(0, review.total - 1)
|
|
2556
|
+
if (review.index >= review.items.length) review.index = Math.max(0, review.items.length - 1)
|
|
2557
|
+
renderReview()
|
|
2558
|
+
} catch (e) {
|
|
2559
|
+
toast(e.message, 'error')
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
function wireReview() {
|
|
2564
|
+
document.querySelectorAll('[data-review-index]').forEach((el) => {
|
|
2565
|
+
el.addEventListener('click', (e) => {
|
|
2566
|
+
if (e.target.closest('button,select,a')) return
|
|
2567
|
+
selectReview(Number(el.dataset.reviewIndex))
|
|
2568
|
+
})
|
|
2569
|
+
})
|
|
2570
|
+
document.querySelectorAll('[data-review-act]').forEach((b) => {
|
|
2571
|
+
b.addEventListener('click', () => actOnReview(Number(b.dataset.i), b.dataset.reviewAct))
|
|
2572
|
+
})
|
|
2573
|
+
document.querySelectorAll('[data-review-page]').forEach((b) => {
|
|
2574
|
+
b.addEventListener('click', () => {
|
|
2575
|
+
review.offset += b.dataset.reviewPage === 'next' ? review.limit : -review.limit
|
|
2576
|
+
review.offset = Math.max(0, review.offset)
|
|
2577
|
+
loadReview()
|
|
2578
|
+
})
|
|
2579
|
+
})
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
document.getElementById('review-classifier').addEventListener('change', () => {
|
|
2583
|
+
review.offset = 0
|
|
2584
|
+
loadReview()
|
|
2585
|
+
})
|
|
2586
|
+
document.getElementById('review-state').addEventListener('change', () => {
|
|
2587
|
+
review.offset = 0
|
|
2588
|
+
loadReview()
|
|
2589
|
+
})
|
|
2590
|
+
document.getElementById('review-min').addEventListener('change', () => {
|
|
2591
|
+
review.offset = 0
|
|
2592
|
+
loadReview()
|
|
2593
|
+
})
|
|
2594
|
+
|
|
2595
|
+
document.getElementById('review-scan-btn').addEventListener('click', async () => {
|
|
2596
|
+
const classifier = document.getElementById('review-classifier').value
|
|
2597
|
+
try {
|
|
2598
|
+
await api('/admin/review/scan', {
|
|
2599
|
+
method: 'POST',
|
|
2600
|
+
body: JSON.stringify(classifier ? { classifier } : {}),
|
|
2601
|
+
})
|
|
2602
|
+
toast('Scan started')
|
|
2603
|
+
if (scanPoll) clearInterval(scanPoll)
|
|
2604
|
+
scanPoll = setInterval(async () => {
|
|
2605
|
+
try {
|
|
2606
|
+
const stats = await api('/admin/review/stats')
|
|
2607
|
+
renderScanBar(stats)
|
|
2608
|
+
if (!stats.scan?.running) {
|
|
2609
|
+
clearInterval(scanPoll)
|
|
2610
|
+
scanPoll = null
|
|
2611
|
+
loadReview()
|
|
2612
|
+
}
|
|
2613
|
+
} catch {
|
|
2614
|
+
clearInterval(scanPoll)
|
|
2615
|
+
scanPoll = null
|
|
2616
|
+
}
|
|
2617
|
+
}, 2000)
|
|
2618
|
+
} catch (e) {
|
|
2619
|
+
toast(e.message, 'error')
|
|
2620
|
+
}
|
|
2621
|
+
})
|
|
2622
|
+
|
|
2623
|
+
document.addEventListener('keydown', (e) => {
|
|
2624
|
+
const panel = document.getElementById('panel-review')
|
|
2625
|
+
if (!panel?.classList.contains('active')) return
|
|
2626
|
+
if (e.target.matches('input,select,textarea')) return
|
|
2627
|
+
const key = e.key.toLowerCase()
|
|
2628
|
+
if (key === 'j' || e.key === 'ArrowDown') {
|
|
2629
|
+
e.preventDefault()
|
|
2630
|
+
selectReview(review.index + 1)
|
|
2631
|
+
} else if (key === 'k' || e.key === 'ArrowUp') {
|
|
2632
|
+
e.preventDefault()
|
|
2633
|
+
selectReview(review.index - 1)
|
|
2634
|
+
} else if (key === 't') {
|
|
2635
|
+
e.preventDefault()
|
|
2636
|
+
actOnReview(review.index, 'takedown')
|
|
2637
|
+
} else if (key === 'l') {
|
|
2638
|
+
e.preventDefault()
|
|
2639
|
+
actOnReview(review.index, 'label')
|
|
2640
|
+
} else if (key === 'd' || key === 'x') {
|
|
2641
|
+
e.preventDefault()
|
|
2642
|
+
actOnReview(review.index, 'dismiss')
|
|
2643
|
+
}
|
|
2644
|
+
})
|
|
2645
|
+
|
|
2646
|
+
async function loadReports() {
|
|
2647
|
+
populateReportsLabelFilter()
|
|
2648
|
+
const status = document.getElementById('reports-status').value
|
|
2649
|
+
const label = document.getElementById('reports-label-filter').value
|
|
2650
|
+
const container = document.getElementById('reports-results')
|
|
2651
|
+
container.innerHTML = '<div class="loading">Loading</div>'
|
|
2652
|
+
try {
|
|
2653
|
+
let url = `/admin/reports?status=${status}&limit=${reportsPage.limit}&offset=${reportsPage.offset}`
|
|
2654
|
+
if (label) url += `&label=${encodeURIComponent(label)}`
|
|
2655
|
+
const result = await api(url)
|
|
2656
|
+
renderReports(result.reports || [], result.total)
|
|
2657
|
+
} catch (e) {
|
|
2658
|
+
container.innerHTML = `<div class="empty-state">${escapeHtml(e.message)}</div>`
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
function renderReports(reports, total) {
|
|
2663
|
+
const container = document.getElementById('reports-results')
|
|
2664
|
+
if (!reports.length) {
|
|
2665
|
+
container.innerHTML = '<div class="empty-state">No reports found</div>'
|
|
2666
|
+
return
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
const showPagination = total != null && total > reportsPage.limit
|
|
2670
|
+
const paginationHtml = showPagination
|
|
2671
|
+
? `
|
|
2672
|
+
<div class="pagination">
|
|
2673
|
+
<span>${reportsPage.offset + 1}\u2013${Math.min(reportsPage.offset + reportsPage.limit, total)} of ${total.toLocaleString()}</span>
|
|
2674
|
+
<div class="pagination-buttons">
|
|
2675
|
+
<button class="btn btn-sm" data-reports-page="prev" ${reportsPage.offset === 0 ? 'disabled' : ''}>Prev</button>
|
|
2676
|
+
<button class="btn btn-sm" data-reports-page="next" ${reportsPage.offset + reportsPage.limit >= total ? 'disabled' : ''}>Next</button>
|
|
2677
|
+
</div>
|
|
2678
|
+
</div>
|
|
2679
|
+
`
|
|
2680
|
+
: ''
|
|
2681
|
+
|
|
2682
|
+
const countLabel =
|
|
2683
|
+
total != null
|
|
2684
|
+
? `${total.toLocaleString()} report${total !== 1 ? 's' : ''}`
|
|
2685
|
+
: `${reports.length} result${reports.length !== 1 ? 's' : ''}`
|
|
2686
|
+
|
|
2687
|
+
const isOpen = document.getElementById('reports-status').value === 'open'
|
|
2688
|
+
|
|
2689
|
+
container.innerHTML = `
|
|
2690
|
+
<div class="card">
|
|
2691
|
+
<div class="result-count">${countLabel}</div>
|
|
2692
|
+
${reports
|
|
2693
|
+
.map((r) => {
|
|
2694
|
+
const reporterDisplay = r.reported_by_handle
|
|
2695
|
+
? `@${escapeHtml(r.reported_by_handle)}`
|
|
2696
|
+
: escapeHtml(r.reported_by)
|
|
2697
|
+
const date = new Date(r.created_at).toLocaleString()
|
|
2698
|
+
return `<div class="record-card">
|
|
2699
|
+
<div class="record-header">
|
|
2700
|
+
<div class="record-meta">
|
|
2701
|
+
<div class="record-uri" title="${escapeHtml(r.subject_uri)}">${escapeHtml(r.subject_uri)}</div>
|
|
2702
|
+
<div class="record-summary">
|
|
2703
|
+
<span class="label-tag">${escapeHtml(r.label)}</span>
|
|
2704
|
+
reported by ${reporterDisplay} · ${date}
|
|
2705
|
+
</div>
|
|
2706
|
+
${r.reason ? `<div class="record-summary" style="margin-top:0.25rem">${escapeHtml(r.reason)}</div>` : ''}
|
|
2707
|
+
${!isOpen ? `<div class="record-summary" style="margin-top:0.25rem;opacity:0.6">${escapeHtml(r.status)}${r.resolved_by ? ` by ${escapeHtml(r.resolved_by)}` : ''}</div>` : ''}
|
|
2708
|
+
</div>
|
|
2709
|
+
${
|
|
2710
|
+
isOpen
|
|
2711
|
+
? `<div class="record-actions">
|
|
2712
|
+
<button class="btn btn-sm" data-action="resolve-report" data-id="${r.id}" data-resolve="resolve" style="background:var(--accent);color:white">Apply Label</button>
|
|
2713
|
+
<button class="btn btn-sm" data-action="resolve-report" data-id="${r.id}" data-resolve="dismiss">Dismiss</button>
|
|
2714
|
+
</div>`
|
|
2715
|
+
: ''
|
|
2716
|
+
}
|
|
2717
|
+
</div>
|
|
2718
|
+
</div>`
|
|
2719
|
+
})
|
|
2720
|
+
.join('')}
|
|
2721
|
+
${paginationHtml}
|
|
2722
|
+
</div>
|
|
2723
|
+
`
|
|
2724
|
+
|
|
2725
|
+
container.querySelectorAll('[data-reports-page="prev"]').forEach((b) => {
|
|
2726
|
+
b.addEventListener('click', () => {
|
|
2727
|
+
reportsPage.offset = Math.max(0, reportsPage.offset - reportsPage.limit)
|
|
2728
|
+
loadReports()
|
|
2729
|
+
})
|
|
2730
|
+
})
|
|
2731
|
+
container.querySelectorAll('[data-reports-page="next"]').forEach((b) => {
|
|
2732
|
+
b.addEventListener('click', () => {
|
|
2733
|
+
reportsPage.offset += reportsPage.limit
|
|
2734
|
+
loadReports()
|
|
2735
|
+
})
|
|
2736
|
+
})
|
|
2737
|
+
|
|
2738
|
+
container.querySelectorAll('[data-action="resolve-report"]').forEach((btn) => {
|
|
2739
|
+
btn.addEventListener('click', async () => {
|
|
2740
|
+
const action = btn.dataset.resolve
|
|
2741
|
+
try {
|
|
2742
|
+
await api('/admin/reports/resolve', {
|
|
2743
|
+
method: 'POST',
|
|
2744
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2745
|
+
body: JSON.stringify({ id: parseInt(btn.dataset.id), action }),
|
|
2746
|
+
})
|
|
2747
|
+
toast(action === 'resolve' ? 'Label applied & report resolved' : 'Report dismissed', 'success')
|
|
2748
|
+
loadReports()
|
|
2749
|
+
} catch (e) {
|
|
2750
|
+
toast(e.message, 'error')
|
|
2751
|
+
}
|
|
2752
|
+
})
|
|
2753
|
+
})
|
|
2754
|
+
}
|
|
2164
2755
|
</script>
|
|
2165
2756
|
</body>
|
|
2166
2757
|
</html>
|