@arch-cadre/panel 1.0.7 → 1.0.10

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 (53) hide show
  1. package/dist/ui/activity-log/pages/log-list.cjs +2 -2
  2. package/dist/ui/components/profile/components.cjs +2 -2
  3. package/dist/ui/components/profile/components.d.ts +1 -1
  4. package/dist/ui/components/profile/components.mjs +1 -1
  5. package/dist/ui/rbac/pages/rbac-admin.cjs +14 -14
  6. package/dist/ui/session-manager/components/sessions-list.cjs +5 -5
  7. package/dist/ui/session-manager/components/sessions-list.mjs +1 -1
  8. package/dist/ui/session-manager/pages/sessions-page.cjs +3 -3
  9. package/dist/ui/session-manager/pages/sessions-page.mjs +1 -1
  10. package/package.json +7 -6
  11. package/src/actions/actions.ts +17 -0
  12. package/src/actions/activity-log/index.ts +17 -0
  13. package/src/actions/index.ts +2 -0
  14. package/src/actions/manager.ts +168 -0
  15. package/src/actions/profile.ts +173 -0
  16. package/src/actions/rbac/index.ts +131 -0
  17. package/src/actions/session-manager/index.ts +87 -0
  18. package/src/actions/settings.ts +34 -0
  19. package/src/index.ts +135 -0
  20. package/src/intl.d.ts +9 -0
  21. package/src/navigation.ts +57 -0
  22. package/src/routes.ts +107 -0
  23. package/src/schema/activity-log.ts +16 -0
  24. package/src/schema.ts +1 -0
  25. package/src/types.ts +18 -0
  26. package/src/ui/activity-log/components/ActivityStatsWidget.tsx +37 -0
  27. package/src/ui/activity-log/components/RecentLogsWidget.tsx +74 -0
  28. package/src/ui/activity-log/pages/log-list.tsx +91 -0
  29. package/src/ui/components/app-content.tsx +51 -0
  30. package/src/ui/components/app-header.tsx +65 -0
  31. package/src/ui/components/app-sidebar.tsx +249 -0
  32. package/src/ui/components/app-user.tsx +126 -0
  33. package/src/ui/components/breadcrumb-slot.tsx +52 -0
  34. package/src/ui/components/manager/module-card.tsx +327 -0
  35. package/src/ui/components/manager/module-list.tsx +59 -0
  36. package/src/ui/components/manager/module-upload.tsx +84 -0
  37. package/src/ui/components/profile/components.tsx +311 -0
  38. package/src/ui/components/profile/link.tsx +36 -0
  39. package/src/ui/components/profile/page.tsx +45 -0
  40. package/src/ui/components/sidebar-slot.tsx +47 -0
  41. package/src/ui/dashboard/page.tsx +17 -0
  42. package/src/ui/dashboard/widgets/WelcomeBackUserWidget.tsx +47 -0
  43. package/src/ui/error.tsx +82 -0
  44. package/src/ui/layout.tsx +54 -0
  45. package/src/ui/modules/docs/page.tsx +105 -0
  46. package/src/ui/modules/page.tsx +30 -0
  47. package/src/ui/page.tsx +15 -0
  48. package/src/ui/rbac/pages/rbac-admin.tsx +551 -0
  49. package/src/ui/router.tsx +69 -0
  50. package/src/ui/session-manager/components/sessions-list.tsx +303 -0
  51. package/src/ui/session-manager/pages/sessions-page.tsx +22 -0
  52. package/src/ui/settings/page.tsx +73 -0
  53. package/src/ui/settings-page.tsx +97 -0
@@ -0,0 +1,52 @@
1
+ "use client";
2
+
3
+ import { useTranslation } from "@arch-cadre/intl";
4
+ import {
5
+ Breadcrumb,
6
+ BreadcrumbItem,
7
+ BreadcrumbList,
8
+ BreadcrumbPage,
9
+ BreadcrumbSeparator,
10
+ } from "@arch-cadre/ui/components/breadcrumb";
11
+ import {
12
+ Tooltip,
13
+ TooltipContent,
14
+ TooltipTrigger,
15
+ } from "@arch-cadre/ui/components/tooltip";
16
+ import Image from "next/image";
17
+ import Link from "next/link";
18
+ import * as React from "react";
19
+
20
+ export default function BreadcrumbSlot() {
21
+ const { t } = useTranslation();
22
+
23
+ return (
24
+ <Breadcrumb className="hidden sm:block">
25
+ <BreadcrumbList>
26
+ <BreadcrumbItem>
27
+ <Tooltip>
28
+ <TooltipTrigger asChild>
29
+ <Link href="/kryo">
30
+ <Image
31
+ width={16}
32
+ height={16}
33
+ src="/favicon.svg"
34
+ alt="logo"
35
+ className="size-4"
36
+ aria-hidden={true}
37
+ />
38
+ </Link>
39
+ </TooltipTrigger>
40
+ <TooltipContent>
41
+ <p>{t("Back to Dashboard")}</p>
42
+ </TooltipContent>
43
+ </Tooltip>
44
+ </BreadcrumbItem>
45
+ <BreadcrumbSeparator>&bull;</BreadcrumbSeparator>
46
+ <BreadcrumbItem>
47
+ <BreadcrumbPage>{t("Dashboard")}</BreadcrumbPage>
48
+ </BreadcrumbItem>
49
+ </BreadcrumbList>
50
+ </Breadcrumb>
51
+ );
52
+ }
@@ -0,0 +1,327 @@
1
+ "use client";
2
+
3
+ import { eventBus } from "@arch-cadre/core";
4
+ import { useTranslation } from "@arch-cadre/intl";
5
+ import type { ModuleManifest } from "@arch-cadre/modules";
6
+ import { Badge } from "@arch-cadre/ui/components/badge";
7
+ import { Button } from "@arch-cadre/ui/components/button";
8
+ import {
9
+ Card,
10
+ CardContent,
11
+ CardHeader,
12
+ CardTitle,
13
+ } from "@arch-cadre/ui/components/card";
14
+ import { Switch } from "@arch-cadre/ui/components/switch";
15
+ import { Loader } from "@arch-cadre/ui/shared/loader";
16
+ import { AlertCircle, FileText, ShieldCheck } from "lucide-react";
17
+ import Link from "next/link";
18
+ import { useRouter } from "next/navigation";
19
+ import * as React from "react";
20
+ import { useCallback, useEffect, useRef, useState } from "react";
21
+ import { toast } from "sonner";
22
+ import { toggleModuleAction } from "../../../actions/manager";
23
+
24
+ interface ModuleStatusCheckEvent {
25
+ triggerSource: string;
26
+ dependencies: string[];
27
+ action: "enable" | "disable";
28
+ }
29
+
30
+ interface ModuleCardProps {
31
+ module: ModuleManifest & {
32
+ installed?: boolean;
33
+ hasDocs?: boolean;
34
+ lastStep?: string | null;
35
+ isNpm?: boolean;
36
+ };
37
+ }
38
+
39
+ export function ModuleCard({ module }: ModuleCardProps) {
40
+ const router = useRouter();
41
+ const { t } = useTranslation();
42
+ const [isEnabled, setIsEnabled] = useState(module.enabled);
43
+ const [isProcessing, setIsProcessing] = useState(!!module.lastStep);
44
+ const [localStep, setLocalStep] = useState<string | null>(
45
+ module.lastStep || null,
46
+ );
47
+ const pollerRef = useRef<NodeJS.Timeout | null>(null);
48
+
49
+ const stopPolling = useCallback(() => {
50
+ if (pollerRef.current) {
51
+ clearInterval(pollerRef.current);
52
+ pollerRef.current = null;
53
+ }
54
+ }, []);
55
+
56
+ const startPolling = useCallback(() => {
57
+ if (pollerRef.current) return;
58
+
59
+ pollerRef.current = setInterval(async () => {
60
+ try {
61
+ const res = await fetch(`/api/system/modules/${module.id}/status`);
62
+ const status = await res.json();
63
+
64
+ // Jeśli serwer zwrócił konkretny krok, ustawiamy go
65
+ if (status.lastStep) {
66
+ setLocalStep(status.lastStep);
67
+ }
68
+
69
+ // Jeśli lastStep zniknął, proces się zakończył
70
+ if (!status.lastStep) {
71
+ setIsProcessing(false);
72
+ setLocalStep(null);
73
+ stopPolling();
74
+ router.refresh();
75
+ } else if (status.lastStep.toLowerCase().includes("error")) {
76
+ setIsProcessing(false);
77
+ stopPolling();
78
+ }
79
+ } catch (e) {
80
+ console.error("Polling error", e);
81
+ }
82
+ }, 1000);
83
+ }, [module.id, router, stopPolling]);
84
+
85
+ // Subskrypcja EventBus
86
+ useEffect(() => {
87
+ const eventType = "module:status:check";
88
+ const subscriberId = `card-${module.id}`;
89
+
90
+ eventBus.subscribe<ModuleStatusCheckEvent>(
91
+ eventType,
92
+ subscriberId,
93
+ (event) => {
94
+ const { triggerSource, dependencies, action } = event.payload;
95
+ // Ignorujemy jeśli to my wywołaliśmy akcję LUB jeśli jesteśmy modułem systemowym
96
+ if (triggerSource === module.id || module.system) return;
97
+
98
+ let shouldCheck = false;
99
+
100
+ if (action === "enable") {
101
+ // Jeśli ktoś włącza moduł, który zależy ode mnie, ja muszę zostać sprawdzony/zaktualizowany pierwszy
102
+ if (dependencies.includes(module.id)) {
103
+ shouldCheck = true;
104
+ }
105
+ } else if (action === "disable") {
106
+ // Jeśli ktoś wyłącza moduł, od którego JA zależę, ja też zostanę wyłączony
107
+ if (module.dependencies?.includes(triggerSource)) {
108
+ shouldCheck = true;
109
+ }
110
+ // Jeśli ktoś wyłącza moduł, który zależał ode mnie, ja MOGĘ zostać wyłączony (jeśli nikt inny mnie nie chce)
111
+ if (dependencies.includes(module.id)) {
112
+ shouldCheck = true;
113
+ }
114
+ }
115
+
116
+ if (shouldCheck && !isProcessing) {
117
+ setIsProcessing(true);
118
+ setLocalStep(t("Waiting..."));
119
+ startPolling();
120
+ }
121
+ },
122
+ );
123
+
124
+ return () => eventBus.unsubscribe(eventType, subscriberId);
125
+ }, [
126
+ module.id,
127
+ module.dependencies,
128
+ isProcessing,
129
+ module.system,
130
+ startPolling,
131
+ t,
132
+ ]);
133
+
134
+ // Uruchom polling jeśli moduł przyszedł z serwera jako "w trakcie"
135
+ useEffect(() => {
136
+ if (module.lastStep) {
137
+ setIsProcessing(true);
138
+ setLocalStep(module.lastStep);
139
+ startPolling();
140
+ }
141
+ return () => stopPolling();
142
+ }, [module.lastStep, startPolling, stopPolling]);
143
+
144
+ // Synchronizacja isEnabled przy zmianie propsów (np. po refresh)
145
+ useEffect(() => {
146
+ setIsEnabled(module.enabled);
147
+ }, [module.enabled]);
148
+
149
+ const handleToggle = async (checked: boolean) => {
150
+ setIsProcessing(true);
151
+ setLocalStep(t("Starting..."));
152
+ setIsEnabled(checked);
153
+
154
+ // Publikowanie zdarzenia przez EventBus
155
+ eventBus.publish(
156
+ "module:status:check",
157
+ {
158
+ triggerSource: module.id,
159
+ dependencies: module.dependencies || [],
160
+ action: checked ? "enable" : "disable",
161
+ },
162
+ `card-${module.id}`,
163
+ );
164
+
165
+ try {
166
+ await toggleModuleAction(module.id, checked);
167
+ startPolling();
168
+ } catch (_error) {
169
+ setIsEnabled(!checked);
170
+ setIsProcessing(false);
171
+ setLocalStep(null);
172
+ toast.error(t("error_occurred"));
173
+ }
174
+ };
175
+
176
+ return (
177
+ <Card
178
+ className={`relative overflow-hidden transition-all duration-300 ${!isEnabled && !module.system ? "opacity-75 shadow-none border-dashed" : "shadow-md"}`}
179
+ >
180
+ {isProcessing && (
181
+ <div className="absolute top-0 left-0 w-full h-1 bg-primary/20 overflow-hidden">
182
+ <div
183
+ className={`h-full bg-primary ${localStep?.startsWith("Waiting") ? "animate-pulse opacity-50" : "animate-progress-fast"}`}
184
+ style={{ width: localStep?.startsWith("Waiting") ? "100%" : "40%" }}
185
+ />
186
+ </div>
187
+ )}
188
+
189
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
190
+ <CardTitle className="text-base font-medium flex items-center gap-2">
191
+ {module.name}
192
+ {module.system && <ShieldCheck className="size-4 text-primary" />}
193
+ </CardTitle>
194
+
195
+ <div className="flex items-center gap-2">
196
+ {module.hasDocs && (
197
+ <Button
198
+ asChild
199
+ variant="ghost"
200
+ size="icon"
201
+ className="h-8 w-8"
202
+ title={t("View documentation")}
203
+ >
204
+ <Link href={`/kryo/modules/docs/${module.id}`}>
205
+ <FileText className="size-4" />
206
+ </Link>
207
+ </Button>
208
+ )}
209
+ {!module.system && (
210
+ <Switch
211
+ checked={isEnabled}
212
+ onCheckedChange={handleToggle}
213
+ disabled={isProcessing}
214
+ />
215
+ )}
216
+ </div>
217
+ </CardHeader>
218
+ <CardContent>
219
+ <div className="flex items-center gap-2 mb-4">
220
+ <Badge variant={isEnabled || module.system ? "default" : "secondary"}>
221
+ {isProcessing ? (
222
+ <span className="flex items-center gap-1">
223
+ <Loader
224
+ variant={isEnabled || module.system ? "dark" : "default"}
225
+ className="size-3"
226
+ />
227
+ {localStep?.startsWith("Waiting")
228
+ ? t("Please wait...")
229
+ : t("Working...")}
230
+ </span>
231
+ ) : isEnabled || module.system ? (
232
+ t("Enabled")
233
+ ) : (
234
+ t("Disabled")
235
+ )}
236
+ </Badge>
237
+ {module.installed && (
238
+ <Badge variant="outline" className="text-[10px] h-5 font-mono">
239
+ {t("Installed")}
240
+ </Badge>
241
+ )}
242
+ {module.isNpm && (
243
+ <Badge
244
+ variant="secondary"
245
+ className="text-[10px] h-5 font-mono bg-blue-500/10 text-blue-600 border-blue-500/20"
246
+ >
247
+ NPM
248
+ </Badge>
249
+ )}
250
+ <span className="text-xs text-muted-foreground ml-auto">
251
+ v{module.version}
252
+ </span>
253
+ </div>
254
+
255
+ {isProcessing || localStep ? (
256
+ <div
257
+ className={`p-2 rounded-lg border text-[11px] font-mono mb-2 flex items-center gap-2 ${localStep?.toLowerCase().includes("error") ? "bg-destructive/10 text-destructive border-destructive/20" : localStep?.startsWith("Waiting") ? "bg-amber-500/10 text-amber-600 border-amber-500/20" : "bg-muted/50 text-muted-foreground"}`}
258
+ >
259
+ {localStep?.toLowerCase().includes("error") ? (
260
+ <AlertCircle className="size-3" />
261
+ ) : localStep?.startsWith("Waiting") ? (
262
+ <Loader className="size-3 animate-spin opacity-50" />
263
+ ) : (
264
+ <Loader className="size-3 animate-spin" />
265
+ )}
266
+ <span className="truncate">{localStep || t("Processing")}</span>
267
+ </div>
268
+ ) : (
269
+ <div className="space-y-3">
270
+ <p className="text-sm text-muted-foreground line-clamp-2 h-[40px]">
271
+ {t(module.description!)}
272
+ </p>
273
+
274
+ <div className="flex flex-wrap gap-y-2 gap-x-4 pt-2 border-t border-muted/50">
275
+ {(!module.extends || module.extends.length === 0) &&
276
+ (!module.dependencies || module.dependencies.length === 0) ? (
277
+ <div className="flex items-center gap-1.5">
278
+ <span className="text-[10px] font-black uppercase tracking-tight text-muted-foreground/40 italic">
279
+ {t("No dependencies")}
280
+ </span>
281
+ </div>
282
+ ) : (
283
+ <>
284
+ {module.extends && module.extends.length > 0 && (
285
+ <div className="flex items-center gap-1.5">
286
+ <span className="text-[10px] font-black uppercase tracking-tight text-primary/70">
287
+ {t("Extends")}
288
+ </span>
289
+ <div className="flex gap-1">
290
+ {module.extends.map((ext) => (
291
+ <span
292
+ key={ext}
293
+ className="text-[10px] font-mono bg-primary/5 text-primary px-1 rounded border border-primary/10"
294
+ >
295
+ {ext}
296
+ </span>
297
+ ))}
298
+ </div>
299
+ </div>
300
+ )}
301
+
302
+ {module.dependencies && module.dependencies.length > 0 && (
303
+ <div className="flex items-center gap-1.5">
304
+ <span className="text-[10px] font-black uppercase tracking-tight text-muted-foreground">
305
+ {t("Requires")}
306
+ </span>
307
+ <div className="flex gap-1">
308
+ {module.dependencies.map((dep) => (
309
+ <span
310
+ key={dep}
311
+ className="text-[10px] font-mono bg-muted text-muted-foreground px-1 rounded border"
312
+ >
313
+ {dep}
314
+ </span>
315
+ ))}
316
+ </div>
317
+ </div>
318
+ )}
319
+ </>
320
+ )}
321
+ </div>
322
+ </div>
323
+ )}
324
+ </CardContent>
325
+ </Card>
326
+ );
327
+ }
@@ -0,0 +1,59 @@
1
+ "use client";
2
+
3
+ import { useTranslation } from "@arch-cadre/intl";
4
+
5
+ import { Separator } from "@arch-cadre/ui/components/separator";
6
+ import * as React from "react";
7
+ import { ModuleCard } from "./module-card";
8
+
9
+ export function ModuleMarketplaceList({
10
+ initialModules,
11
+ }: {
12
+ initialModules: any[];
13
+ }) {
14
+ const { t } = useTranslation();
15
+ // Sortowanie i filtrowanie
16
+ const sortedModules = [...initialModules].sort((a, b) =>
17
+ a.name.localeCompare(b.name),
18
+ );
19
+ const coreModules = sortedModules.filter((m) => m.system);
20
+ const publicModules = sortedModules.filter((m) => !m.system);
21
+
22
+ return (
23
+ <div className="space-y-10">
24
+ {/* Sekcja Public */}
25
+ {publicModules.length > 0 && (
26
+ <div className="space-y-6">
27
+ <div className="flex items-center gap-4">
28
+ <h3 className="text-sm font-bold uppercase tracking-widest text-muted-foreground shrink-0">
29
+ {t("Public Modules")}
30
+ </h3>
31
+ <Separator className="flex-1" />
32
+ </div>
33
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
34
+ {publicModules.map((module) => (
35
+ <ModuleCard key={module.id} module={module} />
36
+ ))}
37
+ </div>
38
+ </div>
39
+ )}
40
+
41
+ {/* Sekcja Core */}
42
+ {coreModules.length > 0 && (
43
+ <div className="space-y-6">
44
+ <div className="flex items-center gap-4">
45
+ <h3 className="text-sm font-bold uppercase tracking-widest text-primary shrink-0">
46
+ {t("Core Modules")}
47
+ </h3>
48
+ <Separator className="flex-1" />
49
+ </div>
50
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
51
+ {coreModules.map((module) => (
52
+ <ModuleCard key={module.id} module={module} />
53
+ ))}
54
+ </div>
55
+ </div>
56
+ )}
57
+ </div>
58
+ );
59
+ }
@@ -0,0 +1,84 @@
1
+ "use client";
2
+
3
+ import { useTranslation } from "@arch-cadre/intl";
4
+
5
+ import { Button } from "@arch-cadre/ui/components/button";
6
+ import { Loader2, Lock, Upload } from "lucide-react";
7
+ import * as React from "react";
8
+ import { useEffect, useRef, useState } from "react";
9
+ import { toast } from "sonner";
10
+ import {
11
+ checkDiskWriteAccess,
12
+ uploadModuleAction,
13
+ } from "../../../actions/manager";
14
+
15
+ export function ModuleUpload() {
16
+ const [isUploading, setIsUploading] = useState(false);
17
+ const [canWrite, setCanWrite] = useState<boolean | null>(null);
18
+ const fileInputRef = useRef<HTMLInputElement>(null);
19
+ const { t } = useTranslation();
20
+
21
+ useEffect(() => {
22
+ checkDiskWriteAccess().then((res) => setCanWrite(res.canWrite));
23
+ }, []);
24
+
25
+ const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
26
+ const file = e.target.files?.[0];
27
+ if (!file) return;
28
+
29
+ if (!file.name.endsWith(".zip")) {
30
+ toast.error(t("Invalid file"));
31
+ return;
32
+ }
33
+
34
+ const formData = new FormData();
35
+ formData.append("file", file);
36
+
37
+ setIsUploading(true);
38
+ const id = toast.loading(t("Uploading..."));
39
+
40
+ try {
41
+ const result = await uploadModuleAction(formData);
42
+ if (result.success) {
43
+ toast.success(t("Upload successful"), { id });
44
+ if (fileInputRef.current) fileInputRef.current.value = "";
45
+ } else {
46
+ toast.error(result.error || t("Upload error"), { id });
47
+ }
48
+ } catch (_error) {
49
+ toast.error(t("error_occurred"), { id });
50
+ } finally {
51
+ setIsUploading(false);
52
+ }
53
+ };
54
+
55
+ return (
56
+ <div className="flex items-center gap-4">
57
+ <input
58
+ type="file"
59
+ accept=".zip"
60
+ className="hidden"
61
+ ref={fileInputRef}
62
+ onChange={handleUpload}
63
+ disabled={isUploading}
64
+ />
65
+ <Button
66
+ onClick={() => fileInputRef.current?.click()}
67
+ disabled={isUploading || canWrite === false}
68
+ variant="outline"
69
+ size="sm"
70
+ className="gap-2 w-full sm:w-auto"
71
+ title={canWrite === false ? t("No write access") : undefined}
72
+ >
73
+ {isUploading ? (
74
+ <Loader2 className="size-4 animate-spin" />
75
+ ) : canWrite === false ? (
76
+ <Lock className="size-4 text-destructive" />
77
+ ) : (
78
+ <Upload className="size-4" />
79
+ )}
80
+ {canWrite === false ? t("Upload disabled") : t("Upload module")}
81
+ </Button>
82
+ </div>
83
+ );
84
+ }