Refine dark theme readability and chart visibility across dashboard pages

This commit is contained in:
Nate Emmert committed 2026-03-10 14:54:40 -04:00
1 parent b9da742b3d
commit 0475bf5aa5
20 files changed
+929 -320

No files matched your search

+6 -6
View File
@@ -1,15 +1,15 @@
{ {
"files": { "files": {
"main.css": "/static/css/main.69110889.css", "main.css": "/static/css/main.6034edfe.css",
"main.js": "/static/js/main.820bab7f.js", "main.js": "/static/js/main.243399c3.js",
"static/js/453.d456ce70.chunk.js": "/static/js/453.d456ce70.chunk.js", "static/js/453.d456ce70.chunk.js": "/static/js/453.d456ce70.chunk.js",
"index.html": "/index.html", "index.html": "/index.html",
"main.69110889.css.map": "/static/css/main.69110889.css.map", "main.6034edfe.css.map": "/static/css/main.6034edfe.css.map",
"main.820bab7f.js.map": "/static/js/main.820bab7f.js.map", "main.243399c3.js.map": "/static/js/main.243399c3.js.map",
"453.d456ce70.chunk.js.map": "/static/js/453.d456ce70.chunk.js.map" "453.d456ce70.chunk.js.map": "/static/js/453.d456ce70.chunk.js.map"
}, },
"entrypoints": [ "entrypoints": [
"static/css/main.69110889.css", "static/css/main.6034edfe.css",
"static/js/main.820bab7f.js" "static/js/main.243399c3.js"
] ]
} }
+1 -1
View File
@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" type="image/png" href="/app-icon.png"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="SkyWatch - Infrastructure monitoring and alerting system"/><link rel="apple-touch-icon" href="/app-icon.png"/><link rel="manifest" href="/manifest.json"/><title>SkyWatch</title><script defer="defer" src="/static/js/main.820bab7f.js"></script><link href="/static/css/main.69110889.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html> <!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" type="image/png" href="/app-icon.png"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="SkyWatch - Infrastructure monitoring and alerting system"/><link rel="apple-touch-icon" href="/app-icon.png"/><link rel="manifest" href="/manifest.json"/><title>SkyWatch</title><script defer="defer" src="/static/js/main.243399c3.js"></script><link href="/static/css/main.6034edfe.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
@@ -0,0 +1,2 @@
body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background-color:#020617;color:#e2e8f0;font-family:Manrope,IBM Plex Sans,Segoe UI,sans-serif;margin:0}*{box-sizing:border-box}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}.App{background:radial-gradient(circle at 15% 0,#1e40af33 0,#0000 42%),radial-gradient(circle at 95% 10%,#0f766e24 0,#0000 36%),#020617;min-height:100vh}.navbar{backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);background:#0f172ad1;border-bottom:1px solid #334155;box-shadow:0 4px 18px #0f172a0f;color:#e2e8f0;padding:.85rem 2rem;position:-webkit-sticky;position:sticky;top:0;z-index:30}.navbar h1{font-size:1.5rem;letter-spacing:-.02em;margin:0}.navbar-inner{gap:1rem;justify-content:space-between;width:100%}.navbar-inner,.navbar-left{align-items:center;display:flex}.navbar-left{gap:1.5rem;min-width:0}.navbar-links{align-items:center;display:flex;flex-wrap:wrap;gap:.5rem}.nav-link{border-radius:7px;color:#cbd5e1;font-size:.92rem;font-weight:600;padding:.4rem .7rem;text-decoration:none;transition:background .2s,color .2s,box-shadow .2s}.nav-link:hover{background:#111827;box-shadow:inset 0 0 0 1px #334155;color:#f8fafc}.settings-link{align-items:center;display:inline-flex;gap:.4rem}.setup-badge{background:#facc15;border-radius:999px;color:#111827;font-size:.68rem;font-weight:700;padding:.15rem .5rem}.notifications-pill{align-items:center;background:#111827;border:1px solid #334155;border-radius:999px;color:#e2e8f0;display:flex;font-size:.82rem;gap:.45rem;padding:.3rem .7rem}.container{margin:0 auto;max-width:1400px;padding:2rem}.dashboard-toolbar{align-items:center;display:flex;gap:1rem;justify-content:space-between;margin-bottom:2rem}.dashboard-title{color:#f8fafc;font-size:1.85rem;letter-spacing:-.02em;margin:0}.dashboard-subtitle{color:#94a3b8;font-size:.9rem;margin:.2rem 0 0}.dashboard-actions{align-items:center;display:flex;flex-wrap:wrap;gap:.5rem}.quick-filter-row{display:flex;flex-wrap:wrap;gap:.45rem;margin:.2rem 0 .8rem}.quick-chip{background:#0f172a;border:1px solid #334155;border-radius:999px;color:#cbd5e1;cursor:pointer;font-size:.74rem;font-weight:700;letter-spacing:.02em;padding:.28rem .7rem}.quick-chip:hover{background:#111f34}.quick-chip.active{background:#14b8a6;border-color:#14b8a6;color:#052e2b}.tag-filter-input{background:#111827;border:1px solid #334155;border-radius:10px;color:#e2e8f0;min-width:180px;padding:.5rem .75rem}.actions-menu-wrapper{position:relative}.actions-menu{background:#fff;border:1px solid #dbe2ea;border-radius:8px;box-shadow:0 8px 20px #0f172a24;min-width:220px;padding:.35rem 0;position:absolute;right:0;top:110%;z-index:20}.actions-menu-divider{border:0;border-top:1px solid #eef2f6;margin:.25rem 0}.hidden-file-input{display:none}.toast-stack{position:fixed;right:20px;top:20px;z-index:9999}.toast-item{border:1px solid #0000;border-radius:8px;box-shadow:0 8px 20px #0f172a29;margin-bottom:10px;min-width:280px;padding:12px 16px}.toast-error{background:#fee2e2;border-color:#fecaca;color:#7f1d1d}.toast-success{background:#dcfce7;border-color:#bbf7d0;color:#14532d}.toast-info{background:#dbeafe;border-color:#bfdbfe;color:#1e3a8a}.cc-metrics-grid{grid-gap:.65rem;display:grid;gap:.65rem;grid-template-columns:repeat(6,minmax(0,1fr));margin-bottom:1rem}.cc-metric-card{background:linear-gradient(180deg,#0f172af5,#1e293bf5);border:1px solid #334155;border-radius:10px;color:#e2e8f0;padding:.75rem}.cc-metric-label{color:#94a3b8;font-size:.68rem;letter-spacing:.08em;margin:0;text-transform:uppercase}.cc-metric-value{font-size:1.2rem;font-weight:700;margin:.35rem 0 0}.metric-ok{border-color:#14532d}.metric-down{border-color:#7f1d1d}.metric-maint{border-color:#78350f}.cc-layout{grid-gap:.8rem;align-items:start;display:grid;gap:.8rem;grid-template-columns:minmax(0,1fr) 300px}.cc-main{min-width:0}.cc-rail{display:flex;flex-direction:column;gap:.75rem;position:-webkit-sticky;position:sticky;top:82px}.cc-panel{background:#0f172af0;border:1px solid #334155;border-radius:12px;color:#e2e8f0;padding:.75rem}.cc-panel h3{font-size:.9rem;letter-spacing:.03em;margin:0 0 .65rem}.cc-empty{color:#94a3b8;font-size:.85rem;margin:0}.cc-group-list,.cc-incident-list{display:flex;flex-direction:column;gap:.4rem}.cc-group-item,.cc-incident-item{align-items:center;background:#0f172a;border:1px solid #334155;border-radius:8px;color:#e2e8f0;cursor:pointer;display:flex;justify-content:space-between;padding:.45rem .55rem;width:100%}.cc-group-item:hover,.cc-incident-item:hover{background:#111f34;border-color:#475569}.cc-incident-name{font-size:.85rem;font-weight:600;margin-right:.5rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dashboard-grid{grid-gap:1rem;display:grid;gap:1rem;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));margin-bottom:1.5rem}.resource-list{display:flex;flex-direction:column;gap:.45rem}.resource-list-shell{background:#0f172ae6;border:1px solid #334155;border-radius:12px;max-height:calc(100vhLine truncated
/*# sourceMappingURL=main.6034edfe.css.map*/
@@ -0,0 +1 @@
{"version":3,"file":"static/css/main.6034edfe.css","mappings":"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 truncated
@@ -1,2 +0,0 @@
body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background-color:#f8fafc;color:#0f172a;font-family:Manrope,IBM Plex Sans,Segoe UI,sans-serif;margin:0}*{box-sizing:border-box}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}.App{background:radial-gradient(circle at 15% 0,#dff7f2 0,#0000 40%),radial-gradient(circle at 95% 10%,#e8eefc 0,#0000 38%),#f8fafc;min-height:100vh}.navbar{backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);background:#ffffffd9;border-bottom:1px solid #dbe2ea;box-shadow:0 4px 18px #0f172a0f;color:#0f172a;padding:.85rem 2rem;position:-webkit-sticky;position:sticky;top:0;z-index:30}.navbar h1{font-size:1.5rem;letter-spacing:-.02em;margin:0}.navbar-inner{gap:1rem;justify-content:space-between;width:100%}.navbar-inner,.navbar-left{align-items:center;display:flex}.navbar-left{gap:1.5rem;min-width:0}.navbar-links{align-items:center;display:flex;flex-wrap:wrap;gap:.5rem}.nav-link{border-radius:7px;color:#475569;font-size:.92rem;font-weight:600;padding:.4rem .7rem;text-decoration:none;transition:background .2s,color .2s,box-shadow .2s}.nav-link:hover{background:#fff;box-shadow:inset 0 0 0 1px #dbe2ea;color:#0f172a}.settings-link{align-items:center;display:inline-flex;gap:.4rem}.setup-badge{background:#facc15;border-radius:999px;color:#111827;font-size:.68rem;font-weight:700;padding:.15rem .5rem}.notifications-pill{align-items:center;background:#fff;border:1px solid #dbe2ea;border-radius:999px;color:#334155;display:flex;font-size:.82rem;gap:.45rem;padding:.3rem .7rem}.container{margin:0 auto;max-width:1400px;padding:2rem}.dashboard-toolbar{align-items:center;display:flex;gap:1rem;justify-content:space-between;margin-bottom:2rem}.dashboard-title{font-size:1.85rem;letter-spacing:-.02em;margin:0}.dashboard-subtitle{color:#64748b;font-size:.9rem;margin:.2rem 0 0}.dashboard-actions{align-items:center;display:flex;flex-wrap:wrap;gap:.5rem}.tag-filter-input{background:#ffffffe6;border:1px solid #cbd5e1;border-radius:10px;min-width:180px;padding:.5rem .75rem}.actions-menu-wrapper{position:relative}.actions-menu{background:#fff;border:1px solid #dbe2ea;border-radius:8px;box-shadow:0 8px 20px #0f172a24;min-width:220px;padding:.35rem 0;position:absolute;right:0;top:110%;z-index:20}.actions-menu-divider{border:0;border-top:1px solid #eef2f6;margin:.25rem 0}.hidden-file-input{display:none}.toast-stack{position:fixed;right:20px;top:20px;z-index:9999}.toast-item{border:1px solid #0000;border-radius:8px;box-shadow:0 8px 20px #0f172a29;margin-bottom:10px;min-width:280px;padding:12px 16px}.toast-error{background:#fee2e2;border-color:#fecaca;color:#7f1d1d}.toast-success{background:#dcfce7;border-color:#bbf7d0;color:#14532d}.toast-info{background:#dbeafe;border-color:#bfdbfe;color:#1e3a8a}.cc-metrics-grid{grid-gap:.65rem;display:grid;gap:.65rem;grid-template-columns:repeat(6,minmax(0,1fr));margin-bottom:1rem}.cc-metric-card{background:linear-gradient(180deg,#0f172af5,#1e293bf5);border:1px solid #334155;border-radius:10px;color:#e2e8f0;padding:.75rem}.cc-metric-label{color:#94a3b8;font-size:.68rem;letter-spacing:.08em;margin:0;text-transform:uppercase}.cc-metric-value{font-size:1.2rem;font-weight:700;margin:.35rem 0 0}.metric-ok{border-color:#14532d}.metric-down{border-color:#7f1d1d}.metric-maint{border-color:#78350f}.cc-layout{grid-gap:.8rem;align-items:start;display:grid;gap:.8rem;grid-template-columns:minmax(0,1fr) 300px}.cc-main{min-width:0}.cc-rail{display:flex;flex-direction:column;gap:.75rem;position:-webkit-sticky;position:sticky;top:82px}.cc-panel{background:#0f172af0;border:1px solid #334155;border-radius:12px;color:#e2e8f0;padding:.75rem}.cc-panel h3{font-size:.9rem;letter-spacing:.03em;margin:0 0 .65rem}.cc-empty{color:#94a3b8;font-size:.85rem;margin:0}.cc-group-list,.cc-incident-list{display:flex;flex-direction:column;gap:.4rem}.cc-group-item,.cc-incident-item{align-items:center;background:#0f172a;border:1px solid #334155;border-radius:8px;color:#e2e8f0;cursor:pointer;display:flex;justify-content:space-between;padding:.45rem .55rem;width:100%}.cc-group-item:hover,.cc-incident-item:hover{background:#111f34;border-color:#475569}.cc-incident-name{font-size:.85rem;font-weight:600;margin-right:.5rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dashboard-grid{grid-gap:1rem;display:grid;gap:1rem;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));margin-bottom:1.5rem}.resource-list{display:flex;flex-direction:column;gap:.45rem}.resource-list-shell{background:#ffffffd1;border:1px solid #cbd5e1;border-radius:12px;padding:.65rem}.resource-list-header{grid-gap:1rem;border-bottom:1px solid #e2e8f0;color:#64748b;display:grid;font-size:.72rem;gap:1rem;grid-template-columns:minmax(260px,2fr) minmax(220px,1.25fr) minmax(170px,.8fr);letter-spacing:.08em;margin-bottom:.45rem;padding:.35rem .6rem .6rem;text-transform:uppercase}.resource-list-count{color:#64748b;font-size:.75rem;margin:0 .6rem .55rem}.resource-row{grid-gap:1rem;align-items:cLine truncated
/*# sourceMappingURL=main.69110889.css.map*/
@@ -1 +0,0 @@
{"version":3,"file":"static/css/main.69110889.css","mappings":"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 truncated
@@ -1,3 +1,3 @@
/*! For license information please see main.820bab7f.js.LICENSE.txt */ /*! For license information please see main.243399c3.js.LICENSE.txt */
(()=>{var e={356:(e,t,n)=>{e.exports=n(9645).last},418:(e,t)=>{"use strict";Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),t.toString=function e(t){if(null==t)return"";if("string"===typeof t)return t;if(Array.isArray(t))return t.map(e).join(",");const n=String(t);return"0"===n&&Object.is(Number(t),-0)?"-0":n}},579:(e,t,n)=>{"use strict";e.exports=n(2799)},870:(e,t)=>{"use strict";Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),t.isObject=function(e){return null!==e&&("object"===typeof e||"function"===typeof e)}},1203:(e,t,n)=>{"use strict";Object.defineProperty(t,Symbol.toStringTag,{value:"Module"});const r=n(8420);t.isArrayLike=function(e){return null!=e&&"function"!==typeof e&&r.isLength(e.length)}},1293:(e,t,n)=>{"use strict";Object.defineProperty(t,Symbol.toStringTag,{value:"Module"});const r=n(4830);t.debounce=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};"object"!==typeof n&&(n={});const{leading:a=!1,trailing:i=!0,maxWait:o}=n,l=Array(2);let s;a&&(l[0]="leading"),i&&(l[1]="trailing");let c=null;const u=r.debounce(function(){for(var t=arguments.length,n=new Array(t),r=0;r<t;r++)n[r]=arguments[r];s=e.apply(this,n),c=null},t,{edges:l}),d=function(){for(var t=arguments.length,n=new Array(t),r=0;r<t;r++)n[r]=arguments[r];return null!=o&&(null===c&&(c=Date.now()),Date.now()-c>=o)?(s=e.apply(this,n),c=Date.now(),u.cancel(),u.schedule(),s):(u.apply(this,n),s)};return d.cancel=u.cancel,d.flush=()=>(u.flush(),s),d}},1444:(e,t,n)=>{"use strict";Object.defineProperty(t,Symbol.toStringTag,{value:"Module"});const r=n(418),a=n(3272);t.toPath=function(e){if(Array.isArray(e))return e.map(a.toKey);if("symbol"===typeof e)return[e];const t=[],n=(e=r.toString(e)).length;if(0===n)return t;let i=0,o="",l="",s=!1;for(46===e.charCodeAt(0)&&(t.push(""),i++);i<n;){const r=e[i];l?"\\"===r&&i+1<n?(i++,o+=e[i]):r===l?l="":o+=r:s?'"'===r||"'"===r?l=r:"]"===r?(s=!1,t.push(o),o=""):o+=r:"["===r?(s=!0,o&&(t.push(o),o="")):"."===r?o&&(t.push(o),o=""):o+=r,i++}return o&&t.push(o),t}},2086:(e,t,n)=>{"use strict";e.exports=n(5082)},2104:(e,t,n)=>{"use strict";Object.defineProperty(t,Symbol.toStringTag,{value:"Module"});const r=n(1203),a=n(9921);t.isArrayLikeObject=function(e){return a.isObjectLike(e)&&r.isArrayLike(e)}},2132:(e,t,n)=>{"use strict";var r=n(2897).default;Object.defineProperty(t,Symbol.toStringTag,{value:"Module"});const a=n(870),i=n(4803),o=n(9972);function l(e,t,n,u){if(t===e)return!0;switch(typeof t){case"object":return function(e,t,n,r){if(null==t)return!0;if(Array.isArray(t))return s(e,t,n,r);if(t instanceof Map)return function(e,t,n,r){if(0===t.size)return!0;if(!(e instanceof Map))return!1;for(const[a,i]of t.entries()){if(!1===n(e.get(a),i,a,e,t,r))return!1}return!0}(e,t,n,r);if(t instanceof Set)return c(e,t,n,r);const a=Object.keys(t);if(null==e)return 0===a.length;if(0===a.length)return!0;if(null!==r&&void 0!==r&&r.has(t))return r.get(t)===e;null===r||void 0===r||r.set(t,e);try{for(let o=0;o<a.length;o++){const l=a[o];if(!i.isPrimitive(e)&&!(l in e))return!1;if(void 0===t[l]&&void 0!==e[l])return!1;if(null===t[l]&&null!==e[l])return!1;if(!n(e[l],t[l],l,e,t,r))return!1}return!0}finally{null===r||void 0===r||r.delete(t)}}(e,t,n,u);case"function":return Object.keys(t).length>0?l(e,r({},t),n,u):o.eq(e,t);default:return a.isObject(e)?"string"!==typeof t||""===t:o.eq(e,t)}}function s(e,t,n,r){if(0===t.length)return!0;if(!Array.isArray(e))return!1;const a=new Set;for(let i=0;i<t.length;i++){const o=t[i];let l=!1;for(let s=0;s<e.length;s++){if(a.has(s))continue;let c=!1;if(n(e[s],o,i,e,t,r)&&(c=!0),c){a.add(s),l=!0;break}}if(!l)return!1}return!0}function c(e,t,n,r){return 0===t.size||e instanceof Set&&s([...e],[...t],n,r)}t.isMatchWith=function e(t,n,r){return"function"!==typeof r?e(t,n,()=>{}):l(t,n,function e(t,n,a,i,o,s){const c=r(t,n,a,i,o,s);return void 0!==c?Boolean(c):l(t,n,e,s)},new Map)},t.isSetMatch=c},2204:(e,t,n)=>{"use strict";Object.defineProperty(t,Symbol.toStringTag,{value:"Module"});const r=n(5316),a=n(7312),i=n(3799);t.sortBy=function(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),o=1;o<t;o++)n[o-1]=arguments[o];const l=n.length;return l>1&&i.isIterateeCall(e,n[0],n[1])?n=[]:l>2&&i.isIterateeCall(n[0],n[1],n[2])&&(n=[n[0]]),r.orderBy(e,a.flatten(n),["asc"])}},2330:(e,t,n)=>{"use strict";var r=n(5043);var a="function"===typeof Object.is?Object.is:function(e,t){return e===t&&(0!==e||1/e===1/t)||e!==e&&t!==t},i=r.useState,o=r.useEffect,l=r.useLayoutEffect,s=r.useDebugValue;function c(e){var t=e.getSnapshot;e=e.value;try{var n=t();return!a(e,n)}catch(r){return!0}}var u="undefined"===typeof window||"undefined"===typeof window.document||"undefined"===typeof window.document.createElement?function(e,t){return t()}:function(e,t){var n=t(),r=i({inst:{value:n,getSnapshot:t}}),a=r[0].inst,u=r[1];return l(function(){a.value=n,a.getSnapshot=t,c(a)&&u({inst:a})},[e,n,t]),o(function(){return c(a)&&u({iLine truncated (()=>{var e={356:(e,t,n)=>{e.exports=n(9645).last},418:(e,t)=>{"use strict";Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),t.toString=function e(t){if(null==t)return"";if("string"===typeof t)return t;if(Array.isArray(t))return t.map(e).join(",");const n=String(t);return"0"===n&&Object.is(Number(t),-0)?"-0":n}},579:(e,t,n)=>{"use strict";e.exports=n(2799)},870:(e,t)=>{"use strict";Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),t.isObject=function(e){return null!==e&&("object"===typeof e||"function"===typeof e)}},1203:(e,t,n)=>{"use strict";Object.defineProperty(t,Symbol.toStringTag,{value:"Module"});const r=n(8420);t.isArrayLike=function(e){return null!=e&&"function"!==typeof e&&r.isLength(e.length)}},1293:(e,t,n)=>{"use strict";Object.defineProperty(t,Symbol.toStringTag,{value:"Module"});const r=n(4830);t.debounce=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};"object"!==typeof n&&(n={});const{leading:a=!1,trailing:i=!0,maxWait:o}=n,l=Array(2);let s;a&&(l[0]="leading"),i&&(l[1]="trailing");let c=null;const u=r.debounce(function(){for(var t=arguments.length,n=new Array(t),r=0;r<t;r++)n[r]=arguments[r];s=e.apply(this,n),c=null},t,{edges:l}),d=function(){for(var t=arguments.length,n=new Array(t),r=0;r<t;r++)n[r]=arguments[r];return null!=o&&(null===c&&(c=Date.now()),Date.now()-c>=o)?(s=e.apply(this,n),c=Date.now(),u.cancel(),u.schedule(),s):(u.apply(this,n),s)};return d.cancel=u.cancel,d.flush=()=>(u.flush(),s),d}},1444:(e,t,n)=>{"use strict";Object.defineProperty(t,Symbol.toStringTag,{value:"Module"});const r=n(418),a=n(3272);t.toPath=function(e){if(Array.isArray(e))return e.map(a.toKey);if("symbol"===typeof e)return[e];const t=[],n=(e=r.toString(e)).length;if(0===n)return t;let i=0,o="",l="",s=!1;for(46===e.charCodeAt(0)&&(t.push(""),i++);i<n;){const r=e[i];l?"\\"===r&&i+1<n?(i++,o+=e[i]):r===l?l="":o+=r:s?'"'===r||"'"===r?l=r:"]"===r?(s=!1,t.push(o),o=""):o+=r:"["===r?(s=!0,o&&(t.push(o),o="")):"."===r?o&&(t.push(o),o=""):o+=r,i++}return o&&t.push(o),t}},2086:(e,t,n)=>{"use strict";e.exports=n(5082)},2104:(e,t,n)=>{"use strict";Object.defineProperty(t,Symbol.toStringTag,{value:"Module"});const r=n(1203),a=n(9921);t.isArrayLikeObject=function(e){return a.isObjectLike(e)&&r.isArrayLike(e)}},2132:(e,t,n)=>{"use strict";var r=n(2897).default;Object.defineProperty(t,Symbol.toStringTag,{value:"Module"});const a=n(870),i=n(4803),o=n(9972);function l(e,t,n,u){if(t===e)return!0;switch(typeof t){case"object":return function(e,t,n,r){if(null==t)return!0;if(Array.isArray(t))return s(e,t,n,r);if(t instanceof Map)return function(e,t,n,r){if(0===t.size)return!0;if(!(e instanceof Map))return!1;for(const[a,i]of t.entries()){if(!1===n(e.get(a),i,a,e,t,r))return!1}return!0}(e,t,n,r);if(t instanceof Set)return c(e,t,n,r);const a=Object.keys(t);if(null==e)return 0===a.length;if(0===a.length)return!0;if(null!==r&&void 0!==r&&r.has(t))return r.get(t)===e;null===r||void 0===r||r.set(t,e);try{for(let o=0;o<a.length;o++){const l=a[o];if(!i.isPrimitive(e)&&!(l in e))return!1;if(void 0===t[l]&&void 0!==e[l])return!1;if(null===t[l]&&null!==e[l])return!1;if(!n(e[l],t[l],l,e,t,r))return!1}return!0}finally{null===r||void 0===r||r.delete(t)}}(e,t,n,u);case"function":return Object.keys(t).length>0?l(e,r({},t),n,u):o.eq(e,t);default:return a.isObject(e)?"string"!==typeof t||""===t:o.eq(e,t)}}function s(e,t,n,r){if(0===t.length)return!0;if(!Array.isArray(e))return!1;const a=new Set;for(let i=0;i<t.length;i++){const o=t[i];let l=!1;for(let s=0;s<e.length;s++){if(a.has(s))continue;let c=!1;if(n(e[s],o,i,e,t,r)&&(c=!0),c){a.add(s),l=!0;break}}if(!l)return!1}return!0}function c(e,t,n,r){return 0===t.size||e instanceof Set&&s([...e],[...t],n,r)}t.isMatchWith=function e(t,n,r){return"function"!==typeof r?e(t,n,()=>{}):l(t,n,function e(t,n,a,i,o,s){const c=r(t,n,a,i,o,s);return void 0!==c?Boolean(c):l(t,n,e,s)},new Map)},t.isSetMatch=c},2204:(e,t,n)=>{"use strict";Object.defineProperty(t,Symbol.toStringTag,{value:"Module"});const r=n(5316),a=n(7312),i=n(3799);t.sortBy=function(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),o=1;o<t;o++)n[o-1]=arguments[o];const l=n.length;return l>1&&i.isIterateeCall(e,n[0],n[1])?n=[]:l>2&&i.isIterateeCall(n[0],n[1],n[2])&&(n=[n[0]]),r.orderBy(e,a.flatten(n),["asc"])}},2330:(e,t,n)=>{"use strict";var r=n(5043);var a="function"===typeof Object.is?Object.is:function(e,t){return e===t&&(0!==e||1/e===1/t)||e!==e&&t!==t},i=r.useState,o=r.useEffect,l=r.useLayoutEffect,s=r.useDebugValue;function c(e){var t=e.getSnapshot;e=e.value;try{var n=t();return!a(e,n)}catch(r){return!0}}var u="undefined"===typeof window||"undefined"===typeof window.document||"undefined"===typeof window.document.createElement?function(e,t){return t()}:function(e,t){var n=t(),r=i({inst:{value:n,getSnapshot:t}}),a=r[0].inst,u=r[1];return l(function(){a.value=n,a.getSnapshot=t,c(a)&&u({inst:a})},[e,n,t]),o(function(){return c(a)&&u({iLine truncated
//# sourceMappingURL=main.820bab7f.js.map //# sourceMappingURL=main.243399c3.js.map
@@ -0,0 +1 @@
{"version":3,"file":"static/js/main.243399c3.js","mappings":"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 truncated
@@ -1 +0,0 @@
{"version":3,"file":"static/js/main.820bab7f.js","mappings":"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 truncated
+191 -56
View File
@@ -1,18 +1,18 @@
.App { .App {
min-height: 100vh; min-height: 100vh;
background: background:
radial-gradient(circle at 15% 0%, #dff7f2 0%, transparent 40%), radial-gradient(circle at 15% 0%, rgba(30, 64, 175, 0.2) 0%, transparent 42%),
radial-gradient(circle at 95% 10%, #e8eefc 0%, transparent 38%), radial-gradient(circle at 95% 10%, rgba(15, 118, 110, 0.14) 0%, transparent 36%),
#f8fafc; #020617;
} }
.navbar { .navbar {
background: rgba(255, 255, 255, 0.85); background: rgba(15, 23, 42, 0.82);
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
padding: 0.85rem 2rem; padding: 0.85rem 2rem;
color: #0f172a; color: #e2e8f0;
border-bottom: 1px solid #dbe2ea; border-bottom: 1px solid #334155;
box-shadow: 0 4px 18px rgba(15, 23, 42, 0.06); box-shadow: 0 4px 18px rgba(15, 23, 42, 0.06);
position: sticky; position: sticky;
top: 0; top: 0;
@@ -48,7 +48,7 @@
} }
.nav-link { .nav-link {
color: #475569; color: #cbd5e1;
text-decoration: none; text-decoration: none;
font-weight: 600; font-weight: 600;
font-size: 0.92rem; font-size: 0.92rem;
@@ -58,9 +58,9 @@
} }
.nav-link:hover { .nav-link:hover {
background: #ffffff; background: #111827;
color: #0f172a; color: #f8fafc;
box-shadow: inset 0 0 0 1px #dbe2ea; box-shadow: inset 0 0 0 1px #334155;
} }
.settings-link { .settings-link {
@@ -83,9 +83,9 @@
align-items: center; align-items: center;
gap: 0.45rem; gap: 0.45rem;
font-size: 0.82rem; font-size: 0.82rem;
color: #334155; color: #e2e8f0;
background: #ffffff; background: #111827;
border: 1px solid #dbe2ea; border: 1px solid #334155;
border-radius: 999px; border-radius: 999px;
padding: 0.3rem 0.7rem; padding: 0.3rem 0.7rem;
} }
@@ -108,11 +108,12 @@
margin: 0; margin: 0;
font-size: 1.85rem; font-size: 1.85rem;
letter-spacing: -0.02em; letter-spacing: -0.02em;
color: #f8fafc;
} }
.dashboard-subtitle { .dashboard-subtitle {
margin: 0.2rem 0 0; margin: 0.2rem 0 0;
color: #64748b; color: #94a3b8;
font-size: 0.9rem; font-size: 0.9rem;
} }
@@ -123,12 +124,42 @@
align-items: center; align-items: center;
} }
.quick-filter-row {
display: flex;
gap: 0.45rem;
flex-wrap: wrap;
margin: 0.2rem 0 0.8rem;
}
.quick-chip {
background: #0f172a;
border: 1px solid #334155;
color: #cbd5e1;
border-radius: 999px;
padding: 0.28rem 0.7rem;
font-size: 0.74rem;
font-weight: 700;
letter-spacing: 0.02em;
cursor: pointer;
}
.quick-chip:hover {
background: #111f34;
}
.quick-chip.active {
background: #14b8a6;
border-color: #14b8a6;
color: #052e2b;
}
.tag-filter-input { .tag-filter-input {
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
border-radius: 10px; border-radius: 10px;
border: 1px solid #cbd5e1; border: 1px solid #334155;
min-width: 180px; min-width: 180px;
background: rgba(255, 255, 255, 0.9); background: #111827;
color: #e2e8f0;
} }
.actions-menu-wrapper { .actions-menu-wrapper {
@@ -322,10 +353,12 @@
} }
.resource-list-shell { .resource-list-shell {
background: rgba(255, 255, 255, 0.82); background: rgba(15, 23, 42, 0.9);
border: 1px solid #cbd5e1; border: 1px solid #334155;
border-radius: 12px; border-radius: 12px;
padding: 0.65rem; padding: 0.65rem;
max-height: calc(100vh - 250px);
overflow: auto;
} }
.resource-list-header { .resource-list-header {
@@ -336,15 +369,24 @@
font-size: 0.72rem; font-size: 0.72rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.08em; letter-spacing: 0.08em;
color: #64748b; color: #94a3b8;
border-bottom: 1px solid #e2e8f0; border-bottom: 1px solid #334155;
margin-bottom: 0.45rem; margin-bottom: 0.45rem;
position: sticky;
top: 0;
z-index: 5;
background: rgba(15, 23, 42, 0.98);
} }
.resource-list-count { .resource-list-count {
font-size: 0.75rem; font-size: 0.75rem;
color: #64748b; color: #94a3b8;
margin: 0 0.6rem 0.55rem; margin: 0 0.6rem 0.55rem;
position: sticky;
top: 31px;
z-index: 4;
background: rgba(15, 23, 42, 0.95);
padding-bottom: 0.25rem;
} }
.resource-row { .resource-row {
@@ -353,18 +395,18 @@
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
padding: 0.65rem 0.8rem; padding: 0.65rem 0.8rem;
background: rgba(248, 250, 252, 0.96); background: rgba(15, 23, 42, 0.96);
border: 1px solid #dbe2ea; border: 1px solid #334155;
border-radius: 10px; border-radius: 10px;
box-shadow: 0 3px 10px rgba(15, 23, 42, 0.05); box-shadow: 0 4px 12px rgba(2, 6, 23, 0.55);
cursor: pointer; cursor: pointer;
transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s; transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s;
} }
.resource-row:hover { .resource-row:hover {
transform: translateY(-1px); transform: translateY(-1px);
border-color: #94a3b8; border-color: #60a5fa;
box-shadow: 0 9px 18px rgba(15, 23, 42, 0.1); box-shadow: 0 12px 22px rgba(2, 6, 23, 0.65);
} }
.row-main { .row-main {
@@ -382,13 +424,14 @@
.row-name { .row-name {
margin: 0; margin: 0;
font-size: 0.98rem; font-size: 0.98rem;
color: #f8fafc;
} }
.resource-group-pill { .resource-group-pill {
padding: 0.15rem 0.5rem; padding: 0.15rem 0.5rem;
border-radius: 999px; border-radius: 999px;
background: #e2e8f0; background: #1e293b;
color: #334155; color: #cbd5e1;
font-size: 0.72rem; font-size: 0.72rem;
font-weight: 700; font-weight: 700;
} }
@@ -414,6 +457,7 @@
.row-url, .row-url,
.row-type { .row-type {
margin: 0; margin: 0;
color: #94a3b8;
} }
.row-url { .row-url {
@@ -436,6 +480,12 @@
.row-metrics .stat-label { .row-metrics .stat-label {
font-size: 0.64rem; font-size: 0.64rem;
color: #94a3b8;
}
.row-metrics .stat-value {
color: #e2e8f0;
font-weight: 700;
} }
.row-sparkline { .row-sparkline {
@@ -488,11 +538,40 @@
margin: 0 0 0.2rem 0; margin: 0 0 0.2rem 0;
} }
.trend-chip {
display: inline-block;
margin-left: 0.3rem;
font-size: 0.72rem;
font-weight: 800;
}
.trend-better {
color: #34d399;
}
.trend-worse {
color: #f87171;
}
.trend-flat {
color: #94a3b8;
}
.resource-url { .resource-url {
color: #64748b; color: #64748b;
font-size: 0.8rem; font-size: 0.8rem;
word-break: break-all; word-break: break-all;
} }
.load-more-btn {
width: 100%;
border: 1px dashed #94a3b8;
background: #f8fafc;
color: #334155;
border-radius: 10px;
padding: 0.65rem;
font-weight: 700;
cursor: pointer;
}
.load-more-btn:hover {
background: #f1f5f9;
}
.resource-type { .resource-type {
margin: 0.15rem 0 0; margin: 0.15rem 0 0;
@@ -537,7 +616,7 @@
.stat-value { .stat-value {
font-size: 1.2rem; font-size: 1.2rem;
font-weight: 700; font-weight: 700;
color: #333; color: #e2e8f0;
margin: 0; margin: 0;
} }
@@ -547,7 +626,7 @@
.stat-label { .stat-label {
font-size: 0.75rem; font-size: 0.75rem;
color: #666; color: #94a3b8;
text-transform: uppercase; text-transform: uppercase;
margin: 0; margin: 0;
} }
@@ -567,7 +646,7 @@
.last-check { .last-check {
margin: 0; margin: 0;
font-size: 0.75rem; font-size: 0.75rem;
color: #999; color: #94a3b8;
} }
.group-section { .group-section {
@@ -671,17 +750,48 @@
} }
.btn-icon { .btn-icon {
background: none; background: #0b1325;
border: none; border: 1px solid #334155;
color: #e2e8f0;
min-width: 34px;
min-height: 30px;
border-radius: 8px;
cursor: pointer; cursor: pointer;
font-size: 1rem; font-size: 1rem;
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
opacity: 0.6; opacity: 1;
transition: opacity 0.2s; transition: transform 0.15s, background 0.2s, border-color 0.2s;
} }
.btn-icon:hover { .btn-icon:hover {
opacity: 1; transform: translateY(-1px);
background: #111f34;
border-color: #60a5fa;
}
.btn-icon:focus-visible {
outline: 2px solid #60a5fa;
outline-offset: 2px;
}
.btn-icon.action-edit {
color: #93c5fd;
}
.btn-icon.action-maint {
color: #fbbf24;
}
.btn-icon.action-delete {
color: #fca5a5;
border-color: #7f1d1d;
background: #3f1d1d;
}
.btn-icon.action-delete:hover {
background: #7f1d1d;
border-color: #ef4444;
color: #fee2e2;
} }
.btn-sm { .btn-sm {
@@ -813,17 +923,18 @@
} }
.detail-section { .detail-section {
background: rgba(255, 255, 255, 0.92); background: rgba(15, 23, 42, 0.94);
border: 1px solid #e2e8f0; border: 1px solid #334155;
border-radius: 14px; border-radius: 14px;
padding: 1.5rem; padding: 1.5rem;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.06); box-shadow: 0 10px 24px rgba(2, 6, 23, 0.55);
color: #e2e8f0;
} }
.detail-section h2 { .detail-section h2 {
margin-top: 0; margin-top: 0;
color: #333; color: #f8fafc;
} }
.detail-header-row { .detail-header-row {
@@ -841,7 +952,7 @@
} }
.resource-url-detail { .resource-url-detail {
color: #64748b; color: #94a3b8;
margin: 0; margin: 0;
} }
@@ -875,7 +986,15 @@
.filter-control label { .filter-control label {
font-size: 0.9rem; font-size: 0.9rem;
color: #475569; color: #cbd5e1;
}
.filter-control select {
background: #111827;
color: #e2e8f0;
border: 1px solid #334155;
border-radius: 6px;
padding: 0.3rem 0.5rem;
} }
.controls-pagination { .controls-pagination {
@@ -896,9 +1015,9 @@
.data-table th { .data-table th {
padding: 0.75rem; padding: 0.75rem;
text-align: left; text-align: left;
border-bottom: 2px solid #dbe2ea; border-bottom: 2px solid #334155;
color: #1e293b; color: #e2e8f0;
background: #f1f5f9; background: #111827;
font-size: 0.86rem; font-size: 0.86rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.02em; letter-spacing: 0.02em;
@@ -906,11 +1025,12 @@
.data-table td { .data-table td {
padding: 0.75rem; padding: 0.75rem;
border-bottom: 1px solid #e5e7eb; border-bottom: 1px solid #1f2937;
color: #e2e8f0;
} }
.data-table tbody tr:hover { .data-table tbody tr:hover {
background: #f0f9ff; background: #1e293b;
} }
.text-right { .text-right {
@@ -918,7 +1038,7 @@
} }
.muted-cell { .muted-cell {
color: #64748b; color: #94a3b8;
} }
.w-25 { .w-25 {
@@ -942,14 +1062,14 @@
.incident-reason-toggle { .incident-reason-toggle {
cursor: pointer; cursor: pointer;
color: #0f766e; color: #93c5fd;
flex: 1; flex: 1;
} }
.expand-indicator { .expand-indicator {
margin-left: 0.5rem; margin-left: 0.5rem;
font-size: 0.85rem; font-size: 0.85rem;
color: #64748b; color: #94a3b8;
} }
.btn-compact { .btn-compact {
@@ -958,7 +1078,7 @@
} }
.expanded-row { .expanded-row {
background: #f8fafc; background: #0b1325;
} }
.expanded-cell { .expanded-cell {
@@ -966,7 +1086,7 @@
word-break: break-word; word-break: break-word;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Courier New', monospace; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Courier New', monospace;
font-size: 0.9rem; font-size: 0.9rem;
color: #334155; color: #e2e8f0;
} }
.section-title-row { .section-title-row {
@@ -978,7 +1098,7 @@
} }
.empty-copy { .empty-copy {
color: #64748b; color: #94a3b8;
text-align: center; text-align: center;
padding: 2rem; padding: 2rem;
} }
@@ -986,6 +1106,10 @@
.chart-container { .chart-container {
margin-top: 1rem; margin-top: 1rem;
height: 300px; height: 300px;
background: #0b1325;
border: 1px solid #334155;
border-radius: 12px;
padding: 0.5rem;
} }
.incident-badge { .incident-badge {
@@ -1002,15 +1126,16 @@
display: inline-block; display: inline-block;
margin-bottom: 1rem; margin-bottom: 1rem;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
background: #6c757d; background: #1e293b;
color: white; color: #f8fafc;
text-decoration: none; text-decoration: none;
border-radius: 6px; border-radius: 6px;
border: 1px solid #334155;
transition: background 0.2s; transition: background 0.2s;
} }
.back-button:hover { .back-button:hover {
background: #5a6268; background: #334155;
} }
.empty-state { .empty-state {
@@ -1111,6 +1236,16 @@
position: static; position: static;
} }
.resource-list-shell {
max-height: none;
overflow: visible;
}
.resource-list-count,
.resource-list-header {
position: static;
}
.resource-row { .resource-row {
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 0.7rem; gap: 0.7rem;
+134 -23
View File
@@ -52,6 +52,9 @@ function Dashboard() {
}); });
const [tagFilter, setTagFilter] = useState(''); const [tagFilter, setTagFilter] = useState('');
const [groupFilter, setGroupFilter] = useState('all'); const [groupFilter, setGroupFilter] = useState('all');
const [quickFilter, setQuickFilter] = useState('all');
const [sortKey, setSortKey] = useState('severity');
const [renderLimit, setRenderLimit] = useState(120);
const [groupData, setGroupData] = useState({ name: '', description: '' }); const [groupData, setGroupData] = useState({ name: '', description: '' });
const wsRef = useRef(null); const wsRef = useRef(null);
const actionsRef = useRef(null); const actionsRef = useRef(null);
@@ -238,6 +241,17 @@ function Dashboard() {
} }
}; };
const handleToggleMaintenance = async (resource) => {
try {
await axios.patch(`/api/resources/${resource.id}/maintenance-mode`, {
maintenance_mode: !resource.maintenance_mode,
});
loadResources();
} catch (error) {
alert('Error updating maintenance mode');
}
};
const handleExportCSV = async () => { const handleExportCSV = async () => {
try { try {
const response = await axios.get('/api/resources/export', { const response = await axios.get('/api/resources/export', {
@@ -283,7 +297,28 @@ function Dashboard() {
return acc; return acc;
}, {}); }, {});
// Filter resources by tag/group and sort for stable scanning const getTrend = (recentChecks = []) => {
const withTimes = recentChecks.filter((c) => typeof c.response_time === 'number' && c.response_time > 0);
if (withTimes.length < 2) return 'flat';
const first = withTimes[0].response_time;
const last = withTimes[withTimes.length - 1].response_time;
if (first <= 0) return 'flat';
const deltaPct = ((last - first) / first) * 100;
if (deltaPct <= -12) return 'better';
if (deltaPct >= 12) return 'worse';
return 'flat';
};
const getSeverityScore = (r) => {
const statusScore = r.status === 'down' ? 1000 : r.status === 'unknown' ? 400 : 0;
const incidentScore = r.hasActiveIncident ? 350 : 0;
const maintenanceOffset = r.maintenance_mode ? -300 : 0;
const uptimePenalty = Math.max(0, 100 - parseFloat(r.uptime || 0)) * 3;
const responsePenalty = Math.min(250, Math.round((parseInt(r.avgResponseTime || 0, 10) || 0) / 20));
return statusScore + incidentScore + uptimePenalty + responsePenalty + maintenanceOffset;
};
// Filter resources by tag/group/quick filters
const filteredResources = resources const filteredResources = resources
.filter((r) => { .filter((r) => {
if (tagFilter) { if (tagFilter) {
@@ -292,9 +327,39 @@ function Dashboard() {
} }
if (groupFilter === 'ungrouped') return !r.group_id; if (groupFilter === 'ungrouped') return !r.group_id;
if (groupFilter !== 'all') return String(r.group_id || '') === groupFilter; if (groupFilter !== 'all') return String(r.group_id || '') === groupFilter;
if (quickFilter === 'down' && r.status !== 'down') return false;
if (quickFilter === 'maintenance' && !r.maintenance_mode) return false;
if (quickFilter === 'nodata' && (!r.recentChecks || r.recentChecks.length === 0)) return false;
if (quickFilter === 'slow' && (parseInt(r.avgResponseTime || 0, 10) || 0) < 1500) return false;
return true; return true;
}) });
.sort((a, b) => {
const sortedResources = [...filteredResources].sort((a, b) => {
if (sortKey === 'name') {
return String(a.name || '').localeCompare(String(b.name || ''));
}
if (sortKey === 'status') {
const rank = { down: 0, unknown: 1, up: 2 };
return (rank[a.status] ?? 3) - (rank[b.status] ?? 3);
}
if (sortKey === 'uptime') {
return parseFloat(a.uptime || 0) - parseFloat(b.uptime || 0);
}
if (sortKey === 'response') {
return (parseInt(b.avgResponseTime || 0, 10) || 0) - (parseInt(a.avgResponseTime || 0, 10) || 0);
}
if (sortKey === 'lastcheck') {
const ta = a.lastCheck ? new Date(a.lastCheck).getTime() : 0;
const tb = b.lastCheck ? new Date(b.lastCheck).getTime() : 0;
return tb - ta;
}
// Default: severity first
const severityDiff = getSeverityScore(b) - getSeverityScore(a);
if (severityDiff !== 0) return severityDiff;
const groupA = (groupNameById[a.group_id] || 'Ungrouped').toLowerCase(); const groupA = (groupNameById[a.group_id] || 'Ungrouped').toLowerCase();
const groupB = (groupNameById[b.group_id] || 'Ungrouped').toLowerCase(); const groupB = (groupNameById[b.group_id] || 'Ungrouped').toLowerCase();
if (groupA < groupB) return -1; if (groupA < groupB) return -1;
@@ -302,18 +367,24 @@ function Dashboard() {
return String(a.name || '').localeCompare(String(b.name || '')); return String(a.name || '').localeCompare(String(b.name || ''));
}); });
const totalCount = filteredResources.length; useEffect(() => {
const upCount = filteredResources.filter((r) => r.status === 'up').length; setRenderLimit(120);
const downCount = filteredResources.filter((r) => r.status === 'down').length; }, [tagFilter, groupFilter, quickFilter, sortKey]);
const maintenanceCount = filteredResources.filter((r) => r.maintenance_mode).length;
const visibleResources = sortedResources.slice(0, renderLimit);
const totalCount = sortedResources.length;
const upCount = sortedResources.filter((r) => r.status === 'up').length;
const downCount = sortedResources.filter((r) => r.status === 'down').length;
const maintenanceCount = sortedResources.filter((r) => r.maintenance_mode).length;
const avgUptime = totalCount const avgUptime = totalCount
? (filteredResources.reduce((sum, r) => sum + parseFloat(r.uptime || 0), 0) / totalCount).toFixed(2) ? (sortedResources.reduce((sum, r) => sum + parseFloat(r.uptime || 0), 0) / totalCount).toFixed(2)
: '0.00'; : '0.00';
const avgResponse = totalCount const avgResponse = totalCount
? Math.round(filteredResources.reduce((sum, r) => sum + parseInt(r.avgResponseTime || 0, 10), 0) / totalCount) ? Math.round(sortedResources.reduce((sum, r) => sum + parseInt(r.avgResponseTime || 0, 10), 0) / totalCount)
: 0; : 0;
const activeIncidents = filteredResources const activeIncidents = sortedResources
.filter((r) => r.hasActiveIncident || r.status === 'down') .filter((r) => r.hasActiveIncident || r.status === 'down')
.slice(0, 8); .slice(0, 8);
@@ -321,11 +392,11 @@ function Dashboard() {
.map((g) => ({ .map((g) => ({
id: g.id, id: g.id,
name: g.name, name: g.name,
count: filteredResources.filter((r) => String(r.group_id || '') === String(g.id)).length, count: sortedResources.filter((r) => String(r.group_id || '') === String(g.id)).length,
})) }))
.filter((g) => g.count > 0); .filter((g) => g.count > 0);
const ungroupedCount = filteredResources.filter((r) => !r.group_id).length; const ungroupedCount = sortedResources.filter((r) => !r.group_id).length;
const renderResourceRow = (resource) => { const renderResourceRow = (resource) => {
const sparkData = (resource.recentChecks || []).map((c, idx) => ({ const sparkData = (resource.recentChecks || []).map((c, idx) => ({
@@ -334,6 +405,7 @@ function Dashboard() {
statusValue: c.status === 'up' ? 1 : 0, statusValue: c.status === 'up' ? 1 : 0,
})); }));
const groupName = groupNameById[resource.group_id] || 'Ungrouped'; const groupName = groupNameById[resource.group_id] || 'Ungrouped';
const trend = getTrend(resource.recentChecks || []);
return ( return (
<div <div
@@ -358,7 +430,10 @@ function Dashboard() {
<p className="stat-label">Uptime</p> <p className="stat-label">Uptime</p>
</div> </div>
<div> <div>
<p className="stat-value small">{resource.avgResponseTime}ms</p> <p className="stat-value small">
{resource.avgResponseTime}ms
<span className={`trend-chip trend-${trend}`}>{trend === 'better' ? '↓' : trend === 'worse' ? '↑' : '→'}</span>
</p>
<p className="stat-label">Avg Resp</p> <p className="stat-label">Avg Resp</p>
</div> </div>
<div className="row-sparkline"> <div className="row-sparkline">
@@ -379,8 +454,15 @@ function Dashboard() {
{resource.status} {resource.status}
</span> </span>
<div className="row-actions"> <div className="row-actions">
<button className="btn-icon" title="Edit" onClick={(e) => { e.stopPropagation(); openEditModal(resource); }}>✎</button> <button className="btn-icon action-edit" title="Edit" onClick={(e) => { e.stopPropagation(); openEditModal(resource); }}>✎</button>
<button className="btn-icon" title="Delete" onClick={(e) => { e.stopPropagation(); handleDeleteResource(resource.id); }}>🗑</button> <button
className="btn-icon action-maint"
title={resource.maintenance_mode ? 'End maintenance' : 'Start maintenance'}
onClick={(e) => { e.stopPropagation(); handleToggleMaintenance(resource); }}
>
{resource.maintenance_mode ? '✅' : '🛠'}
</button>
<button className="btn-icon action-delete" title="Delete" onClick={(e) => { e.stopPropagation(); handleDeleteResource(resource.id); }}>🗑</button>
</div> </div>
<p className="last-check"> <p className="last-check">
Last: {resource.lastCheck ? formatLocalTime(resource.lastCheck) : 'Never'} Last: {resource.lastCheck ? formatLocalTime(resource.lastCheck) : 'Never'}
@@ -431,6 +513,18 @@ function Dashboard() {
))} ))}
<option value="ungrouped">Ungrouped</option> <option value="ungrouped">Ungrouped</option>
</select> </select>
<select
value={sortKey}
onChange={(e) => setSortKey(e.target.value)}
className="tag-filter-input"
>
<option value="severity">Sort: Severity</option>
<option value="status">Sort: Status</option>
<option value="uptime">Sort: Uptime</option>
<option value="response">Sort: Response</option>
<option value="lastcheck">Sort: Last Check</option>
<option value="name">Sort: Name</option>
</select>
<div ref={actionsRef} className="actions-menu-wrapper"> <div ref={actionsRef} className="actions-menu-wrapper">
<button <button
className="btn btn-secondary" className="btn btn-secondary"
@@ -487,6 +581,14 @@ function Dashboard() {
</div> </div>
</div> </div>
<div className="quick-filter-row">
<button className={`quick-chip ${quickFilter === 'all' ? 'active' : ''}`} onClick={() => setQuickFilter('all')}>All</button>
<button className={`quick-chip ${quickFilter === 'down' ? 'active' : ''}`} onClick={() => setQuickFilter('down')}>Down</button>
<button className={`quick-chip ${quickFilter === 'maintenance' ? 'active' : ''}`} onClick={() => setQuickFilter('maintenance')}>Maintenance</button>
<button className={`quick-chip ${quickFilter === 'slow' ? 'active' : ''}`} onClick={() => setQuickFilter('slow')}>High Latency</button>
<button className={`quick-chip ${quickFilter === 'nodata' ? 'active' : ''}`} onClick={() => setQuickFilter('nodata')}>No Data</button>
</div>
<div className="cc-metrics-grid"> <div className="cc-metrics-grid">
<div className="cc-metric-card"> <div className="cc-metric-card">
<p className="cc-metric-label">Monitors</p> <p className="cc-metric-label">Monitors</p>
@@ -519,7 +621,7 @@ function Dashboard() {
<h3>No resources yet</h3> <h3>No resources yet</h3>
<p>Add your first resource to start monitoring</p> <p>Add your first resource to start monitoring</p>
</div> </div>
) : filteredResources.length === 0 ? ( ) : sortedResources.length === 0 ? (
<div className="empty-state"> <div className="empty-state">
<h3>No resources match filter</h3> <h3>No resources match filter</h3>
<p>Try adjusting your tag/group filters</p> <p>Try adjusting your tag/group filters</p>
@@ -532,9 +634,14 @@ function Dashboard() {
<div>Metrics</div> <div>Metrics</div>
<div>Status</div> <div>Status</div>
</div> </div>
<div className="resource-list-count">{filteredResources.length} monitors</div> <div className="resource-list-count">{visibleResources.length} of {sortedResources.length} monitors</div>
<div className="resource-list"> <div className="resource-list">
{filteredResources.map(renderResourceRow)} {visibleResources.map(renderResourceRow)}
{visibleResources.length < sortedResources.length && (
<button className="load-more-btn" onClick={() => setRenderLimit((prev) => prev + 120)}>
Load 120 more
</button>
)}
</div> </div>
</section> </section>
@@ -1387,11 +1494,15 @@ function ResourceDetail() {
<div className="chart-container"> <div className="chart-container">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData}> <LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" /> <CartesianGrid strokeDasharray="3 3" stroke="#334155" />
<XAxis dataKey="time" /> <XAxis dataKey="time" tick={{ fill: '#94a3b8', fontSize: 12 }} />
<YAxis /> <YAxis tick={{ fill: '#94a3b8', fontSize: 12 }} />
<Tooltip /> <Tooltip
<Line type="monotone" dataKey="responseTime" stroke="#667eea" strokeWidth={2} /> contentStyle={{ backgroundColor: '#0f172a', border: '1px solid #334155', borderRadius: '8px' }}
labelStyle={{ color: '#cbd5e1' }}
itemStyle={{ color: '#e2e8f0' }}
/>
<Line type="monotone" dataKey="responseTime" stroke="#60a5fa" strokeWidth={2} dot={false} isAnimationActive={false} />
</LineChart> </LineChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
+290
View File
@@ -0,0 +1,290 @@
.cc-page {
padding-top: 1.4rem;
color: #e2e8f0;
}
.cc-alert {
padding: 0.85rem 1rem;
margin-bottom: 1rem;
border-radius: 10px;
border: 1px solid #854d0e;
background: #2b1907;
color: #fcd34d;
}
.cc-page-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
margin-bottom: 1.2rem;
flex-wrap: wrap;
}
.cc-page-title {
margin: 0;
font-size: 1.65rem;
letter-spacing: -0.02em;
color: #f8fafc !important;
text-shadow: 0 1px 0 rgba(15, 23, 42, 0.3);
}
.cc-page h2 {
color: #f8fafc;
}
.cc-controls {
display: flex;
gap: 0.5rem;
align-items: center;
flex-wrap: wrap;
}
.cc-pill-toggle {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
border: 1px solid #334155;
border-radius: 999px;
background: #111827;
color: #f1f5f9;
font-size: 0.82rem;
}
.cc-pill-toggle input {
accent-color: #3b82f6;
}
.cc-pill-toggle:focus-within,
.cc-input-sm:focus,
.cc-select-sm:focus {
outline: 2px solid #60a5fa;
outline-offset: 2px;
}
.cc-surface {
background: #0f172a;
border: 1px solid #334155;
border-radius: 12px;
padding: 1rem;
}
.cc-kpi-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
gap: 0.65rem;
}
.cc-kpi {
background: #0b1325;
border: 1px solid #334155;
border-radius: 10px;
padding: 0.75rem;
}
.cc-kpi-label {
margin: 0;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.07em;
color: #cbd5e1;
}
.cc-kpi-value {
margin: 0.35rem 0 0;
color: #f8fafc;
font-size: 1.45rem;
font-weight: 700;
}
.cc-table-wrap {
overflow: auto;
border: 1px solid #334155;
border-radius: 12px;
background: #0b1325;
}
.cc-table {
width: 100%;
border-collapse: collapse;
}
.cc-table thead tr {
background: #0f172a;
}
.cc-table th {
text-align: left;
padding: 0.75rem;
color: #e2e8f0;
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.07em;
border-bottom: 1px solid #334155;
}
.cc-table td {
padding: 0.75rem;
border-bottom: 1px solid #1f2937;
color: #f1f5f9;
}
.cc-table tbody tr:hover {
background: #1e293b;
}
.cc-pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
margin-top: 1rem;
padding: 1rem;
background: #111827;
border: 1px solid #334155;
border-radius: 10px;
}
.cc-input-sm {
width: 56px;
padding: 0.45rem;
border: 1px solid #334155;
border-radius: 8px;
text-align: center;
background: #111827;
color: #e2e8f0;
}
.cc-select-sm {
padding: 0.45rem;
border: 1px solid #334155;
border-radius: 8px;
background: #111827;
color: #e2e8f0;
}
.cc-empty {
text-align: center;
color: #cbd5e1;
padding: 2.2rem 1rem;
}
.history-resource-card {
margin-bottom: 1.4rem;
}
.history-resource-title {
margin: 0 0 0.45rem;
color: #dbeafe;
}
.history-resource-meta {
margin: 0 0 1rem;
color: #e2e8f0;
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.history-resource-meta strong {
color: #f8fafc;
}
.history-chart-box {
height: 400px;
margin-bottom: 1rem;
background: #111827;
padding: 1rem;
border-radius: 10px;
border: 1px solid #334155;
}
.history-trend-row {
display: flex;
gap: 1rem;
margin-bottom: 0.8rem;
color: #e2e8f0;
flex-wrap: wrap;
}
.notifications-list {
display: grid;
grid-template-columns: 1fr;
gap: 0.75rem;
}
.notification-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
padding: 1rem;
border-radius: 10px;
border: 1px solid #334155;
background: #0b1325;
}
.notification-row.is-unread {
border-color: #1d4ed8;
box-shadow: inset 0 0 0 1px rgba(59, 130, 246, 0.35);
}
.notification-content {
flex: 1;
}
.notification-title {
margin: 0 0 0.4rem;
color: #f8fafc;
font-size: 1rem;
}
.notification-message {
margin: 0 0 0.5rem;
color: #e2e8f0;
}
.notification-meta {
display: flex;
gap: 0.65rem;
flex-wrap: wrap;
color: #cbd5e1;
font-size: 0.84rem;
}
.notification-chip {
background: #111827;
border: 1px solid #334155;
border-radius: 999px;
padding: 0.2rem 0.55rem;
color: #f1f5f9;
}
.notification-actions {
display: flex;
gap: 0.4rem;
flex-shrink: 0;
}
.notification-actions .btn:focus-visible {
outline: 2px solid #60a5fa;
outline-offset: 2px;
}
@media (max-width: 768px) {
.cc-controls {
width: 100%;
}
.cc-controls .btn,
.cc-controls .tag-filter-input,
.cc-controls .cc-pill-toggle {
width: 100%;
}
.cc-pagination {
flex-direction: column;
align-items: stretch;
}
}
+55 -59
View File
@@ -12,6 +12,7 @@ import {
ResponsiveContainer, ResponsiveContainer,
} from 'recharts'; } from 'recharts';
import './App.css'; import './App.css';
import './CommandCenterPages.css';
import { formatChartTime } from './utils/timeUtils'; import { formatChartTime } from './utils/timeUtils';
const asNumberOrZero = (value) => { const asNumberOrZero = (value) => {
@@ -107,23 +108,16 @@ function History() {
if (error) return <div className="container">Error loading history: {error}</div>; if (error) return <div className="container">Error loading history: {error}</div>;
return ( return (
<div className="container"> <div className="container cc-page">
{limitedByRetention && ( {limitedByRetention && (
<div style={{ <div className="cc-alert">
padding: '1rem',
marginBottom: '1rem',
backgroundColor: '#fff3cd',
border: '1px solid #ffc107',
borderRadius: '4px',
color: '#856404'
}}>
⚠️ Data limited to {effectiveDays} days (server retention setting). Requested {days} days. ⚠️ Data limited to {effectiveDays} days (server retention setting). Requested {days} days.
</div> </div>
)} )}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem', flexWrap: 'wrap', gap: '1rem' }}> <div className="cc-page-header">
<h2>Monitoring History</h2> <h2 className="cc-page-title" style={{ color: '#f8fafc', fontWeight: 800 }}>Monitoring History</h2>
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center', flexWrap: 'wrap' }}> <div className="cc-controls">
<div style={{ display: 'flex', gap: '0.5rem' }}> <div className="cc-controls">
{[7, 14, 30].map((d) => ( {[7, 14, 30].map((d) => (
<button <button
key={d} key={d}
@@ -136,21 +130,19 @@ function History() {
</button> </button>
))} ))}
</div> </div>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', userSelect: 'none', background: '#f5f5f5', padding: '0.5rem 1rem', borderRadius: '4px', border: '1px solid #ddd' }}> <label className="cc-pill-toggle">
<input <input
type="checkbox" type="checkbox"
checked={showAveraged} checked={showAveraged}
onChange={(e) => setShowAveraged(e.target.checked)} onChange={(e) => setShowAveraged(e.target.checked)}
style={{ cursor: 'pointer' }}
/> />
<span>Show Averages</span> <span>Show Averages</span>
</label> </label>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', userSelect: 'none', background: '#f5f5f5', padding: '0.5rem 1rem', borderRadius: '4px', border: '1px solid #ddd' }}> <label className="cc-pill-toggle">
<input <input
type="checkbox" type="checkbox"
checked={showTrends} checked={showTrends}
onChange={(e) => setShowTrends(e.target.checked)} onChange={(e) => setShowTrends(e.target.checked)}
style={{ cursor: 'pointer' }}
/> />
<span>Show Week-over-Week Trends</span> <span>Show Week-over-Week Trends</span>
</label> </label>
@@ -169,41 +161,43 @@ function History() {
const chartData = prepareChartData(resource.checks || []); const chartData = prepareChartData(resource.checks || []);
return ( return (
<div key={resource.id} className="detail-section" style={{ marginBottom: '3rem', background: '#fff', padding: '1.5rem', borderRadius: '8px', boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }}> <div key={resource.id} className="detail-section cc-surface history-resource-card">
<h3 style={{ marginBottom: '0.5rem', color: '#667eea', fontSize: '1.5rem' }}>{resource.name}</h3> <h3 className="history-resource-title">{resource.name}</h3>
<p style={{ color: '#666', marginBottom: '1.5rem', fontSize: '0.95rem' }}> <p className="history-resource-meta">
<span style={{ marginRight: '1.5rem' }}>Type: <strong style={{ color: '#333' }}>{resource.type.toUpperCase()}</strong></span> <span>Type: <strong>{resource.type.toUpperCase()}</strong></span>
<span style={{ marginRight: '1.5rem' }}>Uptime: <strong style={{ color: resource.uptime >= 95 ? '#4caf50' : resource.uptime >= 80 ? '#ff9800' : '#f44336' }}>{resource.uptime}%</strong></span> <span>Uptime: <strong style={{ color: resource.uptime >= 95 ? '#22c55e' : resource.uptime >= 80 ? '#f59e0b' : '#ef4444' }}>{resource.uptime}%</strong></span>
<span>Avg Response: <strong style={{ color: '#333' }}>{resource.avgResponseTime}ms</strong></span> <span>Avg Response: <strong>{resource.avgResponseTime}ms</strong></span>
</p> </p>
{chartData.length > 0 ? ( {chartData.length > 0 ? (
<div className="chart-container" style={{ height: '400px', marginBottom: '1.5rem', background: '#fafafa', padding: '1rem', borderRadius: '6px' }}> <div className="chart-container history-chart-box">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<ComposedChart data={chartData} margin={{ top: 10, right: 30, left: 10, bottom: 60 }}> <ComposedChart data={chartData} margin={{ top: 10, right: 30, left: 10, bottom: 60 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#e0e0e0" /> <CartesianGrid strokeDasharray="3 3" stroke="#334155" />
<XAxis <XAxis
dataKey="label" dataKey="label"
tick={{ fontSize: 12, fill: '#666' }} tick={{ fontSize: 12, fill: '#94a3b8' }}
angle={-45} angle={-45}
textAnchor="end" textAnchor="end"
height={80} height={80}
/> />
<YAxis <YAxis
yAxisId="left" yAxisId="left"
label={{ value: 'Response Time (ms)', angle: -90, position: 'insideLeft', style: { fill: '#666' } }} label={{ value: 'Response Time (ms)', angle: -90, position: 'insideLeft', style: { fill: '#94a3b8' } }}
tick={{ fontSize: 12, fill: '#666' }} tick={{ fontSize: 12, fill: '#94a3b8' }}
/> />
<YAxis <YAxis
yAxisId="right" yAxisId="right"
orientation="right" orientation="right"
domain={[0, 1]} domain={[0, 1]}
ticks={[0, 1]} ticks={[0, 1]}
tick={{ fontSize: 12, fill: '#666' }} tick={{ fontSize: 12, fill: '#94a3b8' }}
label={{ value: 'Status', angle: 90, position: 'insideRight', style: { fill: '#666' } }} label={{ value: 'Status', angle: 90, position: 'insideRight', style: { fill: '#94a3b8' } }}
/> />
<Tooltip <Tooltip
contentStyle={{ backgroundColor: '#fff', border: '1px solid #ddd', borderRadius: '4px', padding: '10px' }} contentStyle={{ backgroundColor: '#0f172a', border: '1px solid #334155', borderRadius: '8px', padding: '10px' }}
labelStyle={{ color: '#cbd5e1' }}
itemStyle={{ color: '#e2e8f0' }}
formatter={(value, name) => { formatter={(value, name) => {
if (name === 'Response Time') return `${value}ms`; if (name === 'Response Time') return `${value}ms`;
if (name === 'Status') return value === 1 ? '✓ UP' : '✗ DOWN'; if (name === 'Status') return value === 1 ? '✓ UP' : '✗ DOWN';
@@ -249,18 +243,18 @@ function History() {
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
) : ( ) : (
<div style={{ color: '#999', padding: '2rem', textAlign: 'center', background: '#fafafa', borderRadius: '6px' }}>No check data available</div> <div className="cc-empty">No check data available</div>
)} )}
{showTrends && resource.trendsData && ( {showTrends && resource.trendsData && (
<div className="chart-container" style={{ height: '400px', marginBottom: '1.5rem', background: '#fafafa', padding: '1rem', borderRadius: '6px' }}> <div className="chart-container history-chart-box">
<h4 style={{ marginBottom: '1rem', color: '#667eea' }}>Week-over-Week Comparison</h4> <h4 className="history-resource-title">Week-over-Week Comparison</h4>
{(() => { {(() => {
const comparison = resource.trendsData?.comparison || {}; const comparison = resource.trendsData?.comparison || {};
const responseTimeChange = asNumberOrZero(comparison.response_time_change); const responseTimeChange = asNumberOrZero(comparison.response_time_change);
const uptimeChange = asNumberOrZero(comparison.uptime_change); const uptimeChange = asNumberOrZero(comparison.uptime_change);
return ( return (
<div style={{ display: 'flex', gap: '2rem', marginBottom: '1rem', fontSize: '0.9rem' }}> <div className="history-trend-row">
<div> <div>
<strong>Response Time Change:</strong>{' '} <strong>Response Time Change:</strong>{' '}
<span style={{ color: responseTimeChange >= 0 ? '#f44336' : '#4caf50' }}> <span style={{ color: responseTimeChange >= 0 ? '#f44336' : '#4caf50' }}>
@@ -280,20 +274,22 @@ function History() {
})()} })()}
<ResponsiveContainer width="100%" height="90%"> <ResponsiveContainer width="100%" height="90%">
<ComposedChart data={prepareTrendsChartData(resource.trendsData)} margin={{ top: 10, right: 30, left: 10, bottom: 60 }}> <ComposedChart data={prepareTrendsChartData(resource.trendsData)} margin={{ top: 10, right: 30, left: 10, bottom: 60 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#e0e0e0" /> <CartesianGrid strokeDasharray="3 3" stroke="#334155" />
<XAxis <XAxis
dataKey="day" dataKey="day"
tick={{ fontSize: 12, fill: '#666' }} tick={{ fontSize: 12, fill: '#94a3b8' }}
angle={-45} angle={-45}
textAnchor="end" textAnchor="end"
height={80} height={80}
/> />
<YAxis <YAxis
label={{ value: 'Response Time (ms)', angle: -90, position: 'insideLeft', style: { fill: '#666' } }} label={{ value: 'Response Time (ms)', angle: -90, position: 'insideLeft', style: { fill: '#94a3b8' } }}
tick={{ fontSize: 12, fill: '#666' }} tick={{ fontSize: 12, fill: '#94a3b8' }}
/> />
<Tooltip <Tooltip
contentStyle={{ backgroundColor: '#fff', border: '1px solid #ddd', borderRadius: '4px', padding: '10px' }} contentStyle={{ backgroundColor: '#0f172a', border: '1px solid #334155', borderRadius: '8px', padding: '10px' }}
labelStyle={{ color: '#cbd5e1' }}
itemStyle={{ color: '#e2e8f0' }}
formatter={(value, name) => { formatter={(value, name) => {
if (name === 'Current Period') return `${value}ms`; if (name === 'Current Period') return `${value}ms`;
if (name === 'Previous Period') return `${value}ms`; if (name === 'Previous Period') return `${value}ms`;
@@ -327,22 +323,22 @@ function History() {
</div> </div>
)} )}
<div className="stats-grid" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1rem' }}> <div className="cc-kpi-grid">
<div className="stat" style={{ background: '#f5f5f5', padding: '1rem', borderRadius: '6px' }}> <div className="cc-kpi">
<p className="stat-value" style={{ color: '#667eea', fontSize: '2rem', fontWeight: 'bold' }}>{resource.checks?.length || 0}</p> <p className="cc-kpi-label">Total Checks</p>
<p className="stat-label" style={{ color: '#666', fontSize: '0.9rem' }}>Total Checks</p> <p className="cc-kpi-value" style={{ color: '#60a5fa' }}>{resource.checks?.length || 0}</p>
</div> </div>
<div className="stat" style={{ background: '#f5f5f5', padding: '1rem', borderRadius: '6px' }}> <div className="cc-kpi">
<p className="stat-value" style={{ color: resource.uptime >= 95 ? '#4caf50' : resource.uptime >= 80 ? '#ff9800' : '#f44336', fontSize: '2rem', fontWeight: 'bold' }}>{resource.uptime}%</p> <p className="cc-kpi-label">Uptime</p>
<p className="stat-label" style={{ color: '#666', fontSize: '0.9rem' }}>Uptime</p> <p className="cc-kpi-value" style={{ color: resource.uptime >= 95 ? '#22c55e' : resource.uptime >= 80 ? '#f59e0b' : '#ef4444' }}>{resource.uptime}%</p>
</div> </div>
<div className="stat" style={{ background: '#f5f5f5', padding: '1rem', borderRadius: '6px' }}> <div className="cc-kpi">
<p className="stat-value" style={{ color: '#667eea', fontSize: '2rem', fontWeight: 'bold' }}>{resource.avgResponseTime}ms</p> <p className="cc-kpi-label">Avg Response</p>
<p className="stat-label" style={{ color: '#666', fontSize: '0.9rem' }}>Avg Response</p> <p className="cc-kpi-value" style={{ color: '#a78bfa' }}>{resource.avgResponseTime}ms</p>
</div> </div>
<div className="stat" style={{ background: '#f5f5f5', padding: '1rem', borderRadius: '6px' }}> <div className="cc-kpi">
<p className="stat-value" style={{ color: '#4caf50', fontSize: '2rem', fontWeight: 'bold' }}>{(resource.checks || []).filter((c) => c.status === 'up').length}/{resource.checks?.length || 0}</p> <p className="cc-kpi-label">Successful Checks</p>
<p className="stat-label" style={{ color: '#666', fontSize: '0.9rem' }}>Successful Checks</p> <p className="cc-kpi-value" style={{ color: '#34d399' }}>{(resource.checks || []).filter((c) => c.status === 'up').length}/{resource.checks?.length || 0}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -350,7 +346,7 @@ function History() {
})} })}
{totalItems > pageSize && ( {totalItems > pageSize && (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '1rem', marginTop: '2rem', padding: '1.5rem', background: '#f5f5f5', borderRadius: '8px' }}> <div className="cc-pagination">
<button <button
className="btn btn-secondary" className="btn btn-secondary"
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))} onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
@@ -358,8 +354,8 @@ function History() {
> >
← Previous ← Previous
</button> </button>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}> <div className="cc-controls">
<span style={{ color: '#666', fontSize: '0.9rem' }}>Page</span> <span>Page</span>
<input <input
type="number" type="number"
value={currentPage} value={currentPage}
@@ -367,14 +363,14 @@ function History() {
const page = Math.max(1, Math.min(Math.ceil(totalItems / pageSize), parseInt(e.target.value) || 1)); const page = Math.max(1, Math.min(Math.ceil(totalItems / pageSize), parseInt(e.target.value) || 1));
setCurrentPage(page); setCurrentPage(page);
}} }}
style={{ width: '50px', padding: '0.5rem', border: '1px solid #ddd', borderRadius: '4px', textAlign: 'center' }} className="cc-input-sm"
/> />
<span style={{ color: '#666', fontSize: '0.9rem' }}>of {Math.ceil(totalItems / pageSize)}</span> <span>of {Math.ceil(totalItems / pageSize)}</span>
</div> </div>
<select <select
value={pageSize} value={pageSize}
onChange={(e) => { setPageSize(parseInt(e.target.value)); setCurrentPage(1); }} onChange={(e) => { setPageSize(parseInt(e.target.value)); setCurrentPage(1); }}
style={{ padding: '0.5rem', border: '1px solid #ddd', borderRadius: '4px', background: '#fff' }} className="cc-select-sm"
> >
<option value={5}>5 per page</option> <option value={5}>5 per page</option>
<option value={10}>10 per page</option> <option value={10}>10 per page</option>
+21 -28
View File
@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import axios from 'axios'; import axios from 'axios';
import { formatLocalTime } from './utils/timeUtils'; import { formatLocalTime } from './utils/timeUtils';
import './CommandCenterPages.css';
function Notifications() { function Notifications() {
const [notifications, setNotifications] = useState([]); const [notifications, setNotifications] = useState([]);
@@ -106,11 +107,13 @@ function Notifications() {
const totalPages = Math.ceil(total / itemsPerPage); const totalPages = Math.ceil(total / itemsPerPage);
return ( return (
<div className="container" style={{ paddingTop: '2rem' }}> <div className="container cc-page">
<h2>📬 Notifications</h2> <div className="cc-page-header">
<h2 className="cc-page-title">Notifications</h2>
</div>
<div style={{ marginBottom: '2rem', display: 'flex', gap: '1rem', flexWrap: 'wrap', alignItems: 'center' }}> <div className="cc-controls" style={{ marginBottom: '1rem' }}>
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}> <div className="cc-controls">
<button <button
className={`btn ${filter === 'all' ? 'btn-primary' : 'btn-secondary'}`} className={`btn ${filter === 'all' ? 'btn-primary' : 'btn-secondary'}`}
onClick={() => { setFilter('all'); setCurrentPage(1); }} onClick={() => { setFilter('all'); setCurrentPage(1); }}
@@ -143,50 +146,40 @@ function Notifications() {
</div> </div>
{loading ? ( {loading ? (
<p style={{ textAlign: 'center', color: '#999' }}>Loading...</p> <p className="cc-empty">Loading...</p>
) : notifications.length === 0 ? ( ) : notifications.length === 0 ? (
<div style={{ textAlign: 'center', padding: '3rem 1rem', color: '#999' }}> <div className="cc-empty">
<p>No {filter === 'unread' ? 'unread ' : filter === 'read' ? 'read ' : ''}notifications</p> <p>No {filter === 'unread' ? 'unread ' : filter === 'read' ? 'read ' : ''}notifications</p>
</div> </div>
) : ( ) : (
<> <>
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '1rem' }}> <div className="notifications-list">
{notifications.map((notification) => ( {notifications.map((notification) => (
<div <div
key={notification.id} key={notification.id}
style={{ className={`notification-row ${notification.read ? 'is-read' : 'is-unread'}`}
padding: '1rem',
border: '1px solid #ddd',
borderRadius: '6px',
backgroundColor: notification.read ? 'white' : '#f0f8ff',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
gap: '1rem'
}}
> >
<div style={{ flex: 1 }}> <div className="notification-content">
<h4 style={{ margin: '0 0 0.5rem 0', fontSize: '1rem' }}>{notification.title}</h4> <h4 className="notification-title">{notification.title}</h4>
<p style={{ margin: '0 0 0.5rem 0', color: '#666', fontSize: '0.95rem' }}> <p className="notification-message">
{notification.message} {notification.message}
</p> </p>
<div style={{ display: 'flex', gap: '1rem', fontSize: '0.85rem', color: '#999', flexWrap: 'wrap' }}> <div className="notification-meta">
{notification.resource_name && ( {notification.resource_name && (
<span style={{ backgroundColor: '#f0f0f0', padding: '0.25rem 0.5rem', borderRadius: '3px' }}> <span className="notification-chip">
📌 {notification.resource_name} {notification.resource_name}
</span> </span>
)} )}
<span>⏰ {formatLocalTime(notification.created_at)}</span> <span>{formatLocalTime(notification.created_at)}</span>
</div> </div>
</div> </div>
<div style={{ display: 'flex', gap: '0.5rem', flexShrink: 0 }}> <div className="notification-actions">
{!notification.read && ( {!notification.read && (
<button <button
className="btn btn-sm" className="btn btn-sm"
onClick={() => handleMarkAsRead(notification.id)} onClick={() => handleMarkAsRead(notification.id)}
title="Mark as read" title="Mark as read"
style={{ padding: '0.5rem 0.75rem', fontSize: '0.85rem' }}
> >
✓ Read ✓ Read
</button> </button>
@@ -195,7 +188,7 @@ function Notifications() {
className="btn btn-sm" className="btn btn-sm"
onClick={() => handleDelete(notification.id)} onClick={() => handleDelete(notification.id)}
title="Delete" title="Delete"
style={{ padding: '0.5rem 0.75rem', fontSize: '0.85rem', background: '#dc3545' }} style={{ background: '#b91c1c' }}
> >
✕ Delete ✕ Delete
</button> </button>
@@ -206,7 +199,7 @@ function Notifications() {
{/* Pagination */} {/* Pagination */}
{totalPages > 1 && ( {totalPages > 1 && (
<div style={{ marginTop: '2rem', display: 'flex', justifyContent: 'center', gap: '0.5rem', flexWrap: 'wrap' }}> <div className="cc-pagination">
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => ( {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<button <button
key={page} key={page}
+57 -39
View File
@@ -2,9 +2,14 @@
padding-top: 2rem; padding-top: 2rem;
} }
.observability-page h2 {
color: #f8fafc;
text-shadow: 0 1px 0 rgba(15, 23, 42, 0.3);
}
.obs-tabs { .obs-tabs {
margin-bottom: 2rem; margin-bottom: 2rem;
border-bottom: 1px solid #dbe2ea; border-bottom: 1px solid #334155;
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
flex-wrap: wrap; flex-wrap: wrap;
@@ -19,15 +24,20 @@
background: transparent; background: transparent;
cursor: pointer; cursor: pointer;
font-weight: 600; font-weight: 600;
color: #64748b; color: #cbd5e1;
}
.obs-tab:focus-visible {
outline: 2px solid #60a5fa;
outline-offset: 2px;
} }
.obs-tab.is-active { .obs-tab.is-active {
border-bottom-color: #0ea5a2; border-bottom-color: #60a5fa;
color: #0f172a; color: #f8fafc;
background: #ffffff; background: #0f172a;
border-color: #dbe2ea; border-color: #334155;
border-bottom-color: #0ea5a2; border-bottom-color: #60a5fa;
} }
.obs-summary-grid { .obs-summary-grid {
@@ -39,30 +49,30 @@
.obs-summary-card { .obs-summary-card {
padding: 1rem; padding: 1rem;
border: 1px solid #dbe2ea; border: 1px solid #334155;
border-radius: 14px; border-radius: 14px;
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); background: linear-gradient(180deg, #0f172a 0%, #0b1325 100%);
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.06); box-shadow: none;
} }
.obs-card-label { .obs-card-label {
font-size: 0.85rem; font-size: 0.85rem;
color: #64748b; color: #cbd5e1;
} }
.obs-card-value { .obs-card-value {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 700; font-weight: 700;
margin-top: 0.45rem; margin-top: 0.45rem;
color: #0f172a; color: #f8fafc;
} }
.obs-card-value.is-danger { .obs-card-value.is-danger {
color: #b91c1c; color: #f87171;
} }
.obs-card-value.is-ok { .obs-card-value.is-ok {
color: #0f766e; color: #34d399;
} }
.obs-table-wrap { .obs-table-wrap {
@@ -81,9 +91,9 @@
.obs-table th { .obs-table th {
text-align: left; text-align: left;
padding: 0.75rem; padding: 0.75rem;
background: #f1f5f9; background: #0f172a;
border-bottom: 2px solid #dbe2ea; border-bottom: 2px solid #334155;
color: #1e293b; color: #e2e8f0;
font-size: 0.84rem; font-size: 0.84rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.02em; letter-spacing: 0.02em;
@@ -91,11 +101,12 @@
.obs-table td { .obs-table td {
padding: 0.75rem; padding: 0.75rem;
border-bottom: 1px solid #e5e7eb; border-bottom: 1px solid #1f2937;
color: #f1f5f9;
} }
.obs-table tbody tr:hover { .obs-table tbody tr:hover {
background: #f0f9ff; background: #1e293b;
} }
.obs-right { .obs-right {
@@ -105,38 +116,38 @@
.obs-rate-pill { .obs-rate-pill {
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
border-radius: 4px; border-radius: 4px;
font-size: 0.85rem; font-size: 0.88rem;
font-weight: 600; font-weight: 600;
} }
.obs-rate-pill.is-good { .obs-rate-pill.is-good {
background: #dcfce7; background: #052e16;
color: #166534; color: #4ade80;
} }
.obs-rate-pill.is-warn { .obs-rate-pill.is-warn {
background: #fef3c7; background: #3f2a0f;
color: #92400e; color: #fbbf24;
} }
.obs-empty-copy { .obs-empty-copy {
color: #94a3b8; color: #cbd5e1;
text-align: center; text-align: center;
padding: 2rem; padding: 2rem;
} }
.obs-zebra { .obs-zebra {
background: #f8fafc; background: #0b1325;
} }
.obs-error-code { .obs-error-code {
font-size: 0.85rem; font-size: 0.85rem;
color: #b91c1c; color: #f87171;
} }
.obs-muted-sm { .obs-muted-sm {
font-size: 0.85rem; font-size: 0.85rem;
color: #64748b; color: #cbd5e1;
} }
.obs-small { .obs-small {
@@ -146,15 +157,15 @@
.obs-audit-summary { .obs-audit-summary {
margin-bottom: 2rem; margin-bottom: 2rem;
padding: 1rem; padding: 1rem;
background: #f8fafc; background: #0f172a;
border-radius: 12px; border-radius: 12px;
border: 1px solid #dbe2ea; border: 1px solid #334155;
} }
.obs-audit-summary-line { .obs-audit-summary-line {
margin: 0.25rem 0; margin: 0.25rem 0;
font-size: 0.9rem; font-size: 0.9rem;
color: #64748b; color: #e2e8f0;
} }
.obs-filters-grid { .obs-filters-grid {
@@ -166,15 +177,21 @@
.obs-filter-input { .obs-filter-input {
padding: 0.5rem; padding: 0.5rem;
border: 1px solid #dbe2ea; border: 1px solid #334155;
border-radius: 10px; border-radius: 10px;
background: #fff; background: #111827;
color: #e2e8f0;
} }
.obs-filter-input:focus { .obs-filter-input:focus {
outline: none; outline: none;
border-color: #0ea5a2; border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(14, 165, 162, 0.18); box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2);
}
.obs-toggle-code:focus-visible {
outline: 2px solid #60a5fa;
outline-offset: 2px;
} }
.obs-click-row { .obs-click-row {
@@ -183,11 +200,11 @@
.obs-toggle-code { .obs-toggle-code {
cursor: pointer; cursor: pointer;
color: #0f766e; color: #bfdbfe;
} }
.obs-expanded-row { .obs-expanded-row {
background: #f0f9ff; background: #111b2e;
} }
.obs-expanded-cell { .obs-expanded-cell {
@@ -196,11 +213,12 @@
.obs-pre { .obs-pre {
margin-top: 0.5rem; margin-top: 0.5rem;
background: #fff; background: #0b1325;
color: #e2e8f0;
padding: 0.75rem; padding: 0.75rem;
border-radius: 8px; border-radius: 8px;
overflow-x: auto; overflow-x: auto;
border: 1px solid #dbe2ea; border: 1px solid #334155;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
+54 -60
View File
@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import axios from 'axios'; import axios from 'axios';
import './App.css'; import './App.css';
import './CommandCenterPages.css';
function SLA() { function SLA() {
const [slaData, setSlaData] = useState([]); const [slaData, setSlaData] = useState([]);
@@ -66,22 +67,15 @@ function SLA() {
} : null; } : null;
return ( return (
<div className="container"> <div className="container cc-page">
{limitedByRetention && ( {limitedByRetention && (
<div style={{ <div className="cc-alert">
padding: '1rem',
marginBottom: '1rem',
backgroundColor: '#fff3cd',
border: '1px solid #ffc107',
borderRadius: '4px',
color: '#856404'
}}>
⚠️ SLA data limited to {effectiveDays} days (server retention setting). Requested {days} days. ⚠️ SLA data limited to {effectiveDays} days (server retention setting). Requested {days} days.
</div> </div>
)} )}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem', flexWrap: 'wrap', gap: '1rem' }}> <div className="cc-page-header">
<h2>SLA Dashboard</h2> <h2 className="cc-page-title">SLA Dashboard</h2>
<div style={{ display: 'flex', gap: '0.5rem' }}> <div className="cc-controls">
{[7, 14, 30, 90].map((d) => ( {[7, 14, 30, 90].map((d) => (
<button <button
key={d} key={d}
@@ -95,33 +89,33 @@ function SLA() {
))} ))}
</div> </div>
{limitedByRetention && ( {limitedByRetention && (
<div style={{ background: '#FFF8E1', color: '#8D6E63', padding: '0.5rem 0.75rem', borderRadius: '6px', border: '1px solid #FFECB3' }}> <div className="cc-alert" style={{ marginBottom: 0 }}>
Limited by data retention ({effectiveDays} days). Increase Retention in Settings to view longer periods. Limited by data retention ({effectiveDays} days). Increase Retention in Settings to view longer periods.
</div> </div>
)} )}
</div> </div>
{overallStats && ( {overallStats && (
<div className="stats-grid" style={{ marginBottom: '2rem', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))' }}> <div className="cc-kpi-grid" style={{ marginBottom: '1rem' }}>
<div className="stat" style={{ background: '#f5f5f5', padding: '1.5rem', borderRadius: '8px' }}> <div className="cc-kpi">
<p className="stat-value" style={{ color: '#667eea', fontSize: '2.5rem' }}>{overallStats.totalResources}</p> <p className="cc-kpi-label">Total Resources</p>
<p className="stat-label">Total Resources</p> <p className="cc-kpi-value" style={{ color: '#60a5fa' }}>{overallStats.totalResources}</p>
</div> </div>
<div className="stat" style={{ background: '#f5f5f5', padding: '1.5rem', borderRadius: '8px' }}> <div className="cc-kpi">
<p className="stat-value" style={{ color: '#4caf50', fontSize: '2.5rem' }}>{overallStats.meetingTarget}/{overallStats.totalResources}</p> <p className="cc-kpi-label">Meeting SLA Target</p>
<p className="stat-label">Meeting SLA Target</p> <p className="cc-kpi-value" style={{ color: '#22c55e' }}>{overallStats.meetingTarget}/{overallStats.totalResources}</p>
</div> </div>
<div className="stat" style={{ background: '#f5f5f5', padding: '1.5rem', borderRadius: '8px' }}> <div className="cc-kpi">
<p className="stat-value" style={{ color: '#667eea', fontSize: '2.5rem' }}>{overallStats.avgUptime}%</p> <p className="cc-kpi-label">Average Uptime</p>
<p className="stat-label">Average Uptime</p> <p className="cc-kpi-value" style={{ color: '#a78bfa' }}>{overallStats.avgUptime}%</p>
</div> </div>
<div className="stat" style={{ background: '#f5f5f5', padding: '1.5rem', borderRadius: '8px' }}> <div className="cc-kpi">
<p className="stat-value" style={{ color: '#ff9800', fontSize: '2.5rem' }}>{overallStats.totalIncidents}</p> <p className="cc-kpi-label">Total Incidents</p>
<p className="stat-label">Total Incidents</p> <p className="cc-kpi-value" style={{ color: '#f59e0b' }}>{overallStats.totalIncidents}</p>
</div> </div>
<div className="stat" style={{ background: '#f5f5f5', padding: '1.5rem', borderRadius: '8px' }}> <div className="cc-kpi">
<p className="stat-value" style={{ color: '#f44336', fontSize: '2.5rem' }}>{Math.round(overallStats.totalDowntime)}m</p> <p className="cc-kpi-label">Total Downtime</p>
<p className="stat-label">Total Downtime</p> <p className="cc-kpi-value" style={{ color: '#ef4444' }}>{Math.round(overallStats.totalDowntime)}m</p>
</div> </div>
</div> </div>
)} )}
@@ -132,46 +126,46 @@ function SLA() {
<p>Add resources to start tracking SLA metrics</p> <p>Add resources to start tracking SLA metrics</p>
</div> </div>
) : ( ) : (
<div style={{ overflowX: 'auto' }}> <div className="cc-table-wrap">
<table style={{ width: '100%', borderCollapse: 'collapse', background: '#fff', borderRadius: '8px', overflow: 'hidden', boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }}> <table className="cc-table">
<thead> <thead>
<tr style={{ background: '#667eea', color: 'white' }}> <tr>
<th style={{ padding: '1rem', textAlign: 'left' }}>Resource</th> <th>Resource</th>
<th style={{ padding: '1rem', textAlign: 'center' }}>Target</th> <th style={{ textAlign: 'center' }}>Target</th>
<th style={{ padding: '1rem', textAlign: 'center' }}>Actual</th> <th style={{ textAlign: 'center' }}>Actual</th>
<th style={{ padding: '1rem', textAlign: 'center' }}>Status</th> <th style={{ textAlign: 'center' }}>Status</th>
<th style={{ padding: '1rem', textAlign: 'center' }}>Checks</th> <th style={{ textAlign: 'center' }}>Checks</th>
<th style={{ padding: '1rem', textAlign: 'center' }}>Incidents</th> <th style={{ textAlign: 'center' }}>Incidents</th>
<th style={{ padding: '1rem', textAlign: 'center' }}>Downtime</th> <th style={{ textAlign: 'center' }}>Downtime</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{slaData.map((item, idx) => ( {slaData.map((item, idx) => (
<tr key={item.resource_id} style={{ borderBottom: '1px solid #eee', background: idx % 2 === 0 ? '#fafafa' : '#fff' }}> <tr key={item.resource_id} style={{ background: idx % 2 === 0 ? '#0b1325' : '#0f172a' }}>
<td style={{ padding: '1rem', fontWeight: 'bold' }}>{item.resource_name}</td> <td style={{ fontWeight: 'bold' }}>{item.resource_name}</td>
<td style={{ padding: '1rem', textAlign: 'center' }}>{item.sla_target}%</td> <td style={{ textAlign: 'center' }}>{item.sla_target}%</td>
<td style={{ padding: '1rem', textAlign: 'center', fontWeight: 'bold', color: item.meets_target ? '#4caf50' : '#f44336' }}> <td style={{ textAlign: 'center', fontWeight: 'bold', color: item.meets_target ? '#22c55e' : '#ef4444' }}>
{item.actual_uptime}% {item.actual_uptime}%
</td> </td>
<td style={{ padding: '1rem', textAlign: 'center' }}> <td style={{ textAlign: 'center' }}>
<span style={{ <span style={{
padding: '0.25rem 0.75rem', padding: '0.25rem 0.75rem',
borderRadius: '12px', borderRadius: '12px',
background: item.meets_target ? '#e8f5e9' : '#ffebee', background: item.meets_target ? '#052e16' : '#3f1d1d',
color: item.meets_target ? '#2e7d32' : '#c62828', color: item.meets_target ? '#4ade80' : '#fda4af',
fontSize: '0.875rem', fontSize: '0.875rem',
fontWeight: 'bold' fontWeight: 'bold'
}}> }}>
{item.meets_target ? '✓ PASS' : '✗ FAIL'} {item.meets_target ? '✓ PASS' : '✗ FAIL'}
</span> </span>
</td> </td>
<td style={{ padding: '1rem', textAlign: 'center' }}> <td style={{ textAlign: 'center' }}>
{item.successful_checks}/{item.total_checks} {item.successful_checks}/{item.total_checks}
</td> </td>
<td style={{ padding: '1rem', textAlign: 'center', color: item.incidents > 0 ? '#ff9800' : '#666' }}> <td style={{ textAlign: 'center', color: item.incidents > 0 ? '#f59e0b' : '#94a3b8' }}>
{item.incidents} {item.incidents}
</td> </td>
<td style={{ padding: '1rem', textAlign: 'center' }}> <td style={{ textAlign: 'center' }}>
{item.downtime_minutes > 0 ? `${item.downtime_minutes}m` : '-'} {item.downtime_minutes > 0 ? `${item.downtime_minutes}m` : '-'}
</td> </td>
</tr> </tr>
@@ -180,7 +174,7 @@ function SLA() {
</table> </table>
{totalItems > pageSize && ( {totalItems > pageSize && (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '1rem', marginTop: '2rem', padding: '1.5rem', background: '#f5f5f5', borderRadius: '8px' }}> <div className="cc-pagination" style={{ marginTop: '1rem' }}>
<button <button
className="btn btn-secondary" className="btn btn-secondary"
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))} onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
@@ -188,8 +182,8 @@ function SLA() {
> >
← Previous ← Previous
</button> </button>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}> <div className="cc-controls">
<span style={{ color: '#666', fontSize: '0.9rem' }}>Page</span> <span>Page</span>
<input <input
type="number" type="number"
value={currentPage} value={currentPage}
@@ -197,14 +191,14 @@ function SLA() {
const page = Math.max(1, Math.min(Math.ceil(totalItems / pageSize), parseInt(e.target.value) || 1)); const page = Math.max(1, Math.min(Math.ceil(totalItems / pageSize), parseInt(e.target.value) || 1));
setCurrentPage(page); setCurrentPage(page);
}} }}
style={{ width: '50px', padding: '0.5rem', border: '1px solid #ddd', borderRadius: '4px', textAlign: 'center' }} className="cc-input-sm"
/> />
<span style={{ color: '#666', fontSize: '0.9rem' }}>of {Math.ceil(totalItems / pageSize)}</span> <span>of {Math.ceil(totalItems / pageSize)}</span>
</div> </div>
<select <select
value={pageSize} value={pageSize}
onChange={(e) => { setPageSize(parseInt(e.target.value)); setCurrentPage(1); }} onChange={(e) => { setPageSize(parseInt(e.target.value)); setCurrentPage(1); }}
style={{ padding: '0.5rem', border: '1px solid #ddd', borderRadius: '4px', background: '#fff' }} className="cc-select-sm"
> >
<option value={5}>5 per page</option> <option value={5}>5 per page</option>
<option value={10}>10 per page</option> <option value={10}>10 per page</option>
@@ -223,13 +217,13 @@ function SLA() {
</div> </div>
)} )}
<div style={{ marginTop: '2rem', padding: '1.5rem', background: '#f5f5f5', borderRadius: '8px' }}> <div className="cc-surface" style={{ marginTop: '1rem' }}>
<h3 style={{ marginTop: 0, color: '#667eea' }}>SLA Report Summary</h3> <h3 className="history-resource-title">SLA Report Summary</h3>
<p style={{ color: '#666', lineHeight: '1.6' }}> <p style={{ color: '#cbd5e1', lineHeight: '1.6' }}>
This report shows the Service Level Agreement (SLA) performance for all monitored resources over the last <strong>{effectiveDays} days</strong>. This report shows the Service Level Agreement (SLA) performance for all monitored resources over the last <strong>{effectiveDays} days</strong>.
Resources meeting their target uptime are marked as <strong style={{ color: '#4caf50' }}>PASS</strong>, while those below target are marked as <strong style={{ color: '#f44336' }}>FAIL</strong>. Resources meeting their target uptime are marked as <strong style={{ color: '#22c55e' }}>PASS</strong>, while those below target are marked as <strong style={{ color: '#ef4444' }}>FAIL</strong>.
</p> </p>
<p style={{ color: '#666', lineHeight: '1.6', marginBottom: 0 }}> <p style={{ color: '#cbd5e1', lineHeight: '1.6', marginBottom: 0 }}>
Overall performance: <strong>{overallStats?.meetingTarget || 0}</strong> out of <strong>{overallStats?.totalResources || 0}</strong> resources ({overallStats ? Math.round((overallStats.meetingTarget / overallStats.totalResources) * 100) : 0}%) are meeting their SLA targets. Overall performance: <strong>{overallStats?.meetingTarget || 0}</strong> out of <strong>{overallStats?.totalResources || 0}</strong> resources ({overallStats ? Math.round((overallStats.meetingTarget / overallStats.totalResources) * 100) : 0}%) are meeting their SLA targets.
</p> </p>
</div> </div>
+77 -39
View File
@@ -1,33 +1,39 @@
.settings-wizard { .settings-wizard {
max-width: 900px; max-width: 980px;
margin: 0 auto; margin: 0 auto;
color: #e2e8f0;
}
.settings-wizard > h2 {
color: #f8fafc;
text-shadow: 0 1px 0 rgba(15, 23, 42, 0.3);
} }
.settings-description { .settings-description {
color: #666; color: #cbd5e1;
margin-bottom: 2rem; margin-bottom: 1.4rem;
} }
.settings-section { .settings-section {
background: white; background: rgba(15, 23, 42, 0.85);
border-radius: 8px; border-radius: 12px;
padding: 2rem; padding: 1.35rem;
margin-bottom: 1.5rem; margin-bottom: 1rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.1); border: 1px solid #334155;
} }
.settings-header { .settings-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 1.5rem; margin-bottom: 1rem;
padding-bottom: 1rem; padding-bottom: 0.8rem;
border-bottom: 2px solid #f0f0f0; border-bottom: 1px solid #334155;
} }
.settings-header h3 { .settings-header h3 {
margin: 0; margin: 0;
color: #333; color: #f8fafc;
} }
.settings-form { .settings-form {
@@ -43,23 +49,43 @@
.form-group small { .form-group small {
display: block; display: block;
color: #999; color: #cbd5e1;
font-size: 0.85rem; font-size: 0.85rem;
margin-top: 0.25rem; margin-top: 0.25rem;
} }
.settings-wizard .form-group label {
color: #e2e8f0;
}
.settings-wizard .form-group input,
.settings-wizard .form-group select,
.settings-wizard .form-group textarea {
background: #0b1325;
color: #f8fafc;
border: 1px solid #334155;
}
.settings-wizard .form-group input:focus,
.settings-wizard .form-group select:focus,
.settings-wizard .form-group textarea:focus {
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.28);
}
.info-box { .info-box {
background: #f8f9fa; background: #0b1325;
border-left: 4px solid #667eea; border-left: 4px solid #60a5fa;
padding: 1rem; padding: 1rem;
margin: 1.5rem 0; margin: 1.5rem 0;
border-radius: 4px; border-radius: 8px;
border: 1px solid #334155;
} }
.info-box strong { .info-box strong {
display: block; display: block;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
color: #333; color: #f8fafc;
} }
.info-box ol { .info-box ol {
@@ -69,11 +95,11 @@
.info-box li { .info-box li {
margin: 0.25rem 0; margin: 0.25rem 0;
color: #666; color: #e2e8f0;
} }
.info-box a { .info-box a {
color: #667eea; color: #bfdbfe;
text-decoration: none; text-decoration: none;
} }
@@ -82,12 +108,14 @@
} }
.info-box pre { .info-box pre {
background: white; background: #111827;
color: #f8fafc;
padding: 1rem; padding: 1rem;
border-radius: 4px; border-radius: 8px;
overflow-x: auto; overflow-x: auto;
font-size: 0.85rem; font-size: 0.85rem;
margin-top: 0.5rem; margin-top: 0.5rem;
border: 1px solid #334155;
} }
.toggle-switch { .toggle-switch {
@@ -110,7 +138,7 @@
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background-color: #ccc; background-color: #334155;
transition: .4s; transition: .4s;
border-radius: 34px; border-radius: 34px;
} }
@@ -122,13 +150,17 @@
width: 26px; width: 26px;
left: 4px; left: 4px;
bottom: 4px; bottom: 4px;
background-color: white; background-color: #e2e8f0;
transition: .4s; transition: .4s;
border-radius: 50%; border-radius: 50%;
} }
.toggle-switch input:checked + .toggle-slider { .toggle-switch input:checked + .toggle-slider {
background-color: #667eea; background-color: #2563eb;
}
.toggle-switch input:focus + .toggle-slider {
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.28);
} }
.toggle-switch input:checked + .toggle-slider:before { .toggle-switch input:checked + .toggle-slider:before {
@@ -136,13 +168,19 @@
} }
.btn-secondary { .btn-secondary {
background: #6c757d; background: #1e293b;
color: white; color: #f8fafc;
margin-top: 1rem; margin-top: 1rem;
border: 1px solid #334155;
} }
.btn-secondary:hover:not(:disabled) { .btn-secondary:hover:not(:disabled) {
background: #5a6268; background: #334155;
}
.btn-secondary:focus-visible {
outline: 2px solid #60a5fa;
outline-offset: 2px;
} }
.btn-secondary:disabled { .btn-secondary:disabled {
@@ -157,15 +195,15 @@
} }
.test-result.success { .test-result.success {
background: #d4edda; background: #052e16;
color: #155724; color: #86efac;
border: 1px solid #c3e6cb; border: 1px solid #166534;
} }
.test-result.error { .test-result.error {
background: #f8d7da; background: #3f1d1d;
color: #721c24; color: #fda4af;
border: 1px solid #f5c6cb; border: 1px solid #7f1d1d;
} }
.test-result strong { .test-result strong {
@@ -201,15 +239,15 @@
} }
.save-status.success { .save-status.success {
background: #4caf50; background: #166534;
color: white; color: #dcfce7;
border-left: 4px solid #388e3c; border-left: 4px solid #22c55e;
} }
.save-status.error { .save-status.error {
background: #f44336; background: #7f1d1d;
color: white; color: #fee2e2;
border-left: 4px solid #d32f2f; border-left: 4px solid #ef4444;
} }
.save-status strong { .save-status strong {
+2 -2
View File
@@ -3,8 +3,8 @@ body {
font-family: 'Manrope', 'IBM Plex Sans', 'Segoe UI', sans-serif; font-family: 'Manrope', 'IBM Plex Sans', 'Segoe UI', sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
color: #0f172a; color: #e2e8f0;
background-color: #f8fafc; background-color: #020617;
} }
* { * {
+34
View File
@@ -345,6 +345,40 @@ app.patch('/api/resources/:id/group', (req, res) => {
} }
}); });
// Toggle maintenance mode without overwriting other resource fields
app.patch('/api/resources/:id/maintenance-mode', (req, res) => {
try {
const id = parseInt(req.params.id, 10);
const { maintenance_mode } = req.body;
if (!Number.isInteger(id)) {
return res.status(400).json({ error: 'Invalid resource id' });
}
if (typeof maintenance_mode !== 'boolean') {
return res.status(400).json({ error: 'maintenance_mode must be a boolean' });
}
const stmt = db.prepare('UPDATE resources SET maintenance_mode = ? WHERE id = ?');
const info = stmt.run(maintenance_mode ? 1 : 0, id);
if (info.changes === 0) {
return res.status(404).json({ error: 'Resource not found' });
}
// Invalidate related cache entries and broadcast live update
cache.invalidatePattern('history:');
cache.invalidatePattern('sla:');
if (global.broadcastDashboardUpdate) {
try { global.broadcastDashboardUpdate(); } catch {}
}
const updated = db.prepare('SELECT * FROM resources WHERE id = ?').get(id);
res.json(updated);
} catch (err) {
res.status(500).json({ error: 'Failed to update maintenance mode', details: String(err) });
}
});
// Delete resource // Delete resource
app.delete('/api/resources/:id', (req, res) => { app.delete('/api/resources/:id', (req, res) => {
const resource = db.prepare('SELECT name, url FROM resources WHERE id = ?').get(req.params.id); const resource = db.prepare('SELECT name, url FROM resources WHERE id = ?').get(req.params.id);