diff --git a/apps/workshopper/AUTH.md b/apps/workshopper/AUTH.md index ae99c4d..2ec7531 100644 --- a/apps/workshopper/AUTH.md +++ b/apps/workshopper/AUTH.md @@ -56,7 +56,9 @@ An agent or developer must replicate the exact SAML configuration we built for W * Copy the SAML configuration block (including `server.forward-headers-strategy: framework`) into their `application.yml`. * Create the `SecurityConfig.java` to enable `.saml2Login()` and explicitly permit access to `/saml2/service-provider-metadata/**`. * Modify their `application.yml` and `compose.prod.yaml` to mount the exact same central certificate folder (`/opt/hestia/secrets/saml:/app/certs:ro`) and read them via environment variables. +* **CRITICAL:** Fix the Docker DNS collision in their frontend `nginx.conf`! Both currently proxy to `http://server`, which randomly resolves to the wrong backend on the shared `hestia-edge` network. Change this to their exact unique container names (e.g., `proxy_pass http://examlense-server:8081;` and `proxy_pass http://learninggoalhub-server:8080;`). * **CRITICAL:** Update their frontend `nginx.conf` file to proxy `location ~ ^/(v3/api-docs|swagger-ui|saml2|login/saml2/sso)` to the backend server. Otherwise, NGINX will block the SAML traffic! +* **NOTE:** While editing `compose.prod.yaml`, it is highly recommended to proactively change `POSTGRES_HOST: postgres` to their unique database container names (`examlense-postgres` or `learninggoalhub-postgres`) to prevent future DNS collisions if the database ever joins the edge network. * Once deployed, verify that they each generate their own live metadata URLs (e.g., `https://hestia-test.aet.cit.tum.de/learninggoalhub/saml2/service-provider-metadata/tum`). **2. Host the Privacy Policy:** diff --git a/apps/workshopper/compose.prod.yaml b/apps/workshopper/compose.prod.yaml index 2ba801f..eb1187f 100644 --- a/apps/workshopper/compose.prod.yaml +++ b/apps/workshopper/compose.prod.yaml @@ -21,7 +21,7 @@ services: environment: SPRING_PROFILES_ACTIVE: prod SPRING_JPA_HIBERNATE_DDL_AUTO: update - POSTGRES_HOST: postgres + POSTGRES_HOST: workshopper-postgres POSTGRES_PORT: 5432 POSTGRES_DB: ${POSTGRES_DB:-workshopper} POSTGRES_USER: ${POSTGRES_USER:-workshopper} diff --git a/apps/workshopper/frontend/nginx.conf b/apps/workshopper/frontend/nginx.conf index f20d649..5ea69fd 100644 --- a/apps/workshopper/frontend/nginx.conf +++ b/apps/workshopper/frontend/nginx.conf @@ -20,7 +20,7 @@ server { } location /api/ { - proxy_pass http://server:8081; + proxy_pass http://workshopper-server:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -34,7 +34,7 @@ server { } location ~ ^/(v3/api-docs|swagger-ui|saml2|login/saml2/sso) { - proxy_pass http://server:8081; + proxy_pass http://workshopper-server:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/apps/workshopper/frontend/src/components/SessionsDashboard.tsx b/apps/workshopper/frontend/src/components/SessionsDashboard.tsx index 94d713c..4c03f6e 100644 --- a/apps/workshopper/frontend/src/components/SessionsDashboard.tsx +++ b/apps/workshopper/frontend/src/components/SessionsDashboard.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { getSessionStatus } from "@/lib/utils"; import { GraduationCap, BookOpen, Clock, Loader2, Target, Plus, Trash2, Settings, ArrowRight, Play, Archive, FileText, ChevronRight, X, Edit2, CheckCircle2, FileEdit, MoreVertical, Pencil, Presentation, ChevronDown, Sparkles } from "lucide-react"; import hestiaLogoLight from "@/assets/logos/wordmark-light.svg"; import hestiaLogoDark from "@/assets/logos/wordmark-dark.svg"; @@ -297,34 +298,22 @@ export default function SessionsDashboard({ onNewSession, onNewLecture, onNewSes {(() => { const isFinished = session.status === "complete" && session.currentStep === "finished"; const isReadyForPrep = session.status === "complete" && session.currentStep !== "finished"; - const isDraft = session.status !== "complete"; - // G-1: status text inside icon container so state is readable in grayscale - const statusShort = isFinished ? "Done" : isReadyForPrep ? "Ready" : "Draft"; + const status = getSessionStatus(session); return ( -
- {isFinished ? : isReadyForPrep ? : } - {statusShort} +
+ {status.isFinished ? : status.isReadyForPrep ? : } + {status.statusShort}
); })()}
{(() => { - const isFinished = session.status === "complete" && session.currentStep === "finished"; - const isReadyForPrep = session.status === "complete" && session.currentStep !== "finished"; - const statusLabel = isFinished ? "Completed" : isReadyForPrep ? "Ready for preparation" : "In progress"; - const statusClass = isFinished - ? "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400" - : isReadyForPrep - ? "bg-blue-500/15 text-blue-600 dark:text-blue-400" - : "bg-amber-500/15 text-amber-600 dark:text-amber-400"; + const status = getSessionStatus(session); return (
{session.title || "Workshop Session"} - - {statusLabel} + + {status.statusLabel}
); diff --git a/apps/workshopper/frontend/src/components/WorkshopGeneratedTimetable.tsx b/apps/workshopper/frontend/src/components/WorkshopGeneratedTimetable.tsx index 9f4e467..55d3a64 100644 --- a/apps/workshopper/frontend/src/components/WorkshopGeneratedTimetable.tsx +++ b/apps/workshopper/frontend/src/components/WorkshopGeneratedTimetable.tsx @@ -27,57 +27,14 @@ import { } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { toast } from "@/hooks/use-toast"; +import { SortableBlockRow } from "./timetable/SortableBlockRow"; +import { InlineEditText } from "./timetable/InlineEditText"; -// ── Phase styling ────────────────────────────────────────────────── -const phaseEmojis: Record = { - ARRIVE: "👋", ACTIVATE: "💡", INFORM: "🗣️", PROCESS: "🛠️", - BREAK: "☕", EVALUATE: "✅", SUMMARY: "🏁", LEARNING_CYCLE: "🎯", - CUSTOM: "✏️", BUFFER: "⏳", -}; - -const phaseRowColors: Record = { - ARRIVE: "bg-muted/30 relative before:absolute before:inset-y-0 before:left-0 before:w-[3px] before:bg-primary", - ACTIVATE: "bg-muted/30 relative before:absolute before:inset-y-0 before:left-0 before:w-[3px] before:bg-accent", - INFORM: "bg-muted/30 relative before:absolute before:inset-y-0 before:left-0 before:w-[3px] before:bg-primary/80", - PROCESS: "bg-muted/30 relative before:absolute before:inset-y-0 before:left-0 before:w-[3px] before:bg-accent/80", - BREAK: "bg-muted/30 relative before:absolute before:inset-y-0 before:left-0 before:w-[3px] before:bg-muted-foreground", - EVALUATE: "bg-muted/30 relative before:absolute before:inset-y-0 before:left-0 before:w-[3px] before:bg-primary/60", - SUMMARY: "bg-muted/30 relative before:absolute before:inset-y-0 before:left-0 before:w-[3px] before:bg-accent/60", - LEARNING_CYCLE: "bg-muted/30 relative before:absolute before:inset-y-0 before:left-0 before:w-[3px] before:bg-primary", - CUSTOM: "bg-muted/30 relative before:absolute before:inset-y-0 before:left-0 before:w-[3px] before:bg-border", - BUFFER: "bg-muted/30 relative before:absolute before:inset-y-0 before:left-0 before:w-[3px] before:bg-muted", -}; - -const getStepEmoji = (text: string) => { - const t = text.toLowerCase(); - if (t.includes("lecture") || t.includes("presentation") || t.includes("explain") || t.includes("concept")) return "🧑‍🏫"; - if (t.includes("prompt") || t.includes("question") || t.includes("q&a") || t.includes("quiz")) return "❓"; - if (t.includes("activity") || t.includes("exercise") || t.includes("practice")) return "🛠️"; - if (t.includes("discuss") || t.includes("debate") || t.includes("share")) return "💬"; - if (t.includes("brainstorm") || t.includes("ideate")) return "💡"; - if (t.includes("review") || t.includes("feedback") || t.includes("evaluate")) return "✅"; - if (t.includes("welcome") || t.includes("intro")) return "👋"; - if (t.includes("wrap") || t.includes("summary") || t.includes("conclusion")) return "🏁"; - if (t.includes("break") || t.includes("pause")) return "☕"; - if (t.includes("read") || t.includes("case study")) return "📖"; - if (t.includes("video") || t.includes("watch")) return "🎥"; - if (t.includes("role play") || t.includes("simulate")) return "🎭"; - if (t.includes("icebreaker") || t.includes("game")) return "🎲"; - return "✨"; -}; - -const getStepColorClass = (text: string) => { - return "bg-card border-border/60"; -}; - -const DEFAULT_ACTIVITIES = [ - "Group Discussion", "Case Study", "Role Play", - "Hands-on Practice", "Quiz / Polls", "Q&A Session", - "Peer Review", "Brainstorming", "Think-Pair-Share", -]; +import { phaseEmojis, phaseRowColors, getStepEmoji, getStepColorClass, DEFAULT_ACTIVITIES } from "@/lib/constants"; +import { parseStepDuration } from "@/lib/utils"; // ── Editing state ────────────────────────────────────────────────── -type EditTarget = +export type EditTarget = | { type: "sessionTitle" } | { type: "title"; blockId: string } | { type: "step"; blockId: string; sectionIdx: number; stepIdx: number } @@ -87,7 +44,7 @@ type EditTarget = | null; // ── Internal block with stable dnd ID ───────────────────────────── -interface DndActivityBlock extends ActivityBlock { +export interface DndActivityBlock extends ActivityBlock { dndId: string; lgIndex?: number; } @@ -102,295 +59,9 @@ interface Props { onSaveSession?: (session: WorkshopSession) => void; } -// ── Inline editable text ─────────────────────────────────────────── -function InlineEditText({ - value, editing, onStartEdit, onSave, multiline = false, className = "", inputClassName = "text-sm px-2 py-0.5", boxStyle = false -}: { - value: string; - editing: boolean; - onStartEdit: () => void; - onSave: (v: string) => void; - multiline?: boolean; - className?: string; - inputClassName?: string; - boxStyle?: boolean; -}) { - const [draft, setDraft] = useState(value); - const ref = useRef(null); - - useEffect(() => { - if (editing) { - setDraft(value); - setTimeout(() => ref.current?.focus(), 30); - } - }, [editing, value]); - - const commit = () => onSave(draft); - - if (editing) { - if (multiline) { - return ( -