+27
-18
backend/src/dashboard.zig
+27
-18
backend/src/dashboard.zig
···
7
7
const TagJson = struct { tag: []const u8, count: i64 };
8
8
const TimelineJson = struct { date: []const u8, count: i64 };
9
9
const PubJson = struct { name: []const u8, basePath: []const u8, count: i64 };
10
+
const PlatformJson = struct { platform: []const u8, count: i64 };
10
11
11
12
/// All data needed to render the dashboard
12
13
pub const Data = struct {
13
14
started_at: i64,
14
15
searches: i64,
15
16
publications: i64,
16
-
articles: i64,
17
-
looseleafs: i64,
17
+
documents: i64,
18
18
tags_json: []const u8,
19
19
timeline_json: []const u8,
20
20
top_pubs_json: []const u8,
21
+
platforms_json: []const u8,
21
22
};
22
23
23
24
// all dashboard queries batched into one request
···
30
31
\\ (SELECT service_started_at FROM stats WHERE id = 1) as started_at
31
32
;
32
33
33
-
const DOC_TYPES_SQL =
34
-
\\SELECT
35
-
\\ SUM(CASE WHEN publication_uri != '' THEN 1 ELSE 0 END) as articles,
36
-
\\ SUM(CASE WHEN publication_uri = '' OR publication_uri IS NULL THEN 1 ELSE 0 END) as looseleafs
34
+
const PLATFORMS_SQL =
35
+
\\SELECT platform, COUNT(*) as count
37
36
\\FROM documents
37
+
\\GROUP BY platform
38
+
\\ORDER BY count DESC
38
39
;
39
40
40
41
const TAGS_SQL =
···
69
70
// batch all 5 queries into one HTTP request
70
71
var batch = client.queryBatch(&.{
71
72
.{ .sql = STATS_SQL },
72
-
.{ .sql = DOC_TYPES_SQL },
73
+
.{ .sql = PLATFORMS_SQL },
73
74
.{ .sql = TAGS_SQL },
74
75
.{ .sql = TIMELINE_SQL },
75
76
.{ .sql = TOP_PUBS_SQL },
···
81
82
const started_at = if (stats_row) |r| r.int(4) else 0;
82
83
const searches = if (stats_row) |r| r.int(2) else 0;
83
84
const publications = if (stats_row) |r| r.int(1) else 0;
84
-
85
-
// extract doc types (query 1)
86
-
const doc_row = batch.getFirst(1);
87
-
const articles = if (doc_row) |r| r.int(0) else 0;
88
-
const looseleafs = if (doc_row) |r| r.int(1) else 0;
85
+
const documents = if (stats_row) |r| r.int(0) else 0;
89
86
90
87
return .{
91
88
.started_at = started_at,
92
89
.searches = searches,
93
90
.publications = publications,
94
-
.articles = articles,
95
-
.looseleafs = looseleafs,
91
+
.documents = documents,
96
92
.tags_json = try formatTagsJson(alloc, batch.get(2)),
97
93
.timeline_json = try formatTimelineJson(alloc, batch.get(3)),
98
94
.top_pubs_json = try formatPubsJson(alloc, batch.get(4)),
95
+
.platforms_json = try formatPlatformsJson(alloc, batch.get(1)),
99
96
};
100
97
}
101
98
···
129
126
return try output.toOwnedSlice();
130
127
}
131
128
129
+
fn formatPlatformsJson(alloc: Allocator, rows: []const db.Row) ![]const u8 {
130
+
var output: std.Io.Writer.Allocating = .init(alloc);
131
+
errdefer output.deinit();
132
+
var jw: json.Stringify = .{ .writer = &output.writer };
133
+
try jw.beginArray();
134
+
for (rows) |row| try jw.write(PlatformJson{ .platform = row.text(0), .count = row.int(1) });
135
+
try jw.endArray();
136
+
return try output.toOwnedSlice();
137
+
}
138
+
132
139
/// Generate dashboard data as JSON for API endpoint
133
140
pub fn toJson(alloc: Allocator, data: Data) ![]const u8 {
134
141
var output: std.Io.Writer.Allocating = .init(alloc);
···
146
153
try jw.objectField("publications");
147
154
try jw.write(data.publications);
148
155
149
-
try jw.objectField("articles");
150
-
try jw.write(data.articles);
156
+
try jw.objectField("documents");
157
+
try jw.write(data.documents);
151
158
152
-
try jw.objectField("looseleafs");
153
-
try jw.write(data.looseleafs);
159
+
try jw.objectField("platforms");
160
+
try jw.beginWriteRaw();
161
+
try jw.writer.writeAll(data.platforms_json);
162
+
jw.endWriteRaw();
154
163
155
164
// use beginWriteRaw/endWriteRaw for pre-formatted JSON arrays
156
165
try jw.objectField("tags");
+2
-9
site/dashboard.html
+2
-9
site/dashboard.html
···
30
30
</section>
31
31
32
32
<section>
33
-
<div class="section-title">documents</div>
33
+
<div class="section-title">documents by platform</div>
34
34
<div class="chart-box">
35
-
<div class="doc-row">
36
-
<span class="doc-type">articles</span>
37
-
<span class="doc-count" id="articles">--</span>
38
-
</div>
39
-
<div class="doc-row">
40
-
<span class="doc-type">looseleafs</span>
41
-
<span class="doc-count" id="looseleafs">--</span>
42
-
</div>
35
+
<div id="platforms"></div>
43
36
</div>
44
37
</section>
45
38
+14
-3
site/dashboard.js
+14
-3
site/dashboard.js
···
57
57
if (!tags) return;
58
58
59
59
el.innerHTML = tags.slice(0, 20).map(t =>
60
-
'<a class="tag" href="https://leaflet-search.pages.dev/?tag=' + encodeURIComponent(t.tag) + '">' +
60
+
'<a class="tag" href="https://pub-search.waow.tech/?tag=' + encodeURIComponent(t.tag) + '">' +
61
61
escapeHtml(t.tag) + '<span class="n">' + t.count + '</span></a>'
62
62
).join('');
63
+
}
64
+
65
+
function renderPlatforms(platforms) {
66
+
const el = document.getElementById('platforms');
67
+
if (!platforms) return;
68
+
69
+
platforms.forEach(p => {
70
+
const row = document.createElement('div');
71
+
row.className = 'doc-row';
72
+
row.innerHTML = '<span class="doc-type">' + escapeHtml(p.platform) + '</span><span class="doc-count">' + p.count + '</span>';
73
+
el.appendChild(row);
74
+
});
63
75
}
64
76
65
77
function escapeHtml(str) {
···
83
95
84
96
document.getElementById('searches').textContent = data.searches;
85
97
document.getElementById('publications').textContent = data.publications;
86
-
document.getElementById('articles').textContent = data.articles;
87
-
document.getElementById('looseleafs').textContent = data.looseleafs;
88
98
99
+
renderPlatforms(data.platforms);
89
100
renderTimeline(data.timeline);
90
101
renderPubs(data.topPubs);
91
102
renderTags(data.tags);