• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sonus21 / rqueue / 25600722838

09 May 2026 12:06PM UTC coverage: 83.396% (-5.3%) from 88.677%
25600722838

push

github

web-flow
Nats v2 web (#295)

* ci: compile main sources in coverage_report job

The coverage_report job was producing an effectively empty
jacocoTestReport.xml (3.4KB vs ~1.1MB locally) because no .class files
existed when coverageReportOnly ran — the job checked out source code
and downloaded .exec artifacts, but never compiled. JaCoCo's report
generator skips packages/classes it cannot resolve, so the merged XML
ended up with only <sessioninfo> entries and no <package> elements.

That made coverallsJacoco silently no-op via the
"source file set empty, skipping" branch in CoverallsReporter, so
"Push coverage to Coveralls" reported success without uploading.

Verified by downloading the coverage-report artifact from a recent run
and comparing its XML structure against a local build's report.

Assisted-By: Claude Code

* nats-web: implement pause / soft-delete admin ops and capability-aware Q-detail

Replace the all-stub `NatsRqueueUtilityService` with real impls for the operations
JetStream can model: `pauseUnpauseQueue` persists the `paused` flag on `QueueConfig`
in the queue-config KV bucket and notifies the local listener container so the poller
stops dispatching; `deleteMessage` is a soft delete via `MessageMetadataService`
(stream message persists, dashboard hides via the metadata flag); `getDataType`
reports `STREAM`. `moveMessage`, `enqueueMessage`, and `makeEmpty` deliberately
remain "not supported" — there is no JetStream primitive for those.

Update `RqueueQDetailServiceImpl.getRunningTasks` / `getScheduledTasks` to return
header-only tables when the broker capabilities suppress those sections, instead of
emitting zero rows or 501s on NATS.

20 new unit tests cover the pause/delete paths and lock in the still-unsupported
operations. Updates `nats-task.md` / `nats-task-v2.md` to reflect what landed.

Assisted-By: Claude Code

* nats-web: capability-aware nav / charts and stream-based peek

End-to-end browser-tested the NATS dashboard and shipped the t... (continued)

2566 of 3407 branches covered (75.32%)

Branch coverage included in aggregate %.

795 of 1072 new or added lines in 22 files covered. (74.16%)

312 existing lines in 38 files now uncovered.

7715 of 8921 relevant lines covered (86.48%)

0.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.48
/rqueue-web/src/main/java/com/github/sonus21/rqueue/web/view/RqueueHtmlRenderer.java
1
/*
2
 * Copyright (c) 2020-2026 Sonu Kumar
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * You may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     https://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and limitations under the License.
14
 *
15
 */
16

17
package com.github.sonus21.rqueue.web.view;
18

19
import com.github.sonus21.rqueue.models.Pair;
20
import com.github.sonus21.rqueue.models.db.DeadLetterQueue;
21
import com.github.sonus21.rqueue.models.db.QueueConfig;
22
import com.github.sonus21.rqueue.models.enums.AggregationType;
23
import com.github.sonus21.rqueue.models.enums.ChartDataType;
24
import com.github.sonus21.rqueue.models.enums.DataType;
25
import com.github.sonus21.rqueue.models.enums.NavTab;
26
import com.github.sonus21.rqueue.models.registry.RqueueWorkerPollerView;
27
import com.github.sonus21.rqueue.models.registry.RqueueWorkerView;
28
import com.github.sonus21.rqueue.models.response.DataSelectorResponse;
29
import com.github.sonus21.rqueue.models.response.RedisDataDetail;
30
import com.github.sonus21.rqueue.models.response.SubscriberRow;
31
import com.github.sonus21.rqueue.models.response.TerminalStorageRow;
32
import com.github.sonus21.rqueue.utils.DateTimeUtils;
33
import java.util.List;
34
import java.util.Map;
35
import java.util.Map.Entry;
36
import java.util.stream.Collectors;
37
import org.springframework.stereotype.Component;
38
import org.springframework.util.CollectionUtils;
39

40
@Component
41
public class RqueueHtmlRenderer {
1✔
42

43
  // ---- Escaping helpers ----
44

45
  private static String esc(Object val) {
46
    if (val == null) return "";
1!
47
    return val.toString()
1✔
48
        .replace("&", "&amp;")
1✔
49
        .replace("<", "&lt;")
1✔
50
        .replace(">", "&gt;")
1✔
51
        .replace("\"", "&quot;")
1✔
52
        .replace("'", "&#x27;");
1✔
53
  }
54

55
  private static String jsStr(Object val) {
56
    if (val == null) return "";
1!
57
    return val.toString()
1✔
58
        .replace("\\", "\\\\")
1✔
59
        .replace("\"", "\\\"")
1✔
60
        .replace("\n", "\\n")
1✔
61
        .replace("\r", "\\r")
1✔
62
        .replace("<", "\\u003c")
1✔
63
        .replace(">", "\\u003e");
1✔
64
  }
65

66
  // ---- Format helpers (mirror the Pebble custom functions) ----
67

68
  private static String fmtTime(Long millis) {
69
    if (millis == null || millis <= 0) return "-";
1!
70
    return esc(DateTimeUtils.formatMilliToString(millis));
1✔
71
  }
72

73
  private static String fmtDuration(Long millis) {
74
    return esc(DateTimeUtils.formatMilliToCompactDuration(millis));
1✔
75
  }
76

77
  private static String fmtReadable(Long millis) {
78
    return esc(DateTimeUtils.formatMilliToReadableString(millis));
1✔
79
  }
80

81
  @SuppressWarnings("unchecked")
82
  private static String fmtDlq(List<DeadLetterQueue> queues) {
83
    if (CollectionUtils.isEmpty(queues)) return "";
1✔
84
    return esc(queues.stream().map(DeadLetterQueue::getName).collect(Collectors.joining(", ")));
1✔
85
  }
86

87
  private static String orDefault(String val, String fallback) {
88
    return (val != null && !val.isEmpty()) ? esc(val) : esc(fallback);
1!
89
  }
90

91
  // ---- Model access helpers ----
92

93
  @SuppressWarnings("unchecked")
94
  private static <T> T get(Map<String, Object> m, String key) {
95
    return (T) m.get(key);
1✔
96
  }
97

98
  private static boolean bool(Map<String, Object> m, String key) {
99
    return Boolean.TRUE.equals(m.get(key));
1✔
100
  }
101

102
  // ---- Base layout ----
103

104
  private String base(Map<String, Object> m, String mainContent, String additionalScript) {
105
    String up = esc(get(m, "urlPrefix"));
1✔
106
    String title = m.get("title") != null ? esc(get(m, "title")) : "Rqueue Dashboard";
1!
107
    return """
1✔
108
        <!DOCTYPE html>
109
        <html lang="en">
110
        <head>
111
          <meta charset="utf-8">
112
          <meta content="width=device-width, initial-scale=1.0" name="viewport">
113
          <meta content="default-src 'self' *.gstatic.com *.googleapis.com 'unsafe-inline' 'unsafe-eval'" http-equiv="Content-Security-Policy">
114
          <meta content='Rqueue' name="description">
115
          <meta content='Rqueue, task queue, scheduled queue, scheduled tasks, asynchronous processor' name="keywords">
116
          <title>%s</title>
117
          <link href="%srqueue/img/favicon.ico" rel="shortcut icon">
118
          <link href="%srqueue/img/apple-touch-icon.png" rel="apple-touch-icon">
119
          <link href="%srqueue/img/favicon-16x16.png" rel="icon" sizes="16x16" type="image/png">
120
          <link href="%srqueue/img/favicon-32x32.png" rel="icon" sizes="32x32" type="image/png">
121
          <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,600,600i,700,700i" rel="stylesheet">
122
          <link href="%srqueue/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
123
          <link href="%srqueue/vendor/boxicons/css/boxicons.min.css" rel="stylesheet">
124
          <link href="%srqueue/css/rqueue.css" rel="stylesheet">
125
        </head>
126
        <body>
127
        <header class="fixed-top" id="header">
128
          <div class="container d-flex">
129
            <div class="logo mr-auto">
130
              <h1 class="text-green"><a href="%srqueue">Rqueue</a></h1>
131
            </div>
132
            %s
133
          </div>
134
        </header>
135
        <main id="main">
136
          <div class="alert alert-primary alert-dismissible fade display-none" id="global-error-container" role="alert">
137
            <button aria-label="Close" class="close" data-dismiss="alert" type="button">
138
              <span aria-hidden="true">&times;</span>
139
            </button>
140
            <p id="global-error-message"></p>
141
          </div>
142
          %s
143
        </main>
144
        %s
145
        <div class="modal fade" id="delete-modal">
146
          <div class="modal-dialog modal-confirm">
147
            <div class="modal-content">
148
              <div class="modal-header">
149
                <h4 class="modal-title" id="delete-modal-title">Are you sure?</h4>
150
                <button aria-hidden="true" class="close" data-dismiss="modal" type="button">&times;</button>
151
              </div>
152
              <div class="modal-body">
153
                <p id="delete-modal-body">Do you really want to delete this? This process cannot be undone.</p>
154
              </div>
155
              <div class="modal-footer">
156
                <button class="btn btn-info" data-dismiss="modal" type="button">Cancel</button>
157
                <button class="btn btn-danger delete-btn" type="button">Delete</button>
158
              </div>
159
            </div>
160
          </div>
161
        </div>
162
        <div class="modal fade" id="info-modal">
163
          <div class="modal-dialog modal-info">
164
            <div class="modal-content">
165
              <div class="modal-header">
166
                <h4 class="modal-title" id="info-modal-title">Title</h4>
167
                <button aria-hidden="true" class="close" data-dismiss="modal" type="button">&times;</button>
168
              </div>
169
              <div class="modal-body">
170
                <p id="info-modal-body">Some text</p>
171
              </div>
172
              <div class="modal-footer">
173
                <button class="btn btn-info" data-dismiss="modal" type="button">Ok</button>
174
              </div>
175
            </div>
176
          </div>
177
        </div>
178
        <script src="https://www.gstatic.com/charts/loader.js" type="application/javascript"></script>
179
        <script src="%srqueue/vendor/jquery/jquery.min.js" type="application/javascript"></script>
180
        <script src="%srqueue/vendor/bootstrap/js/bootstrap.bundle.min.js" type="application/javascript"></script>
181
        <script src="%srqueue/js/rqueue.js" type="application/javascript"></script>
182
        <script type="application/javascript">urlPrefix = "%s";</script>
183
        %s
184
        </body>
185
        </html>
186
        """
187
        .formatted(
1✔
188
            title,
189
            up,
190
            up,
191
            up,
192
            up, // favicons x4
193
            up,
194
            up,
195
            up, // css x3
196
            up, // logo href
197
            navBar(m),
1✔
198
            mainContent,
199
            footerHtml(m),
1✔
200
            up,
201
            up,
202
            up, // scripts x3
203
            jsStr(get(m, "urlPrefix")),
1✔
204
            additionalScript);
205
  }
206

207
  private String navBar(Map<String, Object> m) {
208
    String up = esc(get(m, "urlPrefix"));
1✔
209
    boolean hideRunning = bool(m, "hideRunningPanel");
1✔
210
    boolean hideScheduled = bool(m, "hideScheduledPanel");
1✔
211
    StringBuilder sb = new StringBuilder("<nav class=\"nav-menu display-none d-lg-block\"><ul>");
1✔
212
    sb.append(navItem(bool(m, "queuesActive"), up, "queues", "bx-coin-stack", "Queues"));
1✔
213
    sb.append(navItem(bool(m, "workersActive"), up, "workers", "bx-user-pin", "Workers"));
1✔
214
    if (!hideRunning) {
1!
215
      sb.append(navItem(bool(m, "runningActive"), up, "running", "bx-sync", "Running"));
1✔
216
    }
217
    if (!hideScheduled) {
1!
218
      sb.append(navItem(bool(m, "scheduledActive"), up, "scheduled", "bxs-hourglass", "Scheduled"));
1✔
219
    }
220
    sb.append(navItem(bool(m, "pendingActive"), up, "pending", "bx-spreadsheet", "Pending"));
1✔
221
    sb.append(navItem(bool(m, "deadActive"), up, "dead", "bx-ghost", "Dead"));
1✔
222
    sb.append(navItem(bool(m, "utilityActive"), up, "utility", "bxs-wrench", "Utility"));
1✔
223
    sb.append("</ul></nav>");
1✔
224
    return sb.toString();
1✔
225
  }
226

227
  private String navItem(boolean active, String urlPrefix, String path, String icon, String label) {
228
    return "<li class=\"" + (active ? "active" : "") + "\">"
1✔
229
        + "<a href=\"" + urlPrefix + "rqueue/" + path + "\">"
230
        + "<i class='bx " + icon + "'></i><span>" + label + "</span></a></li>";
231
  }
232

233
  private String footerHtml(Map<String, Object> m) {
234
    return """
1✔
235
        <footer id="footer">
236
          <div class="container">
237
            <div class="row">
238
              <ul class="footer-links">
239
                <li><h3><a href="https://github.com/sonus21/rqueue" target="_blank">Rqueue</a></h3></li>
240
                <li><a href="#">Version:&nbsp;<p class="text-white float-right">%s</p></a></li>
241
                <li><a href="%s" target="_blank">Latest Version:&nbsp;<p class="text-white float-right">%s</p></a></li>
242
                <li><a href="#">Time:&nbsp;<p class="text-white float-right">%s (%sMs)</p></a></li>
243
              </ul>
244
            </div>
245
          </div>
246
        </footer>
247
        """
248
        .formatted(
1✔
249
            esc(get(m, "version")),
1✔
250
            esc(get(m, "releaseLink")),
1✔
251
            esc(get(m, "latestVersion")),
1✔
252
            esc(get(m, "time")),
1✔
253
            esc(get(m, "timeInMilli")));
1✔
254
  }
255

256
  // ---- Shared partials ----
257

258
  private String statsChartPartial(Map<String, Object> m) {
259
    List<ChartDataType> typeSelectors = get(m, "typeSelectors");
1✔
260
    List<AggregationType> aggTypes = get(m, "aggregatorTypes");
1✔
261
    DataSelectorResponse aggDateCounter = get(m, "aggregatorDateCounter");
1✔
262

263
    StringBuilder typeCheckboxes = new StringBuilder();
1✔
264
    if (typeSelectors != null) {
1!
265
      for (ChartDataType t : typeSelectors) {
1✔
266
        typeCheckboxes.append(
1✔
267
            """
268
            <div class="form-check">
269
              <input checked="checked" class="form-check-input" id="%s" name="data-type" type="checkbox" value="%s">
270
              <label class="form-check-label" for="%s">%s</label>
271
            </div>
272
            """
273
                .formatted(esc(t.name()), esc(t.name()), esc(t.name()), esc(t.getDescription())));
1✔
274
      }
1✔
275
    }
276

277
    StringBuilder aggTypeOptions = new StringBuilder();
1✔
278
    if (aggTypes != null) {
1!
279
      boolean first = true;
1✔
280
      for (AggregationType a : aggTypes) {
1✔
281
        aggTypeOptions
1✔
282
            .append("<option")
1✔
283
            .append(first ? " selected" : "")
1✔
284
            .append(">")
1✔
285
            .append(esc(a.toString()))
1✔
286
            .append("</option>");
1✔
287
        first = false;
1✔
288
      }
1✔
289
    }
290

291
    String counterTitle = aggDateCounter != null ? esc(aggDateCounter.getTitle()) : "";
1!
292
    StringBuilder counterOptions = new StringBuilder();
1✔
293
    if (aggDateCounter != null && aggDateCounter.getData() != null) {
1!
294
      for (Pair<String, String> p : aggDateCounter.getData()) {
1✔
295
        counterOptions
1✔
296
            .append("<option value=\"")
1✔
297
            .append(esc(p.getFirst()))
1✔
298
            .append("\">")
1✔
299
            .append(esc(p.getSecond()))
1✔
300
            .append("</option>");
1✔
301
      }
1✔
302
    }
303

304
    return """
1✔
305
        <h2 class="text-center">Message Stats</h2>
306
        <div id="stats_chart"></div>
307
        <div class="container">
308
          <div class="row">
309
            <div class="col-md-6 offset-md-3">
310
              <div class="row dashboard-chart-form">
311
                <div class="type_selectors col-md-7">
312
                  <p>Select Data Type</p>
313
                  %s
314
                </div>
315
                <div class="dashboard-form-action col-md-5">
316
                  <div class="form-group">
317
                    <label for="stats-aggregator-type">Select Aggregator Type</label>
318
                    <select class="form-control" id="stats-aggregator-type" name="aggregator-type">%s</select>
319
                  </div>
320
                  <div class="form-group">
321
                    <label for="stats-nday">%s</label>
322
                    <select class="form-control" id="stats-nday" name="aggregator-day-count">%s</select>
323
                  </div>
324
                  <div class="form-group">
325
                    <button class="btn btn-success" id="refresh-chart">Display</button>
326
                  </div>
327
                </div>
328
              </div>
329
            </div>
330
          </div>
331
        </div>
332
        """
333
        .formatted(typeCheckboxes, aggTypeOptions, counterTitle, counterOptions);
1✔
334
  }
335

336
  private String latencyChartPartial(Map<String, Object> m) {
337
    List<AggregationType> aggTypes = get(m, "aggregatorTypes");
1✔
338
    DataSelectorResponse aggDateCounter = get(m, "aggregatorDateCounter");
1✔
339

340
    StringBuilder aggTypeOptions = new StringBuilder();
1✔
341
    if (aggTypes != null) {
1!
342
      boolean first = true;
1✔
343
      for (AggregationType a : aggTypes) {
1✔
344
        aggTypeOptions
1✔
345
            .append("<option")
1✔
346
            .append(first ? " selected" : "")
1✔
347
            .append(">")
1✔
348
            .append(esc(a.toString()))
1✔
349
            .append("</option>");
1✔
350
        first = false;
1✔
351
      }
1✔
352
    }
353

354
    String counterTitle = aggDateCounter != null ? esc(aggDateCounter.getTitle()) : "";
1!
355
    StringBuilder counterOptions = new StringBuilder();
1✔
356
    if (aggDateCounter != null && aggDateCounter.getData() != null) {
1!
357
      for (Pair<String, String> p : aggDateCounter.getData()) {
1✔
358
        counterOptions
1✔
359
            .append("<option value=\"")
1✔
360
            .append(esc(p.getFirst()))
1✔
361
            .append("\">")
1✔
362
            .append(esc(p.getSecond()))
1✔
363
            .append("</option>");
1✔
364
      }
1✔
365
    }
366

367
    return """
1✔
368
        <h2 class="text-center">Latency Graph!</h2>
369
        <div id="latency_chart"></div>
370
        <div class="container">
371
          <div class="row">
372
            <div class="col-md-6 offset-md-3">
373
              <div class="dashboard-chart-form latency-chart-form">
374
                <div class="row">
375
                  <div class="col-md-6">
376
                    <div class="form-group">
377
                      <label for="latency-aggregator-type">Select Aggregator Type</label>
378
                      <select class="form-control" id="latency-aggregator-type" name="latency-aggregator-type">%s</select>
379
                    </div>
380
                  </div>
381
                  <div class="col-md-6">
382
                    <div class="form-group">
383
                      <label for="latency-nday">%s</label>
384
                      <select class="form-control" id="latency-nday" name="latency-day-count">%s</select>
385
                    </div>
386
                  </div>
387
                </div>
388
                <div class="row">
389
                  <div class="col-md-2 offset-md-5">
390
                    <div class="form-group">
391
                      <button class="btn btn-success" id="refresh-latency-chart">Display</button>
392
                    </div>
393
                  </div>
394
                </div>
395
              </div>
396
            </div>
397
          </div>
398
        </div>
399
        """
400
        .formatted(aggTypeOptions, counterTitle, counterOptions);
1✔
401
  }
402

403
  private String dataExplorerModalPartial(Map<String, Object> m) {
404
    String queueName = m.get("queueName") != null ? esc(get(m, "queueName")) : "";
1✔
405
    return """
1✔
406
        <div class="modal fade" id="explore-queue">
407
          <div class="modal-dialog modal-data-explorer modal-lg modal-dialog-centered" role="document">
408
            <div class="modal-content explorer-modal-content">
409
              <div class="modal-header explorer-modal-header">
410
                <div class="explorer-modal-heading">
411
                  <span class="explorer-modal-kicker">Queue Explorer</span>
412
                  <h4 class="modal-title" id="explorer-title">Queue: <b>%s</b></h4>
413
                </div>
414
                <button aria-label="Close" class="close explorer-modal-close" data-dismiss="modal" type="button">&times;</button>
415
              </div>
416
              <div class="modal-body explorer-modal-body">
417
                <div class="explorer-toolbar">
418
                  <div class="explorer-toolbar-left">
419
                    <label class="explorer-select-group" for="page-size">
420
                      <span class="explorer-control-label">Rows</span>
421
                      <select id="page-size">
422
                        <option selected value="10">10</option>
423
                        <option value="25">25</option>
424
                        <option value="50">50</option>
425
                        <option value="100">100</option>
426
                      </select>
427
                    </label>
428
                  </div>
429
                  <div class="explorer-toolbar-actions">
430
                    <button class="btn btn-danger btn-delete-all btn-sm display-none" id="clear-queue" type="button">Delete All</button>
431
                    <button class="btn btn-info btn-sm btn-poll" id="poll-queue-btn" type="button">Refresh</button>
432
                  </div>
433
                </div>
434
                <div class="explorer-table-shell">
435
                  <div class="table-responsive explorer-table-wrap" id="explore-data-table">
436
                    <table class="table table-bordered explorer-table">
437
                      <thead><tr id="table-header"><th></th></tr></thead>
438
                      <tbody id="table-body"></tbody>
439
                    </table>
440
                  </div>
441
                </div>
442
              </div>
443
              <div class="modal-footer data-explorer-footer explorer-modal-footer">
444
                <div class="explorer-pagination">
445
                  <button class="btn btn-outline-secondary" id="previous-page-button" type="button">Previous</button>
446
                  <span class="explorer-page-indicator" id="display-page-number"></span>
447
                  <button class="btn btn-info" id="next-page-button" type="button">Next</button>
448
                </div>
449
              </div>
450
            </div>
451
          </div>
452
        </div>
453
        """
454
        .formatted(queueName);
1✔
455
  }
456

457
  private String paginationControls(
458
      boolean hasPrev,
459
      boolean hasNext,
460
      int prevPage,
461
      int nextPage,
462
      int currentPage,
463
      int totalPages) {
464
    StringBuilder sb = new StringBuilder();
1✔
465
    sb.append("<div class=\"worker-pagination\">");
1✔
466
    if (hasPrev) {
1!
NEW
467
      sb.append("<a class=\"worker-page-btn\" href=\"?page=")
×
NEW
468
          .append(prevPage)
×
NEW
469
          .append("\">Previous</a>");
×
470
    }
471
    sb.append("<span class=\"worker-page-pill\">Page ").append(currentPage).append("</span>");
1✔
472
    if (hasNext) {
1!
NEW
473
      sb.append("<a class=\"worker-page-btn worker-page-btn-primary\" href=\"?page=")
×
NEW
474
          .append(nextPage)
×
NEW
475
          .append("\">Next</a>");
×
476
    }
477
    sb.append("</div>");
1✔
478
    return sb.toString();
1✔
479
  }
480

481
  // ---- Page renderers ----
482

483
  public String renderIndex(Map<String, Object> m) {
484
    String main = statsChartPartial(m) + "<hr/>" + latencyChartPartial(m);
1✔
485
    String script =
1✔
486
        """
487
        <script type="application/javascript">
488
          var chartParams = {'type': 'STATS', 'aggregationType': 'DAILY'};
489
          var latencyChartParams = {'type': 'LATENCY', 'aggregationType': 'DAILY'};
490
          $(document).ready(function () {
491
            drawChart(chartParams, "stats_chart");
492
            drawChart(latencyChartParams, "latency_chart");
493
            $('#refresh-chart').click(function () { refreshStatsChart(chartParams, "stats_chart"); });
494
            $('#refresh-latency-chart').click(function () { refreshLatencyChart(latencyChartParams, "latency_chart"); });
495
            attachChartEventListeners();
496
          });
497
        </script>
498
        """;
499
    return base(m, main, script);
1✔
500
  }
501

502
  public String renderQueues(Map<String, Object> m) {
503
    List<QueueConfig> queues = get(m, "queues");
1✔
504
    List<Entry<String, List<Entry<NavTab, RedisDataDetail>>>> queueConfigs = get(m, "queueConfigs");
1✔
505
    int currentPage = ((Number) m.get("currentPage")).intValue();
1✔
506
    int totalPages = ((Number) m.get("totalPages")).intValue();
1✔
507
    boolean hasPrev = bool(m, "hasPreviousPage");
1✔
508
    boolean hasNext = bool(m, "hasNextPage");
1✔
509
    int prevPage = ((Number) m.get("previousPage")).intValue();
1✔
510
    int nextPage = ((Number) m.get("nextPage")).intValue();
1✔
511
    int totalQueueCount = ((Number) m.get("totalQueueCount")).intValue();
1✔
512
    String storageKicker = orDefault(get(m, "storageKicker"), "Redis");
1✔
513
    String storageDesc = esc(get(m, "storageDescription"));
1✔
514
    String pagination =
1✔
515
        paginationControls(hasPrev, hasNext, prevPage, nextPage, currentPage, totalPages);
1✔
516

517
    StringBuilder cards = new StringBuilder();
1✔
518
    if (queues == null || queues.isEmpty()) {
1!
NEW
519
      cards.append(
×
520
          """
521
          <section class="queue-empty-state" role="alert">
522
            <h2>No queues available.</h2>
523
            <p>Queue definitions will appear here after Rqueue loads listener metadata.</p>
524
          </section>
525
          """);
526
    } else {
527
      cards.append("<section class=\"queue-card-grid\">");
1✔
528
      for (QueueConfig meta : queues) {
1✔
529
        boolean paused = meta.isPaused();
1✔
530
        String name = esc(meta.getName());
1✔
531
        String stateLabel = paused ? "Paused" : "Live";
1!
532
        String stateCss = paused ? "queue-state-paused" : "queue-state-live";
1!
533
        String stateIcon = paused ? "bx-pause-circle" : "bx-check-circle";
1!
534
        String pauseIcon = paused ? "bx-play-circle" : "bx-pause-circle";
1!
535
        String pauseTitle = paused ? "Unpause" : "Pause";
1!
536
        boolean unboundedConc =
1✔
537
            meta.getConcurrency().getMin() == -1 && meta.getConcurrency().getMax() == -1;
1!
538
        String concurrency = unboundedConc
1!
539
            ? "<span aria-label=\"Unbounded concurrency\" class=\"queue-metric-infinity\""
1✔
540
                + " data-placement=\"top\" data-toggle=\"tooltip\" title=\"Unbounded"
541
                + " concurrency\"><i class=\"bx bx-infinite\"></i></span>"
NEW
542
            : esc(meta.getConcurrency().getMin()) + " to "
×
543
                + esc(meta.getConcurrency().getMax());
1✔
544
        String retries = meta.isUnlimitedRetry()
1✔
545
            ? "<span aria-label=\"Unlimited retries\" class=\"queue-metric-infinity\""
1✔
546
                + " data-placement=\"top\" data-toggle=\"tooltip\" title=\"Unlimited"
547
                + " retries\"><i class=\"bx bx-infinite\"></i></span>"
548
            : esc(meta.getNumRetry());
1✔
549

550
        cards.append(
1✔
551
            """
552
            <article class="queue-card">
553
              <div class="queue-card-top">
554
                <div class="queue-card-heading">
555
                  <span class="queue-card-label">Queue</span>
556
                  <h2 class="queue-card-title"><a href="queues/%s">%s</a></h2>
557
                </div>
558
                <div class="queue-card-actions">
559
                  <span aria-label="%s" class="queue-state-badge %s" data-placement="top" data-toggle="tooltip" title="%s">
560
                    <i class="bx %s"></i>
561
                  </span>
562
                  <button class="queue-pause-toggle" type="button">
563
                    <i class="bx pause-queue-btn %s" data-placement="top" data-queue="%s" data-toggle="tooltip" title="%s"></i>
564
                  </button>
565
                </div>
566
              </div>
567
              <div class="queue-card-metrics">
568
                <div class="queue-metric">
569
                  <span aria-label="Concurrency" class="queue-metric-label queue-metric-icon" data-placement="top" data-toggle="tooltip" title="Concurrency"><i class="bx bx-sitemap"></i></span>
570
                  <strong class="queue-metric-value">%s</strong>
571
                </div>
572
                <div class="queue-metric">
573
                  <span aria-label="Retry Count" class="queue-metric-label queue-metric-icon" data-placement="top" data-toggle="tooltip" title="Retry Count"><i class="bx bx-refresh"></i></span>
574
                  <strong class="queue-metric-value">%s</strong>
575
                </div>
576
                <div class="queue-metric">
577
                  <span aria-label="Visibility Timeout" class="queue-metric-label queue-metric-icon" data-placement="top" data-toggle="tooltip" title="Visibility Timeout"><i class="bx bx-time-five"></i></span>
578
                  <strong class="queue-metric-value">%s</strong>
579
                </div>
580
              </div>
581
              <div class="queue-card-meta">
582
                <div class="queue-meta-block">
583
                  <span class="queue-meta-label">Dead Letter Queue(s)</span>
584
                  <div class="queue-meta-value queue-dlq-list">%s</div>
585
                </div>
586
                <div class="queue-meta-grid">
587
                  <div class="queue-meta-block">
588
                    <span class="queue-meta-label">Created</span>
589
                    <div class="queue-meta-value queue-meta-value-inline">%s</div>
590
                  </div>
591
                  <div class="queue-meta-block">
592
                    <span class="queue-meta-label">Updated</span>
593
                    <div class="queue-meta-value queue-meta-value-inline">%s</div>
594
                  </div>
595
                </div>
596
              </div>
597
            </article>
598
            """
599
                .formatted(
1✔
600
                    name,
601
                    name,
602
                    stateLabel,
603
                    stateCss,
604
                    stateLabel,
605
                    stateIcon,
606
                    pauseIcon,
607
                    name,
608
                    pauseTitle,
609
                    concurrency,
610
                    retries,
611
                    fmtDuration(meta.getVisibilityTimeout()),
1✔
612
                    fmtDlq(meta.getDeadLetterQueues()),
1✔
613
                    fmtReadable(meta.getCreatedOn()),
1✔
614
                    fmtReadable(meta.getUpdatedOn())));
1✔
615
      }
1✔
616
      cards.append("</section>");
1✔
617
    }
618

619
    // Storage footprint section
620
    StringBuilder storageGrid = new StringBuilder();
1✔
621
    if (queueConfigs != null) {
1!
622
      for (Entry<String, List<Entry<NavTab, RedisDataDetail>>> config : queueConfigs) {
1✔
623
        StringBuilder rows = new StringBuilder();
1✔
624
        for (Entry<NavTab, RedisDataDetail> meta : config.getValue()) {
1✔
625
          String sizeHtml = meta.getValue().getSize() < 0
1!
NEW
626
              ? "Queue-backed"
×
627
              : esc(meta.getValue().getSize());
1✔
628
          rows.append("<tr><td>")
1✔
629
              .append(esc(meta.getKey()))
1✔
630
              .append("</td>")
1✔
631
              .append("<td>")
1✔
632
              .append(esc(meta.getValue().getName()))
1✔
633
              .append("</td>")
1✔
634
              .append("<td>")
1✔
635
              .append(sizeHtml)
1✔
636
              .append("</td></tr>");
1✔
637
        }
1✔
638
        storageGrid.append(
1✔
639
            """
640
            <article class="queue-storage-card">
641
              <div class="queue-storage-card-head">
642
                <span class="queue-storage-label">Queue</span>
643
                <h3 class="queue-storage-title">%s</h3>
644
              </div>
645
              <div class="table-responsive">
646
                <table class="table queue-storage-table">
647
                  <thead><tr><th>Type</th><th>Name</th><th>Size</th></tr></thead>
648
                  <tbody>%s</tbody>
649
                </table>
650
              </div>
651
            </article>
652
            """
653
                .formatted(esc(config.getKey()), rows));
1✔
654
      }
1✔
655
    }
656

657
    String toolbarNote =
658
        totalQueueCount > 0 ? "Showing queue records for this page." : "No queues are registered.";
1!
659

660
    String main =
1✔
661
        """
662
        <div class="container queue-dashboard">
663
          <section class="queue-hero">
664
            <div class="queue-hero-copy">
665
              <span class="queue-hero-kicker">Queue Catalog</span>
666
              <h1 class="queue-hero-title">Operational View of Every Queue</h1>
667
              <p class="queue-hero-subtitle">Browse queue configuration, retry policy, pause state, and backing %s structures from a single page.</p>
668
            </div>
669
            <div class="queue-hero-meta">
670
              <div class="queue-hero-stat">
671
                <span class="queue-hero-stat-label">Queues</span>
672
                <strong class="queue-hero-stat-value">%d</strong>
673
              </div>
674
              <div class="queue-hero-stat">
675
                <span class="queue-hero-stat-label">Page</span>
676
                <strong class="queue-hero-stat-value">%d / %d</strong>
677
              </div>
678
            </div>
679
          </section>
680
          <section class="queue-toolbar">
681
            <div class="queue-toolbar-note">%s</div>
682
            %s
683
          </section>
684
          %s
685
          <section class="queue-storage-section">
686
            <div class="queue-section-header">
687
              <div>
688
                <span class="queue-section-kicker">%s Layout</span>
689
                <h2 class="queue-section-title">Queue Storage Footprint</h2>
690
              </div>
691
              <p class="queue-section-copy">%s</p>
692
            </div>
693
            <div class="queue-storage-grid">%s</div>
694
          </section>
695
          <section class="queue-toolbar queue-toolbar-bottom">
696
            <div class="queue-toolbar-note">Use each queue card to open detailed queue state and message explorers.</div>
697
            %s
698
          </section>
699
        </div>
700
        """
701
            .formatted(
1✔
702
                storageKicker,
703
                totalQueueCount,
1✔
704
                currentPage,
1✔
705
                totalPages,
1✔
706
                toolbarNote,
707
                pagination,
708
                cards,
709
                storageKicker,
710
                storageDesc,
711
                storageGrid,
712
                paginationControls(hasPrev, hasNext, prevPage, nextPage, currentPage, totalPages));
1✔
713
    return base(m, main, "");
1✔
714
  }
715

716
  public String renderWorkers(Map<String, Object> m) {
NEW
717
    List<RqueueWorkerView> workers = get(m, "workers");
×
NEW
718
    int currentPage = ((Number) m.get("currentPage")).intValue();
×
NEW
719
    int totalPages = ((Number) m.get("totalPages")).intValue();
×
NEW
720
    boolean hasPrev = bool(m, "hasPreviousPage");
×
NEW
721
    boolean hasNext = bool(m, "hasNextPage");
×
NEW
722
    int prevPage = ((Number) m.get("previousPage")).intValue();
×
NEW
723
    int nextPage = ((Number) m.get("nextPage")).intValue();
×
NEW
724
    int totalWorkerCount = ((Number) m.get("totalWorkerCount")).intValue();
×
NEW
725
    String pagination =
×
NEW
726
        paginationControls(hasPrev, hasNext, prevPage, nextPage, currentPage, totalPages);
×
727

NEW
728
    String toolbarNote = totalWorkerCount > 0
×
NEW
729
        ? "Showing worker records for this page."
×
NEW
730
        : "Waiting for worker heartbeats.";
×
731

NEW
732
    StringBuilder workerList = new StringBuilder();
×
NEW
733
    if (workers == null || workers.isEmpty()) {
×
NEW
734
      workerList.append(
×
735
          """
736
          <section class="worker-empty-state" role="alert">
737
            <h2>No worker heartbeat is available yet.</h2>
738
            <p>The page will populate after a worker starts polling queues and reports registry metadata.</p>
739
          </section>
740
          """);
741
    } else {
NEW
742
      workerList.append("<section class=\"worker-list\">");
×
NEW
743
      boolean first = true;
×
NEW
744
      for (RqueueWorkerView worker : workers) {
×
NEW
745
        StringBuilder pollers = new StringBuilder();
×
NEW
746
        for (RqueueWorkerPollerView poller : worker.getPollers()) {
×
NEW
747
          String statusCss = "ACTIVE".equals(poller.getStatus())
×
NEW
748
              ? "worker-status-active"
×
NEW
749
              : "STALE".equals(poller.getStatus())
×
NEW
750
                  ? "worker-status-stale"
×
NEW
751
                  : "PAUSED".equals(poller.getStatus()) ? "worker-status-paused" : "";
×
NEW
752
          String lastPollTime = fmtTime(poller.getLastPollAt());
×
NEW
753
          String lastMsgTime = fmtTime(poller.getLastMessageAt());
×
NEW
754
          String lastExhTime = fmtTime(poller.getLastCapacityExhaustedAt());
×
NEW
755
          String consumerNameHtml = poller.getConsumerName() != null
×
NEW
756
              ? "<span class=\"worker-consumer-name\">" + esc(poller.getConsumerName()) + "</span>"
×
NEW
757
              : "";
×
NEW
758
          pollers.append(
×
759
              """
760
              <article class="worker-queue-card">
761
                <div class="worker-queue-card-head">
762
                  <div>
763
                    <span class="worker-queue-label">Queue</span>
764
                    <h3 class="worker-queue-title"><a href="queues/%s">%s</a></h3>
765
                    %s
766
                  </div>
767
                  <span class="worker-status-badge %s">%s</span>
768
                </div>
769
                <div class="worker-queue-timeline">
770
                  <div class="worker-queue-event">
771
                    <span class="worker-queue-event-label">Last Poll</span>
772
                    <strong class="worker-queue-event-time">%s</strong>
773
                    <span class="worker-queue-event-age">%s</span>
774
                  </div>
775
                  <div class="worker-queue-event">
776
                    <span class="worker-queue-event-label">Last Message</span>
777
                    <strong class="worker-queue-event-time">%s</strong>
778
                    <span class="worker-queue-event-age">%s</span>
779
                  </div>
780
                  <div class="worker-queue-event">
781
                    <span class="worker-queue-event-label">Last Exhausted</span>
782
                    <strong class="worker-queue-event-time">%s</strong>
783
                    <span class="worker-queue-event-age">%s</span>
784
                  </div>
785
                </div>
786
                <div class="worker-queue-footer">
787
                  <span class="worker-queue-footer-label">Exhausted Count</span>
788
                  <strong class="worker-queue-footer-value">%s</strong>
789
                </div>
790
              </article>
791
              """
NEW
792
                  .formatted(
×
NEW
793
                      esc(poller.getQueue()),
×
NEW
794
                      esc(poller.getQueue()),
×
795
                      consumerNameHtml,
796
                      statusCss,
NEW
797
                      esc(poller.getStatus()),
×
798
                      lastPollTime,
NEW
799
                      esc(poller.getLastPollAge()),
×
800
                      lastMsgTime,
NEW
801
                      esc(poller.getLastMessageAge()),
×
802
                      lastExhTime,
NEW
803
                      esc(poller.getLastCapacityExhaustedAge()),
×
NEW
804
                      esc(poller.getCapacityExhaustedCount())));
×
NEW
805
        }
×
NEW
806
        workerList.append(
×
807
            """
808
            <details class="worker-panel" %s>
809
              <summary class="worker-panel-summary">
810
                <div class="worker-panel-main">
811
                  <div class="worker-panel-title-row">
812
                    <span class="worker-id-pill">Worker</span>
813
                    <h2 class="worker-panel-title">%s</h2>
814
                  </div>
815
                  <div class="worker-panel-meta">
816
                    <span class="worker-meta-chip">Host %s</span>
817
                    <span class="worker-meta-chip">PID %s</span>
818
                    <span class="worker-meta-chip">Last Poll %s</span>
819
                    <span class="worker-meta-chip">%s</span>
820
                  </div>
821
                </div>
822
                <div class="worker-panel-stats">
823
                  <div class="worker-stat-chip"><span class="worker-stat-chip-label">Active</span><strong>%s</strong></div>
824
                  <div class="worker-stat-chip worker-stat-chip-warning"><span class="worker-stat-chip-label">Stale</span><strong>%s</strong></div>
825
                  <div class="worker-stat-chip worker-stat-chip-danger"><span class="worker-stat-chip-label">Recent Exhaustion</span><strong>%s</strong></div>
826
                </div>
827
                <span class="worker-panel-caret" aria-hidden="true"></span>
828
              </summary>
829
              <div class="worker-panel-body">
830
                <div class="worker-queue-grid">%s</div>
831
              </div>
832
            </details>
833
            """
NEW
834
                .formatted(
×
NEW
835
                    first ? "open" : "",
×
NEW
836
                    esc(worker.getWorkerId()),
×
NEW
837
                    esc(worker.getHost()),
×
NEW
838
                    esc(worker.getPid()),
×
NEW
839
                    fmtTime(worker.getLastPollAt()),
×
NEW
840
                    esc(worker.getLastPollAge()),
×
NEW
841
                    esc(worker.getActiveQueues()),
×
NEW
842
                    esc(worker.getStaleQueues()),
×
NEW
843
                    esc(worker.getRecentCapacityExhaustedQueues()),
×
844
                    pollers));
NEW
845
        first = false;
×
NEW
846
      }
×
NEW
847
      workerList.append("</section>");
×
848
    }
849

NEW
850
    String main =
×
851
        """
852
        <div class="container worker-dashboard">
853
          <section class="worker-hero">
854
            <div class="worker-hero-copy">
855
              <span class="worker-hero-kicker">Worker Registry</span>
856
              <h1 class="worker-hero-title">Live Pollers Across Your Fleet</h1>
857
              <p class="worker-hero-subtitle">Inspect poll activity, queue ownership, and recent capacity pressure without leaving the dashboard.</p>
858
            </div>
859
            <div class="worker-hero-meta">
860
              <div class="worker-hero-stat">
861
                <span class="worker-hero-stat-label">Workers</span>
862
                <strong class="worker-hero-stat-value">%d</strong>
863
              </div>
864
              <div class="worker-hero-stat">
865
                <span class="worker-hero-stat-label">Page</span>
866
                <strong class="worker-hero-stat-value">%d / %d</strong>
867
              </div>
868
            </div>
869
          </section>
870
          <section class="worker-toolbar">
871
            <div class="worker-toolbar-note">%s</div>
872
            %s
873
          </section>
874
          %s
875
          <section class="worker-toolbar worker-toolbar-bottom">
876
            <div class="worker-toolbar-note">Use the queue cards to drill into the queue detail page.</div>
877
            %s
878
          </section>
879
        </div>
880
        """
NEW
881
            .formatted(
×
NEW
882
                totalWorkerCount,
×
NEW
883
                currentPage,
×
NEW
884
                totalPages,
×
885
                toolbarNote,
886
                pagination,
887
                workerList,
NEW
888
                paginationControls(hasPrev, hasNext, prevPage, nextPage, currentPage, totalPages));
×
NEW
889
    return base(m, main, "");
×
890
  }
891

892
  public String renderQueueDetail(Map<String, Object> m) {
893
    String queueName = esc(get(m, "queueName"));
1✔
894
    QueueConfig config = get(m, "config");
1✔
895
    List<SubscriberRow> subscribers = get(m, "subscribers");
1✔
896
    if (subscribers == null) subscribers = List.of();
1!
897
    List<TerminalStorageRow> terminalRows = get(m, "terminalRows");
1✔
898

899
    // Header
900
    boolean paused = config != null && config.isPaused();
1!
901
    String configName = config != null ? esc(config.getName()) : queueName;
1!
902
    String stateLabel = paused ? "Paused" : "Live";
1!
903
    String stateCss = paused ? "qd-state-paused" : "qd-state-live";
1!
904
    String stateIcon = paused ? "bx-pause-circle" : "bx-check-circle";
1!
905

906
    String pauseBtn = "";
1✔
907
    if (config != null) {
1!
908
      String pauseIcon = paused ? "bx-play-circle" : "bx-pause-circle";
1!
909
      String pauseTitle = paused ? "Unpause" : "Pause";
1!
910
      pauseBtn =
1✔
911
          """
912
          <button class="qd-pause-btn" type="button" title="%s queue">
913
            <i class="bx pause-queue-btn %s" data-queue="%s" data-placement="top" data-toggle="tooltip" title="%s"></i>
914
          </button>
915
          """
916
              .formatted(pauseTitle, pauseIcon, configName, pauseTitle);
1✔
917
    }
918

919
    long totalInFlight =
1✔
920
        subscribers.stream().mapToLong(SubscriberRow::getInFlight).sum();
1✔
921
    long firstPending = subscribers.isEmpty() ? 0 : subscribers.get(0).getPending();
1!
922

923
    // Config chip strip
924
    String configStrip = "";
1✔
925
    if (config != null) {
1!
926
      boolean unboundedConc =
1✔
927
          config.getConcurrency().getMin() == -1 && config.getConcurrency().getMax() == -1;
1!
928
      String concHtml = unboundedConc
1!
929
          ? "<i class=\"bx bx-infinite\"></i> Unbounded"
1✔
NEW
930
          : esc(config.getConcurrency().getMin()) + "–"
×
931
              + esc(config.getConcurrency().getMax());
1✔
932
      String retryHtml = config.isUnlimitedRetry()
1✔
933
          ? "<i class=\"bx bx-infinite\"></i> Unlimited"
1✔
934
          : esc(config.getNumRetry());
1✔
935
      String dlqHtml =
936
          config.getDeadLetterQueues() == null || config.getDeadLetterQueues().isEmpty()
1!
937
              ? "<span class=\"qd-muted\">—</span>"
1✔
938
              : fmtDlq(config.getDeadLetterQueues());
1✔
939

940
      configStrip =
1✔
941
          """
942
          <div class="qd-config">
943
            <div class="qd-config-cell"><span class="qd-config-label"><i class="bx bx-sitemap"></i> Concurrency</span><span class="qd-config-value">%s</span></div>
944
            <div class="qd-config-cell"><span class="qd-config-label"><i class="bx bx-refresh"></i> Retries</span><span class="qd-config-value">%s</span></div>
945
            <div class="qd-config-cell"><span class="qd-config-label"><i class="bx bx-time-five"></i> Visibility</span><span class="qd-config-value">%s</span></div>
946
            <div class="qd-config-cell"><span class="qd-config-label"><i class="bx bx-skull"></i> DLQ</span><span class="qd-config-value">%s</span></div>
947
            <div class="qd-config-cell qd-config-cell-meta"><span class="qd-config-label">Created</span><span class="qd-config-value">%s</span></div>
948
            <div class="qd-config-cell qd-config-cell-meta"><span class="qd-config-label">Updated</span><span class="qd-config-value">%s</span></div>
949
          </div>
950
          """
951
              .formatted(
1✔
952
                  concHtml,
953
                  retryHtml,
954
                  fmtDuration(config.getVisibilityTimeout()),
1✔
955
                  dlqHtml,
956
                  fmtTime(config.getCreatedOn()),
1✔
957
                  fmtTime(config.getUpdatedOn()));
1✔
958
    }
959

960
    // Subscribers table
961
    StringBuilder subRows = new StringBuilder();
1✔
962
    for (SubscriberRow sub : subscribers) {
1✔
963
      String typeLabel = orDefault(
1✔
964
          sub.getTypeLabel(), sub.getDataType() != null ? sub.getDataType().toString() : "");
1!
965
      String statusHtml = sub.getStatus() != null
1!
966
          ? "<span class=\"qd-status qd-status-" + sub.getStatus().toLowerCase() + "\">"
1✔
967
              + esc(sub.getStatus()) + "</span>"
1✔
968
          : "<span class=\"qd-muted\">—</span>";
1✔
969
      String hostHtml = sub.getHost() != null
1!
970
          ? esc(sub.getHost())
1✔
971
              + (sub.getPid() != null
1!
972
                  ? " <small class=\"qd-muted\">/ " + esc(sub.getPid()) + "</small>"
1✔
973
                  : "")
1✔
974
          : "<span class=\"qd-muted\">—</span>";
1✔
975
      String pollHtml = sub.getLastPollAt() > 0
1!
976
          ? "<div class=\"qd-poll-time\">" + fmtTime(sub.getLastPollAt()) + "</div>"
1✔
977
              + (sub.getLastPollAge() != null
1!
978
                  ? "<small class=\"qd-muted\">" + esc(sub.getLastPollAge()) + " ago</small>"
1✔
979
                  : "")
1✔
980
          : "<span class=\"qd-muted\">—</span>";
1✔
981
      String workerCountHtml = sub.getWorkerCount() > 0
1!
982
          ? "<strong>" + sub.getWorkerCount() + "</strong>"
1✔
983
          : "<span class=\"qd-muted\">—</span>";
1✔
984
      String pendingSharedHtml =
985
          sub.isPendingShared() ? "<small class=\"qd-muted\">shared</small>" : "";
1!
986
      String dataTypeName = sub.getDataType() != null ? esc(sub.getDataType().toString()) : "";
1!
987
      subRows.append(
1✔
988
          """
989
          <tr>
990
            <td><a class="qd-link data-explorer" data-name="%s" data-consumer="%s" data-target="#explore-queue" data-toggle="modal" data-type="%s" data-type-label="%s" href="#">%s</a></td>
991
            <td><span class="qd-pill">%s</span></td>
992
            <td><code class="qd-code">%s</code></td>
993
            <td class="qd-num"><strong>%s</strong> %s</td>
994
            <td class="qd-num"><strong>%s</strong></td>
995
            <td class="qd-num">%s</td>
996
            <td>%s</td>
997
            <td>%s</td>
998
            <td>%s</td>
999
          </tr>
1000
          """
1001
              .formatted(
1✔
1002
                  esc(sub.getStorageName()),
1✔
1003
                  esc(sub.getConsumerName()),
1✔
1004
                  dataTypeName,
1005
                  typeLabel,
1006
                  esc(sub.getConsumerName()),
1✔
1007
                  typeLabel,
1008
                  esc(sub.getStorageName()),
1✔
1009
                  sub.getPending(),
1✔
1010
                  pendingSharedHtml,
1011
                  sub.getInFlight(),
1✔
1012
                  workerCountHtml,
1013
                  statusHtml,
1014
                  hostHtml,
1015
                  pollHtml));
1016
    }
1✔
1017

1018
    String subscribersSection;
1019
    if (subscribers.isEmpty()) {
1!
NEW
1020
      subscribersSection = "<div class=\"qd-empty\">No subscribers attached yet.</div>";
×
1021
    } else {
1022
      subscribersSection =
1✔
1023
          """
1024
          <table class="qd-table">
1025
            <thead>
1026
              <tr>
1027
                <th>Consumer</th><th>Type</th><th>Storage</th>
1028
                <th class="qd-num">Pending</th><th class="qd-num">In-Flight</th>
1029
                <th class="qd-num">Workers</th><th>Status</th><th>Host</th><th>Last Poll</th>
1030
              </tr>
1031
            </thead>
1032
            <tbody>%s</tbody>
1033
          </table>
1034
          """
1035
              .formatted(subRows);
1✔
1036
    }
1037

1038
    // Terminal storage table
1039
    String terminalSection = "";
1✔
1040
    if (!CollectionUtils.isEmpty(terminalRows)) {
1!
1041
      StringBuilder termRows = new StringBuilder();
1✔
1042
      for (TerminalStorageRow row : terminalRows) {
1✔
1043
        String rowTypeLabel = orDefault(
1✔
1044
            row.getTypeLabel(), row.getDataType() != null ? row.getDataType().toString() : "");
1!
1045
        String sizeHtml = row.getSize() < 0
1!
NEW
1046
            ? "<span class=\"qd-muted\">Queue-backed</span>"
×
1047
            : (row.isApproximate() ? "<span class=\"qd-muted\">~</span>" : "") + "<strong>"
1!
1048
                + row.getSize() + "</strong>";
1✔
1049
        String tabName = row.getTab() != null ? row.getTab().name() : "";
1!
1050
        String dataTypeName = row.getDataType() != null ? esc(row.getDataType().toString()) : "";
1!
1051
        termRows.append(
1✔
1052
            """
1053
            <tr>
1054
              <td><span class="qd-bucket qd-bucket-%s">%s</span></td>
1055
              <td><span class="qd-pill">%s</span></td>
1056
              <td><a class="qd-link data-explorer" data-name="%s" data-target="#explore-queue" data-toggle="modal" data-type="%s" data-type-label="%s" href="#"><code class="qd-code">%s</code></a></td>
1057
              <td class="qd-num">%s</td>
1058
            </tr>
1059
            """
1060
                .formatted(
1✔
1061
                    esc(tabName.toLowerCase()),
1✔
1062
                    esc(tabName),
1✔
1063
                    rowTypeLabel,
1064
                    esc(row.getStorageName()),
1✔
1065
                    dataTypeName,
1066
                    rowTypeLabel,
1067
                    esc(row.getStorageName()),
1✔
1068
                    sizeHtml));
1069
      }
1✔
1070
      terminalSection =
1✔
1071
          """
1072
          <section class="qd-section">
1073
            <div class="qd-section-head">
1074
              <h2 class="qd-section-title">Terminal Storage <span class="qd-count">%d</span></h2>
1075
              <span class="qd-section-hint">Shared buckets — completed and dead-letter messages.</span>
1076
            </div>
1077
            <table class="qd-table">
1078
              <thead><tr><th>Bucket</th><th>Type</th><th>Storage</th><th class="qd-num">Size</th></tr></thead>
1079
              <tbody>%s</tbody>
1080
            </table>
1081
          </section>
1082
          """
1083
              .formatted(terminalRows.size(), termRows);
1✔
1084
    }
1085

1086
    String main =
1✔
1087
        """
1088
        <div class="container qd">
1089
          <header class="qd-header">
1090
            <div class="qd-header-title">
1091
              <h1 class="qd-name">%s</h1>
1092
              <span class="qd-state %s"><i class="bx %s"></i> %s</span>
1093
              %s
1094
            </div>
1095
            <div class="qd-header-stats">
1096
              <span class="qd-stat"><strong>%d</strong> subscribers</span>
1097
              <span class="qd-stat-sep">·</span>
1098
              <span class="qd-stat"><strong>%d</strong> pending</span>
1099
              <span class="qd-stat-sep">·</span>
1100
              <span class="qd-stat"><strong>%d</strong> in-flight</span>
1101
            </div>
1102
          </header>
1103
          %s
1104
          <section class="qd-section">
1105
            <div class="qd-section-head">
1106
              <h2 class="qd-section-title">Subscribers <span class="qd-count">%d</span></h2>
1107
              <span class="qd-section-hint">Click a consumer to browse its messages.</span>
1108
            </div>
1109
            %s
1110
          </section>
1111
          %s
1112
          <details class="qd-charts" id="queue-detail-charts">
1113
            <summary class="qd-charts-head">
1114
              <span class="qd-section-title">Stats &amp; Latency</span>
1115
              <span class="qd-section-hint">Click to expand</span>
1116
            </summary>
1117
            <div class="qd-charts-body">
1118
              %s
1119
              <hr/>
1120
              %s
1121
            </div>
1122
          </details>
1123
          %s
1124
        </div>
1125
        """
1126
            .formatted(
1✔
1127
                configName,
1128
                stateCss,
1129
                stateIcon,
1130
                stateLabel,
1131
                pauseBtn,
1132
                subscribers.size(),
1✔
1133
                firstPending,
1✔
1134
                totalInFlight,
1✔
1135
                configStrip,
1136
                subscribers.size(),
1✔
1137
                subscribersSection,
1138
                terminalSection,
1139
                statsChartPartial(m),
1✔
1140
                latencyChartPartial(m),
1✔
1141
                dataExplorerModalPartial(m));
1✔
1142

1143
    String script =
1✔
1144
        """
1145
        <script type="application/javascript">
1146
          queueName = "%s";
1147
          dataPageUrl = "rqueue/api/v1/queue-data";
1148
          var chartParams = {"queue": queueName, "type": "STATS", 'aggregationType': 'DAILY'};
1149
          var latencyChartParams = {"queue": queueName, "type": "LATENCY", 'aggregationType': 'DAILY'};
1150
          $(document).ready(function () {
1151
            var chartsRendered = false;
1152
            function renderChartsOnce() {
1153
              if (chartsRendered) return;
1154
              chartsRendered = true;
1155
              drawChart(chartParams, "stats_chart");
1156
              drawChart(latencyChartParams, "latency_chart");
1157
              $('#refresh-chart').click(function () { refreshStatsChart(chartParams, "stats_chart"); });
1158
              $('#refresh-latency-chart').click(function () { refreshLatencyChart(latencyChartParams, "latency_chart"); });
1159
              attachChartEventListeners();
1160
            }
1161
            $('#queue-detail-charts').on('toggle', function () { if (this.open) renderChartsOnce(); });
1162
            $('#explore-queue').on('shown.bs.modal', function () {
1163
              $('#explorer-title').empty().append("Queue:").append("<b>&nbsp;" + queueName + "</b>").append("&nbsp;[" + (dataTypeLabel || dataType) + "]").append("<b>&nbsp;" + dataName + "</b>");
1164
              refreshPage();
1165
            });
1166
          });
1167
        </script>
1168
        """
1169
            .formatted(jsStr(get(m, "queueName")));
1✔
1170
    return base(m, main, script);
1✔
1171
  }
1172

1173
  public String renderRunning(Map<String, Object> m) {
1174
    List<Object> header = get(m, "header");
1✔
1175
    List<List<Object>> tasks = get(m, "tasks");
1✔
1176

1177
    StringBuilder headerRow = new StringBuilder();
1✔
1178
    if (header != null) {
1!
1179
      for (Object h : header) {
1✔
1180
        headerRow.append("<th>").append(esc(h)).append("</th>");
1✔
1181
      }
1✔
1182
    }
1183
    StringBuilder taskRows = new StringBuilder();
1✔
1184
    if (tasks != null) {
1!
1185
      for (List<Object> task : tasks) {
1✔
1186
        taskRows.append("<tr>");
1✔
1187
        for (Object td : task) {
1✔
1188
          taskRows.append("<td>").append(esc(td)).append("</td>");
1✔
1189
        }
1✔
1190
        taskRows.append("</tr>");
1✔
1191
      }
1✔
1192
    }
1193

1194
    String main =
1✔
1195
        """
1196
        <div class="container">
1197
          <div class="row table-responsive">
1198
            <table class="table table-bordered">
1199
              <thead><tr>%s</tr></thead>
1200
              <tbody>%s</tbody>
1201
            </table>
1202
          </div>
1203
        </div>
1204
        """
1205
            .formatted(headerRow, taskRows);
1✔
1206
    return base(m, main, "");
1✔
1207
  }
1208

1209
  public String renderUtility(Map<String, Object> m) {
1210
    boolean hideExploreData = bool(m, "hideExploreData");
1✔
1211
    boolean hideMoveMessages = bool(m, "hideMoveMessages");
1✔
1212

1213
    List<DataType> supportedDataType = get(m, "supportedDataType");
1✔
1214
    StringBuilder typeOptions = new StringBuilder();
1✔
1215
    if (supportedDataType != null) {
1!
1216
      for (DataType type : supportedDataType) {
1✔
1217
        typeOptions
1✔
1218
            .append("<option value=\"")
1✔
1219
            .append(esc(type.name()))
1✔
1220
            .append("\">")
1✔
1221
            .append(esc(type.getDescription()))
1✔
1222
            .append("</option>");
1✔
1223
      }
1✔
1224
    }
1225

1226
    StringBuilder sections = new StringBuilder();
1✔
1227
    sections.append("<div class=\"container\"><div class=\"row\">");
1✔
1228

1229
    if (!hideExploreData) {
1!
1230
      sections.append(
1✔
1231
          """
1232
          <div class="col-md-6">
1233
            <div class="col-md-12"><h2>Explore Data</h2></div>
1234
            <div class="explore-data-form col-md-12">
1235
              <div class="form-group">
1236
                <label for="data-name">Name: <b id="data-name-type"></b></label>
1237
                <input class="form-control" id="data-name" name="data-name" placeholder="__rq::queue::{job-queue}" type="text">
1238
              </div>
1239
              <div class="form-group display-none" id="data-key-form">
1240
                <label for="data-key">Key:</label>
1241
                <input class="form-control" id="data-key" name="data-key" placeholder="any key" type="text">
1242
              </div>
1243
              <div class="clearfix">
1244
                <button class="btn btn-primary" data-target="#explore-queue" data-toggle="modal" id="view-data" type="button">View</button>
1245
              </div>
1246
            </div>
1247
          </div>
1248
          """);
1249
    }
1250

1251
    if (!hideMoveMessages) {
1!
1252
      sections.append(
1✔
1253
          """
1254
          <div class="col-md-6">
1255
            <div class="col-md-12"><h2>Move Messages</h2></div>
1256
            <div class="message-move-form col-md-12">
1257
              <div class="form-group">
1258
                <label for="src-data">Source Data Set: <b id="src-data-type"></b></label>
1259
                <input class="form-control" id="src-data" name="src-data" placeholder="__rq::queue::{job-queue}" type="text">
1260
              </div>
1261
              <div class="form-group">
1262
                <label for="dst-data">Destination Data Set:<b id="dst-data-type"></b></label>
1263
                <input class="form-control" id="dst-data" name="dst-data" placeholder="__rq::queue::{job-morgue}" type="text">
1264
              </div>
1265
              <div class="form-group display-none" id="dst-data-type-input-form">
1266
                <label for="dst-data-type-input">Destination Data Type</label>
1267
                <select class="form-control" id="dst-data-type-input">
1268
                  <option selected="selected" value="">Select Type</option>
1269
                  %s
1270
                </select>
1271
              </div>
1272
              <div class="form-group">
1273
                <label for="number-of-messages">Number of messages: </label>
1274
                <input class="form-control" id="number-of-messages" name="dst-data" placeholder="100" type="text">
1275
              </div>
1276
              <div class="form-group display-none" id="priority-controller-form">
1277
                <hr/>
1278
                <div class="form-group">
1279
                  <label for="priority-type">Priority Type</label>
1280
                  <select class="form-control" id="priority-type" name="priority-type">
1281
                    <option value="">Select</option>
1282
                    <option value="ABS">Absolute</option>
1283
                    <option value="REL">Relative</option>
1284
                  </select>
1285
                </div>
1286
                <div class="form-group">
1287
                  <label for="priority-val">Priority</label>
1288
                  <input class="form-control" id="priority-val" name="priority-val" type="number">
1289
                </div>
1290
              </div>
1291
              <div class="clearfix">
1292
                <button class="btn btn-danger" id="move-button" type="button">Move</button>
1293
              </div>
1294
            </div>
1295
          </div>
1296
          """
1297
              .formatted(typeOptions));
1✔
1298
    }
1299

1300
    if (hideExploreData && hideMoveMessages) {
1!
NEW
1301
      sections.append(
×
1302
          """
1303
          <div class="col-md-12">
1304
            <div class="alert alert-info" role="alert">
1305
              Explore Data and Move Messages are not supported by the active backend.
1306
            </div>
1307
          </div>
1308
          """);
1309
    }
1310

1311
    sections.append("</div></div>");
1✔
1312
    sections.append(dataExplorerModalPartial(m));
1✔
1313
    String main = sections.toString();
1✔
1314

1315
    StringBuilder script = new StringBuilder("<script type=\"application/javascript\">\n");
1✔
1316
    if (!hideExploreData) {
1!
1317
      script.append(
1✔
1318
          """
1319
            dataPageUrl = "rqueue/api/v1/view-data";
1320
            var dataKeyEl = $('#data-key');
1321
            var dataNameEl = $('#data-name');
1322
            dataNameEl.on("change", function () { updateDataType(this, enableKeyForm); });
1323
            dataKeyEl.on("change", function () { $('#view-data').data('key', $(this).val()); });
1324
            $('#explore-queue').on('shown.bs.modal', function () {
1325
              $('#explorer-title').empty().append("Key:").append("<b>&nbsp;" + dataNameEl.val() + "</b>");
1326
              refreshPage();
1327
            });
1328
          """);
1329
    }
1330
    if (!hideMoveMessages) {
1!
1331
      script.append(
1✔
1332
          """
1333
            var srcDataEl = $('#src-data');
1334
            var dstDataEl = $('#dst-data');
1335
            var dstDataTypeInputEl = $('#dst-data-type-input');
1336
            dstDataTypeInputEl.on('change', function () {
1337
              var type = convertVal($(this).val());
1338
              if (type !== 'ZSET') { $('#priority-controller-form').hide(); } else { $('#priority-controller-form').show(); }
1339
            });
1340
            srcDataEl.on('change', function () { disableForms(); updateDataType(this, enableFormsIfRequired); });
1341
            dstDataEl.on('change', function () { disableForms(); updateDataType(this, enableFormsIfRequired); });
1342
          """);
1343
    }
1344
    script.append("</script>");
1✔
1345

1346
    return base(m, main, script.toString());
1✔
1347
  }
1348
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc