commit 77c4f1ccfe2dcf737b877c50acc759c66dc6d0c5 Author: Flatlogic Bot Date: Wed Mar 25 14:10:36 2026 +0000 Initial version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e427ff3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +*/node_modules/ +*/build/ diff --git a/.perm_test_apache b/.perm_test_apache new file mode 100644 index 0000000..e69de29 diff --git a/.perm_test_exec b/.perm_test_exec new file mode 100644 index 0000000..e69de29 diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000..0cf1ba0 --- /dev/null +++ b/backend/.env @@ -0,0 +1,14 @@ +DB_NAME=app_39312 +DB_USER=app_39312 +DB_PASS=1203727f-8ee6-4a3d-8ce4-f332d1a4a2f3 +DB_HOST=127.0.0.1 +DB_PORT=5432 +PORT=3000 +GOOGLE_CLIENT_ID=671001533244-kf1k1gmp6mnl0r030qmvdu6v36ghmim6.apps.googleusercontent.com +GOOGLE_CLIENT_SECRET=Yo4qbKZniqvojzUQ60iKlxqR +MS_CLIENT_ID=4696f457-31af-40de-897c-e00d7d4cff73 +MS_CLIENT_SECRET=m8jzZ.5UpHF3=-dXzyxiZ4e[F8OF54@p +EMAIL_USER=AKIAVEW7G4PQUBGM52OF +EMAIL_PASS=BLnD4hKGb6YkSz3gaQrf8fnyLi3C3/EdjOOsLEDTDPTz +SECRET_KEY=HUEyqESqgQ1yTwzVlO6wprC9Kf1J1xuA +PEXELS_KEY=Vc99rnmOhHhJAbgGQoKLZtsaIVfkeownoQNbTj78VemUjKh08ZYRbf18 diff --git a/backend/src/ai/LocalAIApi.js b/backend/src/ai/LocalAIApi.js new file mode 100644 index 0000000..fd571ae --- /dev/null +++ b/backend/src/ai/LocalAIApi.js @@ -0,0 +1,484 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); +const http = require("http"); +const https = require("https"); +const { URL } = require("url"); + +let CONFIG_CACHE = null; + +class LocalAIApi { + static createResponse(params, options) { + return createResponse(params, options); + } + + static request(pathValue, payload, options) { + return request(pathValue, payload, options); + } + + static fetchStatus(aiRequestId, options) { + return fetchStatus(aiRequestId, options); + } + + static awaitResponse(aiRequestId, options) { + return awaitResponse(aiRequestId, options); + } + + static extractText(response) { + return extractText(response); + } + + static decodeJsonFromResponse(response) { + return decodeJsonFromResponse(response); + } +} + +async function createResponse(params, options = {}) { + const payload = { ...(params || {}) }; + + if (!Array.isArray(payload.input) || payload.input.length === 0) { + return { + success: false, + error: "input_missing", + message: 'Parameter "input" is required and must be a non-empty array.', + }; + } + + const cfg = config(); + if (!payload.model) { + payload.model = cfg.defaultModel; + } + + const initial = await request(options.path, payload, options); + if (!initial.success) { + return initial; + } + + const data = initial.data; + if (data && typeof data === "object" && data.ai_request_id) { + const pollTimeout = Number(options.poll_timeout ?? 300); + const pollInterval = Number(options.poll_interval ?? 5); + return await awaitResponse(data.ai_request_id, { + interval: pollInterval, + timeout: pollTimeout, + headers: options.headers, + timeout_per_call: options.timeout, + verify_tls: options.verify_tls, + }); + } + + return initial; +} + +async function request(pathValue, payload = {}, options = {}) { + const cfg = config(); + const resolvedPath = pathValue || options.path || cfg.responsesPath; + + if (!resolvedPath) { + return { + success: false, + error: "project_id_missing", + message: "PROJECT_ID is not defined; cannot resolve AI proxy endpoint.", + }; + } + + if (!cfg.projectUuid) { + return { + success: false, + error: "project_uuid_missing", + message: "PROJECT_UUID is not defined; aborting AI request.", + }; + } + + const bodyPayload = { ...(payload || {}) }; + if (!bodyPayload.project_uuid) { + bodyPayload.project_uuid = cfg.projectUuid; + } + + const url = buildUrl(resolvedPath, cfg.baseUrl); + const timeout = resolveTimeout(options.timeout, cfg.timeout); + const verifyTls = resolveVerifyTls(options.verify_tls, cfg.verifyTls); + + const headers = { + Accept: "application/json", + "Content-Type": "application/json", + [cfg.projectHeader]: cfg.projectUuid, + }; + if (Array.isArray(options.headers)) { + for (const header of options.headers) { + if (typeof header === "string" && header.includes(":")) { + const [name, value] = header.split(":", 2); + headers[name.trim()] = value.trim(); + } + } + } + + const body = JSON.stringify(bodyPayload); + return sendRequest(url, "POST", body, headers, timeout, verifyTls); +} + +async function fetchStatus(aiRequestId, options = {}) { + const cfg = config(); + if (!cfg.projectUuid) { + return { + success: false, + error: "project_uuid_missing", + message: "PROJECT_UUID is not defined; aborting status check.", + }; + } + + const statusPath = resolveStatusPath(aiRequestId, cfg); + const url = buildUrl(statusPath, cfg.baseUrl); + const timeout = resolveTimeout(options.timeout, cfg.timeout); + const verifyTls = resolveVerifyTls(options.verify_tls, cfg.verifyTls); + + const headers = { + Accept: "application/json", + [cfg.projectHeader]: cfg.projectUuid, + }; + if (Array.isArray(options.headers)) { + for (const header of options.headers) { + if (typeof header === "string" && header.includes(":")) { + const [name, value] = header.split(":", 2); + headers[name.trim()] = value.trim(); + } + } + } + + return sendRequest(url, "GET", null, headers, timeout, verifyTls); +} + +async function awaitResponse(aiRequestId, options = {}) { + const timeout = Number(options.timeout ?? 300); + const interval = Math.max(Number(options.interval ?? 5), 1); + const deadline = Date.now() + Math.max(timeout, interval) * 1000; + + while (true) { + const statusResp = await fetchStatus(aiRequestId, { + headers: options.headers, + timeout: options.timeout_per_call, + verify_tls: options.verify_tls, + }); + + if (statusResp.success) { + const data = statusResp.data || {}; + if (data && typeof data === "object") { + if (data.status === "success") { + return { + success: true, + status: 200, + data: data.response || data, + }; + } + if (data.status === "failed") { + return { + success: false, + status: 500, + error: String(data.error || "AI request failed"), + data, + }; + } + } + } else { + return statusResp; + } + + if (Date.now() >= deadline) { + return { + success: false, + error: "timeout", + message: "Timed out waiting for AI response.", + }; + } + + await sleep(interval * 1000); + } +} + +function extractText(response) { + const payload = response && typeof response === "object" ? response.data || response : null; + if (!payload || typeof payload !== "object") { + return ""; + } + + if (Array.isArray(payload.output)) { + let combined = ""; + for (const item of payload.output) { + if (!item || !Array.isArray(item.content)) { + continue; + } + for (const block of item.content) { + if ( + block && + typeof block === "object" && + block.type === "output_text" && + typeof block.text === "string" && + block.text.length > 0 + ) { + combined += block.text; + } + } + } + if (combined) { + return combined; + } + } + + if ( + payload.choices && + payload.choices[0] && + payload.choices[0].message && + typeof payload.choices[0].message.content === "string" + ) { + return payload.choices[0].message.content; + } + + return ""; +} + +function decodeJsonFromResponse(response) { + const text = extractText(response); + if (!text) { + throw new Error("No text found in AI response."); + } + + const parsed = parseJson(text); + if (parsed.ok && parsed.value && typeof parsed.value === "object") { + return parsed.value; + } + + const stripped = stripJsonFence(text); + if (stripped !== text) { + const parsedStripped = parseJson(stripped); + if (parsedStripped.ok && parsedStripped.value && typeof parsedStripped.value === "object") { + return parsedStripped.value; + } + throw new Error(`JSON parse failed after stripping fences: ${parsedStripped.error}`); + } + + throw new Error(`JSON parse failed: ${parsed.error}`); +} + +function config() { + if (CONFIG_CACHE) { + return CONFIG_CACHE; + } + + ensureEnvLoaded(); + + const baseUrl = process.env.AI_PROXY_BASE_URL || "https://flatlogic.com"; + const projectId = process.env.PROJECT_ID || null; + let responsesPath = process.env.AI_RESPONSES_PATH || null; + if (!responsesPath && projectId) { + responsesPath = `/projects/${projectId}/ai-request`; + } + + const timeout = resolveTimeout(process.env.AI_TIMEOUT, 30); + const verifyTls = resolveVerifyTls(process.env.AI_VERIFY_TLS, true); + + CONFIG_CACHE = { + baseUrl, + responsesPath, + projectId, + projectUuid: process.env.PROJECT_UUID || null, + projectHeader: process.env.AI_PROJECT_HEADER || "project-uuid", + defaultModel: process.env.AI_DEFAULT_MODEL || "gpt-5-mini", + timeout, + verifyTls, + }; + + return CONFIG_CACHE; +} + +function buildUrl(pathValue, baseUrl) { + const trimmed = String(pathValue || "").trim(); + if (trimmed === "") { + return baseUrl; + } + if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) { + return trimmed; + } + if (trimmed.startsWith("/")) { + return `${baseUrl}${trimmed}`; + } + return `${baseUrl}/${trimmed}`; +} + +function resolveStatusPath(aiRequestId, cfg) { + const basePath = (cfg.responsesPath || "").replace(/\/+$/, ""); + if (!basePath) { + return `/ai-request/${encodeURIComponent(String(aiRequestId))}/status`; + } + const normalized = basePath.endsWith("/ai-request") ? basePath : `${basePath}/ai-request`; + return `${normalized}/${encodeURIComponent(String(aiRequestId))}/status`; +} + +function sendRequest(urlString, method, body, headers, timeoutSeconds, verifyTls) { + return new Promise((resolve) => { + let targetUrl; + try { + targetUrl = new URL(urlString); + } catch (err) { + resolve({ + success: false, + error: "invalid_url", + message: err.message, + }); + return; + } + + const isHttps = targetUrl.protocol === "https:"; + const requestFn = isHttps ? https.request : http.request; + const options = { + protocol: targetUrl.protocol, + hostname: targetUrl.hostname, + port: targetUrl.port || (isHttps ? 443 : 80), + path: `${targetUrl.pathname}${targetUrl.search}`, + method: method.toUpperCase(), + headers, + timeout: Math.max(Number(timeoutSeconds || 30), 1) * 1000, + }; + if (isHttps) { + options.rejectUnauthorized = Boolean(verifyTls); + } + + const req = requestFn(options, (res) => { + let responseBody = ""; + res.setEncoding("utf8"); + res.on("data", (chunk) => { + responseBody += chunk; + }); + res.on("end", () => { + const status = res.statusCode || 0; + const parsed = parseJson(responseBody); + const payload = parsed.ok ? parsed.value : responseBody; + + if (status >= 200 && status < 300) { + const result = { + success: true, + status, + data: payload, + }; + if (!parsed.ok) { + result.json_error = parsed.error; + } + resolve(result); + return; + } + + const errorMessage = + parsed.ok && payload && typeof payload === "object" + ? String(payload.error || payload.message || "AI proxy request failed") + : String(responseBody || "AI proxy request failed"); + + resolve({ + success: false, + status, + error: errorMessage, + response: payload, + json_error: parsed.ok ? undefined : parsed.error, + }); + }); + }); + + req.on("timeout", () => { + req.destroy(new Error("request_timeout")); + }); + + req.on("error", (err) => { + resolve({ + success: false, + error: "request_failed", + message: err.message, + }); + }); + + if (body) { + req.write(body); + } + req.end(); + }); +} + +function parseJson(value) { + if (typeof value !== "string" || value.trim() === "") { + return { ok: false, error: "empty_response" }; + } + try { + return { ok: true, value: JSON.parse(value) }; + } catch (err) { + return { ok: false, error: err.message }; + } +} + +function stripJsonFence(text) { + const trimmed = text.trim(); + if (trimmed.startsWith("```json")) { + return trimmed.replace(/^```json/, "").replace(/```$/, "").trim(); + } + if (trimmed.startsWith("```")) { + return trimmed.replace(/^```/, "").replace(/```$/, "").trim(); + } + return text; +} + +function resolveTimeout(value, fallback) { + const parsed = Number.parseInt(String(value ?? fallback), 10); + return Number.isNaN(parsed) ? Number(fallback) : parsed; +} + +function resolveVerifyTls(value, fallback) { + if (value === undefined || value === null) { + return Boolean(fallback); + } + return String(value).toLowerCase() !== "false" && String(value) !== "0"; +} + +function ensureEnvLoaded() { + if (process.env.PROJECT_UUID && process.env.PROJECT_ID) { + return; + } + + const envPath = path.resolve(__dirname, "../../../../.env"); + if (!fs.existsSync(envPath)) { + return; + } + + let content; + try { + content = fs.readFileSync(envPath, "utf8"); + } catch (err) { + throw new Error(`Failed to read executor .env: ${err.message}`); + } + + for (const line of content.split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) { + continue; + } + const [rawKey, ...rest] = trimmed.split("="); + const key = rawKey.trim(); + if (!key) { + continue; + } + const value = rest.join("=").trim().replace(/^['"]|['"]$/g, ""); + if (!process.env[key]) { + process.env[key] = value; + } + } +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +module.exports = { + LocalAIApi, + createResponse, + request, + fetchStatus, + awaitResponse, + extractText, + decodeJsonFromResponse, +}; diff --git a/frontend/.env.local b/frontend/.env.local new file mode 100644 index 0000000..13b664b --- /dev/null +++ b/frontend/.env.local @@ -0,0 +1,2 @@ +NEXT_PUBLIC_BACK_API=/api +NEXT_PUBLIC_TINY_KEY=cnslp6h943xbg36t2tf2xglmrxiw5b7tatycf3kir7n2j7eh diff --git a/gemini.md b/gemini.md new file mode 100644 index 0000000..b0e349f --- /dev/null +++ b/gemini.md @@ -0,0 +1,352 @@ +# Gemini — Working Instructions (Flatlogic SAAS VM) + +## BEFORE WRITING ANY NEW CODE + +- Read this file and understand the project structure. +- Follow existing patterns and model after current code. This is a comprehensive CRUD app with advanced roles/permissions/API and many ready-to-use components, so extend what exists instead of re-inventing it. +- Assume built-in features already exist and must be reused: auth (sign in/up, password reset), authorization/RBAC, CRUD for entities, list/table/card/calendar views, filters/sorting, CSV import/export, file/image uploads, charts/widgets from plain English, email notifications, multitenancy, API, static website, responsive UI, component kit. Verify in code before adding new. + +--- + +You are **Gemini CLI** running inside a **Flatlogic Dev VM** spawned from the **SAAS golden image** (Next.js + Node + Postgres). +Your job is to **edit files in-place** under the current working directory and keep the app running and browsable. +The app is running in **development mode** with hot-reloading, so most code changes take effect immediately without restarts. + +--- + +## Where you are (facts) + +- **CWD / Project root:** this folder (the Flatlogic `WORKSPACE_ROOT`, usually `/home/ubuntu/executor/workspace`). +- **Frontend:** `./frontend` — **Next.js 15** + **Tailwind**, a running dev server managed by **PM2** (`frontend-dev`), DO NOT restart manually unless instructed. +- **Backend:** `./backend` — **Node/Express** + **Sequelize**, a running dev server managed by **PM2** (`backend-dev`), DO NOT restart manually unless instructed. +- **Database:** local **PostgreSQL** on `127.0.0.1:5432`. + +### Env files (platform-managed) + +- **Executor env:** `/home/ubuntu/executor/.env` (never touch executor at all - it runs you). +- **Backend env:** `./backend/.env` (DB + OAuth + email + secret keys). +- **Frontend env:** `./frontend/.env.local` (must include `NEXT_PUBLIC_BACK_API=/api` and `NEXT_PUBLIC_TINY_KEY`). + +--- + +## User Profile and Education Logic + +You will receive contextual information about the user (e.g., programming experience, role, app goals) gathered during their onboarding survey. + +Always consider this contextual information carefully and tailor your responses accordingly: + +### For users with less than 1 year of programming experience: +- Act proactively as a mentor and educator. Explain how their requests translate into technical tasks. If needed, explain basic concepts. +- When a user requests something technically challenging (which the user themselves may not even realize): + - Gently clarify and explain essential concepts (API, databases, code, data sources, architecture, how it works, etc). + - Transparently explain complexity and implementation steps, managing expectations honestly. + - Suggest practical, incremental solutions and timelines clearly. + +### For users with more experience: +- Assume familiarity with programming fundamentals. +- Keep explanations concise unless explicitly asked for clarification or education. + +--- + +## What to do (capabilities & constraints) + +- **THE MOST IMPORTANT: YOU ARE OPERATING IN A LIVE DEV ENVIRONMENT.** Every code change you make should be executable immediately without breaking the app. +- **Edit files directly** in this folder tree. Prefer small, safe changes. +- **Stay within the project root.** Do not write outside this directory. +- **Keep the app immediately browsable.** `/` and `/api` must stay responsive. +- **Never swallow errors.** If something fails, surface the error clearly and debug. +- **Keep ports stable.** Frontend must run on `${FRONT_PORT}` (default 3001), backend on `3000`. +- **Do not modify `node_modules`** or delete generated assets unless explicitly asked. +- **Work in small, verifiable chunks.** For multi-step changes, outline a short plan, wait for confirmation, then execute. +- Keep the user informed. Before making changes, briefly state what you plan to do and which files you expect to touch. During longer tasks, share concise progress updates. After each step, summarize what changed and what the next optional step could be. +- **Post-change checks (required)**: + - Run `npm run lint` in `/workspace/frontend`. If backend code changed, also run `npm run lint` in `/workspace/backend`. + - Fix lint **errors** and rerun until clean. Ignore warnings. + - Then check runtime error logs by reading the files (no `pm2 logs`): + - `~/.pm2/logs/frontend-dev-error.log` + - `~/.pm2/logs/backend-dev-error.log` + - If logs show **no errors**, clear both files (truncate) so the next check is clean. + +## Frontend (Next.js) + +- **Component imports (strict)** + - Never import a component unless you have verified that the file exists under `frontend/src/components` (or a subfolder). + - Do not invent component names or paths. Reuse existing components; if a new one is required, make sure something similar does not already exist. +- **Redux/config imports (strict)** + - Before importing thunks/selectors/constants from a slice/config, open the file and use only the actual exported names. Do not invent names like `getCourses` or `PER_PAGE`. + - If an export is missing, stop and decide whether to reuse another exported function or ask before adding new code. +- **Frontend change checklist (required)** + 1. Locate the target file in `frontend/src` and confirm the component or module already exists. + 2. Open the file that defines the component and verify how it is exported (default vs named). + 3. Mirror existing imports in nearby files; do not guess import paths. + 4. Before adding a new component, search for an existing one that fits the need and reuse it if possible. + 5. After edits, verify the module still compiles by checking for obvious import or type errors. +- **Change execution (required)** + - Break work into small, explicit steps and execute them one-by-one. + - If a change has multiple steps, outline the steps first and wait for user confirmation before proceeding. + +- App root: `./frontend` +- Router: **Pages Router** under `./frontend/src/pages` (no App Router). Global setup is in `./frontend/src/pages/_app.tsx`. +- API base: + - `_app.tsx` sets `axios.defaults.baseURL` to `NEXT_PUBLIC_BACK_API` or `baseURLApi` from `src/config.ts`. + - In this VM, `NEXT_PUBLIC_BACK_API=/api`, so axios uses `/api`. Keep all calls relative (`/auth`, `/users`, `/file`, etc). + - Do not prefix requests with `/api`. `axios.get('/users')` is correct; `axios.get('/api/users')` becomes `/api/api/users` and fails. + - This means all frontend requests go through the same-origin `/api` proxy (Apache -> backend). Do not hardcode backend host/port. +- Auth flow (code): + - `stores/authSlice.ts` calls `POST /auth/signin/local` and stores the JWT in `localStorage` as `token` (and user payload as `user`). + - `_app.tsx` attaches `Authorization: Bearer ` on every request via axios interceptor. + - `GET /auth/me` hydrates `currentUser`. + - `logoutUser` clears localStorage and axios auth header. +- Layouts and navigation: + - Guest layout: `./frontend/src/layouts/Guest` (login/register). + - Authenticated layout: `./frontend/src/layouts/Authenticated`. + - Sidebar entries live in `./frontend/src/menuAside.ts`; add/adjust items there to expose new pages. +- Entities and CRUD: + - Entity pages are under `./frontend/src/pages/` and use Redux slices in `./frontend/src/stores/`. + - Lists/tables/kanban UIs use components in `./frontend/src/components`. +- Uploads and media: + - `UploadService` uses `/file` and `/file/download`. + - `helpers/pexels.ts` hits `/pexels/*` to fetch login/landing imagery. +- Multitenancy UI (when enabled): + - `register.tsx` loads `/org-for-auth` and sends `organizationId` on signup. + - Search page adds `organizationId` in requests. +- Styling: + - Tailwind + global CSS in `./frontend/src/css/main.css`. + - Theme tokens live in `./frontend/src/styles.ts` and `./frontend/src/colors.ts`. + - Theme state is handled by `stores/styleSlice.ts`. + - Tailwind config is ERB-generated at build time (`./frontend/tailwind.config.js`). + - Custom palette: `colors.pavitra` (blue/green/orange/red + 900..300) and `colors.dark` + `green.text`. + - Theme-specific colors are injected as `.*` and `primaryText` based on the schema theme. + - Theme fonts are also injected here (e.g., Ubuntu/Poppins/Nunito Sans). + - Custom utilities: `aside-scrollbars` plugin, `zIndex[-1]`, `flexGrow[5]`, `maxHeight.screen-menu/modal`, `transitionProperty.position/textColor`, `fade-in/fade-out` animations, `borderRadius.3xl`. + - This is a build-time theme injection (not runtime). Changing theme tokens requires a rebuild/regeneration. + - Imports (avoid default/named mismatch): + - Most shared components use **default exports** (e.g., `CardBox`, `SectionTitle`, `LayoutAuthenticated`). + - Correct form: `import CardBox from '../components/CardBox'` (no `{}`). + - Using named import (`import { CardBox } ...`) causes runtime errors like `ReferenceError: CardBox is not defined` or missing export errors. + +--- + +## Backend (Node/Express + Sequelize) + +- App root: `./backend` +- Entry point: `./backend/src/index.js` + - Express + CORS + bodyParser + Passport JWT. + - Registers `/api/auth`, `/api/file`, `/api/pexels`, `/api/search`, `/api/sql`, and `/api/` routes. + - `/api/sql` is a read-only helper: `POST /api/sql` with JSON `{ "sql": "SELECT ..." }`, JWT required, one SELECT statement, returns `{ rows }`. + - Swagger UI is exposed at `/api-docs`. +- Auth: + - `routes/auth.js` handles login/signup/password reset/email verification. + - `passport.authenticate('jwt')` protects entity routes; `auth/me` uses `req.currentUser`. + - Errors are surfaced via `helpers.commonErrorHandler` (400/403/404 pass-through; otherwise 500 with console error). +- Sequelize + data layer: + - Models: `./backend/src/db/models` + - Migrations: `./backend/src/db/migrations` + - Seeders: `./backend/src/db/seeders` + - DB APIs: `./backend/src/db/api` (filters, pagination, CSV export) + - Services: `./backend/src/services` +- Start behavior: + - `npm run start` runs `db:migrate`, `db:seed`, then `watcher.js` (nodemon + auto-run migrations/seeders when new files appear). +- Ports and env: + - DB credentials come from `backend/.env` (`DB_NAME`, `DB_USER`, `DB_PASS`, `DB_HOST`, `DB_PORT`). +- CommonJS import rule (avoid "is not a function"/undefined errors): + - `module.exports = Foo` -> `const Foo = require('./foo');` + - `module.exports = { foo }` or `exports.foo = ...` -> `const { foo } = require('./foo');` + - Example: `backend/src/helpers.js` exports `Helpers` via `module.exports = class Helpers`, so import with `const Helpers = require('../helpers');` and call `Helpers.wrapAsync(...)`. +- Model naming and imports (Sequelize): + - Do not assume naming conventions. Always check the actual model name in its definition and the exact export key. + - Auto-loaders expose model names exactly as defined, including case and pluralization. + - Before importing, search for existing usage in the codebase and follow the same pattern. + - If a model is undefined at runtime, stop and verify the export before changing code, then ask for confirmation. +- Default credentials: + - `backend/src/config.js` defines `admin_email` (`admin@flatlogic.com`) and passwords derived from project UUID. + - Login page shows these defaults; keep them aligned with backend config. +- Multitenancy (when enabled): + - `GET /api/org-for-auth` returns organizations for signup. + - `app_role.globalAccess` decides cross-tenant visibility. + - DB APIs filter by `currentUser.organizationId` unless `globalAccess` is true. + +--- + +## Database usage policy (Postgres) + +- Use Sequelize models/migrations (`backend/src/db/*`). +- Prefer idempotent migrations and keep changes scoped to the task. +- Do not drop or truncate tables or database unless explicitly asked. + +--- + +## SQL helper endpoint (read-only data access) + +- Use `POST /api/sql` to fetch data directly from the DB when you need raw results. +- Request body: `{ "sql": "SELECT ..." }`. +- JWT required, one SELECT statement only, returns `{ rows }`. +- Use it for ad-hoc data retrieval or debugging, not for writes. + +--- + +## AI client (use the platform AI proxy). When user requests AI features: +For example when user requests features like Chat, Content Generation, AI-assisted data-analysis, etc. wherever LLMs are needed, +USE the existing AI-endpoint. + +- Always use `backend/src/ai/LocalAIApi.js` for AI calls; it injects `project_uuid` and the `project-uuid` header and **aborts** if `PROJECT_UUID` is missing. +- The backend is asynchronous: `POST /projects/:id/ai-request` returns `ai_request_id`; status is at `GET /projects/:id/ai-request/:ai_request_id/status`. +- `LocalAIApi.createResponse` polls automatically (default every 5s up to 5 minutes). Tune with `poll_interval` / `poll_timeout`. +- Config is read from env (or executor `.env` via the helper): `AI_PROXY_BASE_URL`, `AI_RESPONSES_PATH`, `AI_PROJECT_HEADER`, `AI_DEFAULT_MODEL`, `AI_TIMEOUT`, `AI_VERIFY_TLS`. Do **not** override defaults unless explicitly required. +- Do not call upstream model APIs directly (no SDK/raw HTTP) — always go through `LocalAIApi`. + +## AI integration guardrails (strict) + +- Use the existing endpoints: `POST /api/ai/response` and its alias `POST /api/openai/response`. Do not create new AI routes unless explicitly asked. +- Do not remove, rename, or merge `openai.js`/`ai.js`, and do not change `backend/src/index.js` route wiring unless explicitly requested. +- Avoid "cleanup" refactors (restructure, consolidate routes, rename files) without a direct user request. + +Example (safe extraction with explicit error handling): +```js +const { LocalAIApi } = require("./ai/LocalAIApi"); + +const resp = await LocalAIApi.createResponse( + { + input: [ + { role: "system", content: "You are an editorial assistant." }, + { role: "user", content: "Summarize this conversation in two sentences." }, + ], + }, + { poll_interval: 5, poll_timeout: 300 } +); + +if (resp.success) { + let text = LocalAIApi.extractText(resp); + if (!text) { + try { + const decoded = LocalAIApi.decodeJsonFromResponse(resp); + text = JSON.stringify(decoded); + } catch (err) { + console.error("AI JSON decode failed:", err); + } + } + // use text +} else { + console.error("AI error:", resp.error || resp.message || resp); +} +``` + +### Troubleshooting AI chat issues + +- If the proxy returns an error (4xx/5xx), **log the exact payload** you sent and the full error response. Do not guess. +- Validate payload structure (Responses API requires `input` as a list of `{role, content}` objects). +- After fixing the serializer, clear any persisted chat history so stale malformed data is not reused. + +--- + +## Frontend AI flow (through Redux + backend proxy) + +**Always** call AI from the frontend through the Redux slice, not directly from the browser: + +1) `frontend/src/stores/openAiSlice.ts` dispatches a thunk. +2) Thunk calls `/api/ai/response` (same as `/api/openai/response`). +3) Backend routes through `backend/src/ai/LocalAIApi.js` to the Flatlogic proxy. +4) Proxy returns a Responses payload; backend forwards it to the frontend. + +### Do not copy backend logic to frontend + +- Never re-implement `LocalAIApi` helpers (like `extractText`) in the frontend. +- Do not change backend AI response format (e.g., `{ text: "..." }`) unless explicitly asked. +- Keep the existing routing and payload shape; use `openAiSlice` and `aiResponse` as-is. + +### AI response format (do not guess) + +- The backend returns a **Responses API** object (not `choices[]` like Chat Completions). +- The assistant text is inside: `data.output[]` → item with `type: "message"` → `content[]` → item with `type: "output_text"` → `text`. +- If the UI shows raw JSON, **do not invent parsing paths**. Use the known structure above or ask before changing. + +### Required endpoint + +- `POST /api/ai/response` (JWT required) +- Body: + ```json + { + "input": [ + { "role": "system", "content": "You are a concise assistant." }, + { "role": "user", "content": "Summarize this conversation." } + ], + "options": { "poll_interval": 5, "poll_timeout": 300 } + } + ``` + +### Redux usage (frontend) + +Use the slice method `aiResponse`: +```ts +dispatch(aiResponse({ + input: [ + { role: 'system', content: 'You are a concise assistant.' }, + { role: 'user', content: prompt }, + ], + options: { poll_interval: 5, poll_timeout: 300 }, +})); +``` + +State fields: +- `openAi.aiResponse`: raw proxy response payload. +- `openAi.isAskingResponse`: loading flag. +- `openAi.errorMessage`: filled on error. + +### Error handling rules + +- Never swallow errors. If the thunk rejects, surface a user‑facing error and log the payload. +- If the proxy returns `input_missing` or a 4xx/5xx, log the full payload you sent and the response object. +- Do not send direct OpenAI SDK requests from the frontend or backend. + +--- + +## Dev server behavior (Next.js) + +- Do not restart `frontend-dev` or `backend-dev` unless the user explicitly asks. +- Do not install or update dependencies, and do not remove `.next` or `node_modules` unless the user explicitly asks. +- If installing or updating dependencies is required, ask the user for approval before running package manager commands. + +## Log handling rules (strict) + +- Read logs only when the user explicitly asks, or to validate a just-made change. +- You are allowed to read these log files directly (outside WORKSPACE_ROOT): + - `/home/ubuntu/.pm2/logs/backend-dev-error.log` (primary) + - `/home/ubuntu/.pm2/logs/frontend-dev-error.log` (only when the user explicitly asks for frontend logs) +- Do not copy logs into the workspace; read them in place. +- Before making code changes, truncate the backend error log to create a clean baseline: + - `/home/ubuntu/.pm2/logs/backend-dev-error.log` + - If the user explicitly asks to check frontend logs, also truncate `/home/ubuntu/.pm2/logs/frontend-dev-error.log` +- Use non-interactive `pm2 logs --lines N` only if broader context is needed (never `-f`). +- Never use interactive/streaming commands (no `tail -f`, no `pm2 logs -f`). +- Read a bounded chunk only (for example, the last 50–100 lines) and stop. +- Only consider errors that are newer than the last change time; ignore older repeats. +- If logs are noisy, show a short snippet (5–20 lines) and ask before taking action. +- Do not restart services or delete caches while checking logs unless the user explicitly asks. + +## Post-change sanity check (required) + +- After code changes, wait 5–10 seconds for hot reload to finish. +- If frontend files changed, run `npm run lint` in `./frontend` (uses the project’s ESLint config for critical errors). +- When reviewing `tsc` output, ignore these error codes: `TS2322`, `TS2367`, `TS2769`, `TS2339`, `TS2741`. +- Also ignore errors that match: `Property 'children' is missing`. +- Report only errors outside that list; do not attempt fixes unless the user asks. +- If backend files changed, do a quick, non-interactive check of `/home/ubuntu/.pm2/logs/backend-dev-error.log`. + +--- + +## Safety & Boundaries + +- Scope: operate strictly inside `WORKSPACE_ROOT` (this directory). +- Dotfiles: never read, write, rename, or delete hidden files or folders (`**/.*`), including `.env*`, unless user explicitly requests it. +- Exception: you may read and truncate `/home/ubuntu/.pm2/logs/frontend-dev-error.log` and `/home/ubuntu/.pm2/logs/backend-dev-error.log`, and run non-interactive `pm2 logs --lines N` when explicitly checking logs. +- System/service folders: do not modify `.git/**`, `.gemini/**`, `node_modules/**`, `vendor/**`. +- Shell commands: use only for safe, read-only inspection within this directory, except for reading/truncating the two error logs listed above or reading non-interactive `pm2 logs` when needed. +- Do not restart services unless the user explicitly asks. + +--- + +## BEFORE WRITING ANY NEW CODE + +- Read this file and understand the project structure. +- Follow existing patterns and model after current code. This is a comprehensive CRUD app with advanced roles/permissions/API and many ready-to-use components, so extend what exists instead of re-inventing it. +- Assume built-in features already exist and must be reused: auth (sign in/up, password reset), authorization/RBAC, CRUD for entities, list/table/card/calendar views, filters/sorting, CSV import/export, file/image uploads, charts/widgets from plain English, email notifications, multitenancy, API, static website, responsive UI, component kit. Verify in code before adding new. diff --git a/recipes.md b/recipes.md new file mode 100644 index 0000000..f18d273 --- /dev/null +++ b/recipes.md @@ -0,0 +1,702 @@ +# General instructions: + +- Follow existing patterns and project instructions. +- Do not silently swallow errors. If you handle an error, log it and rethrow. +- Avoid unnecessary defensive checks that hide bugs. + +# How to find the current user + +1. Import hooks (if they are not already imported): + import { useAppDispatch, useAppSelector } from '../stores/hooks'; +2. Get the current user: + const { currentUser } \= useAppSelector((state) \=\> state.auth); + + Make sure that the useAppSelector call to retrieve currentUser is placed inside your component function so that it has access to the component's lifecycle and state. +3. Structure of currentUser: + id + firstName + lastName + phoneNumber + email + disabled + emailVerified + provider + Other fields for account management. +4. Use the data: + Access the user information via the properties of currentUser, for example, to display or validate. + +# How to change sidebar styles + +1. Open frontend/src/styles.ts +2. Locate the "white" object with sidebar styles (e.g., aside, asideMenuItem, asideMenuItemActive). +3. Modify the desired parameters (colors, fonts, etc.) + +# Modifying sidebar elements (hiding or removing items) + +1. Open file /frontend/src/menuAside.ts. +2. Edit or remove the corresponding objects in the menuAside array to achieve the desired changes. + +# Remove the Tutorial from the App + +1. In the file frontend/src/pages/\_app.tsx, delete the tutorial-related imports: +2. Remove the import for 'intro.js/introjs.css'. +3. Remove the import for the IntroGuide component. +4. Remove the imports of the tutorial steps arrays (e.g., appSteps, landingSteps, loginSteps, usersSteps, rolesSteps). +5. Remove the state and effect handling for the tutorial: +6. Delete the React.useState hooks for stepsEnabled, stepName, and steps. +7. Delete the React.useEffect that checks router.pathname and uses localStorage to determine which tutorial steps to set. +8. Remove the IntroGuide component from the JSX return. +9. In the return block, delete the \ element. +10. Optionally, remove any related CSS or assets if they’re used exclusively for the tutorial. + + +# How to Globally Change the Corner Radius of the Application + +1. Locate the Styles File: Open the main styles file where global styles are defined, typically found at: frontend/src/styles.ts + +2. Modify the Tailwind Corner Radius Parameter: Locate the parameter corners within the file. This parameter controls the corner radius for the application using Tailwind CSS classes. +3. Change its value to your desired Tailwind CSS corner radius class. For example: + const corners \= 'rounded-lg'; // Change to your desired Tailwind class (e.g., 'rounded') + +# Change Application Background Color + +1. Open frontend/src/styles.ts. +2. Locate the style object for the theme you wish to modify (e.g., the "white" theme). +3. Find the bgLayoutColor parameter. It may look like: bgLayoutColor: 'bg-gray-50', +4. Update the value to your desired Tailwind CSS background color class (or custom CSS class). For example: bgLayoutColor: 'bg-blue-100', +5. Save the changes and refresh your application to see the new background color applied. + +# How to Modify Table Columns + +1. Locate the Configuration File: Find the relevant configuration file for the entity you are working with. The path will typically be structured as follows: + frontend/src/components/\[EntityName\]/configure\[EntityName\]Cols.tsx + Where \[EntityName\] is the name of the entity you are modifying (e.g., Orders, Users). +2. Edit Column Definitions: Open the found file and locate the column definitions. Here, you can modify the columns based on user requests. For example: + To remove the supplier column: Delete or comment out the supplier column definition from the array. + To replace the location column with a user column\*\*: Modify the existing location column to reflect the user performing the quantity logs instead. + +# Change Global Font + +1. Open frontend/tailwind.config.js. +2. In the theme.extend block, add a fontFamily key. + For example: fontFamily: { sans: \['YourCustomFont', 'sans-serif'\] } + + +# Modifying front-end routing + +For modifying front-end routing, edit the file "frontend/src/menuAside.ts" to update the routes. + +# Create new custom page on frontend + +1. Create the page file at: frontend/src/pages/{PageName}.tsx +2. Use the following basic structure for your page: + import { mdiChartTimelineVariant } from '@mdi/js'; + import Head from 'next/head'; + import React, { ReactElement } from 'react'; + import CardBox from '../components/CardBox'; + import LayoutAuthenticated from '../layouts/Authenticated'; + import SectionMain from '../components/SectionMain'; + import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'; + import { getPageTitle } from '../config'; + + const PageName= () \=\> { + return (\<\>\\{getPageTitle('Edit profile')}\\\\ {''}\\{Content goes here}\\\);}; + PageName.getLayout \= function getLayout(page: ReactElement) {return \{page}\;}; + export default PageName; +3. Add a corresponding route on the front-end navigation by editing the file: frontend/src/menuAside.ts + For example, add: { href: '/page-href', label: 'Page Label', icon: icon.mdiAccountCircle }, + +# Integrate an External API in the Backend + +1. Create a new route: + In backend/src/routes, create a file (e.g., thirdParty.js). + Define a new endpoint that triggers the business logic for integration. +2. Implement the route handler: + Import necessary services and middleware. + Wrap the handler with a helper (like wrapAsync) to manage errors. + Example: + + router.get('/third-party', wrapAsync(async (req, res) \=\> { const result \= await ThirdPartyService.callExternalAPI(req.query); res.status(200).send(result); })); +3. Register the route: + In backend/src/index.js, + import the new route file. + Add it via app.use('/api/third-party', thirdPartyRoutes). +4. Implement the business logic: + In backend/src/services, create a file (e.g., thirdParty.js). + Write a function (e.g., callExternalAPI) that: + + – Accepts necessary parameters (e.g., from req.query). + – Performs the request to the external API (using axios or similar). + – Handle errors explicitly. If you use try/catch, log the error and rethrow. + – Returns the API result. +5. Add validation and testing: + Implement any required input validation. + +# Use Existing Components from Frontend Directory + +1. When generating UI code, check if a reusable component already exists in the folder frontend/src/components/. + For example, review the BaseButton component in frontend/src/components/BaseButton.tsx: + Open the file and inspect its props, styling, and usage logic. + Make sure you understand how the component works and its expected parameters. +2. When writing new code, reference or import BaseButton (or other matching components) instead of creating new ones. +3. Maintain consistency across the project by reusing and possibly extending existing components where appropriate. + +# Making Backend API Requests with Axios + +1. In the \_app.tsx file, the Axios default baseURL is set for API (or falls back to a predefined baseURL). This baseURL already includes the /api/ prefix. This means all backend requests use the full base URL automatically. +2. When making API calls, only use the relative endpoint without including the /api/ prefix. For example, axios.get('/orders') or axios.post('/orders/deleteByIds', { data }). + +Ensure error handling is in place. +STRICT RULE: Do NOT include the /api/ prefix in frontend Axios requests. This prefix is already handled automatically by axios.defaults.baseURL and adding it will result in incorrect URLs. + +# Check Project Dependencies + +To view frontend dependencies, open the file frontend/package.json. +To view backend dependencies, open the file backend/package.json. +Review the "dependencies" and "devDependencies" sections in each file to see the installed packages. + +# Adding a New Language to the Application with next-i18next + +1\. Create a Localization File for the New Language +Create a new JSON file inside the localization directory: +frontend/public/locales/{language\_code}/common.json +For example, if adding Italian (it): +frontend/public/locales/it/common.json +This file should have the same structure as frontend/public/locales/en/common.json (which is the default language file). +2\. Register the New Language in next-i18next.config.mjs +Open: frontend/next-i18next.config.mjs +Add the new language to the locales array: +export const i18n \= { defaultLocale: 'en', locales: \['fr', 'en', 'de', 'es', 'it'\], // Added Italian }; +3\. Add the New Language to the Language Switcher Component +Open: frontend/src/components/LanguageSwitcher.tsx +Add the new language to the LANGS array: +const LANGS \= \[ { value: 'en', label: 'EN' }, { value: 'fr', label: 'FR' }, { value: 'es', label: 'ES' }, { value: 'de', label: 'DE' }, { value: 'it', label: 'IT' } // Added Italian \]; + +# Translating Content on a Page with next-i18next + +1\. Import the Translation Hook +To translate any text within a page or component, first import useTranslation: +import { useTranslation } from 'next-i18next'; +2\. Retrieve the Translation Function +Inside the component function, initialize the translation function by specifying the namespace: +const { t } \= useTranslation('common'); +The 'common' namespace refers to the JSON translation file (e.g., frontend/public/locales/en/common.json). +3\. Use t() to Translate Text +Replace hardcoded text with t() calls: +{t('pages.dashboard.pageTitle')} +You can also set a default value: +t('pages.dashboard.pageTitle', { defaultValue: 'Overview' }) +4\. Dynamic Text with Variables +If your translation includes placeholders (e.g., "Hello, {{name}}"), pass values as arguments: +t('greeting', { name: currentUser.firstName }) + +# How to change the styling of the app + +1. Open frontend/src/styles.ts This file defines theme presets (like basic, white) as objects mapping UI elements (e.g., aside, cardsColor) to Tailwind CSS classes. + To change an existing theme: Locate the desired theme object and modify the Tailwind classes for specific properties (e.g., aside, navBarItemLabel, cardsColor). +2. Open frontend/src/colors.ts This file defines the color palette and how colors are applied to elements like buttons. + Modify lookup objects (e.g., colorsBgLight, colorsText, colorsOutline) to change color definitions. + Adjust the getButtonColor function logic and its internal lookup objects (e.g., colors.bgHover, colors.text) to alter button appearance based on color keys and states. + + +# How to change basic styling and layout within common components + +To adjust hardcoded spacing, layout, sizes, or other base visual properties that are defined directly inside a component's file, you need to edit that component's source code. (Note: Theme-based colors, corners, focus rings, and color-specific styles like button colors are controlled elsewhere). + +1. Open the .tsx file for the component you want to modify (e.g., frontend/src/components/BaseButton.tsx). +2. Locate the className strings or the logic that constructs them based on props or internal conditions. +3. Edit the Tailwind CSS classes directly within these strings, or modify the logic that adds/removes classes. + +Here are specific areas to look at for each component: + +* BaseDivider (BaseDivider.tsx): Look at the className on the \ tag and the classAddon logic. Edit classes like border-t (border style), my-4, \-mx-4 (margins), or conditional classes applied for navBar. +* BaseButtons (BaseButtons.tsx): Look at the className on the main div and the logic using type, mb, classAddon, noWrap props. Edit flex/layout classes (flex, items-center, justify-end, flex-wrap) or default margin values (mb, classAddon). +* CardBoxComponentBody (CardBoxComponentBody.tsx): Look at the className on the main div. Edit flex-1 or the conditional p-4 class applied when noPadding is false to change internal padding. +* BaseButton (BaseButton.tsx): Look at the componentClass array construction logic. Edit base classes like inline-flex, justify-center, items-center, whitespace-nowrap, text-sm, border, transition-colors, focus:ring, duration-100. Edit padding classes (p-0.5, py-1, px-2, px-4) and the logic determining them based on small, label, icon. Edit the rounded-full application logic. Edit opacity classes like opacity-50 or opacity-70 for disabled. +* CardBoxComponentFooter (CardBoxComponentFooter.tsx): Look at the className on the \. Edit the padding class (p-4) to change internal padding. +* CardBoxComponentTitle (CardBoxComponentTitle.tsx): Look at className on the wrapper div and the \. Edit wrapper layout/spacing classes (flex, items-center, justify-center, mb-2) or the title text size (text-xl). +* FormField (FormField.tsx): Look at className on wrapper divs, label, help text div, and the controlClassName string construction. Edit hardcoded margins (mb-4, mb-1, mt-0.5), label/help text size/spacing (text-sm, text-xs). Edit the elementWrapperClass logic for grid layouts. Edit base padding/height/border/background classes within controlClassName (e.g., px-2, py-1, max-w-full, w-full, h-20, h-9, border-gray-300, dark:border-dark-700). Edit conditional classes based on props like isBorderless, isTransparent, hasTextareaHeight, disabled, borderButtom. Edit hardcoded icon classes (positioning, size) on the BaseIcon. + +# How to style layout components (NavBar, Aside, Footer, SectionMain, etc.) + +Layout components like NavBar, AsideMenu, FooterBar, SectionMain, and their layers define the main structure and container styling of the application. Their styling is controlled by hardcoded classes, component props, theme properties defined in frontend/src/styles.ts, and some constants in frontend/src/config.ts. + +* To change hardcoded layout, spacing, sizes, positioning, or basic visual traits within a component: + + Open the component's .tsx file (e.g., frontend/src/components/NavBar.tsx, frontend/src/components/AsideMenuLayer.tsx, frontend/src/components/FooterBar.tsx, frontend/src/components/SectionMain.tsx). Locate the className strings on the HTML elements (\, \, \, div wrappers, или главный wrapper в SectionMain) or logic based on props (like isAsideMobileExpanded, useMargin, display). Edit or add Tailwind CSS classes directly here (e.g., change h-12 to h-16 for Navbar height, modify py-1 px-4 for Footer padding, change w-48 to w-56 for Aside width, adjust fixed, absolute, hidden, block, left-0, \-left-48 positioning classes, или изменить внутренние отступы/маргины в SectionMain). + +* To change layout background colors, aside styles, aside brand area styles, aside scrollbars, or navbar item text styles based on the current theme: + + These styles are read from the application's theme state, defined in frontend/src/styles.ts. Open frontend/src/styles.ts. Find the relevant property (e.g., bgColor, asideStyle, asideBrandStyle, asideScrollbarsStyle, navBarItemLabelStyle, navBarItemLabelHoverStyle) within the theme objects (basic, white, etc.). Modify the Tailwind classes assigned to that property. Layout components reading these properties will update when the theme changes. + +* To change the maximum width of main content containers (used in NavBar and Footer, and potentially wrapping content within SectionMain): + + This is controlled by a constant in the configuration file. Open frontend/src/config.ts. Modify the containerMaxW constant (e.g., change 'container lg:mx-auto px-6' to max-w-screen-xl mx-auto px-4). + +* To apply custom styles from where a component is used (overriding default styles): + + Layout components like NavBar, AsideMenuLayer, FooterBar, SectionMain often accept a className prop. Add Tailwind CSS classes or custom CSS class names to the className prop when using the component in layouts like LayoutAuthenticated or directly on pages. + + +# How to create a public page (without authentication) + +Goal: Create a new page accessible without requiring login, using a clean URL path. + +1. Create the Page File Create a new file in the frontend/src/pages/web\_pages/ directory. Name it appropriately, for example, my-public-page.tsx. The default direct access path will be /web\_pages/my-public-page. + +2. Add the Basic Page Structure Copy the following basic structure into your new file. This includes necessary imports, the page component structure with Header, Footer, and main content area, and the assignment of the public layout (LayoutGuest). + + TypeScript + import React from 'react'; + import type { ReactElement } from 'react'; + import Head from 'next/head'; + // You might need useAppSelector if Header/Footer use global state (e.g., project name) + // import { useAppSelector } from '../../stores/hooks'; // Adjust path if needed + + import LayoutGuest from '../../layouts/Guest'; // Public Layout \- Adjust path if needed + import WebSiteHeader from '../../components/WebPageComponents/Header'; // Adjust path if needed + import WebSiteFooter from '../../components/WebPageComponents/Footer'; // Adjust path if needed + + // \--- Customize: Change function name (e.g., MyPublicPage) \--- + export default function PageName() { + // Example of getting project name, if needed (adjust path if using useAppSelector) + // const projectName \= useAppSelector((state) \=\> state.style.projectName); + const projectName \= 'Your Project Name'; // Customize or get from state/config + + // Optional: Example useEffect for client-side logic + // useEffect(() \=\> { /\* ... \*/ }, \[\]); + + // Example data for Header/Footer, if menu items are needed + // const pages \= \[ /\* ... \*/ \]; + + return ( + \<\> + \ + {/\* Customize: Page Title and Meta Description \*/} + \Your Public Page Title\ + \ + \ + {/\* Customize: Pass necessary props to Header/Footer \*/} + \ + \
{/\* Your page content goes here \*/}\ + {/\* Customize: Pass necessary props to Footer \*/} + \ + \ + ); + } + + // \--- IMPORTANT: Assign Guest Layout \--- + // This line makes the page public (accessible without authentication) + PageName.getLayout \= function getLayout(page: ReactElement) { + return \{page}\; // Use LayoutGuest + }`;` + + (Note: Ensure import paths for LayoutGuest, WebSiteHeader, WebSiteFooter, and potentially useAppSelector are correct relative to the pages/web\_pages/ directory. Replace PageName with your actual component name). + +3. Add Page Content Add the actual content for your public page inside the \{/\* Your page content goes here \*/}\ tags. + +4. Add Rewrite Rule To access your page via a cleaner URL path (e.g., /my-public-page instead of /web\_pages/my-public-page), add a rewrite rule in next.config.mjs. + + * Open frontend/next.config.mjs. + * Inside async rewrites(), add a new object to the array: + JavaScript + async rewrites() { + return \[ + // ... existing rules ... + { + source: '/my-public-page', // \<-- Clean URL path you want + destination: '/web\_pages/{file-name}', // \<-- Actual location of the page file (without .tsx) + }, + // ... + \]; + }, + * + + * Replace /my-public-page and /web\_pages/{file-name} with the actual desired paths and file location (use the file name without .tsx in the destination). +5. Add a link to public navigation If you want this page to appear in the header/footer navigation for public pages, edit the file frontend/src/menuNavBar.ts. Locate the webPagesNavBar array and add a new object to it. Use the source path from your rewrite rule for the href: + + TypeScript + export const webPagesNavBar \= \[ + // ... existing links ... + { + href: '/my-public-page', // \<-- Use the 'source' path from your rewrite rule + label: 'Your Page Label', + }, + // + \]; + +# Public Read Access for Entity Data + +To allow unauthenticated users to read data for a specific entity (e.g., Courses), you need to adjust the backend router, the database permissions, and the frontend fetch logic. + +1. Backend: Remove Auth Middleware + + In backend/src/index.js, remove passport.authenticate from the specific entity's router definition. + Change in backend/src/index.js: + JavaScript + +``` + // Before: +// app.use('/api/courses', passport.authenticate('jwt', { session: false }), coursesRoutes); +// After: +app.use('/api/courses', coursesRoutes); +``` + +2. Backend (DB): Grant Public READ Permission + + In your roles/permissions database migration, add the READ\_\ permission for the Public role. Replace \ (e.g., COURSES). + Change in Migration File (conceptual): + JavaScript + +``` + // In the bulkInsert for rolesPermissionsPermissions table: +{ createdAt, updatedAt, roles_permissionsId: getId("Public"), permissionId: getId('READ_COURSES') }, // Add this line +``` + + + +3. Frontend: Fetch Data + + In your public page component, use axios.get (or Workspace) with the relative path to the entity endpoint. + Change in Frontend Component: + JavaScript + +``` + import axios from 'axios'; // or import fetch +useEffect(() => { + axios.get('/courses') // Use relative path without '/api/' + .then(response => { + // Handle { rows: [...], count: ... } format + const dataList = Array.isArray(response.data.rows) ? response.data.rows : []; + // Use dataList... + }) + .catch(err => console.error(err)); +}, []); +``` + +# Create a Public Entity Details Page (View by ID) + +Create a public page accessible without authentication to display the details of a single entity item, fetched from the backend using its ID provided in the URL query parameter (e.g., /public/users-details?id=abc-123). +Steps: + +1. Backend: Make the GET /api/\[EntityName\]/:id Endpoint Public + + * Explanation: The backend route used to fetch a single item by its ID needs to allow unauthenticated requests. + * Action: + * In backend/src/index.js, identify the router for your entity (e.g., usersRoutes for /api/users). + * Remove the passport.authenticate middleware from this specific router if you want any requests to /api/\[EntityName\] (including GET by ID) to bypass JWT auth. Be cautious: This makes all endpoints under /api/\[EntityName\] potentially accessible. A more granular approach might be needed if only GET is public. + * Ensure your checkCrudPermissions('entityName') middleware is applied to this router. + * In your database migration file for roles/permissions, grant the READ\_\ permission to the Public role. Our modified checkPermissions middleware will handle the authorization check based on the 'Public' role when the user is unauthenticated. + * Location: backend/src/index.js and Database Migration file (backend/src/db/seeders/...). +2. JavaScript + +```javascript +// --- Backend: index.js (Example for users) --- +// Find this section: +// app.use( +// '/api/users', +// passport.authenticate('jwt', { session: false }), // <-- REMOVE THIS LINE +// usersRoutes, // <-- Assuming checkCrudPermissions('users') is inside usersRoutes or applied here +// ); +// Change to: +app.use( + '/api/users', + usersRoutes, // <-- Now accessible without JWT auth (permissions check still applies) +); + +// --- Backend (DB): Migration file (Example for READ_USERS) --- +// In the bulkInsert for rolesPermissionsPermissions table, add: +{ createdAt, updatedAt, roles_permissionsId: getId("Public"), permissionId: getId('READ_USERS') }, // <--- Add this line +``` + +3. Frontend: Create the Public Page File + + * Explanation: Create a new Next.js page file for the entity details. Place it in a directory intended for public pages (e.g., pages/public/) to keep public routes organized and separate from authenticated ones. + * Location: frontend/src/pages/public/\[EntityName\]-details.tsx (Replace \[EntityName\] with your entity name, e.g., users-details.tsx). + * Action: Create the file and add the basic structure using LayoutGuest. +4. TypeScript + +```javascript +// frontend/src/pages/public/[EntityName]-details.tsx (Example for users-details.tsx) +import React, { useEffect, useState } from 'react'; +import type { ReactElement } from 'react'; +import Head from 'next/head'; +import { useRouter } from 'next/router'; +// Import your fetch logic (e.g., using Redux/ Zustand actions or a simple axios call) +// import { useDispatch, useAppSelector } from '../../stores/hooks'; // If using Redux/Zustand +// import { fetchUserById } from '../../stores/users/usersSlice'; // Example action + +import LayoutGuest from '../../layouts/Guest'; // Use the public layout +// Import Header/Footer components for public pages if needed +// import WebSiteHeader from '../../components/WebPageComponents/Header'; +// import WebSiteFooter from '../../components/WebPageComponents/Footer'; + +// --- Customize: Change function name and component name --- +export default function EntityDetailsPage() { // Example: UsersDetailsPage + const router = useRouter(); + // Get the ID from the URL query parameters + // router.query.id might be undefined initially on client-side render + const { id } = router.query; + + // --- Customize: Add state for fetching/data --- + // If using Redux/Zustand: + // const dispatch = useDispatch(); + // const { entityData, loading, error } = useAppSelector(state => state.yourEntitySlice); // Adapt selector + + // If using simple state with axios: + const [entityData, setEntityData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + // import axios from 'axios'; // Need axios + + + // --- Customize: Fetch data when ID is available --- + useEffect(() => { + // Fetch data only if ID is present and is a string + if (id && typeof id === 'string') { + console.log(`Workspaceing entity with ID: ${id}`); // Debugging + + // --- Customize: Your data fetching logic --- + // Example using Redux/Zustand dispatch: + // dispatch(fetchUserById({ id })); // Assuming this action fetches and updates state + + // Example using simple axios: + const fetchItem = async () => { + try { + setLoading(true); + setError(null); + // Call the backend GET /api/[entityName]/:id endpoint + // Use relative path for axios - it adds the baseURL (pointing to backend) + const response = await axios.get(`/users/${id}`); // <-- Adjust endpoint path + const data = response.data; // Assuming backend returns the item object directly for /:id + setEntityData(data); + } catch (err) { + console.error("Failed to fetch entity:", err); + setError(err); // Store error state + } finally { + setLoading(false); // Always set loading to false + } + }; + fetchItem(); // Execute the async fetch function + + } else if (!id) { + // Handle case where ID is missing in the URL + setLoading(false); // Stop loading state + // Optionally set an error or redirect + // setError(new Error("Entity ID is missing in the URL.")); + console.warn("Entity ID is missing in the URL query."); + // Maybe redirect to a list page or 404 page + // router.push('/public/users'); // Example redirect + } + + // Depend on 'id' and dispatch (if using dispatch) + }, [id]); // Re-run effect if ID changes + + + // --- Customize: Display Loading, Error, or Data --- + if (loading) { + return ( + <> + Loading... + {/* */} +

Loading entity details...

+ {/* */} + + ); + } + + if (error) { + return ( + <> + Error + {/* */} +

Error loading details: {error.message}

+ {/* */} + + ); + } + + // If data is loaded and exists, display it + if (!entityData) { + return ( + <> + Not Found + {/* */} +

Entity not found.

+ {/* */} + + ); + } + + + return ( + <> + + {/* Customize: Page Title */} + {`Details: ${entityData.name || entityData.title || 'Unnamed Entity'}`} {/* Example title from data */} + + + {/* */} + +
{/* Add padding */} +

{/* Customize: Display entity title/name */}

+ {/* --- Customize: Display other entity details --- */} +
ID: {entityData.id}
+ {/* Example: */} + {/*
Name: {entityData.name}
*/} + {/*
Description: {entityData.description}
*/} + {/* ... other fields ... */} +
+ + {/* */} + + ); +} + +// --- IMPORTANT: Assign Guest Layout --- +// This line makes the page public (accessible without authentication) +EntityDetailsPage.getLayout = function getLayout(page: ReactElement) { // Example: UsersDetailsPage.getLayout + return {page}; // Use LayoutGuest +}; + +// Remove this line if using getLayout assignment below +// export default EntityDetailsPage; // Example: export default UsersDetailsPage; +``` + +5. Frontend: Add Link to the Page (Optional) + + * Explanation: If you have a list page or another place where you want to link to the details of a specific item, create a link passing the item's ID as a query parameter. + * Location: The page or component where the list is displayed (e.g., a list page, a card component). + * Action: Use the Link component from next/link and provide the path to your new details page with the id in the query object. +6. TypeScript + +```javascript +// Example in a list item rendering +import Link from 'next/link'; + +// Assuming 'item' is an object from your list fetch +// Adjust href path to match your new details page file location + + {item.name || item.title} {/* Link text */} + +``` + +Now, when you navigate to /public/users-details?id=YOUR\_ENTITY\_ID, the page will fetch and display the details for that specific entity, and it will be accessible without requiring a login due to the LayoutGuest. Remember to replace placeholders like \[EntityName\], users-details.tsx, WorkspaceUserById, /users/${id}, and the data display logic with your actual entity's details. + +# How to add AI functionality + +Purpose: Generate custom text, creative content, or ideas using AI within your application. +Handle User-Provided API Key from Chat: + \!\!\!The first step is to ask the user for the key if they don't have it yet. Do not follow any further steps without the key\!\!\! + +* Ask the user for the API key. Save it to the file backend/src/config.js under the name gpt\_key if the key is not already present there. + +How I Assist: I will provide you with the specific text prompt string you need to send to the AI. +How You Use It in Code (Triggered on Component Load): + +1. Import: Import the necessary action, hooks, and useEffect: + TypeScript + +```javascript +import React, { useEffect } from 'react'; // Import useEffect +import { useAppDispatch, useAppSelector } from '../stores/hooks'; // Import Redux hooks +import { askGpt } from '../stores/openAiSlice'; // Import the GPT action +``` + +2. Setup & State: Get the dispatch function and select the relevant state from your openAiSlice: + TypeScript + +```javascript +// Inside your functional component +const dispatch = useAppDispatch(); +const { gptResponse, isAskingQuestion, errorMessage } = useAppSelector((state) => state.openAi); +``` + +3. Define Prompt: Define the prompt string you want to send. + TypeScript + +```javascript +const promptForAchievement = "Write a short (max 20 words), inspiring, and plausible 'achievement of the day' related to productivity or learning."; +``` + +4. Use useEffect to Dispatch: Use the useEffect hook to dispatch the action when the component mounts. + TypeScript + +```javascript +useEffect(() => { + // Dispatch the askGpt action with your prompt + dispatch(askGpt(promptForAchievement)); + + // The empty dependency array [] means this effect will run + // only once after the initial render (on mount). + // In development with React Fast Refresh, this effect might run twice. +}, [dispatch]); // Add dispatch to dependencies (React hooks recommendation) +``` + +5. Display Result: Use the state variables (isAskingQuestion, gptResponse, errorMessage) in your component's JSX to show loading status, the generated text, or any errors. + +\<\!-- end list \--\> +TypeScript + +```javascript +// Example Component Structure (AchievementOfTheDay.tsx) +import React, { useEffect } from 'react'; +import { useAppDispatch, useAppSelector } from '../stores/hooks'; +import { askGpt } from '../stores/openAiSlice'; + +function AchievementOfTheDay() { + const dispatch = useAppDispatch(); + const { gptResponse, isAskingQuestion, errorMessage } = useAppSelector((state) => state.openAi); + + // Define the prompt + const achievementPrompt = "Write a short (max 20 words), inspiring, and plausible 'achievement of the day' related to productivity or learning."; + + // Use useEffect to trigger the API call on component mount + useEffect(() => { + dispatch(askGpt(achievementPrompt)); + // Empty dependency array means run once on mount + }, [dispatch]); // Dependency array includes dispatch + return ( +
+

Your Achievement of the Day:

+ {/* Display loading state */} + {isAskingQuestion &&

Generating achievement...

} + + {/* Display the result if received */} + {gptResponse &&

{gptResponse}

} + + {/* Display error if any */} + {errorMessage &&

{errorMessage}

} +
+ ); +} + +export default AchievementOfTheDay; // Export your component +``` + +Important Notes: + +* useEffect Dependencies: The dependency array \[\] makes the effect run once on mount. If you need the request to trigger based on changes to props or state (e.g., a user ID changing), include those variables in the dependency array (e.g., \[dispatch, userId\]). +* Fast Refresh: In development mode with Fast Refresh, effects with \[\] might run twice. This is expected development behavior. +* Controlling Calls: Be mindful of where you put this useEffect. If placed in a component that renders frequently or in \_app.tsx or a layout without careful conditions, it could lead to many unintended API calls. Dispatch only when necessary. + +More Prompt Examples (Ideas for what you can ask me to generate a prompt for): +Here are more ideas for features where you could use this tool, and the type of prompt you would ask me to create for you to use in your askGpt dispatch: + +* Generate a Product Description: Ask me for a prompt to draft a description for an item. + Prompt Idea: "Write a concise product description (max 60 words) for a \[Product Category\] highlighting \[Key Feature\] and \[Main Benefit\]." +* Suggest User Profile Interests: Ask me for a prompt to suggest interests based on a theme. + * Prompt Idea: "Suggest 5 hobbies or interests related to 'outdoor activities' suitable for a user profile." +* Create a Welcome Email Subject Line: Ask me for a prompt to generate email subject lines. + * Prompt Idea: "Write 3 catchy and welcoming subject lines for a new user registration confirmation email." +* Explain a Concept Simply: Ask me for a prompt to get a simple explanation of something. + * Prompt Idea: "Explain the concept of 'asynchronous programming' in simple terms, suitable for someone new to coding." +* Generate Test Data (Text Format): Ask me for a prompt to generate sample text data. + * Prompt Idea: "Generate a sample short user review for a hypothetical product, commenting on its 'ease of use'." +* Draft an Error Message: Ask me for a prompt to write a user-friendly error message. + * Prompt Idea: "Write a helpful and polite error message for when a file upload fails because the file is too large." + +Now, just tell me what kind of text or idea you need, specify that you want it for triggering on component load (if that's the case), and I'll provide the prompt string for you to use in your useEffect dispatch\! \ No newline at end of file