chore: improve tooling, linting, testing, and CI
CI / check (push) Failing after 14s
Deploy / release (push) Skipped
Deploy / deploy (push) Successful in 2m38s

- Add LICENSE file (CC BY-NC-SA 4.0)
- Add .nvmrc pinning Node 22
- Add Renovate config with daily schedule and Gitea Actions workflow
- Reduce ESLint max-warnings from 1000 to 50
- Re-enable Biome a11y/security recommended rules
- Fix Biome lint issues (a11y, hook deps, SVG labels, checkbox semantics)
- Improve CI: run on pushes to feat/fix branches, add pnpm audit
- Add Vitest coverage with v8 provider and thresholds
- Add E2E tests (auth, admin, navigation specs)
- Add admin-maintenance server action test
- Install @vitest/coverage-v8
- Ignore coverage/ directory
This commit is contained in:
openhands committed 2026-07-27 17:03:09 +02:00
1 parent 0f5c190ab1
commit 423a33200e
28 files changed
+1016 -80

No files matched your search

@@ -144,6 +144,7 @@ export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
}
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown);
// biome-ignore lint/correctness/useExhaustiveDependencies: handleSave is a stable callback from parent
}, [canEdit, isDirty, saving, page, handleSave]);
const loadPage = useCallback(
@@ -187,7 +187,7 @@ export function SortableTree({
setFilter("all");
dispatch({ type: "COLLAPSE_ALL" });
void loadTabTree();
}, [activeTabId, loadTabTree]);
}, [activeTabId, loadTabTree, dispatch]);
// Debounced search
useEffect(() => {
@@ -325,7 +325,6 @@ export function SortableTree({
// ── Keyboard navigation ─────────────────────────────────────
const handleDeleteRef = useRef<(id: number) => Promise<void>>(async () => {});
// biome-ignore lint/correctness/useExhaustiveDependencies: explicitly chosen here, see surrounding code
useEffect(() => {
function onKeyDown(e: KeyboardEvent) {
const tag = (e.target as HTMLElement)?.tagName;
-1
View File
@@ -200,7 +200,6 @@ export function CatalogTree({
}, []);
// Debounced search
// biome-ignore lint/correctness/useExhaustiveDependencies: explicitly chosen here, see surrounding code
useEffect(() => {
if (!searchQuery.trim()) {
setSearchResults(null);
+2 -1
View File
@@ -155,7 +155,6 @@ export function HomeLoginForm({
placeholder="Enter your 2FA code"
inputMode="numeric"
autoComplete="one-time-code"
autoFocus
className="w-full rounded-xl border-2 px-4 py-3 text-sm font-medium transition-all focus:outline-none"
style={{
backgroundColor: "var(--color-background)",
@@ -195,6 +194,8 @@ export function HomeLoginForm({
className="animate-spin h-4 w-4"
viewBox="0 0 24 24"
fill="none"
role="img"
aria-label="Loading"
>
<circle
className="opacity-25"
+2 -1
View File
@@ -133,7 +133,6 @@ export function LoginForm({
placeholder={t("codePlaceholder")}
inputMode="numeric"
autoComplete="one-time-code"
autoFocus
className="input-glow w-full rounded-xl border-2 px-4 py-3 text-sm font-medium transition-all focus:outline-none"
style={{
backgroundColor: "var(--color-background)",
@@ -173,6 +172,8 @@ export function LoginForm({
className="animate-spin h-4 w-4"
viewBox="0 0 24 24"
fill="none"
role="img"
aria-label="Loading"
>
<circle
className="opacity-25"
+18 -50
View File
@@ -291,62 +291,28 @@ export function RegisterForm({
}}
>
<div className="flex items-center gap-3 text-sm">
<button
type="button"
role="checkbox"
aria-checked={termsAccepted}
onClick={() => setTermsAccepted(!termsAccepted)}
className="w-5 h-5 rounded border-2 flex items-center justify-center shrink-0 transition-all duration-200"
style={{
borderColor: termsAccepted
? "var(--color-primary)"
: "color-mix(in srgb, var(--color-text-muted) 30%, transparent)",
backgroundColor: termsAccepted
? "var(--color-primary)"
: "transparent",
}}
>
{termsAccepted && (
<svg
className="w-3 h-3"
style={{ color: "var(--color-primary-foreground)" }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
role="img"
aria-label="Terms accepted"
>
<title>Terms accepted</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M5 13l4 4L19 7"
/>
</svg>
)}
</button>
<input
type="hidden"
type="checkbox"
id="terms"
name="terms"
value={termsAccepted ? "1" : ""}
checked={termsAccepted}
onChange={(e) => setTermsAccepted(e.target.checked)}
className="w-5 h-5 rounded border-2 accent-(--color-primary) shrink-0 cursor-pointer"
/>
<span
role="checkbox"
aria-checked={termsAccepted}
tabIndex={0}
<label
htmlFor="terms"
className="font-semibold cursor-pointer select-none"
style={{ color: "var(--color-text-readable)" }}
onClick={() => setTermsAccepted(!termsAccepted)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setTermsAccepted(!termsAccepted);
}
}}
className="font-semibold cursor-pointer select-none"
style={{ color: "var(--color-text-readable)" }}
onClick={() => setTermsAccepted(!termsAccepted)}
>
{t("termsAccept", { hotel: hotelName })}
</span>
</label>
</div>
</div>
@@ -372,11 +338,13 @@ export function RegisterForm({
>
{isPending ? (
<span className="inline-flex items-center justify-center gap-2">
<svg
className="animate-spin h-4 w-4"
viewBox="0 0 24 24"
fill="none"
>
<svg
className="animate-spin h-4 w-4"
viewBox="0 0 24 24"
fill="none"
role="img"
aria-label="Loading"
>
<circle
className="opacity-25"
cx="12"
+1 -1
View File
@@ -15,7 +15,7 @@ export function PwaRegister() {
}
});
navigator.serviceWorker.register("/sw.js?" + SW_VERSION).catch(() => {});
navigator.serviceWorker.register(`/sw.js?${SW_VERSION}`).catch(() => {});
}, []);
return null;
}
+2
View File
@@ -220,6 +220,8 @@ export async function TopHeader({ session }: { session: Session | null }) {
strokeLinecap="round"
strokeLinejoin="round"
style={{ color: textColor }}
role="img"
aria-label="Notifications"
>
<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" />
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" />
+3 -1
View File
@@ -4,10 +4,12 @@ import type * as React from "react";
import { cn } from "@/lib/utils";
function Label({ className, ...props }: React.ComponentProps<"label">) {
function Label({ className, htmlFor, ...props }: React.ComponentProps<"label">) {
return (
// biome-ignore lint/a11y/noLabelWithoutControl: reusable component, input is associated via htmlFor at usage site
<label
data-slot="label"
htmlFor={htmlFor}
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className,