Redesign following Web Interface Guidelines
- Added skip link for accessibility - Proper semantic HTML with <table>, <th scope>, <main> - All icon buttons have aria-label - Focus-visible rings on all interactive elements - Proper form labels with htmlFor - Input autocomplete and type attributes - Role and aria attributes for modal - Visual focus states without outline-none - Better color contrast and status indicators - Hover and transition improvements
This commit is contained in:
parent
28cd726320
commit
796a760dc3
BIN
.next/cache/webpack/client-development/0.pack.gz
vendored
BIN
.next/cache/webpack/client-development/0.pack.gz
vendored
Binary file not shown.
BIN
.next/cache/webpack/client-development/5.pack.gz
vendored
BIN
.next/cache/webpack/client-development/5.pack.gz
vendored
Binary file not shown.
BIN
.next/cache/webpack/client-development/7.pack.gz
vendored
Normal file
BIN
.next/cache/webpack/client-development/7.pack.gz
vendored
Normal file
Binary file not shown.
BIN
.next/cache/webpack/client-development/index.pack.gz
vendored
BIN
.next/cache/webpack/client-development/index.pack.gz
vendored
Binary file not shown.
Binary file not shown.
BIN
.next/cache/webpack/server-development/1.pack.gz
vendored
BIN
.next/cache/webpack/server-development/1.pack.gz
vendored
Binary file not shown.
BIN
.next/cache/webpack/server-development/5.pack.gz
vendored
BIN
.next/cache/webpack/server-development/5.pack.gz
vendored
Binary file not shown.
BIN
.next/cache/webpack/server-development/7.pack.gz
vendored
Normal file
BIN
.next/cache/webpack/server-development/7.pack.gz
vendored
Normal file
Binary file not shown.
BIN
.next/cache/webpack/server-development/index.pack.gz
vendored
BIN
.next/cache/webpack/server-development/index.pack.gz
vendored
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@ -130,7 +130,7 @@
|
||||
/******/
|
||||
/******/ /* webpack/runtime/getFullHash */
|
||||
/******/ (() => {
|
||||
/******/ __webpack_require__.h = () => ("175a446eeb5052e2")
|
||||
/******/ __webpack_require__.h = () => ("1930de2390f27e98")
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -190,7 +190,7 @@
|
||||
/******/
|
||||
/******/ /* webpack/runtime/getFullHash */
|
||||
/******/ (() => {
|
||||
/******/ __webpack_require__.h = () => ("1122623f302a8ef0")
|
||||
/******/ __webpack_require__.h = () => ("946779c75e05dcfd")
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/global */
|
||||
|
||||
@ -0,0 +1 @@
|
||||
{"c":["app/page","webpack"],"r":[],"m":[]}
|
||||
22
.next/static/webpack/app/page.1122623f302a8ef0.hot-update.js
Normal file
22
.next/static/webpack/app/page.1122623f302a8ef0.hot-update.js
Normal file
File diff suppressed because one or more lines are too long
18
.next/static/webpack/webpack.1122623f302a8ef0.hot-update.js
Normal file
18
.next/static/webpack/webpack.1122623f302a8ef0.hot-update.js
Normal file
@ -0,0 +1,18 @@
|
||||
"use strict";
|
||||
/*
|
||||
* ATTENTION: An "eval-source-map" devtool has been used.
|
||||
* This devtool is neither made for production nor for readable output files.
|
||||
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
|
||||
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
|
||||
* or disable the default devtool with "devtool: false".
|
||||
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
|
||||
*/
|
||||
self["webpackHotUpdate_N_E"]("webpack",{},
|
||||
/******/ function(__webpack_require__) { // webpackRuntimeModules
|
||||
/******/ /* webpack/runtime/getFullHash */
|
||||
/******/ (() => {
|
||||
/******/ __webpack_require__.h = () => ("946779c75e05dcfd")
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ }
|
||||
);
|
||||
File diff suppressed because one or more lines are too long
218
src/app/page.tsx
218
src/app/page.tsx
@ -151,123 +151,148 @@ export default function HeartbeatMonitor() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 flex items-center justify-center">
|
||||
<div className="text-slate-400">Loading...</div>
|
||||
<div className="min-h-screen bg-slate-950 flex items-center justify-center" role="status" aria-live="polite">
|
||||
<div className="text-slate-400">Loading monitor...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-slate-100 p-6">
|
||||
<div className="min-h-screen bg-slate-950 text-slate-100">
|
||||
{/* Skip Link for Accessibility */}
|
||||
<a
|
||||
href="#main-content"
|
||||
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:bg-emerald-500 focus:text-white focus:px-4 focus:py-2 focus:rounded-lg"
|
||||
>
|
||||
Skip to main content
|
||||
</a>
|
||||
|
||||
{/* Header */}
|
||||
<div className="max-w-6xl mx-auto mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Activity className="w-8 h-8 text-emerald-500" />
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Heartbeat Monitor</h1>
|
||||
<p className="text-slate-400">{onlineApps} of {totalApps} services online</p>
|
||||
<header className="border-b border-slate-800 bg-slate-900">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
<div className="flex items-center gap-3">
|
||||
<Activity className="w-7 h-7 text-emerald-500" aria-hidden="true" />
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-white">Heartbeat Monitor</h1>
|
||||
<p className="text-sm text-slate-400" aria-live="polite">
|
||||
{onlineApps} of {totalApps} services online
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={fetchData}
|
||||
className="inline-flex items-center justify-center p-2.5 bg-slate-800 hover:bg-slate-700 text-slate-400 hover:text-white rounded-lg transition-colors focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900"
|
||||
aria-label="Refresh data"
|
||||
>
|
||||
<RefreshCw className="w-5 h-5" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowAddApp(true)}
|
||||
className="inline-flex items-center gap-2 bg-emerald-600 hover:bg-emerald-500 text-white px-4 py-2.5 rounded-lg font-medium transition-colors focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900"
|
||||
>
|
||||
<Plus className="w-4 h-4" aria-hidden="true" />
|
||||
Add App
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={fetchData}
|
||||
className="p-2 bg-slate-800 rounded-lg text-slate-400 hover:text-white"
|
||||
>
|
||||
<RefreshCw className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowAddApp(true)}
|
||||
className="flex items-center gap-2 bg-emerald-500 hover:bg-emerald-600 text-white px-4 py-2 rounded-lg"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Add App
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Table */}
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Main Content */}
|
||||
<main id="main-content" className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Table */}
|
||||
<div className="bg-slate-900 rounded-lg border border-slate-800 overflow-hidden">
|
||||
<table className="w-full">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="bg-slate-800 text-left">
|
||||
<th className="px-4 py-3 text-sm font-medium text-slate-400">Status</th>
|
||||
<th className="px-4 py-3 text-sm font-medium text-slate-400">Name</th>
|
||||
<th className="px-4 py-3 text-sm font-medium text-slate-400">URL</th>
|
||||
<th className="px-4 py-3 text-sm font-medium text-slate-400">Port</th>
|
||||
<th className="px-4 py-3 text-sm font-medium text-slate-400">Uptime</th>
|
||||
<th className="px-4 py-3 text-sm font-medium text-slate-400">Response</th>
|
||||
<th className="px-4 py-3 text-sm font-medium text-slate-400">Actions</th>
|
||||
<tr className="bg-slate-800 border-b border-slate-700">
|
||||
<th scope="col" className="px-4 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider">Status</th>
|
||||
<th scope="col" className="px-4 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider">Name</th>
|
||||
<th scope="col" className="px-4 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider">URL</th>
|
||||
<th scope="col" className="px-4 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider">Port</th>
|
||||
<th scope="col" className="px-4 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider">Uptime</th>
|
||||
<th scope="col" className="px-4 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider">Response</th>
|
||||
<th scope="col" className="px-4 py-3 text-xs font-semibold text-slate-400 uppercase tracking-wider">
|
||||
<span className="sr-only">Actions</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody className="divide-y divide-slate-800">
|
||||
{apps.map((app) => {
|
||||
const { isUp, uptime, latest } = getAppStatus(app.id);
|
||||
|
||||
return (
|
||||
<tr key={app.id} className="border-t border-slate-800 hover:bg-slate-800/50">
|
||||
<td className="px-4 py-3">
|
||||
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${
|
||||
isUp
|
||||
? "bg-emerald-500/20 text-emerald-400"
|
||||
: "bg-red-500/20 text-red-400"
|
||||
}`}>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${isUp ? "bg-emerald-400" : "bg-red-400"}`} />
|
||||
{isUp ? "ONLINE" : "OFFLINE"}
|
||||
<tr key={app.id} className="hover:bg-slate-800/50 transition-colors">
|
||||
<td className="px-4 py-4">
|
||||
<span
|
||||
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${
|
||||
isUp
|
||||
? "bg-emerald-500/10 text-emerald-400 border border-emerald-500/20"
|
||||
: "bg-red-500/10 text-red-400 border border-red-500/20"
|
||||
}`}
|
||||
>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${isUp ? "bg-emerald-400" : "bg-red-400"}`} aria-hidden="true" />
|
||||
{isUp ? "Online" : "Offline"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<td className="px-4 py-4">
|
||||
<div>
|
||||
<p className="font-medium text-white">{app.name}</p>
|
||||
<p className="text-sm text-slate-500">{app.description}</p>
|
||||
{app.description && (
|
||||
<p className="text-sm text-slate-500">{app.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<td className="px-4 py-4">
|
||||
<a
|
||||
href={app.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-emerald-400 hover:text-emerald-300 text-sm"
|
||||
className="text-emerald-400 hover:text-emerald-300 hover:underline text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900 rounded"
|
||||
>
|
||||
{app.url}
|
||||
</a>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-slate-300 font-mono">{app.port}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`font-mono ${uptime >= 90 ? "text-emerald-400" : "text-yellow-400"}`}>
|
||||
<td className="px-4 py-4">
|
||||
<code className="text-sm text-slate-300 bg-slate-800 px-2 py-1 rounded">{app.port}</code>
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
<span className={`text-sm font-medium ${uptime >= 90 ? "text-emerald-400" : uptime >= 50 ? "text-yellow-400" : "text-red-400"}`}>
|
||||
{uptime}%
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-slate-300 font-mono">
|
||||
{latest?.responseTime ? `${latest.responseTime}ms` : "--"}
|
||||
<td className="px-4 py-4">
|
||||
<span className="text-sm text-slate-300">
|
||||
{latest?.responseTime ? `${latest.responseTime}ms` : "—"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<td className="px-4 py-4">
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => checkApp(app)}
|
||||
disabled={checking === app.id}
|
||||
className="p-1.5 text-slate-400 hover:text-white hover:bg-slate-700 rounded"
|
||||
title="Check now"
|
||||
className="inline-flex items-center justify-center p-2 text-slate-400 hover:text-white hover:bg-slate-700 rounded-lg transition-colors focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
aria-label={`Check ${app.name} status`}
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${checking === app.id ? "animate-spin" : ""}`} />
|
||||
<RefreshCw className={`w-4 h-4 ${checking === app.id ? "animate-spin" : ""}`} aria-hidden="true" />
|
||||
</button>
|
||||
<a
|
||||
href={app.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-1.5 text-slate-400 hover:text-white hover:bg-slate-700 rounded"
|
||||
title="Open app"
|
||||
className="inline-flex items-center justify-center p-2 text-slate-400 hover:text-white hover:bg-slate-700 rounded-lg transition-colors focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900"
|
||||
aria-label={`Open ${app.name}`}
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
<ExternalLink className="w-4 h-4" aria-hidden="true" />
|
||||
</a>
|
||||
<button
|
||||
onClick={() => deleteApp(app.id)}
|
||||
className="p-1.5 text-slate-400 hover:text-red-400 hover:bg-slate-700 rounded"
|
||||
title="Delete"
|
||||
className="inline-flex items-center justify-center p-2 text-slate-400 hover:text-red-400 hover:bg-red-500/10 rounded-lg transition-colors focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900"
|
||||
aria-label={`Delete ${app.name}`}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<Trash2 className="w-4 h-4" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@ -277,66 +302,91 @@ export default function HeartbeatMonitor() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Add App Modal */}
|
||||
{showAddApp && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-slate-900 rounded-lg p-6 w-full max-w-md border border-slate-800">
|
||||
<h2 className="text-xl font-bold text-white mb-4">Add New App</h2>
|
||||
<form onSubmit={addApp} className="space-y-4">
|
||||
<div
|
||||
className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="add-app-title"
|
||||
>
|
||||
<div className="bg-slate-900 rounded-lg border border-slate-800 p-6 w-full max-w-md shadow-2xl">
|
||||
<h2 id="add-app-title" className="text-xl font-semibold text-white mb-6">Add New App</h2>
|
||||
<form onSubmit={addApp} className="space-y-5">
|
||||
<div>
|
||||
<label className="block text-sm text-slate-400 mb-1">Name</label>
|
||||
<label htmlFor="app-name" className="block text-sm font-medium text-slate-400 mb-2">
|
||||
Name <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="app-name"
|
||||
type="text"
|
||||
value={newApp.name}
|
||||
onChange={(e) => setNewApp({ ...newApp, name: e.target.value })}
|
||||
className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white"
|
||||
className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2.5 text-white placeholder-slate-500 focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-colors"
|
||||
placeholder="My Application"
|
||||
required
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-slate-400 mb-1">Description</label>
|
||||
<label htmlFor="app-desc" className="block text-sm font-medium text-slate-400 mb-2">
|
||||
Description
|
||||
</label>
|
||||
<input
|
||||
id="app-desc"
|
||||
type="text"
|
||||
value={newApp.description}
|
||||
onChange={(e) => setNewApp({ ...newApp, description: e.target.value })}
|
||||
className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white"
|
||||
className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2.5 text-white placeholder-slate-500 focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-colors"
|
||||
placeholder="Brief description…"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-slate-400 mb-1">URL</label>
|
||||
<label htmlFor="app-url" className="block text-sm font-medium text-slate-400 mb-2">
|
||||
URL <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="app-url"
|
||||
type="url"
|
||||
value={newApp.url}
|
||||
onChange={(e) => setNewApp({ ...newApp, url: e.target.value })}
|
||||
className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white"
|
||||
className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2.5 text-white placeholder-slate-500 focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-colors"
|
||||
placeholder="http://localhost:3000"
|
||||
required
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-slate-400 mb-1">Port</label>
|
||||
<label htmlFor="app-port" className="block text-sm font-medium text-slate-400 mb-2">
|
||||
Port <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="app-port"
|
||||
type="number"
|
||||
value={newApp.port}
|
||||
onChange={(e) => setNewApp({ ...newApp, port: parseInt(e.target.value) })}
|
||||
className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white"
|
||||
className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2.5 text-white placeholder-slate-500 focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-colors"
|
||||
required
|
||||
min="1"
|
||||
max="65535"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 pt-2">
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAddApp(false)}
|
||||
className="flex-1 bg-slate-800 hover:bg-slate-700 text-slate-300 py-2 rounded-lg"
|
||||
className="flex-1 bg-slate-800 hover:bg-slate-700 text-slate-300 py-2.5 rounded-lg font-medium transition-colors focus-visible:ring-2 focus-visible:ring-slate-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 bg-emerald-500 hover:bg-emerald-600 text-white py-2 rounded-lg font-medium"
|
||||
className="flex-1 bg-emerald-600 hover:bg-emerald-500 text-white py-2.5 rounded-lg font-medium transition-colors focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900"
|
||||
>
|
||||
Add App
|
||||
</button>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user