shimwrappercheck 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +198 -192
- package/dashboard/README.md +13 -0
- package/dashboard/app/{agents → [locale]/agents}/page.tsx +11 -14
- package/dashboard/app/{config → [locale]/config}/page.tsx +11 -13
- package/dashboard/app/[locale]/error.tsx +25 -0
- package/dashboard/app/[locale]/layout.tsx +42 -0
- package/dashboard/app/[locale]/not-found.tsx +27 -0
- package/dashboard/app/[locale]/page.tsx +53 -0
- package/dashboard/app/[locale]/settings/page.tsx +741 -0
- package/dashboard/app/api/agents-md/route.ts +2 -8
- package/dashboard/app/api/check-tools/route.ts +134 -0
- package/dashboard/app/api/config/route.ts +2 -8
- package/dashboard/app/api/info/route.ts +26 -0
- package/dashboard/app/api/run-checks/route.ts +9 -2
- package/dashboard/app/api/settings/route.ts +74 -16
- package/dashboard/app/api/status/route.ts +3 -5
- package/dashboard/app/api/ui-config/route.ts +62 -0
- package/dashboard/app/global-error.tsx +31 -0
- package/dashboard/app/globals.css +26 -0
- package/dashboard/app/layout.tsx +2 -12
- package/dashboard/app/not-found.tsx +22 -0
- package/dashboard/components/AvailableChecks.tsx +260 -0
- package/dashboard/components/CheckCard.tsx +415 -65
- package/dashboard/components/CheckCardList.tsx +28 -23
- package/dashboard/components/Header.tsx +52 -16
- package/dashboard/components/Icons.tsx +20 -0
- package/dashboard/components/LayoutContent.tsx +24 -0
- package/dashboard/components/MyShimChecks.tsx +257 -0
- package/dashboard/components/Nav.tsx +9 -6
- package/dashboard/components/SetDocumentLang.tsx +18 -0
- package/dashboard/components/SidebarMyShim.tsx +108 -19
- package/dashboard/components/StatusCard.tsx +5 -12
- package/dashboard/components/TriggerCommandos.tsx +311 -42
- package/dashboard/lib/checks.ts +134 -17
- package/dashboard/lib/presets.ts +60 -28
- package/dashboard/lib/projectRoot.ts +22 -12
- package/dashboard/next-env.d.ts +2 -1
- package/dashboard/next.config.js +10 -1
- package/dashboard/package.json +11 -6
- package/dashboard/scripts/find-port-and-dev.js +48 -15
- package/dashboard/tailwind.config.js +1 -4
- package/dashboard/tsconfig.json +9 -2
- package/package.json +4 -6
- package/scripts/run-checks.sh +77 -24
- package/scripts/supabase-checked.sh +23 -7
- package/scripts/update-readme.js +72 -0
- package/templates/run-checks.sh +80 -27
- package/dashboard/app/page.tsx +0 -151
- package/dashboard/app/settings/page.tsx +0 -467
|
@@ -1,29 +1,65 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Header: shimwrappercheck
|
|
2
|
+
* Header: "shimwrappercheck" zentriert; rechts Schalter [myshim | Settings].
|
|
3
|
+
* myshim = Ansicht My Shim + Check Library, Settings = Templates & Information.
|
|
3
4
|
* Location: /components/Header.tsx
|
|
4
5
|
*/
|
|
5
6
|
"use client";
|
|
6
7
|
|
|
7
|
-
import
|
|
8
|
+
import { useTranslations, useLocale } from "next-intl";
|
|
9
|
+
import { Link, usePathname } from "@/i18n/navigation";
|
|
10
|
+
import { IconSettings } from "@/components/Icons";
|
|
8
11
|
|
|
9
12
|
export default function Header() {
|
|
13
|
+
const t = useTranslations("common");
|
|
14
|
+
const tHeader = useTranslations("header");
|
|
15
|
+
const pathname = usePathname();
|
|
16
|
+
const isSettings = pathname === "/settings";
|
|
17
|
+
|
|
10
18
|
return (
|
|
11
|
-
<header className="h-
|
|
12
|
-
<
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
<
|
|
17
|
-
|
|
19
|
+
<header className="h-14 border-b border-white/20 flex items-center justify-between px-6 bg-[#0f0f0f] shrink-0">
|
|
20
|
+
<div className="w-24 flex items-center gap-2" aria-hidden>
|
|
21
|
+
<LocaleSwitcher />
|
|
22
|
+
</div>
|
|
23
|
+
<div className="flex-1 flex justify-center pointer-events-none">
|
|
24
|
+
<span className="text-xl font-semibold text-white">{t("appName")}</span>
|
|
25
|
+
</div>
|
|
26
|
+
<div className="flex items-center gap-0 rounded overflow-hidden border border-white/80">
|
|
27
|
+
<Link
|
|
28
|
+
href="/"
|
|
29
|
+
className={`px-4 py-2 text-sm font-medium transition-colors ${
|
|
30
|
+
!isSettings ? "bg-white text-black" : "text-white hover:bg-white/10"
|
|
31
|
+
}`}
|
|
32
|
+
>
|
|
33
|
+
{t("myshim")}
|
|
18
34
|
</Link>
|
|
19
|
-
<Link
|
|
20
|
-
|
|
35
|
+
<Link
|
|
36
|
+
href="/settings"
|
|
37
|
+
className={`px-4 py-2 text-sm font-medium transition-colors flex items-center gap-1.5 ${
|
|
38
|
+
isSettings ? "bg-white text-black" : "text-white hover:bg-white/10"
|
|
39
|
+
}`}
|
|
40
|
+
aria-label={tHeader("settingsAria")}
|
|
41
|
+
>
|
|
42
|
+
<IconSettings />
|
|
43
|
+
{t("settings")}
|
|
21
44
|
</Link>
|
|
22
|
-
|
|
23
|
-
AGENTS.md
|
|
24
|
-
</Link>
|
|
25
|
-
</nav>
|
|
26
|
-
<div className="absolute left-1/2 -translate-x-1/2 text-xl font-semibold text-white">shimwrappercheck</div>
|
|
45
|
+
</div>
|
|
27
46
|
</header>
|
|
28
47
|
);
|
|
29
48
|
}
|
|
49
|
+
|
|
50
|
+
function LocaleSwitcher() {
|
|
51
|
+
const pathname = usePathname();
|
|
52
|
+
const locale = useLocale();
|
|
53
|
+
const activeClass = "px-2 py-1 bg-white text-black font-medium";
|
|
54
|
+
const inactiveClass = "px-2 py-1 bg-white/10 hover:bg-white/20 text-white";
|
|
55
|
+
return (
|
|
56
|
+
<div className="flex rounded overflow-hidden border border-white/50 text-xs">
|
|
57
|
+
<Link href={pathname} locale="de" className={locale === "de" ? activeClass : inactiveClass}>
|
|
58
|
+
DE
|
|
59
|
+
</Link>
|
|
60
|
+
<Link href={pathname} locale="en" className={locale === "en" ? activeClass : inactiveClass}>
|
|
61
|
+
EN
|
|
62
|
+
</Link>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared SVG icons used across the dashboard. Ensures consistent look (solid where needed).
|
|
3
|
+
* Location: /components/Icons.tsx
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
type IconProps = { className?: string };
|
|
7
|
+
|
|
8
|
+
/** Settings (cog) – solid fill so it renders clearly at all sizes. */
|
|
9
|
+
export function IconSettings({ className = "w-4 h-4" }: IconProps) {
|
|
10
|
+
return (
|
|
11
|
+
<svg className={className} fill="currentColor" viewBox="0 0 24 24" aria-hidden>
|
|
12
|
+
<path
|
|
13
|
+
fillRule="evenodd"
|
|
14
|
+
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
|
|
15
|
+
clipRule="evenodd"
|
|
16
|
+
/>
|
|
17
|
+
<path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
18
|
+
</svg>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client wrapper: zeigt Sidebar nur auf /, auf /settings nur Main (volle Breite).
|
|
3
|
+
* Location: /components/LayoutContent.tsx
|
|
4
|
+
*/
|
|
5
|
+
"use client";
|
|
6
|
+
|
|
7
|
+
import { usePathname } from "@/i18n/navigation";
|
|
8
|
+
import SidebarMyShim from "./SidebarMyShim";
|
|
9
|
+
|
|
10
|
+
export default function LayoutContent({ children }: { children: React.ReactNode }) {
|
|
11
|
+
const pathname = usePathname();
|
|
12
|
+
const showSidebar = pathname !== "/settings";
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="flex flex-1 min-h-0 w-full">
|
|
16
|
+
{showSidebar && (
|
|
17
|
+
<aside className="flex-1 min-h-0 border-r border-white/20 flex flex-col overflow-hidden min-w-0">
|
|
18
|
+
<SidebarMyShim />
|
|
19
|
+
</aside>
|
|
20
|
+
)}
|
|
21
|
+
<main className="flex-1 min-h-0 min-w-0 relative bg-[#0f0f0f] p-6 overflow-auto">{children}</main>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* My Shim – My Checks: Titel + Zeitstempel, Suchfeld, Karten mit Tags (frontend/backend) und Info-/Settings-Icon.
|
|
3
|
+
* Drop-Zone: Check von Check Library hierher ziehen = aktivieren.
|
|
4
|
+
* Location: /components/MyShimChecks.tsx
|
|
5
|
+
*/
|
|
6
|
+
"use client";
|
|
7
|
+
|
|
8
|
+
import { useState, useEffect } from "react";
|
|
9
|
+
import { useTranslations } from "next-intl";
|
|
10
|
+
import type { SettingsData, CheckToggles } from "@/lib/presets";
|
|
11
|
+
import { CHECK_DEFINITIONS } from "@/lib/checks";
|
|
12
|
+
import type { CheckDef } from "@/lib/checks";
|
|
13
|
+
import CheckCard, { type ToolStatus } from "./CheckCard";
|
|
14
|
+
|
|
15
|
+
function formatTimestamp(d: Date): string {
|
|
16
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
17
|
+
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
18
|
+
const year = d.getFullYear();
|
|
19
|
+
const h = String(d.getHours()).padStart(2, "0");
|
|
20
|
+
const min = String(d.getMinutes()).padStart(2, "0");
|
|
21
|
+
const sec = String(d.getSeconds()).padStart(2, "0");
|
|
22
|
+
return `${day}.${month}.${year} - ${h}:${min}:${sec}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function MyShimChecks({
|
|
26
|
+
settings,
|
|
27
|
+
onSave,
|
|
28
|
+
lastUpdated,
|
|
29
|
+
}: {
|
|
30
|
+
settings: SettingsData | null;
|
|
31
|
+
onSave: (s: SettingsData) => void;
|
|
32
|
+
lastUpdated: Date | null;
|
|
33
|
+
}) {
|
|
34
|
+
const t = useTranslations("common");
|
|
35
|
+
const tMyChecks = useTranslations("myChecks");
|
|
36
|
+
const [search, setSearch] = useState("");
|
|
37
|
+
const [dropHighlight, setDropHighlight] = useState(false);
|
|
38
|
+
const [dropTargetId, setDropTargetId] = useState<string | null>(null);
|
|
39
|
+
const [toolStatusMap, setToolStatusMap] = useState<Record<string, ToolStatus>>({});
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
fetch("/api/check-tools")
|
|
43
|
+
.then((r) => r.json())
|
|
44
|
+
.then((data) => setToolStatusMap(data.tools ?? {}))
|
|
45
|
+
.catch(() => setToolStatusMap({}));
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
const order = settings?.checkOrder ?? [];
|
|
49
|
+
const list = order.map((id) => CHECK_DEFINITIONS.find((c) => c.id === id)).filter(Boolean) as CheckDef[];
|
|
50
|
+
const filtered = search.trim()
|
|
51
|
+
? list.filter((c) => c.label.toLowerCase().includes(search.trim().toLowerCase()))
|
|
52
|
+
: list;
|
|
53
|
+
|
|
54
|
+
const handleRemoveFromMyShim = (id: string) => {
|
|
55
|
+
if (!settings) return;
|
|
56
|
+
const order = (settings.checkOrder ?? []).filter((x) => x !== id);
|
|
57
|
+
const nextToggles = { ...settings.checkToggles } as Record<string, boolean>;
|
|
58
|
+
nextToggles[id] = false;
|
|
59
|
+
onSave({ ...settings, checkOrder: order, checkToggles: nextToggles as unknown as CheckToggles });
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const handleSettingsChange = (checkId: string, partial: Record<string, unknown>) => {
|
|
63
|
+
if (!settings) return;
|
|
64
|
+
onSave({
|
|
65
|
+
...settings,
|
|
66
|
+
checkSettings: {
|
|
67
|
+
...settings.checkSettings,
|
|
68
|
+
[checkId]: { ...(settings.checkSettings as Record<string, Record<string, unknown>>)?.[checkId], ...partial },
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const onDragOver = (e: React.DragEvent) => {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
e.stopPropagation();
|
|
76
|
+
e.dataTransfer.dropEffect = "move";
|
|
77
|
+
setDropHighlight(true);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const onDragEnter = (e: React.DragEvent) => {
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
e.dataTransfer.dropEffect = "move";
|
|
83
|
+
setDropHighlight(true);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const onDragLeave = (e: React.DragEvent) => {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
|
|
89
|
+
setDropHighlight(false);
|
|
90
|
+
setDropTargetId(null);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const onDropList = async (e: React.DragEvent) => {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
e.stopPropagation();
|
|
97
|
+
setDropHighlight(false);
|
|
98
|
+
setDropTargetId(null);
|
|
99
|
+
const id = (e.dataTransfer.getData("checkId") || e.dataTransfer.getData("text/plain"))?.trim();
|
|
100
|
+
if (!id || !settings) return;
|
|
101
|
+
let base = settings;
|
|
102
|
+
if (!base?.presets?.length) {
|
|
103
|
+
try {
|
|
104
|
+
const res = await fetch("/api/settings");
|
|
105
|
+
const data = await res.json();
|
|
106
|
+
if (data?.presets?.length) base = data;
|
|
107
|
+
} catch {
|
|
108
|
+
/* ignore */
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (!base) return;
|
|
112
|
+
const order = base.checkOrder ?? [];
|
|
113
|
+
if (order.includes(id)) return;
|
|
114
|
+
const nextToggles = { ...base.checkToggles } as Record<string, boolean>;
|
|
115
|
+
nextToggles[id] = true;
|
|
116
|
+
onSave({ ...base, checkOrder: [...order, id], checkToggles: nextToggles as unknown as CheckToggles });
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const moveOrder = (draggedId: string, targetId: string) => {
|
|
120
|
+
if (!settings) return;
|
|
121
|
+
const order = settings.checkOrder ?? [];
|
|
122
|
+
const fromIdx = order.indexOf(draggedId);
|
|
123
|
+
const toIdx = order.indexOf(targetId);
|
|
124
|
+
if (fromIdx === -1 || toIdx === -1 || fromIdx === toIdx) return;
|
|
125
|
+
const next = order.slice();
|
|
126
|
+
next.splice(fromIdx, 1);
|
|
127
|
+
const newToIdx = fromIdx < toIdx ? toIdx - 1 : toIdx;
|
|
128
|
+
next.splice(newToIdx, 0, draggedId);
|
|
129
|
+
onSave({ ...settings, checkOrder: next });
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const onDropOnItem = (e: React.DragEvent, targetId: string) => {
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
e.stopPropagation();
|
|
135
|
+
setDropTargetId(null);
|
|
136
|
+
setDropHighlight(false);
|
|
137
|
+
const draggedId = (e.dataTransfer.getData("checkId") || e.dataTransfer.getData("text/plain"))?.trim();
|
|
138
|
+
if (!draggedId || !settings) return;
|
|
139
|
+
const order = settings.checkOrder ?? [];
|
|
140
|
+
if (order.includes(draggedId)) {
|
|
141
|
+
moveOrder(draggedId, targetId);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const nextToggles = { ...settings.checkToggles } as Record<string, boolean>;
|
|
145
|
+
nextToggles[draggedId] = true;
|
|
146
|
+
const toIdx = order.indexOf(targetId);
|
|
147
|
+
const insertIdx = toIdx === -1 ? order.length : toIdx;
|
|
148
|
+
const nextOrder = [...order.slice(0, insertIdx), draggedId, ...order.slice(insertIdx)];
|
|
149
|
+
onSave({ ...settings, checkOrder: nextOrder, checkToggles: nextToggles as unknown as CheckToggles });
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div className="space-y-3">
|
|
154
|
+
<div className="flex items-center justify-between gap-2 flex-wrap">
|
|
155
|
+
<h3 className="text-sm font-medium text-white">{tMyChecks("title")}</h3>
|
|
156
|
+
<span className="text-xs text-green-500 shrink-0">
|
|
157
|
+
{t("updated")} {lastUpdated ? formatTimestamp(lastUpdated) : "–"}
|
|
158
|
+
</span>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<div className="relative">
|
|
162
|
+
<input
|
|
163
|
+
type="text"
|
|
164
|
+
placeholder={t("search")}
|
|
165
|
+
className="input w-full input-sm bg-[#0f0f0f] border border-white/80 text-white pr-8 rounded placeholder-neutral-500"
|
|
166
|
+
value={search}
|
|
167
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
168
|
+
/>
|
|
169
|
+
<svg
|
|
170
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 text-white/80 pointer-events-none"
|
|
171
|
+
fill="none"
|
|
172
|
+
stroke="currentColor"
|
|
173
|
+
viewBox="0 0 24 24"
|
|
174
|
+
>
|
|
175
|
+
<path
|
|
176
|
+
strokeLinecap="round"
|
|
177
|
+
strokeLinejoin="round"
|
|
178
|
+
strokeWidth={2}
|
|
179
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
180
|
+
/>
|
|
181
|
+
</svg>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<ul
|
|
185
|
+
className={`space-y-2 list-none p-0 m-0 min-h-[140px] rounded-lg border-2 border-dashed transition-all duration-200 ${
|
|
186
|
+
dropHighlight
|
|
187
|
+
? "border-green-400 bg-green-500/20 shadow-[inset_0_0_0_2px_rgba(34,197,94,0.3)]"
|
|
188
|
+
: "border-white/40 bg-white/5"
|
|
189
|
+
}`}
|
|
190
|
+
onDragEnter={onDragEnter}
|
|
191
|
+
onDragOver={onDragOver}
|
|
192
|
+
onDragLeave={onDragLeave}
|
|
193
|
+
onDrop={onDropList}
|
|
194
|
+
>
|
|
195
|
+
{dropHighlight && (
|
|
196
|
+
<li className="list-none py-3 text-center pointer-events-none">
|
|
197
|
+
<span className="inline-block px-3 py-1.5 rounded bg-green-500/30 text-green-200 text-sm font-medium">
|
|
198
|
+
{t("dropHereActivate")}
|
|
199
|
+
</span>
|
|
200
|
+
</li>
|
|
201
|
+
)}
|
|
202
|
+
{filtered.length === 0 && !dropHighlight ? (
|
|
203
|
+
<li className="list-none py-6 text-center text-neutral-500 text-sm px-2">{t("noMyChecksYet")}</li>
|
|
204
|
+
) : (
|
|
205
|
+
filtered.map((def) => (
|
|
206
|
+
<li
|
|
207
|
+
key={def.id}
|
|
208
|
+
draggable
|
|
209
|
+
onDragStart={(e) => {
|
|
210
|
+
e.dataTransfer.setData("checkId", def.id);
|
|
211
|
+
e.dataTransfer.setData("text/plain", def.id);
|
|
212
|
+
e.dataTransfer.effectAllowed = "move";
|
|
213
|
+
}}
|
|
214
|
+
onDragOver={(e) => {
|
|
215
|
+
e.preventDefault();
|
|
216
|
+
e.stopPropagation();
|
|
217
|
+
e.dataTransfer.dropEffect = "move";
|
|
218
|
+
setDropTargetId(def.id);
|
|
219
|
+
}}
|
|
220
|
+
onDragLeave={(e) => {
|
|
221
|
+
if (!e.currentTarget.contains(e.relatedTarget as Node)) setDropTargetId(null);
|
|
222
|
+
}}
|
|
223
|
+
onDrop={(e) => onDropOnItem(e, def.id)}
|
|
224
|
+
className={`list-none cursor-grab active:cursor-grabbing transition-colors ${dropTargetId === def.id ? "ring-2 ring-primary/60 ring-inset rounded-lg" : ""}`}
|
|
225
|
+
>
|
|
226
|
+
<CheckCard
|
|
227
|
+
def={def}
|
|
228
|
+
orderIndex={order.indexOf(def.id) + 1 || undefined}
|
|
229
|
+
enabled={true}
|
|
230
|
+
onToggle={() => {}}
|
|
231
|
+
checkSettings={(settings?.checkSettings as Record<string, Record<string, unknown>>)?.[def.id]}
|
|
232
|
+
onSettingsChange={(partial) => handleSettingsChange(def.id, partial)}
|
|
233
|
+
dragHandle={<span className="text-neutral-500 select-none">⋮⋮</span>}
|
|
234
|
+
leftTags={[...def.tags, def.role]}
|
|
235
|
+
statusTag="active"
|
|
236
|
+
inlineStyle
|
|
237
|
+
hideEnabledToggle
|
|
238
|
+
toolStatus={toolStatusMap[def.id]}
|
|
239
|
+
headerExtra={
|
|
240
|
+
<button
|
|
241
|
+
type="button"
|
|
242
|
+
className="btn btn-ghost btn-xs text-neutral-400 hover:text-red-400 shrink-0"
|
|
243
|
+
onClick={() => handleRemoveFromMyShim(def.id)}
|
|
244
|
+
title={t("removeFromMyShim")}
|
|
245
|
+
>
|
|
246
|
+
{t("remove")}
|
|
247
|
+
</button>
|
|
248
|
+
}
|
|
249
|
+
/>
|
|
250
|
+
</li>
|
|
251
|
+
))
|
|
252
|
+
)}
|
|
253
|
+
</ul>
|
|
254
|
+
<p className="text-xs text-neutral-500 mt-1.5 px-0.5">{t("dragFromLibrary")}</p>
|
|
255
|
+
</div>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
@@ -4,28 +4,31 @@
|
|
|
4
4
|
*/
|
|
5
5
|
"use client";
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import { useTranslations } from "next-intl";
|
|
8
|
+
import { Link } from "@/i18n/navigation";
|
|
8
9
|
|
|
9
10
|
export default function Nav() {
|
|
11
|
+
const t = useTranslations("common");
|
|
12
|
+
const tNav = useTranslations("nav");
|
|
10
13
|
return (
|
|
11
14
|
<div className="navbar bg-base-100 shadow-lg">
|
|
12
15
|
<div className="flex-1">
|
|
13
16
|
<Link href="/" className="btn btn-ghost text-xl">
|
|
14
|
-
|
|
17
|
+
{t("appName")}
|
|
15
18
|
</Link>
|
|
16
19
|
</div>
|
|
17
20
|
<div className="flex-none gap-2">
|
|
18
21
|
<Link href="/" className="btn btn-ghost btn-sm">
|
|
19
|
-
|
|
22
|
+
{tNav("dashboard")}
|
|
20
23
|
</Link>
|
|
21
24
|
<Link href="/settings" className="btn btn-ghost btn-sm">
|
|
22
|
-
|
|
25
|
+
{tNav("settings")}
|
|
23
26
|
</Link>
|
|
24
27
|
<Link href="/config" className="btn btn-ghost btn-sm">
|
|
25
|
-
|
|
28
|
+
{tNav("config")}
|
|
26
29
|
</Link>
|
|
27
30
|
<Link href="/agents" className="btn btn-ghost btn-sm">
|
|
28
|
-
|
|
31
|
+
{tNav("agentsMd")}
|
|
29
32
|
</Link>
|
|
30
33
|
</div>
|
|
31
34
|
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sets document.documentElement.lang from current locale (for accessibility).
|
|
3
|
+
* Location: /components/SetDocumentLang.tsx
|
|
4
|
+
*/
|
|
5
|
+
"use client";
|
|
6
|
+
|
|
7
|
+
import { useLocale } from "next-intl";
|
|
8
|
+
import { useEffect } from "react";
|
|
9
|
+
|
|
10
|
+
export default function SetDocumentLang() {
|
|
11
|
+
const locale = useLocale();
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (typeof document !== "undefined") {
|
|
14
|
+
document.documentElement.lang = locale;
|
|
15
|
+
}
|
|
16
|
+
}, [locale]);
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
@@ -1,43 +1,132 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Left sidebar "My Shim": Trigger
|
|
2
|
+
* Left sidebar "My Shim": My Trigger Commandos + My Checks (Referenz-Layout mit Zeitstempel, Tabs, Karten).
|
|
3
3
|
* Location: /components/SidebarMyShim.tsx
|
|
4
4
|
*/
|
|
5
5
|
"use client";
|
|
6
6
|
|
|
7
|
-
import { useEffect, useState } from "react";
|
|
8
|
-
import
|
|
7
|
+
import React, { useEffect, useState, useRef, useCallback } from "react";
|
|
8
|
+
import { useTranslations } from "next-intl";
|
|
9
|
+
import { Link } from "@/i18n/navigation";
|
|
10
|
+
import type { SettingsData, CheckToggles } from "@/lib/presets";
|
|
11
|
+
import { IconSettings } from "@/components/Icons";
|
|
9
12
|
import TriggerCommandos from "@/components/TriggerCommandos";
|
|
10
|
-
import
|
|
13
|
+
import MyShimChecks from "@/components/MyShimChecks";
|
|
11
14
|
|
|
12
15
|
export default function SidebarMyShim() {
|
|
16
|
+
const tSidebar = useTranslations("sidebar");
|
|
17
|
+
const tCommon = useTranslations("common");
|
|
13
18
|
const [settings, setSettings] = useState<SettingsData | null>(null);
|
|
19
|
+
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
|
|
20
|
+
const sidebarRef = useRef<HTMLDivElement>(null);
|
|
21
|
+
const settingsRef = useRef<SettingsData | null>(settings);
|
|
22
|
+
settingsRef.current = settings;
|
|
14
23
|
|
|
15
24
|
useEffect(() => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
const load = () => {
|
|
26
|
+
fetch("/api/settings")
|
|
27
|
+
.then((r) => r.json())
|
|
28
|
+
.then((data) => setSettings(data))
|
|
29
|
+
.catch(() => setSettings(null));
|
|
30
|
+
};
|
|
31
|
+
load();
|
|
32
|
+
const handler = () => load();
|
|
33
|
+
window.addEventListener("settings-updated", handler);
|
|
34
|
+
return () => window.removeEventListener("settings-updated", handler);
|
|
20
35
|
}, []);
|
|
21
36
|
|
|
22
|
-
const saveSettings = (next: SettingsData) => {
|
|
37
|
+
const saveSettings = useCallback((next: SettingsData) => {
|
|
23
38
|
setSettings(next);
|
|
24
39
|
fetch("/api/settings", {
|
|
25
40
|
method: "POST",
|
|
26
41
|
headers: { "Content-Type": "application/json" },
|
|
27
42
|
body: JSON.stringify(next),
|
|
28
|
-
})
|
|
29
|
-
|
|
43
|
+
})
|
|
44
|
+
.then(() => {
|
|
45
|
+
setLastUpdated(new Date());
|
|
46
|
+
if (typeof window !== "undefined") window.dispatchEvent(new Event("settings-updated"));
|
|
47
|
+
})
|
|
48
|
+
.catch(() => {});
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
const activePreset = settings?.presets?.find((p) => p.id === settings.activePresetId);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const el = sidebarRef.current;
|
|
55
|
+
if (!el) return;
|
|
56
|
+
const handleDragOver = (e: DragEvent) => {
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
e.stopPropagation();
|
|
59
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
|
60
|
+
};
|
|
61
|
+
const handleDrop = (e: DragEvent) => {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
e.stopPropagation();
|
|
64
|
+
const dt = e.dataTransfer;
|
|
65
|
+
if (!dt) return;
|
|
66
|
+
const id = dt.getData("text/plain") || dt.getData("checkId");
|
|
67
|
+
const checkId = (id || "").trim();
|
|
68
|
+
if (!checkId) return;
|
|
69
|
+
const apply = (base: SettingsData) => {
|
|
70
|
+
const order = base.checkOrder ?? [];
|
|
71
|
+
if (order.includes(checkId)) return;
|
|
72
|
+
const nextToggles = { ...base.checkToggles } as Record<string, boolean>;
|
|
73
|
+
nextToggles[checkId] = true;
|
|
74
|
+
saveSettings({
|
|
75
|
+
...base,
|
|
76
|
+
checkOrder: [...order, checkId],
|
|
77
|
+
checkToggles: nextToggles as unknown as CheckToggles,
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
const base = settingsRef.current;
|
|
81
|
+
if (base?.presets?.length) {
|
|
82
|
+
apply(base);
|
|
83
|
+
} else {
|
|
84
|
+
fetch("/api/settings")
|
|
85
|
+
.then((r) => r.json())
|
|
86
|
+
.then((data) => {
|
|
87
|
+
if (data?.presets?.length) apply(data);
|
|
88
|
+
})
|
|
89
|
+
.catch(() => {});
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
el.addEventListener("dragover", handleDragOver, false);
|
|
93
|
+
el.addEventListener("drop", handleDrop, false);
|
|
94
|
+
return () => {
|
|
95
|
+
el.removeEventListener("dragover", handleDragOver);
|
|
96
|
+
el.removeEventListener("drop", handleDrop);
|
|
97
|
+
};
|
|
98
|
+
}, [saveSettings]);
|
|
30
99
|
|
|
31
100
|
return (
|
|
32
|
-
<div className="p-4 space-y-6">
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
<
|
|
36
|
-
|
|
101
|
+
<div ref={sidebarRef} className="p-4 space-y-6 flex flex-col min-h-0 overflow-y-auto">
|
|
102
|
+
<div className="flex items-center justify-between gap-2 shrink-0 min-w-0">
|
|
103
|
+
<h2 className="text-lg font-semibold text-white shrink-0">{tSidebar("myActiveShim")}</h2>
|
|
104
|
+
<div className="flex items-center gap-1 min-w-0">
|
|
105
|
+
{activePreset?.name != null && activePreset.name !== "" && (
|
|
106
|
+
<span
|
|
107
|
+
className="text-xs font-medium bg-violet-600 text-white rounded px-2 py-0.5 truncate min-w-0"
|
|
108
|
+
title={activePreset.name}
|
|
109
|
+
>
|
|
110
|
+
{activePreset.name}
|
|
111
|
+
</span>
|
|
112
|
+
)}
|
|
113
|
+
<Link
|
|
114
|
+
href="/settings"
|
|
115
|
+
className="btn btn-ghost btn-xs btn-square text-white/80 hover:text-white hover:bg-white/10 shrink-0"
|
|
116
|
+
aria-label={tCommon("presetsAndSettings")}
|
|
117
|
+
title={tCommon("presetsAndSettings")}
|
|
118
|
+
>
|
|
119
|
+
<IconSettings />
|
|
120
|
+
</Link>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
<section className="shrink-0">
|
|
124
|
+
<TriggerCommandos settings={settings} onSave={saveSettings} lastUpdated={lastUpdated} />
|
|
37
125
|
</section>
|
|
38
|
-
<section>
|
|
39
|
-
<
|
|
40
|
-
|
|
126
|
+
<section className="flex flex-col min-h-0 flex-1">
|
|
127
|
+
<div className="min-h-[200px] overflow-y-auto">
|
|
128
|
+
<MyShimChecks settings={settings} onSave={saveSettings} lastUpdated={lastUpdated} />
|
|
129
|
+
</div>
|
|
41
130
|
</section>
|
|
42
131
|
</div>
|
|
43
132
|
);
|
|
@@ -4,22 +4,15 @@
|
|
|
4
4
|
*/
|
|
5
5
|
"use client";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}: {
|
|
12
|
-
label: string;
|
|
13
|
-
ok: boolean;
|
|
14
|
-
detail?: string;
|
|
15
|
-
}) {
|
|
7
|
+
import { useTranslations } from "next-intl";
|
|
8
|
+
|
|
9
|
+
export default function StatusCard({ label, ok, detail }: { label: string; ok: boolean; detail?: string }) {
|
|
10
|
+
const t = useTranslations("common");
|
|
16
11
|
return (
|
|
17
12
|
<div className="card bg-neutral-800 border border-neutral-600 shadow-md">
|
|
18
13
|
<div className="card-body p-4">
|
|
19
14
|
<h3 className="card-title text-sm text-white">{label}</h3>
|
|
20
|
-
<p className={ok ? "text-green-400" : "text-amber-400"}>
|
|
21
|
-
{ok ? "✓ Vorhanden" : "— Nicht gefunden"}
|
|
22
|
-
</p>
|
|
15
|
+
<p className={ok ? "text-green-400" : "text-amber-400"}>{ok ? t("statusPresent") : t("statusMissing")}</p>
|
|
23
16
|
{detail && <p className="text-xs text-neutral-400">{detail}</p>}
|
|
24
17
|
</div>
|
|
25
18
|
</div>
|