diff --git a/.env.development b/.env.development deleted file mode 100644 index e8ed795..0000000 --- a/.env.development +++ /dev/null @@ -1,3 +0,0 @@ -# Relative URL: Vite proxies /api → Odoo :8069 (avoids "Failed to fetch" from CORS). -VITE_API_BASE_URL=/api -VITE_APP_NAME=EnCoach diff --git a/Dockerfile b/Dockerfile index 5545dd4..cae190b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,13 +18,25 @@ server { root /usr/share/nginx/html; index index.html; + # Default nginx body cap is 1 MB which causes HTTP 413 on: + # * resource uploads (PDF / audio / video) + # * /api/exam/generation/submit (whole exam with generated questions) + # * /api/approval-requests (attachments) + # Match Odoo's raised limit in odoo-docker.conf (128 MB). + client_max_body_size 128m; + client_body_buffer_size 1m; + location /api/ { proxy_pass http://backend:8069/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 300s; + # OpenAI streaming calls + multi-module AI generation can run >3 min. + # Keep read/send timeouts above Odoo's limit_time_real (900s). + proxy_read_timeout 900s; + proxy_send_timeout 900s; + proxy_request_buffering off; } location / { diff --git a/e2e/login.spec.ts b/e2e/login.spec.ts new file mode 100644 index 0000000..3be2f7c --- /dev/null +++ b/e2e/login.spec.ts @@ -0,0 +1,27 @@ +import { expect, test } from "@playwright/test"; + +/** + * Smoke test: the app loads, serves the login page, and the main form + * controls are present and reachable. We deliberately avoid hitting the + * real backend — this is a lightweight sanity check that catches broken + * routing / bundle / asset problems before they reach production. + */ +test("login page renders the sign-in form", async ({ page }) => { + await page.goto("/login"); + await expect( + page.getByRole("heading", { name: /sign in|login|welcome/i }), + ).toBeVisible({ timeout: 10_000 }); + + const email = page.getByLabel(/email/i); + const password = page.getByLabel(/password/i); + await expect(email).toBeVisible(); + await expect(password).toBeVisible(); + + await expect(page.getByRole("button", { name: /sign in|log in/i })).toBeVisible(); +}); + +test("root redirects to login for anonymous users", async ({ page }) => { + const response = await page.goto("/"); + expect(response?.ok()).toBeTruthy(); + await page.waitForURL(/\/login/i, { timeout: 10_000 }); +}); diff --git a/index.html b/index.html index c54f102..173935e 100644 --- a/index.html +++ b/index.html @@ -6,12 +6,18 @@
Subscription checkout will be available here.
+ +
- {!collapsed && (
-
- En
- Coach
-
+ {collapsed ? (
+
+ ) : (
+
)}
+ {t("errors.unexpectedDescription")} +
+ {this.state.error && ( +{this.state.error.message}
+ {message}
+{material.description}
+ )} +{t(subtitleKey)}
++ {t(step.descriptionKey)} +
+
-
- En
- Coach
-
+
+
{user?.name}
+{user?.name ?? t("userMenu.userFallback")}
{user?.email}
Loading alerts…
+{t("ai.loadingAlerts")}
-
{error instanceof Error ? error.message : "Could not load alerts."}
+{error instanceof Error ? error.message : t("ai.couldNotLoadAlerts")}
No AI alerts right now.
+{t("ai.noAlertsRightNow")}
-
{alert.description}
Ask me anything about the platform,
-or click a quick action above.
+{t("ai.emptyLine1")}
+{t("ai.emptyLine2")}
Something went wrong. Try again.
- ) : showResults ? ( + ) : showResults && optData ? ({s.impact} impact
-{s.suggestion}
- {s.details ?{s.details}
: null} -{optData.impact} impact
+{optData.summary}
Could not load insights.
+{t("ai.couldNotLoadInsights")}
)} {mutation.isSuccess && items.length === 0 && ( -No insights available for this view.
+{t("ai.noInsightsAvailable")}
)} {!mutation.isPending && items.length > 0 && ({item.description}
- {item.metric != null && item.value != null && ( -- {item.metric}: {item.value} + {item.recommendation && ( +
+ {item.recommendation}
)}{r.title}
-{r.description}
- {r.url && ( -{result.answer}
+{t("chrome.aiRelatedQueries")}
+ {result.suggestions.map((s, i) => ( +- No results for "{query}". Try a different question or keyword. -
+{t("chrome.aiNoResults", { q: query })}
- {isError ? (error instanceof Error ? error.message : "Could not load tip.") : "No tip available."} + {isError ? (error instanceof Error ? error.message : t("ai.couldNotLoadTip")) : t("ai.noTipAvailable")}
No tip for this context yet.
+{t("ai.noTipForContext")}
{data.content}
+{data.tip}
{mutation.data.feedback}
+{mutation.data.tips.join(" ")}
{mutation.data.improved}
+{mutation.data.improved_text}
{note}
+{c.original} → {c.revised} — {c.reason}
No grammar issues flagged.
)} - {mutation.data.feedback ? ( -{mutation.data.feedback}
+ {mutation.data.tips?.length > 0 ? ( +{mutation.data.tips.join("; ")}
) : null}
{mutation.data.feedback}
- {mutation.data.improved ? ( -{mutation.data.improved}
+{mutation.data.tips?.join(" ") ?? ""}
+ {mutation.data.improved_text ? ( +{mutation.data.improved_text}
) : null}{String(value)}
; + } + if (Array.isArray(value)) { + if (value.length === 0) return null; + return ( ++ {material.body_text || "No content available yet."} +
+ )} +{t(subtitleKey)}
} +{t(current.descriptionKey)}
+ )} +