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.
Files changed (49) hide show
  1. package/README.md +198 -192
  2. package/dashboard/README.md +13 -0
  3. package/dashboard/app/{agents → [locale]/agents}/page.tsx +11 -14
  4. package/dashboard/app/{config → [locale]/config}/page.tsx +11 -13
  5. package/dashboard/app/[locale]/error.tsx +25 -0
  6. package/dashboard/app/[locale]/layout.tsx +42 -0
  7. package/dashboard/app/[locale]/not-found.tsx +27 -0
  8. package/dashboard/app/[locale]/page.tsx +53 -0
  9. package/dashboard/app/[locale]/settings/page.tsx +741 -0
  10. package/dashboard/app/api/agents-md/route.ts +2 -8
  11. package/dashboard/app/api/check-tools/route.ts +134 -0
  12. package/dashboard/app/api/config/route.ts +2 -8
  13. package/dashboard/app/api/info/route.ts +26 -0
  14. package/dashboard/app/api/run-checks/route.ts +9 -2
  15. package/dashboard/app/api/settings/route.ts +74 -16
  16. package/dashboard/app/api/status/route.ts +3 -5
  17. package/dashboard/app/api/ui-config/route.ts +62 -0
  18. package/dashboard/app/global-error.tsx +31 -0
  19. package/dashboard/app/globals.css +26 -0
  20. package/dashboard/app/layout.tsx +2 -12
  21. package/dashboard/app/not-found.tsx +22 -0
  22. package/dashboard/components/AvailableChecks.tsx +260 -0
  23. package/dashboard/components/CheckCard.tsx +415 -65
  24. package/dashboard/components/CheckCardList.tsx +28 -23
  25. package/dashboard/components/Header.tsx +52 -16
  26. package/dashboard/components/Icons.tsx +20 -0
  27. package/dashboard/components/LayoutContent.tsx +24 -0
  28. package/dashboard/components/MyShimChecks.tsx +257 -0
  29. package/dashboard/components/Nav.tsx +9 -6
  30. package/dashboard/components/SetDocumentLang.tsx +18 -0
  31. package/dashboard/components/SidebarMyShim.tsx +108 -19
  32. package/dashboard/components/StatusCard.tsx +5 -12
  33. package/dashboard/components/TriggerCommandos.tsx +311 -42
  34. package/dashboard/lib/checks.ts +134 -17
  35. package/dashboard/lib/presets.ts +60 -28
  36. package/dashboard/lib/projectRoot.ts +22 -12
  37. package/dashboard/next-env.d.ts +2 -1
  38. package/dashboard/next.config.js +10 -1
  39. package/dashboard/package.json +11 -6
  40. package/dashboard/scripts/find-port-and-dev.js +48 -15
  41. package/dashboard/tailwind.config.js +1 -4
  42. package/dashboard/tsconfig.json +9 -2
  43. package/package.json +4 -6
  44. package/scripts/run-checks.sh +77 -24
  45. package/scripts/supabase-checked.sh +23 -7
  46. package/scripts/update-readme.js +72 -0
  47. package/templates/run-checks.sh +80 -27
  48. package/dashboard/app/page.tsx +0 -151
  49. package/dashboard/app/settings/page.tsx +0 -467
@@ -1,29 +1,65 @@
1
1
  /**
2
- * Header: shimwrappercheck centered at top; nav left.
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 Link from "next/link";
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-16 border-b border-neutral-700 flex items-center justify-between px-6 bg-[#0f0f0f]">
12
- <nav className="flex gap-2">
13
- <Link href="/" className="text-sm text-neutral-300 hover:text-white">
14
- Dashboard
15
- </Link>
16
- <Link href="/settings" className="text-sm text-neutral-300 hover:text-white">
17
- Einstellungen
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 href="/config" className="text-sm text-neutral-300 hover:text-white">
20
- Config
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
- <Link href="/agents" className="text-sm text-neutral-300 hover:text-white">
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 Link from "next/link";
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
- shimwrappercheck
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
- Dashboard
22
+ {tNav("dashboard")}
20
23
  </Link>
21
24
  <Link href="/settings" className="btn btn-ghost btn-sm">
22
- Einstellungen
25
+ {tNav("settings")}
23
26
  </Link>
24
27
  <Link href="/config" className="btn btn-ghost btn-sm">
25
- Config
28
+ {tNav("config")}
26
29
  </Link>
27
30
  <Link href="/agents" className="btn btn-ghost btn-sm">
28
- AGENTS.md
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 commandos + Checks (simple cards with search).
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 type { SettingsData } from "@/lib/presets";
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 CheckCardList from "@/components/CheckCardList";
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
- fetch("/api/settings")
17
- .then((r) => r.json())
18
- .then((data) => setSettings(data))
19
- .catch(() => setSettings(null));
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
- }).catch(() => {});
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
- <h2 className="text-lg font-semibold text-white">My Shim</h2>
34
- <section>
35
- <h3 className="text-sm font-medium text-white mb-2">Trigger commandos</h3>
36
- <TriggerCommandos settings={settings} onSave={saveSettings} />
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
- <h3 className="text-sm font-medium text-white mb-2">Checks</h3>
40
- <CheckCardList settings={settings} onSave={saveSettings} variant="simple" />
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
- export default function StatusCard({
8
- label,
9
- ok,
10
- detail,
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>