clusterinfo.html 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta name="mobile-web-app-capable" content="yes">
  5. <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1"/>
  6. <meta charset="UTF-8">
  7. <title>Cluster Info</title>
  8. <script src="../../script/jquery.min.js"></script>
  9. <script src="../../script/ao_module.js"></script>
  10. <script src="../../script/applocale.js"></script>
  11. <script src="../cluster/cluster.common.js"></script>
  12. <link rel="stylesheet" href="../cluster/cluster.css">
  13. </head>
  14. <body>
  15. <div id="cl-root">
  16. <div id="cl-header">
  17. <img src="../cluster/img/small_icon.png" alt="">
  18. <div style="flex:1">
  19. <h1 locale="cluster/info/title">Cluster Info</h1>
  20. <p locale="cluster/info/tagline">Health of this node, its peers, the namespace and the scheduler</p>
  21. </div>
  22. <span id="cl-refresh"></span>
  23. <div class="cl-links">
  24. <a href="../cluster/cluster.html" onclick="return CL.openPage('Cluster Settings', this.href)" locale="cluster/link/settings">Cluster Settings</a>
  25. <a href="../cluster/jobs.html" onclick="return CL.openPage('Cluster Jobs', this.href)" locale="cluster/link/jobs">Cluster Jobs</a>
  26. </div>
  27. </div>
  28. <div class="cl-notice" id="notice-standalone" style="display:none">
  29. <b locale="cluster/info/standalone">This node is not part of a cluster.</b>
  30. <span locale="cluster/info/standalonehint">Create or join one in Cluster Settings. Until then the cluster:/ drive stays hidden.</span>
  31. </div>
  32. <div class="cl-notice" id="notice-novolume" style="display:none">
  33. <b locale="cluster/info/novolume">The cluster has no storage yet.</b>
  34. <span locale="cluster/info/novolumehint">Add a volume in Cluster Settings to make the cluster:/ drive appear in File Manager.</span>
  35. </div>
  36. <div class="cl-grid">
  37. <!-- This node -->
  38. <div class="cl-card">
  39. <div class="cl-card-head">
  40. <span><span locale="cluster/thisnode">This node</span><span class="cl-sub cl-mono" id="cl-nodeid"></span></span>
  41. <button class="cl-btn small" onclick="refreshCapabilities()" title="Re-detect capabilities">
  42. <svg viewBox="0 0 16 16" fill="none"><path d="M13 8a5 5 0 1 1-1.5-3.6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M13 2v3h-3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
  43. <span locale="cluster/redetect">Re-detect</span>
  44. </button>
  45. </div>
  46. <div class="cl-card-body">
  47. <dl class="cl-kv">
  48. <dt locale="cluster/reach">Reachability</dt><dd id="v-reach">-</dd>
  49. <dt locale="cluster/version">Version</dt><dd id="v-version">-</dd>
  50. <dt locale="cluster/platform">Platform</dt><dd id="v-platform">-</dd>
  51. <dt locale="cluster/key">Node key</dt><dd class="cl-mono" id="v-key">-</dd>
  52. <dt locale="cluster/caps">Capabilities</dt><dd><div class="cl-chips" id="v-caps"></div></dd>
  53. </dl>
  54. <div class="cl-sep"></div>
  55. <dl class="cl-kv" id="v-health"></dl>
  56. </div>
  57. </div>
  58. <!-- Cluster summary -->
  59. <div class="cl-card" id="card-summary" style="display:none">
  60. <div class="cl-card-head"><span id="v-clustername">Cluster</span></div>
  61. <div class="cl-card-body">
  62. <dl class="cl-kv">
  63. <dt locale="cluster/clusterid">Cluster ID</dt><dd class="cl-mono" id="v-clusterid">-</dd>
  64. <dt locale="cluster/created">Created</dt><dd id="v-created">-</dd>
  65. <dt locale="cluster/members">Members</dt><dd id="v-membercount">-</dd>
  66. </dl>
  67. <div class="cl-sep"></div>
  68. <dl class="cl-kv" id="meta-kv"></dl>
  69. </div>
  70. </div>
  71. </div>
  72. <!-- Nodes -->
  73. <div class="cl-card" id="card-nodes" style="display:none">
  74. <div class="cl-card-head"><span locale="cluster/nodes">Nodes</span></div>
  75. <div class="cl-card-body" style="padding:0; overflow-x:auto">
  76. <table class="cl-table">
  77. <thead><tr>
  78. <th locale="cluster/node">Node</th>
  79. <th locale="cluster/state">State</th>
  80. <th locale="cluster/route">Route</th>
  81. <th locale="cluster/platform">Platform</th>
  82. <th locale="cluster/load">Load</th>
  83. <th locale="cluster/lastseen">Last seen</th>
  84. <th></th>
  85. </tr></thead>
  86. <tbody id="nodes-body"></tbody>
  87. </table>
  88. </div>
  89. <div class="cl-card-body" style="padding-top:0"><div class="cl-msg" id="msg-nodes"></div></div>
  90. </div>
  91. <!-- Replication -->
  92. <div class="cl-card" id="card-repl" style="display:none">
  93. <div class="cl-card-head">
  94. <span><span locale="cluster/repl/title">Replication</span><span class="cl-sub" id="repl-summary"></span></span>
  95. <span>
  96. <button class="cl-btn small" onclick="verifyCopies()"><span locale="cluster/repl/verify">Verify this node</span></button>
  97. <button class="cl-btn small" onclick="planNow()"><span locale="cluster/repl/plan">Repair now</span></button>
  98. </span>
  99. </div>
  100. <div class="cl-card-body">
  101. <div class="cl-grid">
  102. <dl class="cl-kv" id="repl-kv"></dl>
  103. <div>
  104. <div class="cl-field">
  105. <label locale="cluster/repl/inspect">Inspect a file</label>
  106. <div class="cl-row">
  107. <input type="text" id="inspect-path" placeholder="cluster:/photos/a.jpg">
  108. <button class="cl-btn cl-fixed" onclick="inspectFile()"><span locale="cluster/repl/inspectbtn">Show copies</span></button>
  109. </div>
  110. </div>
  111. <ul class="cl-inline-list" id="inspect-list"></ul>
  112. </div>
  113. </div>
  114. <div style="overflow-x:auto; margin-top:12px">
  115. <table class="cl-table">
  116. <thead><tr><th locale="cluster/repl/task">Task</th><th locale="cluster/repl/route">From / to</th><th locale="cluster/state">State</th><th locale="cluster/when">When</th></tr></thead>
  117. <tbody id="repl-tasks"></tbody>
  118. </table>
  119. </div>
  120. <div class="cl-msg" id="msg-repl"></div>
  121. </div>
  122. </div>
  123. <!-- Scheduling -->
  124. <div class="cl-card" id="card-sched" style="display:none">
  125. <div class="cl-card-head">
  126. <span><span locale="cluster/sched/title">Scheduling</span><span class="cl-sub" id="sched-summary"></span></span>
  127. </div>
  128. <div class="cl-card-body">
  129. <div class="cl-hint" locale="cluster/sched/infohint">How each node scores right now with the weights set in Cluster Settings. Jobs, new files and extra copies go to the highest score.</div>
  130. <div style="overflow-x:auto; margin-top:10px">
  131. <table class="cl-table">
  132. <thead><tr>
  133. <th locale="cluster/node">Node</th><th locale="cluster/sched/cpu">CPU free</th><th locale="cluster/sched/mem">RAM free</th>
  134. <th locale="cluster/sched/diskfree">Disk free</th><th locale="cluster/sched/ping">Round trip</th><th locale="cluster/sched/score">Score</th>
  135. </tr></thead>
  136. <tbody id="sched-body"></tbody>
  137. </table>
  138. </div>
  139. <div id="sched-matrix-wrap" style="display:none; margin-top:12px">
  140. <div class="cl-hint" locale="cluster/sched/matrixhint">Round trip between each pair of nodes, in milliseconds. Copies of a file are spread over the slowest pairs, because distance usually means a different site.</div>
  141. <div style="overflow-x:auto; margin-top:6px">
  142. <table class="cl-table" id="sched-matrix"></table>
  143. </div>
  144. </div>
  145. <div class="cl-sep"></div>
  146. <div class="cl-field">
  147. <label locale="cluster/sched/explain">Why did a job land there?</label>
  148. <div class="cl-row">
  149. <input type="text" id="sched-job" placeholder="job id">
  150. <button class="cl-btn cl-fixed" onclick="explainJob()"><span locale="cluster/sched/explainbtn">Explain</span></button>
  151. </div>
  152. <div class="cl-hint" locale="cluster/sched/explainhint">Copy a job id from the Cluster Jobs page to see the ranking it was placed with.</div>
  153. </div>
  154. <ul class="cl-inline-list" id="sched-explain"></ul>
  155. <div class="cl-msg" id="msg-sched"></div>
  156. </div>
  157. </div>
  158. </div>
  159. <script>
  160. var t = CL.t, fmt = CL.fmt, esc = CL.esc;
  161. var API = '../../system/cluster/';
  162. var clusterStatus = null;
  163. CL.applyTheme('cl-root');
  164. function post(path, data, cb) { CL.apiPost(API + path, data, cb); }
  165. function fail(box, err, r) {
  166. var e = err || CL.apiResult(r);
  167. if (e) { CL.showMsg(box, e, false); return true; }
  168. return false;
  169. }
  170. function nodeName(id) {
  171. if (!clusterStatus) return id;
  172. for (var i = 0; i < clusterStatus.nodes.length; i++) if (clusterStatus.nodes[i].id === id) return clusterStatus.nodes[i].name;
  173. return String(id || '').substring(0, 8);
  174. }
  175. function badge(state, label) { return '<span class="cl-badge ' + esc(state) + '">' + esc(label || CL.state(state)) + '</span>'; }
  176. /* This node */
  177. function healthRows(h) {
  178. if (!h || !h.timestamp) return '<dt>' + esc(t('cluster/load', 'Load')) + '</dt><dd style="color:var(--cl-muted)">' + esc(t('cluster/noreport', 'no report yet')) + '</dd>';
  179. var ramPct = h.ramTotal > 0 ? h.ramUsed / h.ramTotal * 100 : 0;
  180. var diskPct = h.diskTotal > 0 ? (h.diskTotal - h.diskFree) / h.diskTotal * 100 : 0;
  181. return '<dt>CPU</dt><dd>' + h.cpuUsage.toFixed(0) + '%' + CL.bar(h.cpuUsage) + '</dd>' +
  182. '<dt>RAM</dt><dd>' + (h.ramTotal > 0 ? CL.fmtBytes(h.ramUsed) + ' / ' + CL.fmtBytes(h.ramTotal) + CL.bar(ramPct) : '-') + '</dd>' +
  183. '<dt>' + esc(t('cluster/disk', 'Disk')) + '</dt><dd>' + (h.diskTotal > 0 ? esc(fmt(t('cluster/diskfree', '{0} free of {1}'), CL.fmtBytes(h.diskFree), CL.fmtBytes(h.diskTotal))) + CL.bar(diskPct) : '-') + '</dd>' +
  184. '<dt>' + esc(t('cluster/uptime', 'Uptime')) + '</dt><dd>' + esc(fmt(t('cluster/uptimevalue', '{0}h {1}m'), Math.floor(h.uptime / 3600), Math.floor((h.uptime % 3600) / 60))) + '</dd>';
  185. }
  186. function routeOf(n) {
  187. if (n.local) return esc(t('cluster/thisnode', 'This node'));
  188. if (n.tunnel) return esc(t('cluster/tunnelhere', 'Tunnel to this node'));
  189. if (n.advertiseUrl) return '<a href="' + esc(n.advertiseUrl) + '" target="_blank" style="color:var(--cl-accent)">' + esc(n.advertiseUrl.replace(/^https?:\/\//, '')) + '</a>';
  190. if (n.tunnelVia) return esc(fmt(t('cluster/tunnelvia', 'Tunnel via {0}'), nodeName(n.tunnelVia)));
  191. return '<span style="color:var(--cl-muted)">' + esc(t('cluster/noroute', 'No route')) + '</span>';
  192. }
  193. function render() {
  194. var s = clusterStatus, local = s.local;
  195. document.getElementById('cl-nodeid').textContent = ' ' + local.nodeId;
  196. var reach;
  197. if (local.reachable) {
  198. reach = badge('ONLINE', t('cluster/direct', 'DIRECT')) + ' ' + esc(local.config.advertiseUrl);
  199. } else if (local.tunnelConnected) {
  200. reach = badge('ONLINE', t('cluster/tunnel', 'TUNNEL')) + ' ' + esc(fmt(t('cluster/via', 'via {0}'), nodeName(local.tunnelHost)));
  201. } else if (s.inCluster) {
  202. reach = badge('OFFLINE', t('cluster/notunnel', 'NO TUNNEL')) + ' ' + esc(t('cluster/waitinghost', 'waiting for a reachable member'));
  203. } else {
  204. reach = badge('UNKNOWN', t('cluster/natonly', 'NO URL')) + ' ' + esc(t('cluster/willtunnel', 'will tunnel through a member once in a cluster'));
  205. }
  206. document.getElementById('v-reach').innerHTML = reach;
  207. document.getElementById('v-version').textContent = local.version;
  208. var c = local.capabilities || {};
  209. document.getElementById('v-platform').textContent = fmt(t('cluster/platformline', '{0} / {1}, {2} cores'), c.os || '?', c.arch || '?', c.cpuCores || '?') + (c.totalRam > 0 ? ', ' + CL.fmtBytes(c.totalRam) + ' RAM' : '') + (c.hostname ? ' (' + c.hostname + ')' : '');
  210. document.getElementById('v-key').textContent = (local.publicKey || '').substring(0, 20) + '...';
  211. var chips = '';
  212. Object.keys(c.features || {}).filter(function (k) { return c.features[k]; }).sort().forEach(function (f) { chips += '<span class="cl-chip">' + esc(f) + '</span>'; });
  213. document.getElementById('v-caps').innerHTML = chips || '<span style="color:var(--cl-muted)">-</span>';
  214. document.getElementById('v-health').innerHTML = healthRows(local.health);
  215. document.getElementById('notice-standalone').style.display = s.inCluster ? 'none' : '';
  216. ['card-summary', 'card-nodes', 'card-repl', 'card-sched'].forEach(function (id) {
  217. document.getElementById(id).style.display = s.inCluster ? '' : 'none';
  218. });
  219. if (s.inCluster) {
  220. document.getElementById('v-clustername').textContent = s.cluster.name;
  221. document.getElementById('v-clusterid').textContent = s.cluster.id;
  222. document.getElementById('v-created').textContent = CL.fmtDate(s.cluster.created);
  223. document.getElementById('v-membercount').textContent = s.nodes.length;
  224. renderNodes(s.nodes || []);
  225. } else {
  226. document.getElementById('notice-novolume').style.display = 'none';
  227. }
  228. document.getElementById('cl-refresh').textContent = fmt(t('cluster/updated', 'Updated {0}'), new Date().toLocaleTimeString());
  229. }
  230. function renderNodes(nodes) {
  231. var html = '';
  232. nodes.forEach(function (n) {
  233. var c = n.capabilities || {}, h = n.health || {};
  234. var load = h.timestamp
  235. ? 'CPU ' + h.cpuUsage.toFixed(0) + '%' + (h.ramTotal > 0 ? ' · RAM ' + (h.ramUsed / h.ramTotal * 100).toFixed(0) + '%' : '') + (h.diskTotal > 0 ? ' · ' + esc(fmt(t('cluster/freeamount', '{0} free'), CL.fmtBytes(h.diskFree))) : '')
  236. : '<span style="color:var(--cl-muted)">-</span>';
  237. var feats = Object.keys(c.features || {}).filter(function (k) { return c.features[k] && ['ffmpeg', 'docker', 'cuda', 'gpu', 'git'].indexOf(k) >= 0; });
  238. var probe = n.local ? '' : '<button class="cl-btn small" onclick="probeNode(\'' + esc(n.id) + '\', this)">' + esc(t('cluster/probe', 'Probe')) + '</button>';
  239. html += '<tr>' +
  240. '<td><div class="cl-node-name">' + esc(n.name) + (n.local ? ' <span class="cl-chip">' + esc(t('cluster/you', 'you')) + '</span>' : '') + '</div><div class="cl-node-meta cl-mono">' + esc(n.id) + '</div></td>' +
  241. '<td>' + badge(n.state) + '</td>' +
  242. '<td>' + routeOf(n) + '</td>' +
  243. '<td>' + esc(c.os || '?') + '/' + esc(c.arch || '?') + '<div class="cl-node-meta">' + esc(fmt(t('cluster/coresvalue', '{0} cores'), c.cpuCores || '?')) + (c.totalRam > 0 ? ', ' + CL.fmtBytes(c.totalRam) : '') + (feats.length ? ' · ' + esc(feats.join(', ')) : '') + '</div></td>' +
  244. '<td>' + load + '</td>' +
  245. '<td>' + esc(n.local ? t('cluster/now', 'now') : CL.fmtAgo(n.lastSeen)) + '<div class="cl-node-meta">' + esc(n.version) + '</div></td>' +
  246. '<td>' + probe + '</td></tr>';
  247. });
  248. document.getElementById('nodes-body').innerHTML = html || '<tr><td colspan="7" class="cl-empty">' + esc(t('cluster/nonodes', 'No nodes')) + '</td></tr>';
  249. }
  250. function refreshCapabilities() {
  251. $.getJSON(API + 'capabilities?refresh=true', function () { loadStatus(); });
  252. }
  253. function probeNode(id, btn) {
  254. btn.disabled = true;
  255. $.getJSON(API + 'node/probe?id=' + encodeURIComponent(id), function (r) {
  256. btn.disabled = false;
  257. var route = String(r.route || '');
  258. route = route.indexOf('relay via ') === 0
  259. ? fmt(t('cluster/route/relay', 'relay via {0}'), nodeName(route.substring(10)))
  260. : t('cluster/route/' + route, route);
  261. if (r.ok) CL.showMsg('msg-nodes', fmt(t('cluster/probeok', '{0}: {1} ms ({2})'), nodeName(id), r.latencyMs, route), true);
  262. else CL.showMsg('msg-nodes', fmt(t('cluster/probefail', '{0}: {1} ({2})'), nodeName(id), CL.tr(r.error || t('cluster/unreachable', 'unreachable')), route), false);
  263. }).fail(function () { btn.disabled = false; });
  264. }
  265. /* Namespace */
  266. function loadMeta() {
  267. $.getJSON(API + 'meta/status', function (st) {
  268. if (CL.apiResult(st)) return;
  269. var leader = st.leader
  270. ? esc(st.leaderName) + (st.isLeader ? ' <span class="cl-chip">' + esc(t('cluster/thisnode', 'This node')) + '</span>' : '') + ' <span class="cl-node-meta">' + esc(fmt(t('cluster/meta/term', 'term {0}'), st.term)) + '</span>'
  271. : badge('UNKNOWN', t('cluster/meta/noleader', 'ELECTING'));
  272. document.getElementById('meta-kv').innerHTML =
  273. '<dt>' + esc(t('cluster/meta/leader', 'Leader')) + '</dt><dd>' + leader + '</dd>' +
  274. '<dt>' + esc(t('cluster/meta/entries', 'Entries')) + '</dt><dd>' + esc(fmt(t('cluster/meta/entriesvalue', '{0} files, {1} folders, {2} volumes'), st.files, st.dirs, st.volumes)) + '</dd>' +
  275. '<dt>' + esc(t('cluster/meta/log', 'Log')) + '</dt><dd>' + st.applied + ' / ' + st.lastSeq + (st.pending ? ' <span style="color:var(--cl-warn)">' + esc(fmt(t('cluster/meta/pending', '{0} pending'), st.pending)) + '</span>' : '') + '</dd>';
  276. document.getElementById('notice-novolume').style.display = (clusterStatus && clusterStatus.inCluster && st.volumes === 0) ? '' : 'none';
  277. });
  278. }
  279. /* Replication */
  280. function loadRepl() {
  281. $.getJSON(API + 'repl/status', function (st) {
  282. if (CL.apiResult(st)) return;
  283. var problems = st.underReplicated + st.noHealthyCopy + st.staleCopies;
  284. document.getElementById('repl-summary').textContent = ' ' + (problems === 0 ? t('cluster/repl/healthy', 'all files at policy') : fmt(t('cluster/repl/issues', '{0} issues'), problems));
  285. var planner = st.isLeader ? badge('ONLINE', t('cluster/thisnode', 'This node')) : (st.leader ? esc(st.leaderName) : badge('UNKNOWN', t('cluster/meta/noleader', 'ELECTING')));
  286. if (st.lastPlan) planner += ' <span class="cl-node-meta">' + esc(fmt(t('cluster/repl/lastrun', 'last run {0}'), CL.fmtAgo(st.lastPlan))) + '</span>';
  287. var inflight = fmt(t('cluster/repl/inflightvalue', '{0} running, {1} queued'), st.running, st.queued) + (st.evacuating ? ', ' + fmt(t('cluster/repl/evacuating', '{0} volume(s) evacuating'), st.evacuating) : '');
  288. document.getElementById('repl-kv').innerHTML =
  289. '<dt>' + esc(t('cluster/repl/planner', 'Planner')) + '</dt><dd>' + planner + '</dd>' +
  290. '<dt>' + esc(t('cluster/repl/files', 'Files')) + '</dt><dd>' + st.files + '</dd>' +
  291. '<dt>' + esc(t('cluster/repl/under', 'Under-replicated')) + '</dt><dd>' + (st.underReplicated ? '<span style="color:var(--cl-warn)">' + st.underReplicated + '</span>' : '0') + '</dd>' +
  292. '<dt>' + esc(t('cluster/repl/nocopy', 'No healthy copy')) + '</dt><dd>' + (st.noHealthyCopy ? '<span style="color:var(--cl-danger)">' + st.noHealthyCopy + '</span>' : '0') + '</dd>' +
  293. '<dt>' + esc(t('cluster/repl/stale', 'Stale copies')) + '</dt><dd>' + st.staleCopies + '</dd>' +
  294. '<dt>' + esc(t('cluster/repl/over', 'Extra copies')) + '</dt><dd>' + st.overReplicated + '</dd>' +
  295. '<dt>' + esc(t('cluster/repl/inflight', 'In flight')) + '</dt><dd>' + esc(inflight) + '</dd>';
  296. var tasks = (st.tasks || []).concat((st.history || []).slice(0, 10));
  297. var html = '';
  298. tasks.forEach(function (tk) {
  299. var cls = tk.state === 'done' ? 'ONLINE' : (tk.state === 'failed' ? 'OFFLINE' : (tk.state === 'running' ? 'DEGRADED' : 'UNKNOWN'));
  300. html += '<tr><td><div class="cl-node-name">' + esc(tk.path) + '</div><div class="cl-node-meta">' + esc(t('cluster/repl/reason/' + tk.reason, tk.reason)) + ' · ' + CL.fmtBytes(tk.size) + (tk.error ? ' · <span style="color:var(--cl-danger)">' + esc(CL.tr(tk.error)) + '</span>' : '') + '</div></td>' +
  301. '<td>' + esc(fmt(t('cluster/repl/fromto', '{0} to {1}'), nodeName(tk.sourceNode), nodeName(tk.targetNode))) + '</td>' +
  302. '<td>' + badge(cls, CL.state(tk.state)) + (tk.attempts > 1 ? ' <span class="cl-node-meta">x' + tk.attempts + '</span>' : '') + '</td>' +
  303. '<td>' + esc(CL.fmtAgo(tk.updated || tk.created)) + '</td></tr>';
  304. });
  305. document.getElementById('repl-tasks').innerHTML = html || '<tr><td colspan="4" class="cl-empty">' + esc(t('cluster/repl/notasks', 'No replication tasks')) + '</td></tr>';
  306. });
  307. }
  308. function planNow() {
  309. post('repl/plan', {}, function (err, r) {
  310. if (fail('msg-repl', err, r)) return;
  311. CL.showMsg('msg-repl', fmt(t('cluster/repl/planned', 'Planner ran: {0} queued, {1} dropped, {2} marked stale'), r.queued, r.dropped, r.markedStale), true);
  312. loadRepl();
  313. });
  314. }
  315. function verifyCopies() {
  316. post('repl/verify', { budget: 512 }, function (err, r) {
  317. if (fail('msg-repl', err, r)) return;
  318. CL.showMsg('msg-repl', fmt(t('cluster/repl/verified', 'Verified {0} copies ({1}), {2} stale'), r.checked, CL.fmtBytes(r.bytes), r.stale), r.stale === 0);
  319. loadRepl();
  320. });
  321. }
  322. function inspectFile() {
  323. var p = document.getElementById('inspect-path').value.trim();
  324. if (!p) return;
  325. $.getJSON(API + 'meta/stat?path=' + encodeURIComponent(p), function (rec) {
  326. if (fail('msg-repl', null, rec)) return;
  327. var html = '<li><span>' + esc(rec.path) + '</span><span class="cl-node-meta">' + (rec.isDir ? esc(t('cluster/folder', 'folder')) : CL.fmtBytes(rec.size) + ' · ' + esc((rec.checksum || '').substring(0, 12))) + '</span></li>';
  328. (rec.locations || []).forEach(function (l) {
  329. var cls = l.state === 'verified' || l.state === 'committed' ? 'ONLINE' : (l.state === 'stale' || l.state === 'failed' ? 'OFFLINE' : 'DEGRADED');
  330. //Named by ID as well: two nodes may answer to the same name
  331. html += '<li><span>' + esc(nodeName(l.nodeId)) + (l.volumeId === rec.primary ? ' <span class="cl-chip">' + esc(t('cluster/repl/primary', 'primary')) + '</span>' : '') +
  332. '<div class="cl-node-meta cl-mono">' + esc(l.nodeId) + '</div></span><span>' + badge(cls, CL.state(l.state)) + ' <span class="cl-node-meta">' + esc(CL.fmtAgo(l.updated)) + '</span></span></li>';
  333. });
  334. if (!rec.isDir && (rec.locations || []).length === 0) html += '<li class="cl-empty">' + esc(t('cluster/repl/nolocations', 'No copies recorded')) + '</li>';
  335. document.getElementById('inspect-list').innerHTML = html;
  336. }).fail(function () { CL.showMsg('msg-repl', t('cluster/repl/notfound', 'Path not found in the namespace'), false); });
  337. }
  338. /* Scheduling */
  339. function loadSched() {
  340. $.getJSON(API + 'sched/status', function (st) {
  341. if (CL.apiResult(st)) return;
  342. var rows = '';
  343. (st.nodes || []).forEach(function (n) {
  344. var pct = Math.round(n.score * 100);
  345. rows += '<tr><td><div class="cl-node-name">' + esc(n.name) + (n.local ? ' <span class="cl-chip">' + esc(t('cluster/thisnode', 'This node')) + '</span>' : '') + '</div>' +
  346. '<div class="cl-node-meta">' + badge(String(n.state).toUpperCase()) + ' ' + (n.features ? esc(fmt(t('cluster/sched/features', '{0} features'), n.features)) : '') + '</div></td>' +
  347. '<td>' + Math.round(n.cpuFree * 100) + '%</td>' +
  348. '<td>' + Math.round(n.memFree * 100) + '%</td>' +
  349. '<td>' + Math.round(n.diskFree * 100) + '%</td>' +
  350. '<td>' + (n.local ? esc(t('cluster/sched/here', 'local')) : (n.latencyMs < 0 ? '-' : Math.round(n.latencyMs) + ' ms')) + '</td>' +
  351. '<td>' + pct + '%' + CL.bar(pct) + '</td></tr>';
  352. });
  353. document.getElementById('sched-body').innerHTML = rows || '<tr><td colspan="6" class="cl-empty">' + esc(t('cluster/sched/nonodes', 'No nodes to score')) + '</td></tr>';
  354. var top = (st.nodes || [])[0];
  355. document.getElementById('sched-summary').textContent = top ? ' ' + fmt(t('cluster/sched/next', 'next job would run on {0}'), top.name) : '';
  356. renderMatrix(st);
  357. });
  358. }
  359. function renderMatrix(st) {
  360. var wrap = document.getElementById('sched-matrix-wrap');
  361. var nodes = st.nodes || [], matrix = st.matrix || {};
  362. if (nodes.length < 2 || Object.keys(matrix).length === 0) { wrap.style.display = 'none'; return; }
  363. var head = '<thead><tr><th>' + esc(t('cluster/sched/from', 'From / to')) + '</th>';
  364. nodes.forEach(function (n) { head += '<th>' + esc(n.name) + '</th>'; });
  365. var body = '</tr></thead><tbody>';
  366. nodes.forEach(function (from) {
  367. body += '<tr><td><div class="cl-node-name">' + esc(from.name) + '</div></td>';
  368. nodes.forEach(function (to) {
  369. var cell;
  370. if (from.id === to.id) cell = '<span class="cl-node-meta">-</span>';
  371. else {
  372. var v = (matrix[from.id] || {})[to.id];
  373. cell = (v === undefined) ? '<span class="cl-node-meta">' + esc(t('cluster/sched/unmeasured', 'not measured')) + '</span>' : (v < 10 ? v.toFixed(1) : Math.round(v)) + ' ms';
  374. }
  375. body += '<td>' + cell + '</td>';
  376. });
  377. body += '</tr>';
  378. });
  379. document.getElementById('sched-matrix').innerHTML = head + body + '</tbody>';
  380. wrap.style.display = '';
  381. }
  382. function explainJob() {
  383. var id = document.getElementById('sched-job').value.trim();
  384. if (!id) return;
  385. $.getJSON(API + 'sched/explain?job=' + encodeURIComponent(id), function (res) {
  386. if (fail('msg-sched', null, res)) return;
  387. var html = '<li><span>' + esc(res.job || id) + '</span><span class="cl-node-meta">' + esc(CL.state(res.status)) + (res.node ? ' · ' + esc(nodeName(res.node)) : '') + '</span></li>';
  388. (res.ranking || []).forEach(function (r) {
  389. var detail = r.eligible ? (r.factors || []).map(function (f) {
  390. return esc(t('cluster/sched/factor/' + f.name, f.name)) + ' ' + (f.points >= 0 ? '+' : '') + f.points.toFixed(2);
  391. }).join(', ') : esc(CL.tr(r.reason));
  392. html += '<li><span>' + esc(r.nodeName) + ' <span class="cl-node-meta">' + detail + '</span></span>' +
  393. '<span>' + (r.eligible ? (r.score * 100).toFixed(0) + '%' : badge('OFFLINE', t('cluster/sched/skipped', 'SKIPPED'))) + '</span></li>';
  394. });
  395. document.getElementById('sched-explain').innerHTML = html;
  396. }).fail(function () { CL.showMsg('msg-sched', t('cluster/sched/nojob', 'No such job on this cluster'), false); });
  397. }
  398. function loadStatus() {
  399. $.getJSON(API + 'status', function (data) {
  400. if (CL.apiResult(data)) return;
  401. clusterStatus = data;
  402. render();
  403. if (data.inCluster) { loadMeta(); loadRepl(); loadSched(); }
  404. });
  405. }
  406. CL.init(function () {
  407. loadStatus();
  408. CL.every(loadStatus, 10000, 'cl-root');
  409. });
  410. </script>
  411. </body>
  412. </html>