add session-based connection management and React dashboard

Backend: adds JDBC session support, login/status/logout endpoints, and
new DTOs (AuthResponse, ConnectionStatusResponse, LoginResult). Frontend
replaces the Vite boilerplate with a Dashboard, ServiceCard, and
ConnectModal backed by a typed API client.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 03:30:36 +02:00
parent 0127706262
commit 43bbcece7a
30 changed files with 1307 additions and 350 deletions
+84
View File
@@ -0,0 +1,84 @@
import { useEffect, useRef, useState, type SyntheticEvent } from 'react'
import { login } from '../api/connections'
import type { LoginRequest } from '../types/connection'
import './ConnectModal.scss'
interface Props {
serviceType: string
label: string
onClose: () => void
onSuccess: () => void
}
export function ConnectModal({ serviceType, label, onClose, onSuccess }: Readonly<Props>) {
const [appUrl, setAppUrl] = useState('')
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const firstInputRef = useRef<HTMLInputElement>(null)
const dialogRef = useRef<HTMLDialogElement>(null)
useEffect(() => {
const dialog = dialogRef.current
if (!dialog) return
dialog.showModal()
const handleCancel = (e: Event) => {
e.preventDefault()
onClose()
}
dialog.addEventListener('cancel', handleCancel)
return () => dialog.removeEventListener('cancel', handleCancel)
}, [onClose])
const handleSubmit = async (e: SyntheticEvent<HTMLFormElement>) => {
e.preventDefault()
setError(null)
setLoading(true)
try {
const req: LoginRequest = { appUrl, serviceType, username, password, stayLoggedIn: true }
await login(req)
onSuccess()
} catch (err) {
setError(err instanceof Error ? err.message : 'Login failed')
} finally {
setLoading(false)
}
}
return (
<dialog className="modal" ref={dialogRef}>
<div className="modal__header">
<h2 className="modal__title" id="modal-title">Connect to {label}</h2>
<button className="modal__close" onClick={onClose} aria-label="Close">×</button>
</div>
<form className="modal__form" onSubmit={handleSubmit}>
<div className="modal__field">
<label className="modal__label" htmlFor="appUrl">App URL</label>
<input id="appUrl" ref={firstInputRef} className="modal__input" type="url"
placeholder="https://homebox.example.com"
value={appUrl} onChange={e => setAppUrl(e.target.value)} required />
</div>
<div className="modal__field">
<label className="modal__label" htmlFor="username">Username</label>
<input id="username" className="modal__input" type="text"
autoComplete="username"
value={username} onChange={e => setUsername(e.target.value)} required />
</div>
<div className="modal__field">
<label className="modal__label" htmlFor="password">Password</label>
<input id="password" className="modal__input" type="password"
autoComplete="current-password"
value={password} onChange={e => setPassword(e.target.value)} required />
</div>
{error && <p className="modal__error">{error}</p>}
<button className="modal__submit" type="submit" disabled={loading}>
{loading ? 'Connecting…' : 'Connect'}
</button>
</form>
</dialog>
)
}