Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
dc0d6d8
docs(project): add KB management UX implementation plan
rschlaefli Aug 24, 2026
4451642
enhance(kb): prioritize resource workspace
rschlaefli Aug 24, 2026
07452f7
enhance(kb): present resources as metadata table
rschlaefli Aug 24, 2026
12dcd4c
enhance(kb): unify resource creation
rschlaefli Aug 24, 2026
edc4c90
enhance(kb): streamline detail workspace
rschlaefli Aug 24, 2026
1133e0f
enhance(kb): collapse secondary configuration
rschlaefli Aug 24, 2026
dfa33dd
fix(kb): protect resource creation lifecycle
rschlaefli Aug 24, 2026
b900805
docs(project): update KB UX verification progress
rschlaefli Aug 24, 2026
2b3d8cb
fix(kb): align UX package static checks
rschlaefli Aug 24, 2026
ac821a5
fix(kb): lock resource mode during uploads
rschlaefli Aug 24, 2026
f817a28
fix(kb): preserve creation close and narrow layout proof
rschlaefli Aug 24, 2026
248d58c
docs(project): record KB UX final review
rschlaefli Aug 24, 2026
574ca21
docs(project): record full KB verification
rschlaefli Aug 24, 2026
c58032b
enhance(playwright): run local E2E from host
rschlaefli Aug 25, 2026
25a32cb
docs(project): record KB UX PR metadata
rschlaefli Aug 25, 2026
42773ea
fix(dev): harden local runtime for KB verification
rschlaefli Aug 25, 2026
da6149e
docs(dev): clarify local cache policy
rschlaefli Aug 25, 2026
0f552de
docs(project): record runtime review
rschlaefli Aug 25, 2026
601d2a6
docs(project): record PR publication
rschlaefli Aug 25, 2026
dbc554b
merge: reconcile KB management UX with lifecycle head
rschlaefli Aug 25, 2026
6638b21
fix(kb): address management UX review feedback
rschlaefli Aug 25, 2026
02cef1b
Merge branch 'rs/kb-v3-ai-finalization' into rs/kb-management-ux
rschlaefli Aug 25, 2026
d5bac25
test(kb): focus management UX checks on desktop
rschlaefli Aug 25, 2026
95842ae
Merge commit 'f85a3dda9466633c66dae9b48f158b373aa1aa75' into rs/kb-ma…
rschlaefli Aug 25, 2026
89fb7d7
test(kb): stabilize management UX E2E setup
rschlaefli Aug 25, 2026
a035b1b
fix(kb): keep resource creation modal consistent
rschlaefli Aug 25, 2026
9a0792a
merge: refresh KB lifecycle parent
rschlaefli Aug 25, 2026
d9747b3
docs(kb): close desktop UX verification plan
rschlaefli Aug 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/klicker-frontend-ui/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Conventions (design system, Tailwind v4, Apollo, i18n, CSP): [docs/frontend-conv
## App boundaries

- `frontend-manage` (lecturer), `frontend-pwa` (student; also has a localforage offline side-channel for live-quiz answers — don't bypass `storageHelpers.ts`), `frontend-control` (mobile controller), `auth` (login flows — auth changes also need [docs/auth-model.md](../../../docs/auth-model.md)).
- Knowledge-base management is a reusable package mounted by `frontend-manage`: edit `packages/kb-management`, not duplicate app-local components. Verify `/resources/knowledgeBases` plus the detail route at desktop and mobile widths, both locales, and every changed empty/active/success/failure state.
- Knowledge-base management is a reusable package mounted by `frontend-manage`: edit `packages/kb-management`, not duplicate app-local components. The detail resource workspace uses a semantic metadata table, one `+ Add resource` chooser for Website/Document with Video disabled until supported, and an overflow menu for destructive row actions. Preserve the explicit `ADDED`-before-Ingest lifecycle and keep the chooser non-dismissible after a file upload ticket is requested until confirmation or terminal failure. Verify `/resources/knowledgeBases` plus the detail route at desktop and mobile widths, both locales, and every changed empty/active/success/failure state.
- The KB navigation item is an interim `user.privatePreview` discovery gate. Direct catalog/detail URLs must render the localized `KB_PREVIEW_ACCESS_REQUIRED` service error for a non-preview lecturer; never rely on hidden navigation as authorization.
- The knowledge-resource Ingest action accepts only the resource identifier. Do not expose transport tuning in the UI unless the GraphQL and ingestion-platform contracts add a real user-controlled setting.
- Keep full KB attempt history out of the two-second detail poll. Load the bounded, owner-checked history query only when a lecturer expands a resource, while the parent query carries only the latest run needed for operation status.
Expand Down
1 change: 1 addition & 0 deletions .agents/skills/klicker-playwright-e2e/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ never download browsers into a DevPod.
```bash
# auto-detects routed worktrees / plain devcontainer / host-run apps
bash util/run-host-e2e.sh --project=chromium tests/A-login.spec.ts
bash util/run-host-e2e.sh --project=chromium tests/Y-kb-management-ux.spec.ts

# inspect the resolved URL + database mapping without running anything
bash util/run-host-e2e.sh --print
Expand Down
7 changes: 6 additions & 1 deletion .devcontainer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,12 @@ replacement. HTTP readiness remains in
`devrouter ensure .`; the root build script forces production mode even though
the live container exports `NODE_ENV=development`. Rerun ensure after
`pnpm run build` so stale Next.js dev output can trigger the single
container-recreate budget.
container-recreate budget. The repository runtime guard also fingerprints
dependencies, clears only the five owned `.next/dev` directories on each true
managed start, and checks semantic readiness for each Next.js app. If a route
returns the known stale `404` HTML response, it requests one bounded full-cache
repair for that app and rechecks the apps; unexpected responses fail closed
without deleting caches.

The image also carries uv `0.11.12` and selects Python 3.12, matching the
analytics image and lint CI so the root quality gate runs inside the container.
Expand Down
1 change: 1 addition & 0 deletions .devcontainer/post-create.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ retry() {

echo "[post-create] Installing dependencies (pnpm)..."
pnpm install --no-frozen-lockfile
bash ./util/dev-runtime.sh stamp-dependencies

# Build the workspace PACKAGES (graphql, prisma, util, markdown, transactional,
# types, i18n, ...) the apps import — turbo orders them by their dep graph, and
Expand Down
66 changes: 60 additions & 6 deletions .devcontainer/post-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,66 @@ done
# Run every routed app, both Hatchet workers, and both internal MCP services
# without Infisical. Devrouter owns generic locking, process-group identity,
# and bounded replacement; this repository owns only the application command
# and environment above.
"$DEVROUTER_PROCESS_HELPER" ensure \
--name klicker-dev \
--match 'turbo run dev' \
--log /tmp/dev.log \
-- pnpm run dev:container
# and environment above. The runtime wrapper detects dependency changes and
# repairs confirmed stale Next.js dev output once before failing closed.
start_managed_runtime() {
local runtime_fingerprint runtime_generation

runtime_fingerprint="$(bash ./util/dev-runtime.sh fingerprint)"
runtime_generation="$(bash ./util/dev-runtime.sh generation)"
"$DEVROUTER_PROCESS_HELPER" ensure \
--name klicker-dev \
--match 'turbo run dev' \
--log /tmp/dev.log \
-- bash ./util/dev-runtime.sh start "$runtime_fingerprint" "$runtime_generation" \
-- pnpm run dev:container
}

start_managed_runtime

STALE_NEXT_APPS=()
run_readiness_pass() {
local app status=0

STALE_NEXT_APPS=()
for app in auth chat frontend-control frontend-manage frontend-pwa; do
status=0
bash ./util/dev-runtime.sh wait-app "$app" || status=$?
if [ "$status" -eq 20 ]; then
STALE_NEXT_APPS+=("$app")
elif [ "$status" -ne 0 ]; then
echo "[post-start] ERROR: $app failed semantic readiness; no cache cleanup was attempted." >&2
echo '[post-start] Inspect /tmp/dev.log for the application failure.' >&2
return "$status"
fi
done

if [ "${#STALE_NEXT_APPS[@]}" -gt 0 ]; then
echo "[post-start] Confirmed stale Next.js route state: ${STALE_NEXT_APPS[*]}." >&2
return 20
fi
return 0
}

READINESS_STATUS=0
run_readiness_pass || READINESS_STATUS=$?
if [ "$READINESS_STATUS" -eq 20 ]; then
echo "[post-start] Repairing the confirmed stale .next caches once: ${STALE_NEXT_APPS[*]}."
for app in "${STALE_NEXT_APPS[@]}"; do
bash ./util/dev-runtime.sh request-repair "$app"
done
start_managed_runtime

READINESS_STATUS=0
run_readiness_pass || READINESS_STATUS=$?
if [ "$READINESS_STATUS" -ne 0 ]; then
echo '[post-start] ERROR: The runtime remained unhealthy after its one repair attempt.' >&2
echo '[post-start] Inspect /tmp/dev.log; no further cache cleanup was attempted.' >&2
exit 1
fi
elif [ "$READINESS_STATUS" -ne 0 ]; then
exit 1
fi

if [ -s /etc/devrouter/mkcert-rootCA.pem ]; then
cat <<EOF
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ output/
# Devcontainer: runtime-minted Hatchet client token (post-create writes it)
.devcontainer/.hatchet.env
.devcontainer/.local-kb-services.env
.devcontainer/.runtime/
playwright/playwright-report/
playwright/test-results/

Expand Down
29 changes: 9 additions & 20 deletions apps/frontend-manage/src/components/common/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ import {
CountCatalogSharingRequestsDocument,
GetUserCoursesDocument,
GetUserRunningLiveQuizzesDocument,
User,
type User,
UserRole,
} from '@klicker-uzh/graphql/dist/ops'
import {
Navigation,
NavigationItemProps,
NavigationMenuItemProps,
type NavigationItemProps,
type NavigationMenuItemProps,
} from '@uzh-bf/design-system'
import { useTranslations } from 'next-intl'
import Image from 'next/image'
import { useRouter } from 'next/router'
import { useTranslations } from 'next-intl'
import { useState } from 'react'
import { twMerge } from 'tailwind-merge'
import SupportModal from './SupportModal'
Expand All @@ -42,17 +42,6 @@ function Header({ user }: { user?: User | null }): React.ReactElement {
const courses = courseData?.userCourses

const resourceElements: NavigationMenuItemProps[] = [
...(user?.privatePreview
? [
{
key: 'knowledge-bases-item',
type: 'link' as const,
label: t('kb.title'),
onClick: () => router.push('/resources/knowledgeBases'),
data: { cy: 'knowledge-bases' },
},
]
: []),
{
key: 'knowledge-bases-item',
type: 'link' as const,
Expand Down Expand Up @@ -131,23 +120,23 @@ function Header({ user }: { user?: User | null }): React.ReactElement {
key: 'library-menubar-item',
label: t('manage.general.library'),
onClick: () => router.push('/'),
active: router.pathname == '/',
active: router.pathname === '/',
data: { cy: 'library' },
},
{
type: 'button',
key: 'activities-menubar-item',
label: t('shared.generic.activities'),
onClick: () => router.push('/activities'),
active: router.pathname == '/activities',
active: router.pathname === '/activities',
data: { cy: 'activities' },
},
{
type: 'button',
key: 'courses-menubar-item',
label: t('manage.general.courses'),
onClick: () => router.push('/courses'),
active: router.pathname == '/courses',
active: router.pathname === '/courses',
data: { cy: 'courses' },
},

Expand All @@ -158,7 +147,7 @@ function Header({ user }: { user?: User | null }): React.ReactElement {
icon: faBolt,
active:
router.pathname.startsWith('/resources/knowledgeBases') ||
router.pathname == '/resources/answerCollections' ||
router.pathname === '/resources/answerCollections' ||
router.pathname === '/resources/chatbots' ||
router.pathname === '/resources/catalog' ||
router.pathname === '/resources/userGroups' ||
Expand Down Expand Up @@ -295,7 +284,7 @@ function Header({ user }: { user?: User | null }): React.ReactElement {
type: 'link',
label: t('shared.generic.logout'),
onClick: () =>
router.push(process.env.NEXT_PUBLIC_AUTH_URL + '/logout'),
router.push(`${process.env.NEXT_PUBLIC_AUTH_URL}/logout`),
data: { cy: 'logout' },
},
],
Expand Down
2 changes: 2 additions & 0 deletions docs/frontend-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ The lecturer routes `apps/frontend-manage/src/pages/resources/knowledgeBases.tsx

The catalog uses server search and cursor-driven “load more” rather than loading all owned KBs. The detail page keeps metadata/metrics separate from `packages/kb-management/src/components/KnowledgeBaseResourceList.tsx:KnowledgeBaseResourceList`, which owns server search plus design-system type/status filters, selection, confirmed bulk deletion, the source inspector, and contextual Ingest/Retry/Re-ingest/Delete actions. While any loaded row is `QUEUED`/`PROCESSING`, the two-second interval fetches page zero plus pages known to contain active rows and runs a full loaded-window walk every tenth tick. Cursor or page-length drift triggers an immediate full walk. Promise-only polls use `ApolloClient.query` with `no-cache`; generation fencing, the latest loaded-count ref, and shared cache merge preserve the loaded window and remove rows from selection when they become active. Show indeterminate real-operation progress and safe-to-leave messaging rather than invented percentages.

The detail resource workspace presents that loaded window as a semantic design-system table with source, operation, serving, update, selection, and contextual action metadata. Use one `+ Add resource` action that opens the chooser for Website or Document; keep Video visibly unavailable until its ingestion contract exists. Keep destructive resource deletion in the row overflow menu, and preserve the explicit inspector Ingest action for resources that are only `ADDED`. When a file upload has requested its ticket, keep the chooser open and non-dismissible until upload confirmation or a terminal failure.

The inspector loads the owner-checked five-attempt history lazily. Full attempt history must stay outside the two-second list poll. Lecturer-facing failure detail is localized from stable status/error codes; raw platform messages are not rendered. Transport tuning is not user-controlled. Changes must preserve EN/DE messages, `data-cy` hooks, keyboard/focus behavior, and browser evidence for desktop plus 390 px mobile states, including search/filter, selection/confirmation, empty, active, ready, failed, and replacement-cutover feedback where affected.

KB and resource deletion dialogs explain the two observable phases: the item disappears immediately, while stored files and the external index are removed in the background. Success toasts confirm removal from the lecturer view without claiming that external cleanup has already completed.
Expand Down
2 changes: 2 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ The image does include the repository's development toolchain: pnpm `11.5.0`, uv

`devrouter doctor --repo .` is the static check. `devrouter ensure .` is the runtime authority: it resolves the checkout-specific overlay and fails unless the actual container aliases, Git mount, managed process, and routes agree.

The managed adapter adds a repository-owned semantic readiness guard. It fingerprints dependencies, keeps dependency changes from reusing stale Next.js dev output, and repairs confirmed stale `404` HTML responses once per startup. Unexpected responses fail closed without cache deletion. Run `pnpm run dev:doctor` for the read-only runtime diagnosis and `pnpm run test:dev-runtime` for the shell-level guard checks.

### Path B: Host-based Setup (Legacy)

Runs all services on your host machine. Needs Traefik (`*.klicker.com` reverse proxy), mkcert, `/etc/hosts` configurations, and Infisical for secret injection.
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"dev:cleverreach": "./util/_run_with_infisical.sh --env dev-cleverreach pnpm run dev:raw",
"dev:container": "turbo run dev --filter=@klicker-uzh/backend-docker --filter=@klicker-uzh/auth --filter=@klicker-uzh/frontend-pwa --filter=@klicker-uzh/frontend-manage --filter=@klicker-uzh/frontend-control --filter=@klicker-uzh/olat-api --filter=@klicker-uzh/response-api --filter=@klicker-uzh/lti-service --filter=@klicker-uzh/chat --filter=@klicker-uzh/hatchet-worker-general --filter=@klicker-uzh/hatchet-worker-response-processor --filter=@klicker-uzh/mcp-lecturer --filter=@klicker-uzh/mcp-student --concurrency 30",
"dev:docs": "turbo run dev:docs",
"dev:doctor": "bash ./util/dev-runtime.sh doctor",
"dev:lti": "./util/_run_with_infisical.sh --env dev-lti turbo run dev:lti",
"dev:offline": "./util/_run_with_infisical.sh --env dev turbo run dev:offline --concurrency 30",
"dev:playwright": "./util/_run_with_infisical.sh --env dev-playwright bash ./util/_with_local_test_origins.sh cross-env NODE_ENV=test turbo run dev:test --concurrency 30",
Expand Down Expand Up @@ -100,6 +101,7 @@
"syncpack:mismatches": "syncpack list-mismatches",
"syncpack:mismatches:fix": "syncpack fix-mismatches",
"syncpack:update": "syncpack update",
"test:dev-runtime": "bash ./util/test-dev-runtime.sh",
"test:run": "turbo run test:run",
"test:run:playwright": "pnpm --filter @klicker-uzh/playwright test:run",
"test:watch": "run-p --npm-path pnpm test dev:playwright"
Expand Down
18 changes: 18 additions & 0 deletions packages/i18n/messages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1540,6 +1540,7 @@ Da die KlickerUZH-App noch nicht im iOS-App-Store verfügbar ist, folgen Sie die
'{resources, plural, one {# Ressource} other {# Ressourcen}} · {chatbots, plural, one {# verknüpfter Chatbot} other {# verknüpfte Chatbots}}',
loadMore: 'Weitere Wissensdatenbanken laden',
notFound: 'Die Wissensdatenbank konnte nicht gefunden werden.',
detailFallbackTitle: 'Wissensdatenbank',
backToList: 'Zurück zu den Wissensdatenbanken',
metricsTitle: 'Nutzung und Verknüpfungen',
metricVisibleResources: 'Sichtbare Ressourcen',
Expand Down Expand Up @@ -1574,6 +1575,8 @@ Da die KlickerUZH-App noch nicht im iOS-App-Store verfügbar ist, folgen Sie die
linkSuccess: 'Link wurde zur Wissensdatenbank hinzugefügt.',
linkError: 'Der Link konnte nicht hinzugefügt werden.',
resourcesTitle: 'Ressourcen',
resourceColumn: 'Ressource',
resourceActions: 'Aktionen',
resourcesLoadError: 'Die Ressourcen konnten nicht geladen werden.',
searchResources: 'Ressourcen suchen',
searchResourcesPlaceholder: 'Titel, Dateiname oder URL suchen',
Expand All @@ -1582,13 +1585,28 @@ Da die KlickerUZH-App noch nicht im iOS-App-Store verfügbar ist, folgen Sie die
filterAll: 'Alle',
typeFile: 'Datei',
typeUrl: 'Link',
addResource: 'Ressource hinzufügen',
addResourceTitle: 'Ressource hinzufügen',
addResourceDescription:
'Wählen Sie, wie Sie diese Ressource hinzufügen möchten.',
addWebsite: 'Website',
addWebsiteDescription: 'Eine Website-URL zur Verarbeitung registrieren.',
addDocument: 'Dokument',
addDocumentDescription: 'Eine PDF-, TXT- oder Markdown-Datei hochladen.',
addVideo: 'Video',
comingSoon: 'Demnächst verfügbar',
configure: 'Konfigurieren',
backToResourceTypes: 'Zurück',
noResourceResults: 'Keine Ressourcen entsprechen diesen Filtern.',
resourceResultCount:
'{count, plural, =0 {Keine Ressourcen} one {# Ressource} other {# Ressourcen}}',
selectAllPage: 'Bis zu 50 verfügbare Ressourcen auswählen',
selectResource: '„{title}“ auswählen',
loadMoreResources: 'Weitere Ressourcen laden',
noResources: 'Es wurden noch keine Ressourcen hinzugefügt.',
emptyResourceHint:
'Verwenden Sie oben «Ressource hinzufügen», um eine Website oder ein Dokument hinzuzufügen.',
updatedAtLabel: 'Aktualisiert',
updatedAt: 'Aktualisiert {date}',
statusAdded: 'Hinzugefügt',
statusQueued: 'In Warteschlange',
Expand Down
16 changes: 16 additions & 0 deletions packages/i18n/messages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1531,6 +1531,7 @@ Since the KlickerUZH app is not yet available in the iOS App Store, follow these
'{resources, plural, one {# resource} other {# resources}} · {chatbots, plural, one {# connected chatbot} other {# connected chatbots}}',
loadMore: 'Load more knowledge bases',
notFound: 'The knowledge base could not be found.',
detailFallbackTitle: 'Knowledge base',
backToList: 'Back to knowledge bases',
metricsTitle: 'Usage and connections',
metricVisibleResources: 'Visible resources',
Expand Down Expand Up @@ -1563,6 +1564,8 @@ Since the KlickerUZH app is not yet available in the iOS App Store, follow these
linkSuccess: 'Link added to the knowledge base.',
linkError: 'The link could not be added.',
resourcesTitle: 'Resources',
resourceColumn: 'Resource',
resourceActions: 'Actions',
resourcesLoadError: 'The resources could not be loaded.',
searchResources: 'Search resources',
searchResourcesPlaceholder: 'Search title, filename or URL',
Expand All @@ -1571,13 +1574,26 @@ Since the KlickerUZH app is not yet available in the iOS App Store, follow these
filterAll: 'All',
typeFile: 'File',
typeUrl: 'Link',
addResource: 'Add resource',
addResourceTitle: 'Add a resource',
addResourceDescription: 'Choose how you want to add this resource.',
addWebsite: 'Website',
addWebsiteDescription: 'Register a website URL for ingestion.',
addDocument: 'Document',
addDocumentDescription: 'Upload a PDF, TXT or Markdown file.',
addVideo: 'Video',
comingSoon: 'Coming soon',
configure: 'Configure',
backToResourceTypes: 'Back',
noResourceResults: 'No resources match these filters.',
resourceResultCount:
'{count, plural, =0 {No resources} one {# resource} other {# resources}}',
selectAllPage: 'Select up to 50 available resources',
selectResource: 'Select “{title}”',
loadMoreResources: 'Load more resources',
noResources: 'No resources have been added yet.',
emptyResourceHint: 'Use Add resource above to add a website or document.',
updatedAtLabel: 'Updated',
updatedAt: 'Updated {date}',
statusAdded: 'Added',
statusQueued: 'Queued',
Expand Down
Loading
Loading