-
-
-
-
-
- } />
-
-
-
-
-
+
+
+
+
+
+
+
+ } />
+
+
+
+
+
+
);
}
diff --git a/frontend/app/components/RevenueOverTimeChart.tsx b/frontend/app/components/RevenueOverTimeChart.tsx
index 4fe9c04..822aabf 100644
--- a/frontend/app/components/RevenueOverTimeChart.tsx
+++ b/frontend/app/components/RevenueOverTimeChart.tsx
@@ -28,41 +28,43 @@ export default function RevenueOverTimeChart({ data }: Props) {
}
return (
-
-
-
-
-
-
-
-
-
-
-
- formatDateShort(String(l))} />}
- />
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ formatDateShort(String(l))} />}
+ />
+
+
+
+
);
}
diff --git a/frontend/app/globals.css b/frontend/app/globals.css
index 06f5f70..a14931e 100644
--- a/frontend/app/globals.css
+++ b/frontend/app/globals.css
@@ -52,6 +52,12 @@
padding: 0;
}
+:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+ border-radius: 4px;
+}
+
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg);
diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx
index 15e0de5..d48b54f 100644
--- a/frontend/app/layout.tsx
+++ b/frontend/app/layout.tsx
@@ -3,7 +3,7 @@ import "./globals.css";
export const metadata: Metadata = {
title: "Connect Analyzer",
- description: "Sales data analysis — prototype with simulated data",
+ description: "Análisis de datos de ventas — prototipo con datos simulados",
};
export default function RootLayout({
@@ -12,7 +12,7 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
-
+
{children}
);
diff --git a/frontend/app/lib/dashboard.test.ts b/frontend/app/lib/dashboard.test.ts
new file mode 100644
index 0000000..e5d92c7
--- /dev/null
+++ b/frontend/app/lib/dashboard.test.ts
@@ -0,0 +1,30 @@
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { fetchDashboard } from "./dashboard";
+
+afterEach(() => {
+ vi.restoreAllMocks();
+});
+
+describe("fetchDashboard", () => {
+ it("returns the sales on a successful response", async () => {
+ vi.spyOn(globalThis, "fetch").mockResolvedValue({
+ ok: true,
+ json: async () => [
+ { date: "2026-01-01", customerId: "C1", productName: "A", quantity: 1, amount: 10 },
+ ],
+ } as Response);
+
+ const { sales } = await fetchDashboard();
+
+ expect(sales).toHaveLength(1);
+ });
+
+ it("throws when the backend responds with an error", async () => {
+ vi.spyOn(globalThis, "fetch").mockResolvedValue({
+ ok: false,
+ status: 502,
+ } as Response);
+
+ await expect(fetchDashboard()).rejects.toThrow(/502/);
+ });
+});
diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx
index ca99772..04c17c4 100644
--- a/frontend/app/page.tsx
+++ b/frontend/app/page.tsx
@@ -8,7 +8,7 @@ export default async function Page() {
diff --git a/frontend/next.config.ts b/frontend/next.config.ts
index e9ffa30..075ba9b 100644
--- a/frontend/next.config.ts
+++ b/frontend/next.config.ts
@@ -1,7 +1,36 @@
import type { NextConfig } from "next";
+// Security headers for the public demo. The frontend fetches the backend server-side, so
+// the browser only talks to its own origin → connect-src 'self'. 'unsafe-inline' is kept
+// for script/style because Next (App Router hydration) and Recharts emit inline ones and
+// this demo doesn't wire up CSP nonces.
+const csp = [
+ "default-src 'self'",
+ "script-src 'self' 'unsafe-inline'",
+ "style-src 'self' 'unsafe-inline'",
+ "img-src 'self' data:",
+ "font-src 'self'",
+ "connect-src 'self'",
+ "base-uri 'self'",
+ "form-action 'self'",
+ "frame-ancestors 'none'",
+].join("; ");
+
+const securityHeaders = [
+ { key: "Content-Security-Policy", value: csp },
+ { key: "X-Content-Type-Options", value: "nosniff" },
+ { key: "X-Frame-Options", value: "DENY" },
+ { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
+ {
+ key: "Permissions-Policy",
+ value: "camera=(), microphone=(), geolocation=()",
+ },
+];
+
const nextConfig: NextConfig = {
- /* config options here */
+ async headers() {
+ return [{ source: "/:path*", headers: securityHeaders }];
+ },
};
export default nextConfig;
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 9769ebc..2f5d3f3 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -8,9 +8,9 @@
"name": "frontend",
"version": "0.1.0",
"dependencies": {
- "next": "16.2.6",
- "react": "19.2.4",
- "react-dom": "19.2.4",
+ "next": "16.2.7",
+ "react": "19.2.7",
+ "react-dom": "19.2.7",
"recharts": "^2.15.0"
},
"devDependencies": {
@@ -22,7 +22,7 @@
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9",
- "eslint-config-next": "16.2.6",
+ "eslint-config-next": "16.2.7",
"jsdom": "^25.0.1",
"typescript": "^5",
"vitest": "^2.1.8"
@@ -1632,15 +1632,15 @@
}
},
"node_modules/@next/env": {
- "version": "16.2.6",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz",
- "integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==",
+ "version": "16.2.7",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.7.tgz",
+ "integrity": "sha512-tMJizPlj6ZYpBMMdK8S0LJufrP4QTdR6pcv9KQ/bVETPAmg0j1mlHE9G2c38UyGHxoBapgwuj7XjbGJ2RcDFOg==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
- "version": "16.2.6",
- "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.6.tgz",
- "integrity": "sha512-Z8l6o4JWKUl755x4R+wogD86KPeU+Ckw4K+SYG4kHeOJtRenDeK+OSbGcqZpDtbwn9DsJVdir2UxmwXuinUbUw==",
+ "version": "16.2.7",
+ "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.7.tgz",
+ "integrity": "sha512-VbS+QgMHqvIDMTIqD2xMBKK1otIpdAUKA8VLHFwR9h6OfU/mOm7w/69nQcvdmI8hCk99Wr2AsGLn/PJ/tMHw1w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1648,9 +1648,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
- "version": "16.2.6",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz",
- "integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==",
+ "version": "16.2.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.7.tgz",
+ "integrity": "sha512-vm1EDI/pVaBNNiychmxk3fft+OhQPVD9cIM/tReLZIQ3TfQ4kqI9DwKk00dzuS1ulC7icbrzCFrmRRlk9PfNdw==",
"cpu": [
"arm64"
],
@@ -1664,9 +1664,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
- "version": "16.2.6",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz",
- "integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==",
+ "version": "16.2.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.7.tgz",
+ "integrity": "sha512-O3IRSv1ZBL1zs0WrIgefTEcTKFVn+ryxBNe54erJ6KsD+2f/Mmt7g2jOYh8PSBdUwPtKQJuCsTMlZ7tIu2AcsQ==",
"cpu": [
"x64"
],
@@ -1680,9 +1680,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
- "version": "16.2.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz",
- "integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==",
+ "version": "16.2.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.7.tgz",
+ "integrity": "sha512-Re6PZtjBDd0aMU+VcZcC/PrIvj4WhrjDYtMhhCVQamWN4L90EVP0pcEOBQD25prSlw7OzNw5QpHLWMilRLsRNw==",
"cpu": [
"arm64"
],
@@ -1696,9 +1696,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
- "version": "16.2.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz",
- "integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==",
+ "version": "16.2.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.7.tgz",
+ "integrity": "sha512-qyogG9QtBzWxgJfeGBvOEHI3851gTfCF3wLZ5RDLTBJGAmE9p1qDwKCOdrBrvBzRvYDT+gUDp72pzlSEfAXgNA==",
"cpu": [
"arm64"
],
@@ -1712,9 +1712,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
- "version": "16.2.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz",
- "integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==",
+ "version": "16.2.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.7.tgz",
+ "integrity": "sha512-Vhe4ZDuBpmMogrGi5D4R2Kq4JAQlj6+wvgaFYy31zfES0zPmt6TLA+cuYpM/OLrPZjo2MYQTHVqNUSCR6+fDZQ==",
"cpu": [
"x64"
],
@@ -1728,9 +1728,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
- "version": "16.2.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz",
- "integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==",
+ "version": "16.2.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.7.tgz",
+ "integrity": "sha512-srvian89JahFLw1YLBEuhvPJ0DO5lpUeJQMXy4xYo7g628ZlNgXdNkqoxSAv9OYrBfByh6vxISMwW/mRbzCY+g==",
"cpu": [
"x64"
],
@@ -1744,9 +1744,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
- "version": "16.2.6",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz",
- "integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==",
+ "version": "16.2.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.7.tgz",
+ "integrity": "sha512-GX3wvLpULFuRFJzwHaKfm7QZJ18F4ZSuxlPJ96BoBglCzBmdSjyeBKF+ZhWhvL/ckxNfLnNa7bsObO2ipYpszw==",
"cpu": [
"arm64"
],
@@ -1760,9 +1760,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
- "version": "16.2.6",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz",
- "integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==",
+ "version": "16.2.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.7.tgz",
+ "integrity": "sha512-J4WlM72NMk076Qsg0jTdK3SNXatlSdnjW7L7oNGLst1tAGjHrJh/FYi+pw9wyIjEtGRKDNzD0zuiY16oWYWVaw==",
"cpu": [
"x64"
],
@@ -4534,13 +4534,13 @@
}
},
"node_modules/eslint-config-next": {
- "version": "16.2.6",
- "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.6.tgz",
- "integrity": "sha512-z2ELYSkyrrJ6cuunTU8vhsT/RpouPkjaSah06nVW6Rg2Hpg0Vs8s497/e5s8G8qtdp4ccsiovz5P1rv+5VSW2Q==",
+ "version": "16.2.7",
+ "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.7.tgz",
+ "integrity": "sha512-CQ2aNXkrsjaGA2oJBE1LYnlRdphIAQE9ZQfX9hSv1PNGPyiOMSaVeBfTIO29QxYz+ij/hZudK0cfpCG1HXWstg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@next/eslint-plugin-next": "16.2.6",
+ "@next/eslint-plugin-next": "16.2.7",
"eslint-import-resolver-node": "^0.3.6",
"eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-import": "^2.32.0",
@@ -6348,12 +6348,12 @@
"license": "MIT"
},
"node_modules/next": {
- "version": "16.2.6",
- "resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz",
- "integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==",
+ "version": "16.2.7",
+ "resolved": "https://registry.npmjs.org/next/-/next-16.2.7.tgz",
+ "integrity": "sha512-eMJxgjRzBaj3olkP4cBamHDXL79A8FC6u1GcsO1D1Tsx8bw/LLXUJCaoajVxtnhD3A1IJqIT8IcRJjgBIPJq4w==",
"license": "MIT",
"dependencies": {
- "@next/env": "16.2.6",
+ "@next/env": "16.2.7",
"@swc/helpers": "0.5.15",
"baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001579",
@@ -6367,14 +6367,14 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
- "@next/swc-darwin-arm64": "16.2.6",
- "@next/swc-darwin-x64": "16.2.6",
- "@next/swc-linux-arm64-gnu": "16.2.6",
- "@next/swc-linux-arm64-musl": "16.2.6",
- "@next/swc-linux-x64-gnu": "16.2.6",
- "@next/swc-linux-x64-musl": "16.2.6",
- "@next/swc-win32-arm64-msvc": "16.2.6",
- "@next/swc-win32-x64-msvc": "16.2.6",
+ "@next/swc-darwin-arm64": "16.2.7",
+ "@next/swc-darwin-x64": "16.2.7",
+ "@next/swc-linux-arm64-gnu": "16.2.7",
+ "@next/swc-linux-arm64-musl": "16.2.7",
+ "@next/swc-linux-x64-gnu": "16.2.7",
+ "@next/swc-linux-x64-musl": "16.2.7",
+ "@next/swc-win32-arm64-msvc": "16.2.7",
+ "@next/swc-win32-x64-msvc": "16.2.7",
"sharp": "^0.34.5"
},
"peerDependencies": {
@@ -6841,24 +6841,24 @@
"license": "MIT"
},
"node_modules/react": {
- "version": "19.2.4",
- "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
- "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
+ "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
- "version": "19.2.4",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
- "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
+ "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
"license": "MIT",
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
- "react": "^19.2.4"
+ "react": "^19.2.7"
}
},
"node_modules/react-is": {
diff --git a/frontend/package.json b/frontend/package.json
index 211a060..226f661 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -11,9 +11,9 @@
"test:run": "vitest run"
},
"dependencies": {
- "next": "16.2.6",
- "react": "19.2.4",
- "react-dom": "19.2.4",
+ "next": "16.2.7",
+ "react": "19.2.7",
+ "react-dom": "19.2.7",
"recharts": "^2.15.0"
},
"devDependencies": {
@@ -25,7 +25,7 @@
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9",
- "eslint-config-next": "16.2.6",
+ "eslint-config-next": "16.2.7",
"jsdom": "^25.0.1",
"typescript": "^5",
"vitest": "^2.1.8"