diff --git a/frontend/src/components/StackAlertSheet.tsx b/frontend/src/components/StackAlertSheet.tsx
index 7835fdd2..0c1cb629 100644
--- a/frontend/src/components/StackAlertSheet.tsx
+++ b/frontend/src/components/StackAlertSheet.tsx
@@ -147,7 +147,7 @@ export function StackAlertSheet({ isOpen, onClose, stackName }: StackAlertSheetP
const err = await res.json().catch(() => ({}));
toast.error(err?.error || 'Failed to delete alert rule.');
}
- } catch (e) {
+ } catch {
toast.error('Network error. Could not reach the node.');
} finally {
setIsLoading(false);
diff --git a/frontend/src/components/animate-ui/primitives/animate/slot.tsx b/frontend/src/components/animate-ui/primitives/animate/slot.tsx
index 3142cc40..35b9e0c9 100644
--- a/frontend/src/components/animate-ui/primitives/animate/slot.tsx
+++ b/frontend/src/components/animate-ui/primitives/animate/slot.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable */
'use client';
import * as React from 'react';
diff --git a/frontend/src/components/animate-ui/primitives/effects/highlight.tsx b/frontend/src/components/animate-ui/primitives/effects/highlight.tsx
index 96ccc952..f7e7058d 100644
--- a/frontend/src/components/animate-ui/primitives/effects/highlight.tsx
+++ b/frontend/src/components/animate-ui/primitives/effects/highlight.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable */
'use client';
import * as React from 'react';
diff --git a/frontend/src/components/animate-ui/primitives/radix/dialog.tsx b/frontend/src/components/animate-ui/primitives/radix/dialog.tsx
index 2efa06da..3730aead 100644
--- a/frontend/src/components/animate-ui/primitives/radix/dialog.tsx
+++ b/frontend/src/components/animate-ui/primitives/radix/dialog.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable */
'use client';
import * as React from 'react';
@@ -204,4 +205,4 @@ export {
type DialogDescriptionProps,
type DialogContextType,
type DialogFlipDirection,
-};
+};
\ No newline at end of file
diff --git a/frontend/src/components/animate-ui/primitives/radix/dropdown-menu.tsx b/frontend/src/components/animate-ui/primitives/radix/dropdown-menu.tsx
index 0ee15e5f..b828b906 100644
--- a/frontend/src/components/animate-ui/primitives/radix/dropdown-menu.tsx
+++ b/frontend/src/components/animate-ui/primitives/radix/dropdown-menu.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable */
'use client';
import * as React from 'react';
@@ -560,4 +561,4 @@ export {
type DropdownMenuRadioGroupProps,
type DropdownMenuContextType,
type DropdownMenuSubContextType,
-};
+};
\ No newline at end of file
diff --git a/frontend/src/components/animate-ui/primitives/radix/hover-card.tsx b/frontend/src/components/animate-ui/primitives/radix/hover-card.tsx
index 239843b5..b2ce5ca7 100644
--- a/frontend/src/components/animate-ui/primitives/radix/hover-card.tsx
+++ b/frontend/src/components/animate-ui/primitives/radix/hover-card.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable */
'use client';
import * as React from 'react';
@@ -204,4 +205,4 @@ export {
type HoverCardContentProps,
type HoverCardArrowProps,
type HoverCardContextType,
-};
+};
\ No newline at end of file
diff --git a/frontend/src/components/animate-ui/primitives/radix/popover.tsx b/frontend/src/components/animate-ui/primitives/radix/popover.tsx
index 99ecf2a3..c022ac4b 100644
--- a/frontend/src/components/animate-ui/primitives/radix/popover.tsx
+++ b/frontend/src/components/animate-ui/primitives/radix/popover.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable */
'use client';
import * as React from 'react';
@@ -159,4 +160,4 @@ export {
type PopoverCloseProps,
type PopoverArrowProps,
type PopoverContextType,
-};
+};
\ No newline at end of file
diff --git a/frontend/src/components/animate-ui/primitives/radix/sheet.tsx b/frontend/src/components/animate-ui/primitives/radix/sheet.tsx
index b789c2e2..59f7a0ae 100644
--- a/frontend/src/components/animate-ui/primitives/radix/sheet.tsx
+++ b/frontend/src/components/animate-ui/primitives/radix/sheet.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable */
'use client';
import * as React from 'react';
@@ -188,4 +189,4 @@ export {
type SheetFooterProps,
type SheetTitleProps,
type SheetDescriptionProps,
-};
+};
\ No newline at end of file
diff --git a/frontend/src/components/animate-ui/primitives/radix/switch.tsx b/frontend/src/components/animate-ui/primitives/radix/switch.tsx
index 800a28fd..c414bf99 100644
--- a/frontend/src/components/animate-ui/primitives/radix/switch.tsx
+++ b/frontend/src/components/animate-ui/primitives/radix/switch.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable */
'use client';
import * as React from 'react';
@@ -152,4 +153,4 @@ export {
type SwitchIconProps,
type SwitchIconPosition,
type SwitchContextType,
-};
+};
\ No newline at end of file
diff --git a/frontend/src/components/animate-ui/primitives/radix/tooltip.tsx b/frontend/src/components/animate-ui/primitives/radix/tooltip.tsx
index ee7413ba..39e21586 100644
--- a/frontend/src/components/animate-ui/primitives/radix/tooltip.tsx
+++ b/frontend/src/components/animate-ui/primitives/radix/tooltip.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable */
'use client';
import * as React from 'react';
@@ -217,4 +218,4 @@ export {
type TooltipContentProps,
type TooltipArrowProps,
type TooltipContextType,
-};
+};
\ No newline at end of file
diff --git a/frontend/src/components/animate-ui/primitives/texts/sliding-number.tsx b/frontend/src/components/animate-ui/primitives/texts/sliding-number.tsx
index 561173f5..b4750d2f 100644
--- a/frontend/src/components/animate-ui/primitives/texts/sliding-number.tsx
+++ b/frontend/src/components/animate-ui/primitives/texts/sliding-number.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable */
'use client';
import * as React from 'react';
@@ -350,4 +351,4 @@ function SlidingNumber({
);
}
-export { SlidingNumber, type SlidingNumberProps };
+export { SlidingNumber, type SlidingNumberProps };
\ No newline at end of file
diff --git a/frontend/src/components/ui/alert-dialog.tsx b/frontend/src/components/ui/alert-dialog.tsx
index e48cb48e..3a4ef9e8 100644
--- a/frontend/src/components/ui/alert-dialog.tsx
+++ b/frontend/src/components/ui/alert-dialog.tsx
@@ -1,6 +1,5 @@
import * as React from 'react';
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
-import { motion } from 'motion/react';
import { cn } from '@/lib/utils';
import { buttonVariants } from '@/components/ui/button';
@@ -30,18 +29,16 @@ const AlertDialogContent = React.forwardRef<
>(({ className, children, ...props }, ref) => (
-
-
- {children}
-
+
+ {children}
));
diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx
index e87d62bf..644e9dd5 100644
--- a/frontend/src/components/ui/badge.tsx
+++ b/frontend/src/components/ui/badge.tsx
@@ -33,4 +33,5 @@ function Badge({ className, variant, ...props }: BadgeProps) {
)
}
+// eslint-disable-next-line react-refresh/only-export-components
export { Badge, badgeVariants }
diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx
index 65d4fcd9..d8f7202c 100644
--- a/frontend/src/components/ui/button.tsx
+++ b/frontend/src/components/ui/button.tsx
@@ -54,4 +54,5 @@ const Button = React.forwardRef
(
)
Button.displayName = "Button"
+// eslint-disable-next-line react-refresh/only-export-components
export { Button, buttonVariants }
diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx
index 55b308ea..81691514 100644
--- a/frontend/src/context/AuthContext.tsx
+++ b/frontend/src/context/AuthContext.tsx
@@ -108,6 +108,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
);
}
+// eslint-disable-next-line react-refresh/only-export-components
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
diff --git a/frontend/src/context/NodeContext.tsx b/frontend/src/context/NodeContext.tsx
index 2d368ca4..1a297861 100644
--- a/frontend/src/context/NodeContext.tsx
+++ b/frontend/src/context/NodeContext.tsx
@@ -98,6 +98,7 @@ export function NodeProvider({ children }: { children: React.ReactNode }) {
);
}
+// eslint-disable-next-line react-refresh/only-export-components
export function useNodes() {
const context = useContext(NodeContext);
if (!context) {
diff --git a/frontend/src/hooks/use-data-state.tsx b/frontend/src/hooks/use-data-state.tsx
index 7166f3ef..59decef4 100644
--- a/frontend/src/hooks/use-data-state.tsx
+++ b/frontend/src/hooks/use-data-state.tsx
@@ -51,4 +51,5 @@ function useDataState(
return [value, localRef];
}
+// eslint-disable-next-line react-refresh/only-export-components
export { useDataState, type DataStateValue };
diff --git a/frontend/src/hooks/use-is-in-view.tsx b/frontend/src/hooks/use-is-in-view.tsx
index 3738c569..0648e4e5 100644
--- a/frontend/src/hooks/use-is-in-view.tsx
+++ b/frontend/src/hooks/use-is-in-view.tsx
@@ -22,4 +22,5 @@ function useIsInView(
return { ref: localRef, isInView };
}
+// eslint-disable-next-line react-refresh/only-export-components
export { useIsInView, type UseIsInViewOptions };
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index 7fbd64b4..3282f165 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -39,7 +39,7 @@ export async function apiFetch(
if (errData.error && errData.error.includes('not found') && errData.error.includes('Node')) {
window.dispatchEvent(new Event('node-not-found'));
}
- } catch (e) {
+ } catch {
// Ignore JSON parse errors, caller handles standard 404s
}
}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index bef5202a..a7fa8ce9 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -2,9 +2,12 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
+import ErrorBoundary from './components/ErrorBoundary.tsx'
createRoot(document.getElementById('root')!).render(
-
+
+
+
,
)
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 00000000..b83f7fa3
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,79 @@
+{
+ "name": "sencho",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "sencho",
+ "version": "1.0.0",
+ "license": "ISC",
+ "devDependencies": {
+ "@playwright/test": "^1.58.2"
+ }
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.58.2",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
+ "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.58.2"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.58.2",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
+ "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.58.2"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.58.2",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
+ "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..8647da7d
--- /dev/null
+++ b/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "sencho",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "test": "cd backend && npm test",
+ "test:e2e": "playwright test",
+ "test:e2e:ui": "playwright test --ui"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/AnsoCode/Sencho.git"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "type": "commonjs",
+ "bugs": {
+ "url": "https://github.com/AnsoCode/Sencho/issues"
+ },
+ "homepage": "https://github.com/AnsoCode/Sencho#readme",
+ "devDependencies": {
+ "@playwright/test": "^1.58.2"
+ }
+}
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 00000000..e59903a6
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,35 @@
+import { defineConfig, devices } from '@playwright/test';
+
+/**
+ * Playwright E2E config for Sencho.
+ *
+ * Before running: ensure both dev servers are up:
+ * cd backend && npm run dev &
+ * cd frontend && npm run dev &
+ *
+ * Or use the webServer config below (which starts them automatically).
+ */
+export default defineConfig({
+ testDir: './e2e',
+ // Don't stop on first failure — show all results
+ maxFailures: 0,
+ // How long to wait for a single test
+ timeout: 30_000,
+ // How long to wait for an expect() assertion
+ expect: { timeout: 5_000 },
+ // Run tests serially — Sencho is a single-user app and tests share DB state
+ workers: 1,
+ reporter: [['list'], ['html', { outputFolder: 'e2e/report', open: 'never' }]],
+
+ use: {
+ baseURL: 'http://localhost:5173',
+ // Persist auth state between tests in the same file
+ storageState: undefined,
+ trace: 'on-first-retry',
+ screenshot: 'only-on-failure',
+ },
+
+ projects: [
+ { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
+ ],
+});