Compare commits
162 Commits
claude/rea
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 39e448e807 | |||
|
|
17cb30003d | ||
|
|
85452f66a7 | ||
| e73700763d | |||
|
|
182fb2357f | ||
| b1d3686d35 | |||
|
|
b1330ebe3e | ||
|
|
5214c9db3b | ||
| 48715df147 | |||
| 85204938df | |||
|
|
cbce4555ff | ||
|
|
6bf8bad5fb | ||
|
|
54137122a7 | ||
| 8decdd454f | |||
|
|
897b46d4a1 | ||
|
|
80f738ddcb | ||
| 1a1351ddcb | |||
|
|
d79e69aad2 | ||
|
|
89d3395a62 | ||
|
|
d5191073f0 | ||
| fab7a12f7b | |||
|
|
fe99381cb7 | ||
|
|
3af105bccd | ||
| 5f34a6f825 | |||
|
|
9395ea11fe | ||
| e310b6d85b | |||
| 2b2921f6ed | |||
|
|
2274de4de7 | ||
|
|
eb08c4ad96 | ||
|
|
218f6e7d64 | ||
|
|
43a71e2110 | ||
|
|
d1a1cc913c | ||
|
|
8160242be5 | ||
|
|
d38c75beb1 | ||
|
|
6a1f5556a9 | ||
|
|
ad5be01b70 | ||
|
|
ce276f0296 | ||
|
|
47a738fde3 | ||
|
|
9fb22b8090 | ||
|
|
6309ae716b | ||
|
|
6ea8860b96 | ||
|
|
7b6a5b4bf0 | ||
|
|
3aa32c383e | ||
|
|
07af2783dc | ||
|
|
7066f881f9 | ||
|
|
febc9dc7f2 | ||
|
|
46d2735237 | ||
|
|
daec9cf5b5 | ||
|
|
9b3d7fde8c | ||
|
|
84e7468841 | ||
|
|
9ea5d5444d | ||
|
|
a653812cd8 | ||
|
|
f16438c1bc | ||
|
|
7164317908 | ||
|
|
3c04bab1b9 | ||
|
|
80532b6d1b | ||
|
|
e4030868b4 | ||
|
|
7d1fe83f72 | ||
|
|
b24ddc5490 | ||
|
|
74e18d2638 | ||
|
|
9ba9ec94ad | ||
|
|
8291a52ba9 | ||
|
|
6968bebbc2 | ||
|
|
ac88c95b46 | ||
|
|
d4066a8f61 | ||
|
|
042dfb2d92 | ||
|
|
18e98380d9 | ||
|
|
cd151aace4 | ||
|
|
55bcb689e7 | ||
|
|
8930ac03ae | ||
|
|
f55ec950aa | ||
|
|
967c68d27d | ||
|
|
b07c4808a6 | ||
|
|
e9f37056b6 | ||
|
|
5965793f11 | ||
|
|
10d5066be8 | ||
|
|
c5e23c77cd | ||
|
|
8e01b21a50 | ||
|
|
7d8999255f | ||
|
|
815cf0f673 | ||
|
|
8684ffa35b | ||
|
|
14286d6cb1 | ||
|
|
f4d0c85c55 | ||
|
|
472685f0d7 | ||
|
|
dda20612e9 | ||
|
|
881148357e | ||
|
|
7b6ae265db | ||
|
|
ca8945ea5b | ||
|
|
dc628644b6 | ||
|
|
3626cc0525 | ||
|
|
25cae2fea9 | ||
| a38ad5d1e0 | |||
|
|
229246f7b6 | ||
| 70db8b5c2d | |||
|
|
66e0c275da | ||
|
|
c82e4fc3a1 | ||
| fd3b849c19 | |||
|
|
aeb197d5f4 | ||
| 9771928926 | |||
|
|
40eff976b4 | ||
| 33529dfb2b | |||
|
|
f838755991 | ||
|
|
8a8745fad2 | ||
| a5c18ccd0f | |||
|
|
b85785a0cb | ||
|
|
4a8dbee7df | ||
| d07d15b2a7 | |||
|
|
db5bb854c3 | ||
|
|
6fe66bbd93 | ||
|
|
2752fb95d9 | ||
|
|
caaf2b9eba | ||
|
|
d6c1813f75 | ||
|
|
d5655d2232 | ||
|
|
08aaed591f | ||
|
|
8529def748 | ||
|
|
d23b0b6b16 | ||
|
|
fa5dcaeb01 | ||
|
|
190d1a6e0b | ||
|
|
d2b067735c | ||
|
|
00541f4a08 | ||
|
|
3555048308 | ||
|
|
590912ac04 | ||
|
|
d71caa41f6 | ||
|
|
f68d76e3d2 | ||
|
|
7b84545dc5 | ||
|
|
f35550f270 | ||
|
|
9f3e1113a6 | ||
|
|
07a41938f1 | ||
|
|
3afef7785e | ||
|
|
d68164880f | ||
|
|
08f5b1fe18 | ||
|
|
228d0d7a54 | ||
|
|
06eb974825 | ||
|
|
5659ff6726 | ||
|
|
5b37c04588 | ||
|
|
98e32d8ac0 | ||
|
|
43d01dff58 | ||
|
|
b28956bbe7 | ||
|
|
e2de7f0729 | ||
|
|
2374413282 | ||
|
|
77d411b77e | ||
|
|
775c3030d5 | ||
|
|
1bc51b3cb6 | ||
|
|
1ba647fecd | ||
|
|
271baba860 | ||
|
|
8b57e10754 | ||
|
|
e68335e159 | ||
|
|
bb39a5fa71 | ||
|
|
267fa340a8 | ||
|
|
6e25cdbf25 | ||
|
|
4b7562749d | ||
|
|
7234074c21 | ||
|
|
0a73ab3466 | ||
|
|
aaafe413f6 | ||
|
|
e223836d7d | ||
|
|
0673122536 | ||
|
|
bb0aef92cf | ||
|
|
74ba5d3dc0 | ||
|
|
42d7209773 | ||
|
|
59e746a8a8 | ||
|
|
e783c5f1e7 | ||
|
|
3a4f6d7c22 |
48
.agents/skills/caveman/README.md
Normal file
48
.agents/skills/caveman/README.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# caveman
|
||||||
|
|
||||||
|
Talk like smart caveman. Same brain, fewer tokens.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
Compress every model response to caveman-style prose. Drops articles, filler, pleasantries, and hedging. Keeps every technical detail, code block, error string, and symbol exact. Cuts ~65-75% of output tokens with full accuracy preserved. Mode persists for the whole session until changed or stopped.
|
||||||
|
|
||||||
|
Six intensity levels:
|
||||||
|
|
||||||
|
| Level | What change |
|
||||||
|
|-------|-------------|
|
||||||
|
| `lite` | Drop filler/hedging. Sentences stay full. Professional but tight. |
|
||||||
|
| `full` | Default. Drop articles, fragments OK, short synonyms. |
|
||||||
|
| `ultra` | Bare fragments. Abbreviations (DB, auth, fn). Arrows for causality. |
|
||||||
|
| `wenyan-lite` | Classical Chinese register, light compression. |
|
||||||
|
| `wenyan-full` | Maximum 文言文. 80-90% character reduction. |
|
||||||
|
| `wenyan-ultra` | Extreme classical compression. |
|
||||||
|
|
||||||
|
Auto-clarity rule: caveman drops to normal prose for security warnings, irreversible-action confirmations, multi-step sequences where fragment ambiguity risks misread, and when user repeats a question. Resumes after the clear part.
|
||||||
|
|
||||||
|
## How to invoke
|
||||||
|
|
||||||
|
```
|
||||||
|
/caveman # full mode (default)
|
||||||
|
/caveman lite # lighter compression
|
||||||
|
/caveman ultra # extreme compression
|
||||||
|
/caveman wenyan # classical Chinese
|
||||||
|
stop caveman # back to normal prose
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example output
|
||||||
|
|
||||||
|
Question: "Why does my React component re-render?"
|
||||||
|
|
||||||
|
Normal prose:
|
||||||
|
> Your component re-renders because you create a new object reference each render. Wrapping it in `useMemo` will fix the issue.
|
||||||
|
|
||||||
|
Caveman (full):
|
||||||
|
> New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`.
|
||||||
|
|
||||||
|
Caveman (ultra):
|
||||||
|
> Inline obj prop → new ref → re-render. `useMemo`.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions
|
||||||
|
- [Caveman README](../../README.md) — repo overview, install, benchmarks
|
||||||
74
.agents/skills/caveman/SKILL.md
Normal file
74
.agents/skills/caveman/SKILL.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
name: caveman
|
||||||
|
description: >
|
||||||
|
Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman
|
||||||
|
while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra,
|
||||||
|
wenyan-lite, wenyan-full, wenyan-ultra.
|
||||||
|
Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens",
|
||||||
|
"be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested.
|
||||||
|
---
|
||||||
|
|
||||||
|
Respond terse like smart caveman. All technical substance stay. Only fluff die.
|
||||||
|
|
||||||
|
## Persistence
|
||||||
|
|
||||||
|
ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. Off only: "stop caveman" / "normal mode".
|
||||||
|
|
||||||
|
Default: **full**. Switch: `/caveman lite|full|ultra`.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Technical terms exact. Code blocks unchanged. Errors quoted exact.
|
||||||
|
|
||||||
|
Pattern: `[thing] [action] [reason]. [next step].`
|
||||||
|
|
||||||
|
Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
|
||||||
|
Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
|
||||||
|
|
||||||
|
## Intensity
|
||||||
|
|
||||||
|
| Level | What change |
|
||||||
|
|-------|------------|
|
||||||
|
| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight |
|
||||||
|
| **full** | Drop articles, fragments OK, short synonyms. Classic caveman |
|
||||||
|
| **ultra** | Abbreviate prose words (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X → Y), one word when one word enough. Code symbols, function names, API names, error strings: never abbreviate |
|
||||||
|
| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register |
|
||||||
|
| **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) |
|
||||||
|
| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse |
|
||||||
|
|
||||||
|
Example — "Why React component re-render?"
|
||||||
|
- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`."
|
||||||
|
- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
|
||||||
|
- ultra: "Inline obj prop → new ref → re-render. `useMemo`."
|
||||||
|
- wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。"
|
||||||
|
- wenyan-full: "物出新參照,致重繪。useMemo .Wrap之。"
|
||||||
|
- wenyan-ultra: "新參照→重繪。useMemo Wrap。"
|
||||||
|
|
||||||
|
Example — "Explain database connection pooling."
|
||||||
|
- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead."
|
||||||
|
- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead."
|
||||||
|
- ultra: "Pool = reuse DB conn. Skip handshake → fast under load."
|
||||||
|
- wenyan-full: "池reuse open connection。不每req新開。skip handshake overhead。"
|
||||||
|
- wenyan-ultra: "池reuse conn。skip handshake → fast。"
|
||||||
|
|
||||||
|
## Auto-Clarity
|
||||||
|
|
||||||
|
Drop caveman when:
|
||||||
|
- Security warnings
|
||||||
|
- Irreversible action confirmations
|
||||||
|
- Multi-step sequences where fragment order or omitted conjunctions risk misread
|
||||||
|
- Compression itself creates technical ambiguity (e.g., `"migrate table drop column backup first"` — order unclear without articles/conjunctions)
|
||||||
|
- User asks to clarify or repeats question
|
||||||
|
|
||||||
|
Resume caveman after clear part done.
|
||||||
|
|
||||||
|
Example — destructive op:
|
||||||
|
> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone.
|
||||||
|
> ```sql
|
||||||
|
> DROP TABLE users;
|
||||||
|
> ```
|
||||||
|
> Caveman resume. Verify backup exist first.
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
|
||||||
|
Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end.
|
||||||
35
.github/workflows/deploy-dev.yml
vendored
Normal file
35
.github/workflows/deploy-dev.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
name: Deploy to Dev
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Ansible and collections
|
||||||
|
run: |
|
||||||
|
pip install --break-system-packages ansible
|
||||||
|
ansible-galaxy collection install ansible.posix community.docker
|
||||||
|
|
||||||
|
- name: Set up SSH key
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${{ secrets.SRV_SSH_KEY }}" > ~/.ssh/deploy_key
|
||||||
|
chmod 600 ~/.ssh/deploy_key
|
||||||
|
|
||||||
|
- name: Run Ansible playbook
|
||||||
|
run: |
|
||||||
|
ansible-playbook \
|
||||||
|
-i infra/development/hosts.ini \
|
||||||
|
-e "ansible_ssh_private_key_file=~/.ssh/deploy_key" \
|
||||||
|
-e "anthropic_api_key=${{ secrets.ANTHROPIC_API_KEY }}" \
|
||||||
|
-e "entra_tenant_id=${{ secrets.ENTRA_TENANT_ID }}" \
|
||||||
|
-e "entra_client_id=${{ secrets.ENTRA_CLIENT_ID }}" \
|
||||||
|
-e "entra_client_secret=${{ secrets.ENTRA_CLIENT_SECRET }}" \
|
||||||
|
-e "entra_admin_emails=${{ secrets.ENTRA_ADMIN_EMAILS }}" \
|
||||||
|
infra/development/site/deploy-playbook.yml
|
||||||
37
.github/workflows/deploy-prod.yml
vendored
Normal file
37
.github/workflows/deploy-prod.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
name: Deploy to Production
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
|
||||||
|
- name: Install Ansible and collections
|
||||||
|
run: |
|
||||||
|
pip install --break-system-packages ansible
|
||||||
|
ansible-galaxy collection install ansible.posix community.docker
|
||||||
|
|
||||||
|
- name: Set up SSH key
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${{ secrets.SRV_SSH_KEY }}" > ~/.ssh/deploy_key
|
||||||
|
chmod 600 ~/.ssh/deploy_key
|
||||||
|
|
||||||
|
- name: Run Ansible playbook
|
||||||
|
run: |
|
||||||
|
ansible-playbook \
|
||||||
|
-i infra/production/hosts.ini \
|
||||||
|
-e "ansible_ssh_private_key_file=~/.ssh/deploy_key" \
|
||||||
|
-e "anthropic_api_key=${{ secrets.ANTHROPIC_API_KEY }}" \
|
||||||
|
-e "entra_tenant_id=${{ secrets.ENTRA_TENANT_ID }}" \
|
||||||
|
-e "entra_client_id=${{ secrets.ENTRA_CLIENT_ID }}" \
|
||||||
|
-e "entra_client_secret=${{ secrets.ENTRA_CLIENT_SECRET }}" \
|
||||||
|
-e "entra_admin_emails=${{ secrets.ENTRA_ADMIN_EMAILS }}" \
|
||||||
|
infra/production/site/deploy-playbook.yml
|
||||||
41
.github/workflows/on-create-tag.yml
vendored
Normal file
41
.github/workflows/on-create-tag.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
name: On Create Tag
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
uses: ./.github/workflows/publish-image.yml
|
||||||
|
with:
|
||||||
|
build_mode: production
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
update-compose:
|
||||||
|
needs: publish
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout main
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
token: ${{ github.token }}
|
||||||
|
|
||||||
|
- name: Update image tag in production compose.yml
|
||||||
|
run: |
|
||||||
|
VERSION=${GITHUB_REF_NAME#v}
|
||||||
|
sed -i "s|\(image:.*:\)[^[:space:]]*|\1${VERSION}|" infra/production/site/compose.yml
|
||||||
|
|
||||||
|
- name: Commit and push
|
||||||
|
run: |
|
||||||
|
git config user.name "respellion-bot"
|
||||||
|
git config user.email "no-reply.git@respellion.nl"
|
||||||
|
git add infra/production/site/compose.yml
|
||||||
|
git commit -m "chore: update prod image to ${GITHUB_REF_NAME#v}"
|
||||||
|
git push origin main
|
||||||
|
|
||||||
|
deploy-prod:
|
||||||
|
needs: update-compose
|
||||||
|
uses: ./.github/workflows/deploy-prod.yml
|
||||||
|
secrets: inherit
|
||||||
20
.github/workflows/on-pull-request-to-main.yml
vendored
Normal file
20
.github/workflows/on-pull-request-to-main.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
name: On Pull Request to Main
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
uses: ./.github/workflows/test.yml
|
||||||
|
|
||||||
|
publish:
|
||||||
|
needs: test
|
||||||
|
uses: ./.github/workflows/publish-image.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
deploy-dev:
|
||||||
|
needs: publish
|
||||||
|
uses: ./.github/workflows/deploy-dev.yml
|
||||||
|
secrets: inherit
|
||||||
21
.github/workflows/on-push-main.yml
vendored
Normal file
21
.github/workflows/on-push-main.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: On Push to Main
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
uses: ./.github/workflows/test.yml
|
||||||
|
|
||||||
|
publish:
|
||||||
|
needs: test
|
||||||
|
uses: ./.github/workflows/publish-image.yml
|
||||||
|
with:
|
||||||
|
build_mode: development
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
deploy-dev:
|
||||||
|
needs: publish
|
||||||
|
uses: ./.github/workflows/deploy-dev.yml
|
||||||
|
secrets: inherit
|
||||||
56
.github/workflows/publish-image.yml
vendored
Normal file
56
.github/workflows/publish-image.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
name: Publish Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
build_mode:
|
||||||
|
description: "Build mode (development or production)"
|
||||||
|
type: string
|
||||||
|
default: production
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
build_mode:
|
||||||
|
description: "Build mode (development or production)"
|
||||||
|
type: string
|
||||||
|
default: production
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.labs.respellion.tech
|
||||||
|
IMAGE: respellion/learning-platform/learning-platform
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to the Container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: respellion-bot
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE }}
|
||||||
|
tags: |
|
||||||
|
type=sha,format=short,prefix=
|
||||||
|
type=raw,value=latest
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
build-args: |
|
||||||
|
BUILD_MODE=${{ inputs.build_mode }}
|
||||||
64
.github/workflows/test.yml
vendored
Normal file
64
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
name: Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: docker build -t learning-platform .
|
||||||
|
|
||||||
|
# The production Caddyfile is otherwise never exercised by CI — the test
|
||||||
|
# image below swaps in Caddyfile.test — so a parse error only surfaced at
|
||||||
|
# deploy time, after the broken container had already replaced the healthy
|
||||||
|
# one (issue #20/#26). Validate it here, inside the freshly built image
|
||||||
|
# where it lives at /etc/caddy/Caddyfile. This runs caddy against the
|
||||||
|
# exact file that ships and needs no bind mount — a `-v "$PWD":...` mount
|
||||||
|
# does not propagate to the sibling Docker daemon in the Gitea runner,
|
||||||
|
# which is why the earlier mount-based step failed with
|
||||||
|
# "open Caddyfile: no such file or directory" (#26).
|
||||||
|
# Caddyfile.test needs no separate check: the "Test homepage" step below
|
||||||
|
# runs the test image and fails if that config can't serve.
|
||||||
|
- name: Validate production Caddyfile
|
||||||
|
run: |
|
||||||
|
docker run --rm --entrypoint caddy learning-platform \
|
||||||
|
validate --adapter caddyfile --config /etc/caddy/Caddyfile
|
||||||
|
|
||||||
|
- name: Build test image
|
||||||
|
run: |
|
||||||
|
cat > Dockerfile.test <<'EOF'
|
||||||
|
FROM learning-platform
|
||||||
|
COPY Caddyfile.test /etc/caddy/Caddyfile
|
||||||
|
EOF
|
||||||
|
docker build -t learning-platform-test -f Dockerfile.test .
|
||||||
|
|
||||||
|
- name: Start container
|
||||||
|
run: |
|
||||||
|
docker run -d --network gitea --name test-container learning-platform-test
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
- name: Test homepage
|
||||||
|
run: |
|
||||||
|
status=$(curl -s -o /dev/null -w "%{http_code}" http://test-container)
|
||||||
|
if [ "$status" != "200" ]; then
|
||||||
|
echo "Homepage failed with status $status"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✓ Homepage OK"
|
||||||
|
|
||||||
|
- name: Test security headers
|
||||||
|
run: |
|
||||||
|
headers=$(curl -sI http://test-container)
|
||||||
|
echo "$headers" | grep -q "X-Frame-Options" || (echo "Missing X-Frame-Options" && exit 1)
|
||||||
|
echo "$headers" | grep -q "X-Content-Type-Options" || (echo "Missing X-Content-Type-Options" && exit 1)
|
||||||
|
echo "✓ Security headers OK"
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
if: always()
|
||||||
|
run: docker stop test-container && docker rm test-container
|
||||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -22,3 +22,16 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
# Secrets
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# PocketBase
|
||||||
|
pb_data/
|
||||||
|
pocketbase.exe
|
||||||
|
pocketbase.zip
|
||||||
|
|
||||||
|
# Claude Code local agent state (settings.local.json holds per-user
|
||||||
|
# command allowlists; never share across teammates)
|
||||||
|
.claude/
|
||||||
|
|||||||
172
AI_AGENT.md
172
AI_AGENT.md
@@ -2,57 +2,153 @@
|
|||||||
|
|
||||||
Welcome, fellow AI agent! If you are reading this, you are tasked with maintaining or extending the Respellion Learning Platform. This document provides the critical context, architectural patterns, and design standards you need to successfully work on this codebase.
|
Welcome, fellow AI agent! If you are reading this, you are tasked with maintaining or extending the Respellion Learning Platform. This document provides the critical context, architectural patterns, and design standards you need to successfully work on this codebase.
|
||||||
|
|
||||||
|
> **Last updated:** 2026-05-26 — Per-user curriculum start (employees enroll on first login; week/cycle derived from each user's `curriculum_started_at`, no longer from the ISO calendar week). Documents the real React/Vite + PocketBase stack, TF-IDF retrieval, 3 learning-content types, 3 micro-learning types, and the tiered Claude model setup.
|
||||||
|
|
||||||
## 1. Architectural Overview
|
## 1. Architectural Overview
|
||||||
This is a single-page React application built with **Vite**. It is completely self-contained and currently runs without a dedicated backend database.
|
This is a single-page React application built with **Vite**, backed by **PocketBase** as the database and auth layer. There is no separate backend server — the browser talks directly to PocketBase, and to the Anthropic API through a reverse proxy.
|
||||||
* **Frontend:** React, React Router, Tailwind CSS.
|
* **Frontend:** React 19, React Router 7, Vanilla CSS (via CSS variables) + Tailwind v4 utilities mapped to those variables.
|
||||||
* **Animations:** Framer Motion (used extensively for page transitions, podium effects, and gamification feedback).
|
* **Backend:** PocketBase (self-hosted, SQLite). All data is stored in PocketBase collections, not localStorage.
|
||||||
|
* **Animations:** Framer Motion (page transitions, podium effects, gamification feedback).
|
||||||
* **Icons:** Lucide React.
|
* **Icons:** Lucide React.
|
||||||
* **Visualizations:** D3.js (used strictly for the Admin Knowledge Graph).
|
* **Visualizations:** D3.js (used strictly for the Admin Knowledge Graph).
|
||||||
|
* **Retrieval:** A dependency-free TF-IDF index over the knowledge graph (`src/lib/retrieval.js`). There is **no Qdrant and no embeddings API** — older specs that mention them describe a design that was never built.
|
||||||
|
|
||||||
|
> The top-level `app/` directory is abandoned Next.js scaffolding from that original design. It is not built or deployed. Ignore it; the real app is `src/`.
|
||||||
|
|
||||||
## 2. State Management & Storage (Critical)
|
## 2. State Management & Storage (Critical)
|
||||||
There is **no backend database**. All data is persisted locally in the browser using a custom wrapper around `window.localStorage` located at `src/lib/storage.js`.
|
All persistent data lives in **PocketBase**. The data access layer is in `src/lib/db.js`, which wraps the PocketBase SDK client from `src/lib/pb.js`.
|
||||||
|
|
||||||
**Namespacing Convention:**
|
**PocketBase Collections (current):**
|
||||||
You must strictly adhere to these prefixes when reading/writing to `storage.js`:
|
* `topics` — Knowledge graph nodes (`id`, `label`, `type`, `description`, `learning_relevance`, `relevance_locked`, `theme`, `complexity_weight`, `difficulty`).
|
||||||
* `kb:*` - Knowledge base data (e.g., `kb:topics`, `kb:relations`, `kb:content:{topicId}`).
|
* `relations` — Knowledge graph edges (`source`, `target`, `type` — `related_to` / `depends_on` / `part_of` / `executed_by`).
|
||||||
* `admin:*` - Admin settings (e.g., `admin:anthropic_key`, `admin:sources`, `admin:use_simulation`).
|
* `content` — AI-generated learning modules per topic (`topic_id`, `data`). The `data` field is a **merged JSON object** containing only the content types generated so far (e.g. `{ article: {...}, slides: [...] }`). New types are shallow-merged in by `learningService.js`; nothing is overwritten.
|
||||||
* `user:{id}:*` - Specific user progress (e.g., `user:{id}:week:{n}:learn_done`).
|
* `micro_learnings` — Generated micro-learning artifacts (`topic_id`, `type`, `content`, `status`). One record per topic per type. `status='published'` items are visible to employees.
|
||||||
* `quiz:bank:{topicId}` - Cached banks of AI-generated multiple-choice questions.
|
* `micro_learning_completions` — Append-only completion events (`team_member_id`, `micro_learning_id`, `topic_id`, `type`, `session_week`).
|
||||||
* `quiz:result:{userId}:week:{n}` - The exact score and payload of a user's weekly test.
|
* `curriculum_versions` — Versioned 26-week schedules (`version_number`, `status`, `generation_reason`, `confirmed_by`, `confirmed_at`, `schedule` JSON, `coverage_stats` JSON). Exactly one `active` at a time.
|
||||||
* `leaderboard:current` - The active points ledger. Array of objects: `{ userId, name, points, testsCompleted, perfectScores }`.
|
* `leaderboard` — Points ledger (`user_id`, `name`, `points`, `tests_completed`, `learnings_completed`).
|
||||||
* `users:registry` - Array of registered users, including admins.
|
* `team_members` — Registered users with PIN auth (`name`, `role`, `pin`, `curriculum_started_at`, `enrollment_status`).
|
||||||
|
* `sources` — Uploaded source documents and extraction status (`name`, `status`, `error`, `progress`).
|
||||||
|
* `settings` — Key/value store (`key`, `value`).
|
||||||
|
* `llm_calls` — Per-call telemetry (`task`, `model`, `tier`, `duration_ms`, token counts, `stop_reason`, `ok`, `error_msg`).
|
||||||
|
|
||||||
|
**Dropped collections:** `quiz_banks`, `quiz_results`, `quiz_cache`, `learn_progress`, and the legacy `curriculum` (v1) collection no longer exist. The matching `db.js` helpers are deprecated stubs — do not build on them.
|
||||||
|
|
||||||
|
**localStorage** is only used for **admin browser settings** (not user data):
|
||||||
|
* `admin:model:fast` / `admin:model:standard` / `admin:model:reasoning` — per-tier model overrides (legacy `admin:model` still honored for `standard`).
|
||||||
|
* `admin:use_simulation` — when true, `callLLM` returns stub data instead of calling Anthropic. Useful for UI work without spending tokens.
|
||||||
|
* `kb:suggestions` — Pending/approved/rejected graph deltas proposed by R42. Always mutated via `kbStore` (see §9).
|
||||||
|
* `quiz:active:{userId}` — Boolean flag set while a user is mid-quiz. R42's launcher is hidden when this is true (quiz-integrity rule).
|
||||||
|
* `chat:thread:{userId}` — Persisted R42 conversation, capped at 50 messages.
|
||||||
|
|
||||||
|
**Session:** Login is PIN-based. The logged-in user's ID is stored in `sessionStorage` under `respellion_session` and resolved against `team_members` on app load (`src/store/AppContext.jsx`).
|
||||||
|
|
||||||
|
**Per-user curriculum position (important — changed):** Each employee starts the curriculum when *they* choose. On first login a blocking onboarding screen (`/onboarding`) records `curriculum_started_at` and flips `enrollment_status` to `active`. `AppContext` derives `state.weekNumber` — an absolute counter starting at 1 — from `getPersonalWeekNumber(curriculum_started_at)` (= `floor(days_since_start / 7) + 1`). The 26-week slot and cycle come from `getCurriculumWeek(n)` (`((n-1) % 26) + 1`) and `getCurriculumCycle(n)` (`floor((n-1)/26)+1`) in `curriculumService.js`. The cycle is **detached from the ISO calendar** — week 1 is simply the first 7 days after the user's start. After week 26 the cycle restarts at week 1 with the same content. `state.weekNumber` is `0` until the user enrolls.
|
||||||
|
|
||||||
|
> Do **not** reintroduce ISO-week-based scheduling or a shared `admin:current_week`. There is no global "current week" anymore — every employee has their own.
|
||||||
|
|
||||||
|
**Auto-Cancellation:** The PocketBase JS SDK has auto-cancellation enabled by default, which aborts concurrent identical requests (common under React StrictMode and `Promise.all`) with `ClientResponseError 0`. It is **globally disabled** via `pb.autoCancellation(false)` in `src/lib/pb.js`. Never re-enable it.
|
||||||
|
|
||||||
|
**PocketBase URL:** Resolved from `VITE_PB_URL`, else `window.location.origin` (`src/lib/pb.js`). In production Caddy proxies `/api/*` and `/_/*` to the PocketBase container.
|
||||||
|
|
||||||
|
**Important:** All `db.js` functions are `async`. Always `await` them — omitting `await` silently passes a Promise where data is expected.
|
||||||
|
|
||||||
## 3. The AI Integration (Anthropic)
|
## 3. The AI Integration (Anthropic)
|
||||||
The application acts as a proxy to the Anthropic API (`claude-sonnet-4`).
|
All Anthropic calls go through one wrapper.
|
||||||
* **Location:** `src/lib/api.js`.
|
* **Location:** `src/lib/llm.js`, function `callLLM(...)`. Callers must never reach `/api/anthropic` directly.
|
||||||
* **Proxy:** In Docker, `/api/anthropic` is proxied via Nginx to bypass CORS restrictions. If the user reports CORS errors during local dev, ensure the Vite proxy in `vite.config.js` matches the Nginx spec.
|
* **Proxy:** Requests hit `/api/anthropic/v1/messages`. In Docker, Caddy proxies this to `https://api.anthropic.com` and injects the `x-api-key` header server-side. In local dev, `vite.config.js` does the same using `process.env.ANTHROPIC_API_KEY`. There is **no client-side API key**.
|
||||||
* **JSON Enforcement:** Prompts *must* strictly enforce that Claude returns *only* raw JSON. Do not let the AI wrap the response in markdown blocks (`\`\`\``) unless you build a regex to strip them (which is currently implemented via `.match(/\{[\s\S]*\}/)`).
|
* **Model tiers:** `fast` = `claude-haiku-4-5-20251001`, `standard` = `claude-sonnet-4-6`, `reasoning` = `claude-opus-4-7`. Choose a tier per task; admins can override per tier from Settings.
|
||||||
|
* **Structured output:** Prefer Anthropic **tool use** with a forced `toolChoice`. Tool inputs are validated against Zod schemas in `src/lib/llmSchemas.js` (auto-looked-up via `toolSchemaRegistry`). For text responses, `parseStructuredText` strips code fences and extracts the outermost balanced JSON.
|
||||||
|
* **Prompt caching:** Wrap stable system text with `cachedSystem(text)` to attach `cache_control: ephemeral`.
|
||||||
|
* **Retry/limits:** `src/lib/llmRetry.js` handles exponential backoff with jitter on retryable statuses (408/425/429/5xx/529), honors `Retry-After`, and provides rate limiters (e.g. the extraction limiter caps ~20 req/min, burst 2). Default `maxTokens` is 4096; extraction and long-form content use 8192.
|
||||||
|
* **Telemetry:** Every call is logged (best-effort, non-blocking) to the `llm_calls` collection.
|
||||||
|
|
||||||
## 4. Design System & Aesthetics
|
## 4. Design System & Aesthetics
|
||||||
Respellion relies on a premium, modern aesthetic.
|
Respellion relies on a premium, modern aesthetic.
|
||||||
* **CSS Variables:** Rely heavily on the variables defined in `src/index.css`.
|
* **CSS Variables:** Rely on the variables in `src/index.css` — `var(--color-bg)`, `var(--color-paper)`, `var(--color-teal)`, `var(--color-accent)`, radii `var(--r-sm)`, `var(--r-lg)`, `var(--r-org)`.
|
||||||
* Colors: `var(--color-bg)`, `var(--color-paper)`, `var(--color-teal)`, `var(--color-accent)`.
|
* **Tailwind:** Tailwind v4 utilities are mapped to these variables. Use classes like `bg-teal`, `text-fg-muted`, `border-bg-warm`. Avoid raw hex codes.
|
||||||
* Radii: `var(--r-sm)`, `var(--r-lg)`, `var(--r-org)` (an organic, pill-like shape used for badges and podiums).
|
* **`stylesheet.css`** (repo root) is the authoritative visual reference and is frozen — do not edit it.
|
||||||
* **Tailwind:** Tailwind is mapped to these CSS variables. Avoid hardcoding random hex codes. Use the existing Tailwind classes like `bg-teal`, `text-fg-muted`, and `border-bg-warm`.
|
* **Components:** Reuse the UI primitives in `src/components/ui/` (`Card.jsx`, `Button.jsx`, `Tag.jsx`, `Input.jsx`).
|
||||||
* **Components:** Always reuse the UI primitives in `src/components/ui/` (`Card.jsx`, `Button.jsx`, `Tag.jsx`, `Input.jsx`).
|
|
||||||
|
|
||||||
## 5. Gamification Rules
|
## 5. Learning Content Types (on-demand, per topic)
|
||||||
If you are extending the Gamification system (`src/pages/Leaderboard.jsx`):
|
`src/lib/learningService.js` generates **three** long-form content types into the `content` collection, on demand:
|
||||||
* Tests grant **+2 points** per correct answer.
|
| Type | Schema key | Description |
|
||||||
* A 100% score grants the **Perfectionist** badge.
|
|---|---|---|
|
||||||
* Completing 1 test grants **First Steps**, completing 5 grants **Veteran**.
|
| `article` | `content.article` | Title, intro, sections, key takeaways |
|
||||||
* Do not reset points dynamically. Read from `leaderboard:current`, mutate, and save back immediately.
|
| `slides` | `content.slides` | Slides with bullets and speaker notes |
|
||||||
|
| `infographic` | `content.infographic` | Headline, tagline, stats, steps, quote |
|
||||||
|
|
||||||
## 6. Docker & Deployment
|
`generateLearningContent(topic, force, selectedType)` accepts one of the three types, or `'all'` (admin regeneration). Cache-hit logic checks `content[selectedType]` directly; new data is shallow-merged so other types are preserved. **There is no podcast type.**
|
||||||
The app is fully containerized.
|
|
||||||
* **Build:** `docker build -t respellion-app:latest .`
|
|
||||||
* **Run:** `docker run -d -p 8080:80 --name respellion respellion-app:latest`
|
|
||||||
* **Nginx:** Ensure any new frontend routes are caught by the Nginx `try_files $uri $uri/ /index.html;` directive defined in `nginx.conf` to prevent 404s on page refresh.
|
|
||||||
|
|
||||||
## 7. How to Add New Features
|
Article refinement uses targeted patch tools (`set_intro`, `set_section`, `add_section`, `remove_section`, `replace_takeaways`) so the model edits only what changed.
|
||||||
1. **Define State:** Decide where the data lives in `localStorage`.
|
|
||||||
2. **Build UI:** Create components using `Card`, `Button`, and `framer-motion` for smooth entry (`animate-in fade-in slide-in-from-bottom-4`).
|
## 6. Micro-Learnings (the weekly session)
|
||||||
3. **Wire Logic:** Connect to `src/store/AppContext.jsx` if it requires global user context, otherwise manage locally and persist to `storage.js`.
|
`src/lib/microLearningService.js` generates short, single-topic interactions into the `micro_learnings` collection. **Three** types are currently active (a former `reflection_prompt` type was dropped):
|
||||||
4. **Test:** Run the Docker container to ensure no build errors exist.
|
| Type | Tier | Shape |
|
||||||
|
|---|---|---|
|
||||||
|
| `concept_explainer` | standard | `{ sections: [{ title, content (HTML) }] }` (≥3 sections) |
|
||||||
|
| `scenario_quiz` | standard | `{ scenario, options: [{ text, isCorrect, explanation }] }` (3–4 options, 1 correct) |
|
||||||
|
| `flashcard_set` | fast (Haiku) | `{ cards: [{ front, back }] }` (5–10 cards) |
|
||||||
|
|
||||||
|
`getOrGenerateMicroLearning(topicId, type)` returns a cached published record if present, else generates and stores one. Completions are recorded via `useMicroLearningCompletions` into `micro_learning_completions` (append-only). A weekly session is "done" when every required topic has at least one completed micro-learning.
|
||||||
|
|
||||||
|
## 7. Weekly Test
|
||||||
|
`src/lib/testService.js` builds a 5-question multiple-choice quiz for the user's current week.
|
||||||
|
* Primary topic comes from the active curriculum week (else a deterministic hash fallback), plus a few review topics for breadth.
|
||||||
|
* Generated in **one** `fast`-tier batch call (`emit_quiz_questions`), with quality gates: no duplicate options, no banned fillers ("all of the above"), explanations ≥20 chars, and a check that `correctIndex` isn't dominated by one position.
|
||||||
|
* Scoring: **+2 points per correct answer** (`saveTestResult` → `score * 2`), written to the `leaderboard` collection.
|
||||||
|
|
||||||
|
## 8. Gamification
|
||||||
|
If you are extending gamification (`src/pages/Leaderboard.jsx`):
|
||||||
|
* Tests grant **+2 points** per correct answer (`testService.js → saveTestResult`).
|
||||||
|
* Badges are computed at render time: **First Steps** (1 test), **Veteran** (5 tests), **Perfectionist** (a 100% score).
|
||||||
|
* Points accumulate in `leaderboard` via `db.upsertLeaderboardEntry`. Admins are excluded from the public board.
|
||||||
|
|
||||||
|
## 9. R42 Chatbot
|
||||||
|
The platform ships a global chatbot avatar called **R42**, rendered as the Respellion `{ r }` brand mark in three states (idle / typing / error).
|
||||||
|
* **Mark component:** `src/components/ui/Mark.jsx` — pure SVG with `state`, `size`, `theme`, `brace`, `letter` props. Uses the `BallPill` font (`@font-face` in `src/index.css`), falling back to JetBrains Mono.
|
||||||
|
* **Chat module:** `src/components/chat/`.
|
||||||
|
* `ChatLauncher.jsx` — global FAB; auto-hides when `quiz:active:{userId}` is set; listens to the `respellion:quiz-state` window event.
|
||||||
|
* `ChatWindow.jsx` — chat panel; renders messages from `useChat`; surfaces graph-delta confirmation chips.
|
||||||
|
* `useChat.js` — owns the message list, persists to `chat:thread:{userId}` (cap 50; only the last ~12 turns are sent to the API), calls `callLLM`.
|
||||||
|
* `prompts.js` — the cacheable system prompt blocks, greeting, and the `propose_graph_delta` tool spec (max 3 topics / 5 relations).
|
||||||
|
* `rag.js` — builds KB context using the TF-IDF index from `src/lib/retrieval.js` (top-K topics + verbatim mentions), filters relations to retrieved topics, and validates proposed deltas (dedupe by id/label, no orphan/self relations, hard caps).
|
||||||
|
* **Model:** R42 runs on the `fast`/standard Claude tier (Haiku/Sonnet); low latency matters for chat.
|
||||||
|
* **Quiz-integrity rule:** `src/pages/Testen.jsx` sets `quiz:active:{userId}=true` on start and clears it on every non-quiz phase + unmount, dispatching `respellion:quiz-state`. Never bypass this — letting users ask R42 mid-quiz would break scoring.
|
||||||
|
* **Graph refinement:** when R42 proposes a `propose_graph_delta`, `rag.js` validates it and a confirmation chip appears inline. **Admin clicks Yes** → `kbStore.applyDelta` writes to PocketBase immediately. **Non-admin clicks Yes** → `kbStore.appendSuggestion` queues a `pending` entry in `kb:suggestions`.
|
||||||
|
* **Admin approval UI:** `src/components/admin/SuggestionsQueue.jsx` lets admins approve (re-runs `applyDelta`) or reject queued suggestions.
|
||||||
|
* **kbStore:** `src/lib/kbStore.js` is the single source of truth for chatbot-path KB mutations. It dispatches `respellion:kb-updated` after writes so the D3 graph and queue refresh.
|
||||||
|
|
||||||
|
## 10. Admin Panel
|
||||||
|
`src/pages/Admin/index.jsx` is tabbed: **Sources** (upload + extraction), **Content** (review/refine generated content), **Quizzes**, **Curriculum** (generate/preview/activate a 26-week schedule), **Graph** (D3 knowledge graph + suggestions queue), **Team** (manage members), **Settings** (model overrides, simulation toggle, smoke-test reset). Source upload lives in `src/components/admin/UploadZone.jsx` (`.txt` / `.md`, ≤5 MB).
|
||||||
|
|
||||||
|
## 11. How to Add New Features
|
||||||
|
1. **Schema:** add a PocketBase collection via the PB Admin UI or a migration in `pb_migrations/` (and mirror it in `scripts/setup-pb-collections.mjs`).
|
||||||
|
2. **DB helpers:** add async CRUD in `src/lib/db.js`.
|
||||||
|
3. **UI:** build with `Card`, `Button`, `Tag`, and Framer Motion entry animations.
|
||||||
|
4. **Logic:** connect to `src/store/AppContext.jsx` for global user/week context; otherwise keep state local.
|
||||||
|
5. **Verify:** `npm test`, `npm run lint`, `npm run build`.
|
||||||
|
|
||||||
|
## 12. Known Gotchas & Decisions
|
||||||
|
* **Per-user curriculum start.** Week/cycle derive from each user's `curriculum_started_at`, not the calendar. There is no shared current week. New users are gated through `/onboarding` until enrolled.
|
||||||
|
* **No podcast type.** Three content types only: `article`, `slides`, `infographic`.
|
||||||
|
* **Three micro-learning types.** `concept_explainer`, `scenario_quiz`, `flashcard_set`. `reflection_prompt` was dropped.
|
||||||
|
* **No Qdrant / no embeddings.** Retrieval is local TF-IDF (`src/lib/retrieval.js`).
|
||||||
|
* **Caddy, not Nginx.** The reverse proxy is Caddy (`Caddyfile`).
|
||||||
|
* **PocketBase auto-cancellation is OFF.** Set in `src/lib/pb.js`; never re-enable.
|
||||||
|
* **Go through `callLLM`.** Never call the Anthropic proxy directly; you lose retry, schema validation, and telemetry.
|
||||||
|
* **AI token budget.** Truncation surfaces as `LLMTruncatedError` (`stop_reason: max_tokens`). For extraction, tighten the prompt's topic cap before raising `max_tokens`.
|
||||||
|
* **PocketBase migrations & the `_migrations` ledger (issue #18).** The deployed Labs/prod databases were originally provisioned WITHOUT `--migrationsDir` (schema came from `scripts/setup-pb-collections.mjs`), so their ledger started empty. `pb_migrations/1000000000_baseline_ledger_sync.js` marks the historical files as applied on such databases — never remove it, never rename an applied migration file (the ledger is filename-based), and give new migrations a timestamp that sorts after the existing ones. A failed migration aborts `pocketbase serve` → container crash-loop → 502 on every `/api/*` call; the deploy playbooks gate on `/api/health` to catch this.
|
||||||
|
* **OIDC provider config lives in a hook, not a migration.** `pb_hooks/entra_oidc.pb.js` reconciles the Entra provider from `ENTRA_*` env on every bootstrap + cron tick. Don't move provider credentials back into a one-shot migration — an apply while the env is empty would permanently disable OAuth2.
|
||||||
|
* **JSVM hook scope.** PocketBase runs each hook callback as an isolated program: top-level functions in a `*.pb.js` file are NOT in scope inside callbacks. Shared helpers live in `pb_hooks/utils.js` and are pulled in per-callback via `require(`${__hooks}/utils.js`)`.
|
||||||
|
|
||||||
|
## 13. 26-Week Per-User Curriculum System
|
||||||
|
The platform uses a **26-week perpetual curriculum cycle**. Every employee covers the knowledge base in focused, thematic weekly blocks, **starting whenever they enroll**.
|
||||||
|
* **Service:** `src/lib/curriculumService.js` — week/cycle math, AI schedule generation, version lifecycle, progress.
|
||||||
|
* **DB:** `curriculum_versions` holds generated JSON schedules; `topics` carry `theme`, `complexity_weight`, `difficulty` as generation input.
|
||||||
|
* **Version lifecycle:** `draft` → `active` → `superseded`. Only one active version at a time (`CurriculumManager.jsx`).
|
||||||
|
* **Per-user weeks:** `getPersonalWeekNumber(startedAt)` yields an absolute week counter; `getCurriculumWeek`/`getCurriculumCycle` map it to the 1–26 slot and cycle. Same content each cycle.
|
||||||
|
* **Topic enrichment:** a one-off AI step assigns `theme`/`complexity_weight`/`difficulty` to topics missing them before generation (`enrichTopicsForCurriculum`, batches of 20).
|
||||||
|
* **Progress:** `getYearProgress(userId, personalWeekNumber)` computes completion for the current cycle.
|
||||||
|
* **Fallback:** if no active version exists, `getAssignedTopic()` falls back to deterministic hash-based assignment. Keep the fallback.
|
||||||
|
|
||||||
Good luck! You are building a platform that empowers continuous learning. Keep it fast, keep it beautiful, and keep the user engaged.
|
Good luck! You are building a platform that empowers continuous learning. Keep it fast, keep it beautiful, and keep the user engaged.
|
||||||
|
|||||||
1016
CHANGELOG.md
Normal file
1016
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
142
CLAUDE.md
Normal file
142
CLAUDE.md
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
## What this is
|
||||||
|
|
||||||
|
The **Respellion Learning Platform** — an internal AI-powered learning app that
|
||||||
|
keeps employees current with the company's evolving knowledge base. Employees
|
||||||
|
follow a perpetual **26-week curriculum**, each working through weekly learning
|
||||||
|
sessions and tests. An AI assistant called **R42** is available on every screen.
|
||||||
|
Admins upload source documents that Claude extracts into a knowledge graph.
|
||||||
|
|
||||||
|
This is a **single-page React application** (Vite) backed by **PocketBase**.
|
||||||
|
There is **no separate backend** — all logic runs in the browser and talks
|
||||||
|
directly to PocketBase collections and (via a proxy) the Anthropic API.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech stack
|
||||||
|
|
||||||
|
| Layer | Technology |
|
||||||
|
|---|---|
|
||||||
|
| Frontend | React 19 + Vite 8, React Router 7 |
|
||||||
|
| Styling | Vanilla CSS (CSS custom properties) + Tailwind v4 utilities mapped to those variables |
|
||||||
|
| Animation | Framer Motion |
|
||||||
|
| Icons | Lucide React |
|
||||||
|
| Graph viz | D3.js (admin knowledge graph only) |
|
||||||
|
| Backend / DB / auth | PocketBase (self-hosted, SQLite) |
|
||||||
|
| AI | Anthropic Claude via a reverse-proxy (Caddy in prod, Vite proxy in dev) |
|
||||||
|
| Retrieval (RAG) | Local TF-IDF over the knowledge graph — **no Qdrant, no embeddings API** |
|
||||||
|
| Validation | Zod on all AI tool output |
|
||||||
|
| Infra | Docker + Caddy; Ansible playbooks under `infra/` |
|
||||||
|
|
||||||
|
**Claude models** (`src/lib/llm.js`, by tier):
|
||||||
|
- `fast` → `claude-haiku-4-5-20251001` (R42 chat, quiz batches, flashcards)
|
||||||
|
- `standard` → `claude-sonnet-4-6` (extraction, article/slides/infographic, curriculum)
|
||||||
|
- `reasoning` → `claude-opus-4-7`
|
||||||
|
|
||||||
|
Admins can override any tier's model string from the Settings tab (persisted in
|
||||||
|
localStorage as `admin:model:{tier}`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Repository structure
|
||||||
|
|
||||||
|
```
|
||||||
|
repo/
|
||||||
|
├── CLAUDE.md ← you are here
|
||||||
|
├── AI_AGENT.md ← detailed architecture/patterns guide — read this
|
||||||
|
├── README.md ← quickstart
|
||||||
|
├── PROTECTED.md ← files you must not modify
|
||||||
|
├── docs/ ← spec files (describe the system as built)
|
||||||
|
├── src/ ← THE APPLICATION (React/Vite)
|
||||||
|
│ ├── pages/ ← route screens (Dashboard, Leren, Testen, Leaderboard, Login, Onboarding, Admin/)
|
||||||
|
│ ├── components/ ← ui primitives, admin panels, chat (R42), micro_learning
|
||||||
|
│ ├── lib/ ← services: llm, db, extractionPipeline, learningService,
|
||||||
|
│ │ microLearningService, testService, curriculumService, retrieval, pb
|
||||||
|
│ ├── hooks/ ← React hooks
|
||||||
|
│ └── store/ ← AppContext (global state)
|
||||||
|
├── pb_migrations/ ← PocketBase schema migrations (JS, applied by the PB binary)
|
||||||
|
├── scripts/ ← setup-pb-collections.mjs (local collection bootstrap)
|
||||||
|
├── public/ ← static assets, fonts
|
||||||
|
├── infra/ ← Ansible deploy playbooks (development / production)
|
||||||
|
├── Caddyfile, Dockerfile, docker-compose.yml ← deployment (frozen)
|
||||||
|
└── stylesheet.css ← authoritative visual reference (frozen)
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note on `/app`:** the top-level `app/` directory is **abandoned scaffolding**
|
||||||
|
> from the original Next.js + Qdrant design that was never shipped. It is not
|
||||||
|
> wired into `package.json`, Vite, or Docker. **Ignore it.** The real app is `/src`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Absolute constraints
|
||||||
|
|
||||||
|
1. Never modify any file listed in `PROTECTED.md`.
|
||||||
|
2. Never modify `stylesheet.css` — it is the authoritative visual reference. Use the
|
||||||
|
CSS variables in `src/index.css` and the Tailwind classes mapped to them.
|
||||||
|
3. Treat the deployment files as frozen unless explicitly asked: `Dockerfile`,
|
||||||
|
`Caddyfile`, `docker-compose.yml`, `infra/`, `.github/workflows/`.
|
||||||
|
4. Never delete files without explicit confirmation.
|
||||||
|
5. Never re-enable PocketBase auto-cancellation — `pb.autoCancellation(false)` in
|
||||||
|
`src/lib/pb.js` is deliberate (see AI_AGENT.md §2).
|
||||||
|
6. There is **no podcast** learning type. It was removed. Do not re-add it.
|
||||||
|
7. Ask before acting when scope is unclear.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code conventions
|
||||||
|
|
||||||
|
- All `src/lib/db.js` functions are `async` — always `await` them.
|
||||||
|
- All Claude tool output is validated through Zod (`src/lib/llmSchemas.js`) before use.
|
||||||
|
Never reach `/api/anthropic` directly — go through `callLLM` in `src/lib/llm.js`.
|
||||||
|
- No client-side API key. The Anthropic key is injected server-side by the proxy.
|
||||||
|
- Reuse the UI primitives in `src/components/ui/` (`Card`, `Button`, `Tag`, `Input`).
|
||||||
|
- Mobile-first — the employee app targets 375px width and scales up.
|
||||||
|
- No hardcoded hex colors; use the design-system CSS variables / Tailwind tokens.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Running locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
./pocketbase.exe serve # or the muchobien/pocketbase Docker image
|
||||||
|
node scripts/setup-pb-collections.mjs # first run only — bootstraps collections
|
||||||
|
npm run dev # Vite dev server (proxies /api/anthropic)
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `ANTHROPIC_API_KEY` in the environment so the Vite proxy can inject it
|
||||||
|
(`vite.config.js`). PocketBase migrations in `pb_migrations/` apply automatically
|
||||||
|
when the PB binary starts.
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `npm run dev` | Vite dev server |
|
||||||
|
| `npm run build` | Production build to `dist/` |
|
||||||
|
| `npm test` | Vitest unit tests |
|
||||||
|
| `npm run lint` | ESLint |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation map
|
||||||
|
|
||||||
|
- `AI_AGENT.md` — detailed patterns, gotchas, and subsystem guide. **Start here for any real work.**
|
||||||
|
- `docs/architecture.md` — system design and data flows
|
||||||
|
- `docs/data-model.md` — every PocketBase collection and field
|
||||||
|
- `docs/ingestion-spec.md` — upload → knowledge-graph extraction
|
||||||
|
- `docs/generation-spec.md` — learning content + micro-learning generation
|
||||||
|
- `docs/curriculum-spec.md` — 26-week per-user curriculum engine
|
||||||
|
- `docs/r42-spec.md` — the R42 chatbot
|
||||||
|
- `docs/auth-spec.md` — Azure (Entra ID) login: OIDC via PocketBase, provisioning, roles
|
||||||
|
- `docs/frontend-spec.md` — screens, routing, onboarding
|
||||||
|
- `docs/gamification-spec.md` — points, badges, leaderboard
|
||||||
|
- `micro-learning-spec.md` — micro-learning learner experience
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When you are uncertain
|
||||||
|
|
||||||
|
Check the spec files in `docs/` and `AI_AGENT.md` first. If the spec does not
|
||||||
|
cover it, read the actual code in `src/` — it is the source of truth. Flag gaps
|
||||||
|
explicitly rather than assuming. Do not treat the abandoned `/app` scaffolding or
|
||||||
|
`legacy`-style references in old comments as guidance.
|
||||||
67
Caddyfile
Normal file
67
Caddyfile
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
:80 {
|
||||||
|
encode gzip zstd
|
||||||
|
|
||||||
|
header {
|
||||||
|
X-Frame-Options "SAMEORIGIN"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
X-XSS-Protection "1; mode=block"
|
||||||
|
Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
Content-Security-Policy "default-src 'self'; connect-src 'self' https://api.github.com https://raw.githubusercontent.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com"
|
||||||
|
Permissions-Policy "geolocation=(), microphone=(), camera=()"
|
||||||
|
}
|
||||||
|
|
||||||
|
handle /api/anthropic/* {
|
||||||
|
uri strip_prefix /api/anthropic
|
||||||
|
reverse_proxy https://api.anthropic.com {
|
||||||
|
header_up Host api.anthropic.com
|
||||||
|
header_up x-api-key "{$ANTHROPIC_API_KEY}"
|
||||||
|
header_up anthropic-dangerous-direct-browser-access "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
handle /api/* {
|
||||||
|
reverse_proxy pocketbase-learning:8090
|
||||||
|
}
|
||||||
|
|
||||||
|
handle /_/* {
|
||||||
|
reverse_proxy pocketbase-learning:8090
|
||||||
|
}
|
||||||
|
|
||||||
|
handle {
|
||||||
|
root * /srv
|
||||||
|
|
||||||
|
@static {
|
||||||
|
path *.css *.js *.png *.jpg *.jpeg *.gif *.webp *.svg *.woff *.woff2 *.ttf
|
||||||
|
}
|
||||||
|
header @static Cache-Control "public, max-age=31536000, immutable"
|
||||||
|
|
||||||
|
@html {
|
||||||
|
path *.html /
|
||||||
|
}
|
||||||
|
header @html Cache-Control "public, max-age=0, must-revalidate"
|
||||||
|
|
||||||
|
try_files {path} /index.html
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_errors {
|
||||||
|
# Don't mask API errors with the SPA shell — return a proper
|
||||||
|
# JSON error so the frontend can display a meaningful message.
|
||||||
|
# NOTE: `respond` only accepts `body`/`close` subdirectives; the
|
||||||
|
# Content-Type must be set via a separate `header` directive. An
|
||||||
|
# invalid subdirective is a Caddyfile parse error and crash-loops
|
||||||
|
# the container at startup (issue #20).
|
||||||
|
@api path /api/*
|
||||||
|
header @api Content-Type application/json
|
||||||
|
respond @api `{"code":{err.status_code},"message":"Backend unavailable"}` {err.status_code}
|
||||||
|
|
||||||
|
rewrite * /index.html
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
|
||||||
|
log {
|
||||||
|
output stdout
|
||||||
|
format console
|
||||||
|
}
|
||||||
|
}
|
||||||
25
Caddyfile.test
Normal file
25
Caddyfile.test
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
:80 {
|
||||||
|
root * /srv
|
||||||
|
file_server
|
||||||
|
|
||||||
|
encode gzip
|
||||||
|
|
||||||
|
header {
|
||||||
|
X-Frame-Options "SAMEORIGIN"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
X-XSS-Protection "1; mode=block"
|
||||||
|
Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
}
|
||||||
|
|
||||||
|
try_files {path} /index.html
|
||||||
|
|
||||||
|
handle_errors {
|
||||||
|
rewrite * /index.html
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
|
||||||
|
log {
|
||||||
|
output stdout
|
||||||
|
format console
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
# 🎯 Overdrachtsdocument – Productie-deployment van **Respellion Learning Platform** met Portainer
|
|
||||||
|
|
||||||
> **Doel** – Dit document geeft een *complete* handleiding zodat een ontwikkelaar of AI-agent de Respellion Learning Platform applicatie kan bouwen, configureren en in productie draaien op een Hetzner-VPS via Portainer.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Project-structuur
|
|
||||||
|
|
||||||
```
|
|
||||||
learning-platform/
|
|
||||||
│
|
|
||||||
├─ Dockerfile # Productie Dockerfile (Nginx + React/Vite)
|
|
||||||
├─ nginx.conf # Nginx configuratie voor Anthropic API proxy
|
|
||||||
├─ docker-compose.prod.yml # Productie-compose configuratie (Caddy SSL + Frontend)
|
|
||||||
└─ src/ # React frontend broncode
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Opmerking**: Deze applicatie is een 100% frontend applicatie ("Single Page Application"). De interne state wordt opgeslagen in `localStorage`. Er is **geen externe backend of database (zoals Postgres)** nodig. De enige netwerk calls gaan via de interne Nginx proxy direct naar de Anthropic API.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Kernbestanden – inhoud & rationale
|
|
||||||
|
|
||||||
### 2.1 `docker-compose.prod.yml`
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
frontend:
|
|
||||||
build: .
|
|
||||||
restart: always
|
|
||||||
expose:
|
|
||||||
- "80"
|
|
||||||
|
|
||||||
caddy:
|
|
||||||
image: caddy:2-alpine
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
- "443:443"
|
|
||||||
environment:
|
|
||||||
- DOMAIN_NAME=${DOMAIN_NAME}
|
|
||||||
entrypoint:
|
|
||||||
- /bin/sh
|
|
||||||
- -c
|
|
||||||
- |
|
|
||||||
# Build Caddyfile on container start
|
|
||||||
CLEAN_DOMAIN=$(printf '%s' "$DOMAIN_NAME" | tr -d ';')
|
|
||||||
cat > /etc/caddy/Caddyfile <<EOF
|
|
||||||
${CLEAN_DOMAIN} {
|
|
||||||
reverse_proxy /* frontend:80
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
# Run Caddy with the generated file
|
|
||||||
caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
|
|
||||||
volumes:
|
|
||||||
- caddy_data:/data
|
|
||||||
- caddy_config:/config
|
|
||||||
depends_on:
|
|
||||||
- frontend
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
caddy_data:
|
|
||||||
caddy_config:
|
|
||||||
```
|
|
||||||
|
|
||||||
**Belangrijkste punten**
|
|
||||||
|
|
||||||
| Item | Waarom |
|
|
||||||
|------|--------|
|
|
||||||
| **`frontend` (Nginx)** | Bevat de React applicatie en proxy-configuratie voor Anthropic. Exposeert intern poort 80. |
|
|
||||||
| **`caddy` (Reverse Proxy)** | Ontvangt alle externe requests op poort 80 en 443 en haalt volautomatisch een SSL Let's Encrypt certificaat op voor `DOMAIN_NAME`. Stuurt verkeer door naar `frontend:80`. |
|
|
||||||
| **Volumes `caddy_data/config`** | Slaan de SSL certificaten persistent op zodat Caddy niet bij elke herstart de rate-limits van Let's Encrypt overschrijdt. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2.2 `Dockerfile` (Productie)
|
|
||||||
|
|
||||||
De applicatie wordt in 2 fasen ("multi-stage") gebouwd:
|
|
||||||
1. **Node.js Build:** `npm install` en `npm run build` worden uitgevoerd om de statische Vite applicatie te compileren naar de `dist/` map.
|
|
||||||
2. **Nginx Serve:** Een lightweight Nginx Alpine image wordt gestart. Het pakt de `dist/` bestanden en past `nginx.conf` toe om API-aanroepen (CORS blokkades) te omzeilen door als proxy te fungeren.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Omgevingsvariabelen (te definiëren in Portainer)
|
|
||||||
|
|
||||||
| Variabele | Voorbeeld | Toelichting |
|
|
||||||
|-----------|-----------|-------------|
|
|
||||||
| `DOMAIN_NAME` | `leren.mijndomein.nl` | De domeinnaam die Caddy zal beveiligen met HTTPS. Zorg dat je DNS A-record naar je VPS-IP wijst. |
|
|
||||||
|
|
||||||
*(De Anthropic API key wordt ingevuld via de instellingen in de applicatie (Settings menu) door een admin, en veilig opgeslagen in localStorage van de gebruiker. Hij hoeft dus niet in Portainer te staan!)*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Deploy-procedure (stap-voor-stap)
|
|
||||||
|
|
||||||
### Portainer – stack aanmaken / bijwerken
|
|
||||||
|
|
||||||
| Stap | Actie | Details |
|
|
||||||
|------|-------|---------|
|
|
||||||
| **1** | Navigeer naar **Stacks → + Add stack** | Geef een herkenbare naam (bijv. `respellion-platform`). |
|
|
||||||
| **2** | **Repository URL** invullen | `https://github.com/<gebruiker>/learning-platform.git` |
|
|
||||||
| **3** | **Compose path** = `docker-compose.prod.yml` | Zorg dat dit exact overeenkomt met de bestandsnaam in de repo. |
|
|
||||||
| **4** | **Environment variables** toevoegen | Voeg `DOMAIN_NAME` toe. |
|
|
||||||
| **5** | **Deploy the stack** | Portainer start de applicatie en Caddy haalt direct het SSL certificaat op (max. 60 sec). |
|
|
||||||
| **6** | **Controle** | Surf naar `https://jouw-domein.nl`. Je zou de Respellion inlogscherm moeten zien. |
|
|
||||||
|
|
||||||
### Handmatige Force-Rebuild
|
|
||||||
|
|
||||||
Wanneer de repo is geüpdatet en je wil de nieuwe versie afdwingen via command-line:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f docker-compose.prod.yml up -d --build --no-cache
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Veelvoorkomende valkuilen & Fixes
|
|
||||||
|
|
||||||
| Symptom | Oorzaak | Oplossing |
|
|
||||||
|---------|---------|-----------|
|
|
||||||
| **SSL Error / ERR_CONNECTION_REFUSED** | DNS is nog niet actief of poort 443 staat dicht in je firewall (Hetzner). | Check Hetzner Cloud Console en open TCP poort 80 & 443 in de Inbound Firewall. |
|
|
||||||
| **Anthropic API Werkt Niet** | Foute key of Nginx proxy mist configuratie. | Open de app, ga naar Admin -> Settings en vul je Anthropic Key in. Controleer ook of `nginx.conf` correct wordt ingeladen in Nginx. |
|
|
||||||
| **Witte pagina na deploy** | Oude Docker image wordt gebruikt. | Doe een force-rebuild (`--build --no-cache`) in Portainer of via de console. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Checklist voor een nieuwe implementatie
|
|
||||||
|
|
||||||
- [ ] Repository up-to-date en gepusht naar `main`.
|
|
||||||
- [ ] `docker-compose.prod.yml` is gedefinieerd in Portainer.
|
|
||||||
- [ ] Environment variable `DOMAIN_NAME` is correct ingesteld.
|
|
||||||
- [ ] DNS A-Record staat op de juiste server.
|
|
||||||
- [ ] Na eerste inlog: Open "Admin" -> "Settings" en voeg de Anthropic API Key toe, klik op Opslaan.
|
|
||||||
27
Dockerfile
27
Dockerfile
@@ -1,28 +1,21 @@
|
|||||||
# Stage 1: Build the React application
|
FROM node:24-alpine AS builder
|
||||||
FROM node:22-alpine as build
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy package files and install dependencies
|
COPY package.json package-lock.json ./
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
# Copy the rest of the application files
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Build the Vite project for production
|
ARG BUILD_MODE=development
|
||||||
RUN npm run build
|
RUN npm run build -- --mode $BUILD_MODE
|
||||||
|
|
||||||
# Stage 2: Serve the application with Nginx
|
FROM caddy:2-alpine
|
||||||
FROM nginx:alpine
|
|
||||||
|
|
||||||
# Copy custom Nginx configuration
|
COPY --from=builder /app/dist /srv
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
COPY Caddyfile /etc/caddy/Caddyfile
|
||||||
|
|
||||||
# Copy the build output to Nginx's html directory
|
EXPOSE 80 443
|
||||||
COPY --from=build /app/dist /usr/share/nginx/html
|
|
||||||
|
|
||||||
# Expose port 80
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
EXPOSE 80
|
CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1
|
||||||
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
|
||||||
|
|||||||
17
LICENSE.md
Normal file
17
LICENSE.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
Copyright (c) 2022 - present, Gani Georgiev
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
and associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||||
|
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||||
|
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
|
||||||
|
is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
30
PROTECTED.md
Normal file
30
PROTECTED.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Protected files — do not modify
|
||||||
|
|
||||||
|
These files and directories must never be modified by Claude Code without an
|
||||||
|
explicit request. They belong to the deployment pipeline and the authoritative
|
||||||
|
visual reference, and are otherwise frozen.
|
||||||
|
|
||||||
|
## Deployment & CI/CD
|
||||||
|
.github/workflows/
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
Caddyfile
|
||||||
|
Caddyfile.test
|
||||||
|
|
||||||
|
## Infrastructure & provisioning
|
||||||
|
infra/ (Ansible playbooks: development / production)
|
||||||
|
|
||||||
|
## Stylesheet
|
||||||
|
stylesheet.css (repo root — authoritative visual reference, use as-is)
|
||||||
|
|
||||||
|
## Abandoned scaffolding (do not edit or rely on)
|
||||||
|
app/ (original Next.js + Qdrant design, never shipped — the real app is /src)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- The active application lives entirely in `/src`. Editing the app means editing `/src`.
|
||||||
|
- PocketBase schema changes go through `pb_migrations/` (and should be mirrored in
|
||||||
|
`scripts/setup-pb-collections.mjs`) — those are NOT frozen.
|
||||||
|
- If a task genuinely requires touching a frozen file (e.g. a deliberate deployment
|
||||||
|
change), confirm with the human first.
|
||||||
93
README.md
93
README.md
@@ -1,16 +1,91 @@
|
|||||||
# React + Vite
|
# Respellion Learning Platform
|
||||||
|
|
||||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
An internal AI-powered learning platform that keeps Respellion employees up to date with the company's evolving knowledge base.
|
||||||
|
|
||||||
Currently, two official plugins are available:
|
## Features
|
||||||
|
|
||||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
- **Onboarding** — On first login each employee enrolls into the curriculum with one tap. Their 26-week cycle starts then and there.
|
||||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
- **Weekly Learning Station** — Each employee gets a topic for their current curriculum week, drawn from the active 26-week schedule (with a deterministic hash fallback when no curriculum is active). They engage via micro-learnings (concept explainer, scenario quiz, flashcard set) and can also generate long-form formats (Article, Slides, Infographic) on demand.
|
||||||
|
- **Weekly Test** — A 5-question AI-generated quiz for the week's topic. Results feed the leaderboard.
|
||||||
|
- **Leaderboard & Gamification** — Points for correct answers, badges for milestones (First Steps, Veteran, Perfectionist).
|
||||||
|
- **R42 Chatbot** — An always-available AI assistant (Claude) grounded in the knowledge graph via local TF-IDF retrieval. It can propose graph updates that admins approve or reject.
|
||||||
|
- **Admin Panel** — Manage the knowledge graph, upload source files, review/refine generated content, generate the curriculum, and manage the team.
|
||||||
|
|
||||||
## React Compiler
|
## Tech Stack
|
||||||
|
|
||||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
| Layer | Technology |
|
||||||
|
|---|---|
|
||||||
|
| Frontend | React 19 + Vite 8, React Router 7 |
|
||||||
|
| Styling | Vanilla CSS (custom properties) + Tailwind v4 utilities |
|
||||||
|
| Animations | Framer Motion |
|
||||||
|
| Icons | Lucide React |
|
||||||
|
| Graph viz | D3.js (admin knowledge graph only) |
|
||||||
|
| Backend / DB / auth | PocketBase (self-hosted, SQLite) |
|
||||||
|
| Retrieval | Local TF-IDF (no Qdrant, no embeddings API) |
|
||||||
|
| AI | Anthropic Claude (via Caddy reverse proxy) |
|
||||||
|
| Infra | Docker + Caddy; Ansible playbooks under `infra/` |
|
||||||
|
|
||||||
## Expanding the ESLint configuration
|
**Claude models (by tier):** `fast` = `claude-haiku-4-5-20251001`, `standard` = `claude-sonnet-4-6`, `reasoning` = `claude-opus-4-7`.
|
||||||
|
|
||||||
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
## Getting Started (local dev)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 2. Start PocketBase
|
||||||
|
./pocketbase.exe serve
|
||||||
|
|
||||||
|
# 3. Bootstrap collections (first run only)
|
||||||
|
node scripts/setup-pb-collections.mjs
|
||||||
|
|
||||||
|
# 4. Start the dev server
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `ANTHROPIC_API_KEY` in your environment — the Vite dev server proxies `/api/anthropic` and injects the key server-side (see `vite.config.js`). PocketBase migrations in `pb_migrations/` apply automatically when the binary starts.
|
||||||
|
|
||||||
|
## Deployment (Docker)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
The `Caddyfile` handles:
|
||||||
|
- SPA fallback routing
|
||||||
|
- `/api/anthropic/*` → Anthropic API (server-side API key injection)
|
||||||
|
- `/api/*` and `/_/*` → the PocketBase sidecar
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `src/lib/llm.js` | Core Anthropic wrapper (`callLLM`): tiers, retry, schema validation, telemetry |
|
||||||
|
| `src/lib/db.js` | All PocketBase data access |
|
||||||
|
| `src/lib/extractionPipeline.js` | Uploaded file → knowledge-graph extraction |
|
||||||
|
| `src/lib/learningService.js` | On-demand content generation (article / slides / infographic) |
|
||||||
|
| `src/lib/microLearningService.js` | Micro-learning generation (3 types) |
|
||||||
|
| `src/lib/curriculumService.js` | 26-week per-user curriculum engine |
|
||||||
|
| `src/lib/retrieval.js` | TF-IDF retrieval for R42 |
|
||||||
|
| `src/store/AppContext.jsx` | Global state; derives the user's curriculum week from their start date |
|
||||||
|
| `AI_AGENT.md` | Detailed context guide for AI coding agents |
|
||||||
|
|
||||||
|
## Content Types
|
||||||
|
|
||||||
|
On-demand long-form content (the `content` collection), generated per type and merged into the cached object:
|
||||||
|
|
||||||
|
| Type | Key in DB | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| Article | `content.article` | Long-form reading |
|
||||||
|
| Slides | `content.slides` | Slide deck with speaker notes |
|
||||||
|
| Infographic | `content.infographic` | Visual summary with stats and steps |
|
||||||
|
|
||||||
|
> The podcast type was removed. Do not re-add it.
|
||||||
|
|
||||||
|
Micro-learnings (the `micro_learnings` collection): `concept_explainer`, `scenario_quiz`, `flashcard_set`.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- **`CLAUDE.md`** — project overview and constraints for AI coding agents.
|
||||||
|
- **`AI_AGENT.md`** — full architectural guide (patterns, gotchas, decisions).
|
||||||
|
- **`docs/`** — subsystem specs (architecture, data model, ingestion, generation, curriculum, R42, frontend, gamification).
|
||||||
|
|||||||
0
app/frontend/.gitkeep
Normal file
0
app/frontend/.gitkeep
Normal file
80
app/frontend/.next/app-build-manifest.json
Normal file
80
app/frontend/.next/app-build-manifest.json
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"pages": {
|
||||||
|
"/page": [
|
||||||
|
"static/chunks/webpack.js",
|
||||||
|
"static/chunks/main-app.js",
|
||||||
|
"static/chunks/app/page.js"
|
||||||
|
],
|
||||||
|
"/layout": [
|
||||||
|
"static/chunks/webpack.js",
|
||||||
|
"static/chunks/main-app.js",
|
||||||
|
"static/css/app/layout.css",
|
||||||
|
"static/chunks/app/layout.js"
|
||||||
|
],
|
||||||
|
"/auth/page": [
|
||||||
|
"static/chunks/webpack.js",
|
||||||
|
"static/chunks/main-app.js",
|
||||||
|
"static/chunks/app/auth/page.js"
|
||||||
|
],
|
||||||
|
"/admin/page": [
|
||||||
|
"static/chunks/webpack.js",
|
||||||
|
"static/chunks/main-app.js",
|
||||||
|
"static/chunks/app/admin/page.js"
|
||||||
|
],
|
||||||
|
"/admin/layout": [
|
||||||
|
"static/chunks/webpack.js",
|
||||||
|
"static/chunks/main-app.js",
|
||||||
|
"static/chunks/app/admin/layout.js"
|
||||||
|
],
|
||||||
|
"/admin/documents/page": [
|
||||||
|
"static/chunks/webpack.js",
|
||||||
|
"static/chunks/main-app.js",
|
||||||
|
"static/chunks/app/admin/documents/page.js"
|
||||||
|
],
|
||||||
|
"/admin/knowledge/page": [
|
||||||
|
"static/chunks/webpack.js",
|
||||||
|
"static/chunks/main-app.js",
|
||||||
|
"static/chunks/app/admin/knowledge/page.js"
|
||||||
|
],
|
||||||
|
"/admin/curriculum/page": [
|
||||||
|
"static/chunks/webpack.js",
|
||||||
|
"static/chunks/main-app.js",
|
||||||
|
"static/chunks/app/admin/curriculum/page.js"
|
||||||
|
],
|
||||||
|
"/app/page": [
|
||||||
|
"static/chunks/webpack.js",
|
||||||
|
"static/chunks/main-app.js",
|
||||||
|
"static/chunks/app/app/page.js"
|
||||||
|
],
|
||||||
|
"/app/layout": [
|
||||||
|
"static/chunks/webpack.js",
|
||||||
|
"static/chunks/main-app.js",
|
||||||
|
"static/chunks/app/app/layout.js"
|
||||||
|
],
|
||||||
|
"/app/session/page": [
|
||||||
|
"static/chunks/webpack.js",
|
||||||
|
"static/chunks/main-app.js",
|
||||||
|
"static/chunks/app/app/session/page.js"
|
||||||
|
],
|
||||||
|
"/app/library/page": [
|
||||||
|
"static/chunks/webpack.js",
|
||||||
|
"static/chunks/main-app.js",
|
||||||
|
"static/chunks/app/app/library/page.js"
|
||||||
|
],
|
||||||
|
"/app/profile/page": [
|
||||||
|
"static/chunks/webpack.js",
|
||||||
|
"static/chunks/main-app.js",
|
||||||
|
"static/chunks/app/app/profile/page.js"
|
||||||
|
],
|
||||||
|
"/app/library/[topicId]/page": [
|
||||||
|
"static/chunks/webpack.js",
|
||||||
|
"static/chunks/main-app.js",
|
||||||
|
"static/chunks/app/app/library/[topicId]/page.js"
|
||||||
|
],
|
||||||
|
"/_not-found/page": [
|
||||||
|
"static/chunks/webpack.js",
|
||||||
|
"static/chunks/main-app.js",
|
||||||
|
"static/chunks/app/_not-found/page.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/frontend/.next/build-manifest.json
Normal file
19
app/frontend/.next/build-manifest.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"polyfillFiles": [
|
||||||
|
"static/chunks/polyfills.js"
|
||||||
|
],
|
||||||
|
"devFiles": [],
|
||||||
|
"ampDevFiles": [],
|
||||||
|
"lowPriorityFiles": [
|
||||||
|
"static/development/_buildManifest.js",
|
||||||
|
"static/development/_ssgManifest.js"
|
||||||
|
],
|
||||||
|
"rootMainFiles": [
|
||||||
|
"static/chunks/webpack.js",
|
||||||
|
"static/chunks/main-app.js"
|
||||||
|
],
|
||||||
|
"pages": {
|
||||||
|
"/_app": []
|
||||||
|
},
|
||||||
|
"ampFirstPages": []
|
||||||
|
}
|
||||||
BIN
app/frontend/.next/cache/webpack/client-development/0.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/0.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/1.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/1.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/10.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/10.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/11.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/11.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/12.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/12.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/13.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/13.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/2.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/2.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/3.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/3.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/4.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/4.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/5.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/5.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/6.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/6.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/7.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/7.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/8.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/8.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/9.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/9.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/index.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/index.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/client-development/index.pack.gz.old
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/client-development/index.pack.gz.old
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/0.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/0.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/1.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/1.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/10.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/10.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/11.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/11.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/12.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/12.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/13.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/13.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/14.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/14.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/2.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/2.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/3.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/3.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/4.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/4.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/5.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/5.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/6.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/6.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/7.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/7.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/8.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/8.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/9.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/9.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/index.pack.gz
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/index.pack.gz
vendored
Normal file
Binary file not shown.
BIN
app/frontend/.next/cache/webpack/server-development/index.pack.gz.old
vendored
Normal file
BIN
app/frontend/.next/cache/webpack/server-development/index.pack.gz.old
vendored
Normal file
Binary file not shown.
1
app/frontend/.next/package.json
Normal file
1
app/frontend/.next/package.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"type": "commonjs"}
|
||||||
1
app/frontend/.next/react-loadable-manifest.json
Normal file
1
app/frontend/.next/react-loadable-manifest.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
8
app/frontend/.next/server/app-paths-manifest.json
Normal file
8
app/frontend/.next/server/app-paths-manifest.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"/_not-found/page": "app/_not-found/page.js",
|
||||||
|
"/auth/page": "app/auth/page.js",
|
||||||
|
"/admin/documents/page": "app/admin/documents/page.js",
|
||||||
|
"/app/session/page": "app/app/session/page.js",
|
||||||
|
"/admin/curriculum/page": "app/admin/curriculum/page.js",
|
||||||
|
"/app/library/page": "app/app/library/page.js"
|
||||||
|
}
|
||||||
133
app/frontend/.next/server/app/_not-found/page.js
Normal file
133
app/frontend/.next/server/app/_not-found/page.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
277
app/frontend/.next/server/app/admin/curriculum/page.js
Normal file
277
app/frontend/.next/server/app/admin/curriculum/page.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
321
app/frontend/.next/server/app/admin/documents/page.js
Normal file
321
app/frontend/.next/server/app/admin/documents/page.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
288
app/frontend/.next/server/app/admin/knowledge/page.js
Normal file
288
app/frontend/.next/server/app/admin/knowledge/page.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
226
app/frontend/.next/server/app/admin/page.js
Normal file
226
app/frontend/.next/server/app/admin/page.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
420
app/frontend/.next/server/app/app/library/[topicId]/page.js
Normal file
420
app/frontend/.next/server/app/app/library/[topicId]/page.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
299
app/frontend/.next/server/app/app/library/page.js
Normal file
299
app/frontend/.next/server/app/app/library/page.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
259
app/frontend/.next/server/app/app/page.js
Normal file
259
app/frontend/.next/server/app/app/page.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
332
app/frontend/.next/server/app/app/profile/page.js
Normal file
332
app/frontend/.next/server/app/app/profile/page.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
453
app/frontend/.next/server/app/app/session/page.js
Normal file
453
app/frontend/.next/server/app/app/session/page.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
205
app/frontend/.next/server/app/auth/page.js
Normal file
205
app/frontend/.next/server/app/auth/page.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
154
app/frontend/.next/server/app/page.js
Normal file
154
app/frontend/.next/server/app/page.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
self.__INTERCEPTION_ROUTE_REWRITE_MANIFEST="[]"
|
||||||
21
app/frontend/.next/server/middleware-build-manifest.js
Normal file
21
app/frontend/.next/server/middleware-build-manifest.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
self.__BUILD_MANIFEST = {
|
||||||
|
"polyfillFiles": [
|
||||||
|
"static/chunks/polyfills.js"
|
||||||
|
],
|
||||||
|
"devFiles": [],
|
||||||
|
"ampDevFiles": [],
|
||||||
|
"lowPriorityFiles": [],
|
||||||
|
"rootMainFiles": [
|
||||||
|
"static/chunks/webpack.js",
|
||||||
|
"static/chunks/main-app.js"
|
||||||
|
],
|
||||||
|
"pages": {
|
||||||
|
"/_app": []
|
||||||
|
},
|
||||||
|
"ampFirstPages": []
|
||||||
|
};
|
||||||
|
self.__BUILD_MANIFEST.lowPriorityFiles = [
|
||||||
|
"/static/" + process.env.__NEXT_BUILD_ID + "/_buildManifest.js",
|
||||||
|
,"/static/" + process.env.__NEXT_BUILD_ID + "/_ssgManifest.js",
|
||||||
|
|
||||||
|
];
|
||||||
6
app/frontend/.next/server/middleware-manifest.json
Normal file
6
app/frontend/.next/server/middleware-manifest.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"middleware": {},
|
||||||
|
"functions": {},
|
||||||
|
"sortedMiddleware": []
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
self.__REACT_LOADABLE_MANIFEST="{}"
|
||||||
1
app/frontend/.next/server/next-font-manifest.js
Normal file
1
app/frontend/.next/server/next-font-manifest.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
self.__NEXT_FONT_MANIFEST="{\"pages\":{},\"app\":{},\"appUsingSizeAdjust\":false,\"pagesUsingSizeAdjust\":false}"
|
||||||
1
app/frontend/.next/server/next-font-manifest.json
Normal file
1
app/frontend/.next/server/next-font-manifest.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"pages":{},"app":{},"appUsingSizeAdjust":false,"pagesUsingSizeAdjust":false}
|
||||||
1
app/frontend/.next/server/pages-manifest.json
Normal file
1
app/frontend/.next/server/pages-manifest.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
1
app/frontend/.next/server/server-reference-manifest.js
Normal file
1
app/frontend/.next/server/server-reference-manifest.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
self.__RSC_SERVER_MANIFEST="{\n \"node\": {},\n \"edge\": {},\n \"encryptionKey\": \"process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY\"\n}"
|
||||||
5
app/frontend/.next/server/server-reference-manifest.json
Normal file
5
app/frontend/.next/server/server-reference-manifest.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"node": {},
|
||||||
|
"edge": {},
|
||||||
|
"encryptionKey": "QnzKIJRR8GrjaHk7QhiM928LOJzfdW5Af6gmYU5xeZs="
|
||||||
|
}
|
||||||
75
app/frontend/.next/server/vendor-chunks/@swc.js
Normal file
75
app/frontend/.next/server/vendor-chunks/@swc.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
"use strict";
|
||||||
|
/*
|
||||||
|
* ATTENTION: An "eval-source-map" devtool has been used.
|
||||||
|
* This devtool is neither made for production nor for readable output files.
|
||||||
|
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
|
||||||
|
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
|
||||||
|
* or disable the default devtool with "devtool: false".
|
||||||
|
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
|
||||||
|
*/
|
||||||
|
exports.id = "vendor-chunks/@swc";
|
||||||
|
exports.ids = ["vendor-chunks/@swc"];
|
||||||
|
exports.modules = {
|
||||||
|
|
||||||
|
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_class_private_field_loose_base.js":
|
||||||
|
/*!**************************************************************************!*\
|
||||||
|
!*** ./node_modules/@swc/helpers/esm/_class_private_field_loose_base.js ***!
|
||||||
|
\**************************************************************************/
|
||||||
|
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
||||||
|
|
||||||
|
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _class_private_field_loose_base),\n/* harmony export */ _class_private_field_loose_base: () => (/* binding */ _class_private_field_loose_base)\n/* harmony export */ });\nfunction _class_private_field_loose_base(receiver, privateKey) {\n if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) {\n throw new TypeError(\"attempted to use private field on non-instance\");\n }\n\n return receiver;\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9fY2xhc3NfcHJpdmF0ZV9maWVsZF9sb29zZV9iYXNlLmpzIiwibWFwcGluZ3MiOiI7Ozs7O0FBQU87QUFDUDtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNnRCIsInNvdXJjZXMiOlsid2VicGFjazovL2xlYXJuaW5nLXBsYXRmb3JtLWZyb250ZW5kLy4vbm9kZV9tb2R1bGVzL0Bzd2MvaGVscGVycy9lc20vX2NsYXNzX3ByaXZhdGVfZmllbGRfbG9vc2VfYmFzZS5qcz9kY2RiIl0sInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBmdW5jdGlvbiBfY2xhc3NfcHJpdmF0ZV9maWVsZF9sb29zZV9iYXNlKHJlY2VpdmVyLCBwcml2YXRlS2V5KSB7XG4gICAgaWYgKCFPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwocmVjZWl2ZXIsIHByaXZhdGVLZXkpKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoXCJhdHRlbXB0ZWQgdG8gdXNlIHByaXZhdGUgZmllbGQgb24gbm9uLWluc3RhbmNlXCIpO1xuICAgIH1cblxuICAgIHJldHVybiByZWNlaXZlcjtcbn1cbmV4cG9ydCB7IF9jbGFzc19wcml2YXRlX2ZpZWxkX2xvb3NlX2Jhc2UgYXMgXyB9O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_class_private_field_loose_base.js\n");
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_class_private_field_loose_key.js":
|
||||||
|
/*!*************************************************************************!*\
|
||||||
|
!*** ./node_modules/@swc/helpers/esm/_class_private_field_loose_key.js ***!
|
||||||
|
\*************************************************************************/
|
||||||
|
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
||||||
|
|
||||||
|
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _class_private_field_loose_key),\n/* harmony export */ _class_private_field_loose_key: () => (/* binding */ _class_private_field_loose_key)\n/* harmony export */ });\nvar id = 0;\n\nfunction _class_private_field_loose_key(name) {\n return \"__private_\" + id++ + \"_\" + name;\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9fY2xhc3NfcHJpdmF0ZV9maWVsZF9sb29zZV9rZXkuanMiLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBQTs7QUFFTztBQUNQO0FBQ0E7QUFDK0MiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9sZWFybmluZy1wbGF0Zm9ybS1mcm9udGVuZC8uL25vZGVfbW9kdWxlcy9Ac3djL2hlbHBlcnMvZXNtL19jbGFzc19wcml2YXRlX2ZpZWxkX2xvb3NlX2tleS5qcz9hZjBiIl0sInNvdXJjZXNDb250ZW50IjpbInZhciBpZCA9IDA7XG5cbmV4cG9ydCBmdW5jdGlvbiBfY2xhc3NfcHJpdmF0ZV9maWVsZF9sb29zZV9rZXkobmFtZSkge1xuICAgIHJldHVybiBcIl9fcHJpdmF0ZV9cIiArIGlkKysgKyBcIl9cIiArIG5hbWU7XG59XG5leHBvcnQgeyBfY2xhc3NfcHJpdmF0ZV9maWVsZF9sb29zZV9rZXkgYXMgXyB9O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_class_private_field_loose_key.js\n");
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_interop_require_default.js":
|
||||||
|
/*!*******************************************************************!*\
|
||||||
|
!*** ./node_modules/@swc/helpers/esm/_interop_require_default.js ***!
|
||||||
|
\*******************************************************************/
|
||||||
|
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
||||||
|
|
||||||
|
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _interop_require_default),\n/* harmony export */ _interop_require_default: () => (/* binding */ _interop_require_default)\n/* harmony export */ });\nfunction _interop_require_default(obj) {\n return obj && obj.__esModule ? obj : { default: obj };\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9faW50ZXJvcF9yZXF1aXJlX2RlZmF1bHQuanMiLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBTztBQUNQLDJDQUEyQztBQUMzQztBQUN5QyIsInNvdXJjZXMiOlsid2VicGFjazovL2xlYXJuaW5nLXBsYXRmb3JtLWZyb250ZW5kLy4vbm9kZV9tb2R1bGVzL0Bzd2MvaGVscGVycy9lc20vX2ludGVyb3BfcmVxdWlyZV9kZWZhdWx0LmpzPzY4ZDkiXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGZ1bmN0aW9uIF9pbnRlcm9wX3JlcXVpcmVfZGVmYXVsdChvYmopIHtcbiAgICByZXR1cm4gb2JqICYmIG9iai5fX2VzTW9kdWxlID8gb2JqIDogeyBkZWZhdWx0OiBvYmogfTtcbn1cbmV4cG9ydCB7IF9pbnRlcm9wX3JlcXVpcmVfZGVmYXVsdCBhcyBfIH07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_interop_require_default.js\n");
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_interop_require_wildcard.js":
|
||||||
|
/*!********************************************************************!*\
|
||||||
|
!*** ./node_modules/@swc/helpers/esm/_interop_require_wildcard.js ***!
|
||||||
|
\********************************************************************/
|
||||||
|
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
||||||
|
|
||||||
|
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _interop_require_wildcard),\n/* harmony export */ _interop_require_wildcard: () => (/* binding */ _interop_require_wildcard)\n/* harmony export */ });\nfunction _getRequireWildcardCache(nodeInterop) {\n if (typeof WeakMap !== \"function\") return null;\n\n var cacheBabelInterop = new WeakMap();\n var cacheNodeInterop = new WeakMap();\n\n return (_getRequireWildcardCache = function(nodeInterop) {\n return nodeInterop ? cacheNodeInterop : cacheBabelInterop;\n })(nodeInterop);\n}\nfunction _interop_require_wildcard(obj, nodeInterop) {\n if (!nodeInterop && obj && obj.__esModule) return obj;\n if (obj === null || typeof obj !== \"object\" && typeof obj !== \"function\") return { default: obj };\n\n var cache = _getRequireWildcardCache(nodeInterop);\n\n if (cache && cache.has(obj)) return cache.get(obj);\n\n var newObj = { __proto__: null };\n var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;\n\n for (var key in obj) {\n if (key !== \"default\" && Object.prototype.hasOwnProperty.call(obj, key)) {\n var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;\n if (desc && (desc.get || desc.set)) Object.defineProperty(newObj, key, desc);\n else newObj[key] = obj[key];\n }\n }\n\n newObj.default = obj;\n\n if (cache) cache.set(obj, newObj);\n\n return newObj;\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9faW50ZXJvcF9yZXF1aXJlX3dpbGRjYXJkLmpzIiwibWFwcGluZ3MiOiI7Ozs7O0FBQUE7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7QUFDQSxLQUFLO0FBQ0w7QUFDTztBQUNQO0FBQ0EsdUZBQXVGOztBQUV2Rjs7QUFFQTs7QUFFQSxtQkFBbUI7QUFDbkI7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7O0FBRUE7O0FBRUE7QUFDQTtBQUMwQyIsInNvdXJjZXMiOlsid2VicGFjazovL2xlYXJuaW5nLXBsYXRmb3JtLWZyb250ZW5kLy4vbm9kZV9tb2R1bGVzL0Bzd2MvaGVscGVycy9lc20vX2ludGVyb3BfcmVxdWlyZV93aWxkY2FyZC5qcz9hYjM4Il0sInNvdXJjZXNDb250ZW50IjpbImZ1bmN0aW9uIF9nZXRSZXF1aXJlV2lsZGNhcmRDYWNoZShub2RlSW50ZXJvcCkge1xuICAgIGlmICh0eXBlb2YgV2Vha01hcCAhPT0gXCJmdW5jdGlvblwiKSByZXR1cm4gbnVsbDtcblxuICAgIHZhciBjYWNoZUJhYmVsSW50ZXJvcCA9IG5ldyBXZWFrTWFwKCk7XG4gICAgdmFyIGNhY2hlTm9kZUludGVyb3AgPSBuZXcgV2Vha01hcCgpO1xuXG4gICAgcmV0dXJuIChfZ2V0UmVxdWlyZVdpbGRjYXJkQ2FjaGUgPSBmdW5jdGlvbihub2RlSW50ZXJvcCkge1xuICAgICAgICByZXR1cm4gbm9kZUludGVyb3AgPyBjYWNoZU5vZGVJbnRlcm9wIDogY2FjaGVCYWJlbEludGVyb3A7XG4gICAgfSkobm9kZUludGVyb3ApO1xufVxuZXhwb3J0IGZ1bmN0aW9uIF9pbnRlcm9wX3JlcXVpcmVfd2lsZGNhcmQob2JqLCBub2RlSW50ZXJvcCkge1xuICAgIGlmICghbm9kZUludGVyb3AgJiYgb2JqICYmIG9iai5fX2VzTW9kdWxlKSByZXR1cm4gb2JqO1xuICAgIGlmIChvYmogPT09IG51bGwgfHwgdHlwZW9mIG9iaiAhPT0gXCJvYmplY3RcIiAmJiB0eXBlb2Ygb2JqICE9PSBcImZ1bmN0aW9uXCIpIHJldHVybiB7IGRlZmF1bHQ6IG9iaiB9O1xuXG4gICAgdmFyIGNhY2hlID0gX2dldFJlcXVpcmVXaWxkY2FyZENhY2hlKG5vZGVJbnRlcm9wKTtcblxuICAgIGlmIChjYWNoZSAmJiBjYWNoZS5oYXMob2JqKSkgcmV0dXJuIGNhY2hlLmdldChvYmopO1xuXG4gICAgdmFyIG5ld09iaiA9IHsgX19wcm90b19fOiBudWxsIH07XG4gICAgdmFyIGhhc1Byb3BlcnR5RGVzY3JpcHRvciA9IE9iamVjdC5kZWZpbmVQcm9wZXJ0eSAmJiBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yO1xuXG4gICAgZm9yICh2YXIga2V5IGluIG9iaikge1xuICAgICAgICBpZiAoa2V5ICE9PSBcImRlZmF1bHRcIiAmJiBPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwob2JqLCBrZXkpKSB7XG4gICAgICAgICAgICB2YXIgZGVzYyA9IGhhc1Byb3BlcnR5RGVzY3JpcHRvciA/IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3Iob2JqLCBrZXkpIDogbnVsbDtcbiAgICAgICAgICAgIGlmIChkZXNjICYmIChkZXNjLmdldCB8fCBkZXNjLnNldCkpIE9iamVjdC5kZWZpbmVQcm9wZXJ0eShuZXdPYmosIGtleSwgZGVzYyk7XG4gICAgICAgICAgICBlbHNlIG5ld09ialtrZXldID0gb2JqW2tleV07XG4gICAgICAgIH1cbiAgICB9XG5cbiAgICBuZXdPYmouZGVmYXVsdCA9IG9iajtcblxuICAgIGlmIChjYWNoZSkgY2FjaGUuc2V0KG9iaiwgbmV3T2JqKTtcblxuICAgIHJldHVybiBuZXdPYmo7XG59XG5leHBvcnQgeyBfaW50ZXJvcF9yZXF1aXJlX3dpbGRjYXJkIGFzIF8gfTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_interop_require_wildcard.js\n");
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_tagged_template_literal_loose.js":
|
||||||
|
/*!*************************************************************************!*\
|
||||||
|
!*** ./node_modules/@swc/helpers/esm/_tagged_template_literal_loose.js ***!
|
||||||
|
\*************************************************************************/
|
||||||
|
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
||||||
|
|
||||||
|
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _tagged_template_literal_loose),\n/* harmony export */ _tagged_template_literal_loose: () => (/* binding */ _tagged_template_literal_loose)\n/* harmony export */ });\nfunction _tagged_template_literal_loose(strings, raw) {\n if (!raw) raw = strings.slice(0);\n\n strings.raw = raw;\n\n return strings;\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9fdGFnZ2VkX3RlbXBsYXRlX2xpdGVyYWxfbG9vc2UuanMiLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBTztBQUNQOztBQUVBOztBQUVBO0FBQ0E7QUFDK0MiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9sZWFybmluZy1wbGF0Zm9ybS1mcm9udGVuZC8uL25vZGVfbW9kdWxlcy9Ac3djL2hlbHBlcnMvZXNtL190YWdnZWRfdGVtcGxhdGVfbGl0ZXJhbF9sb29zZS5qcz83YjYyIl0sInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBmdW5jdGlvbiBfdGFnZ2VkX3RlbXBsYXRlX2xpdGVyYWxfbG9vc2Uoc3RyaW5ncywgcmF3KSB7XG4gICAgaWYgKCFyYXcpIHJhdyA9IHN0cmluZ3Muc2xpY2UoMCk7XG5cbiAgICBzdHJpbmdzLnJhdyA9IHJhdztcblxuICAgIHJldHVybiBzdHJpbmdzO1xufVxuZXhwb3J0IHsgX3RhZ2dlZF90ZW1wbGF0ZV9saXRlcmFsX2xvb3NlIGFzIF8gfTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_tagged_template_literal_loose.js\n");
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ "(rsc)/./node_modules/@swc/helpers/esm/_interop_require_default.js":
|
||||||
|
/*!*******************************************************************!*\
|
||||||
|
!*** ./node_modules/@swc/helpers/esm/_interop_require_default.js ***!
|
||||||
|
\*******************************************************************/
|
||||||
|
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
||||||
|
|
||||||
|
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _interop_require_default),\n/* harmony export */ _interop_require_default: () => (/* binding */ _interop_require_default)\n/* harmony export */ });\nfunction _interop_require_default(obj) {\n return obj && obj.__esModule ? obj : { default: obj };\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHJzYykvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9faW50ZXJvcF9yZXF1aXJlX2RlZmF1bHQuanMiLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBTztBQUNQLDJDQUEyQztBQUMzQztBQUN5QyIsInNvdXJjZXMiOlsid2VicGFjazovL2xlYXJuaW5nLXBsYXRmb3JtLWZyb250ZW5kLy4vbm9kZV9tb2R1bGVzL0Bzd2MvaGVscGVycy9lc20vX2ludGVyb3BfcmVxdWlyZV9kZWZhdWx0LmpzPzFhYjAiXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGZ1bmN0aW9uIF9pbnRlcm9wX3JlcXVpcmVfZGVmYXVsdChvYmopIHtcbiAgICByZXR1cm4gb2JqICYmIG9iai5fX2VzTW9kdWxlID8gb2JqIDogeyBkZWZhdWx0OiBvYmogfTtcbn1cbmV4cG9ydCB7IF9pbnRlcm9wX3JlcXVpcmVfZGVmYXVsdCBhcyBfIH07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///(rsc)/./node_modules/@swc/helpers/esm/_interop_require_default.js\n");
|
||||||
|
|
||||||
|
/***/ })
|
||||||
|
|
||||||
|
};
|
||||||
|
;
|
||||||
2398
app/frontend/.next/server/vendor-chunks/next.js
Normal file
2398
app/frontend/.next/server/vendor-chunks/next.js
Normal file
File diff suppressed because one or more lines are too long
25
app/frontend/.next/server/vendor-chunks/pocketbase.js
Normal file
25
app/frontend/.next/server/vendor-chunks/pocketbase.js
Normal file
File diff suppressed because one or more lines are too long
215
app/frontend/.next/server/webpack-runtime.js
Normal file
215
app/frontend/.next/server/webpack-runtime.js
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
/*
|
||||||
|
* ATTENTION: An "eval-source-map" devtool has been used.
|
||||||
|
* This devtool is neither made for production nor for readable output files.
|
||||||
|
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
|
||||||
|
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
|
||||||
|
* or disable the default devtool with "devtool: false".
|
||||||
|
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
|
||||||
|
*/
|
||||||
|
/******/ (() => { // webpackBootstrap
|
||||||
|
/******/ "use strict";
|
||||||
|
/******/ var __webpack_modules__ = ({});
|
||||||
|
/************************************************************************/
|
||||||
|
/******/ // The module cache
|
||||||
|
/******/ var __webpack_module_cache__ = {};
|
||||||
|
/******/
|
||||||
|
/******/ // The require function
|
||||||
|
/******/ function __webpack_require__(moduleId) {
|
||||||
|
/******/ // Check if module is in cache
|
||||||
|
/******/ var cachedModule = __webpack_module_cache__[moduleId];
|
||||||
|
/******/ if (cachedModule !== undefined) {
|
||||||
|
/******/ return cachedModule.exports;
|
||||||
|
/******/ }
|
||||||
|
/******/ // Create a new module (and put it into the cache)
|
||||||
|
/******/ var module = __webpack_module_cache__[moduleId] = {
|
||||||
|
/******/ id: moduleId,
|
||||||
|
/******/ loaded: false,
|
||||||
|
/******/ exports: {}
|
||||||
|
/******/ };
|
||||||
|
/******/
|
||||||
|
/******/ // Execute the module function
|
||||||
|
/******/ var threw = true;
|
||||||
|
/******/ try {
|
||||||
|
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
|
||||||
|
/******/ threw = false;
|
||||||
|
/******/ } finally {
|
||||||
|
/******/ if(threw) delete __webpack_module_cache__[moduleId];
|
||||||
|
/******/ }
|
||||||
|
/******/
|
||||||
|
/******/ // Flag the module as loaded
|
||||||
|
/******/ module.loaded = true;
|
||||||
|
/******/
|
||||||
|
/******/ // Return the exports of the module
|
||||||
|
/******/ return module.exports;
|
||||||
|
/******/ }
|
||||||
|
/******/
|
||||||
|
/******/ // expose the modules object (__webpack_modules__)
|
||||||
|
/******/ __webpack_require__.m = __webpack_modules__;
|
||||||
|
/******/
|
||||||
|
/************************************************************************/
|
||||||
|
/******/ /* webpack/runtime/compat get default export */
|
||||||
|
/******/ (() => {
|
||||||
|
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
||||||
|
/******/ __webpack_require__.n = (module) => {
|
||||||
|
/******/ var getter = module && module.__esModule ?
|
||||||
|
/******/ () => (module['default']) :
|
||||||
|
/******/ () => (module);
|
||||||
|
/******/ __webpack_require__.d(getter, { a: getter });
|
||||||
|
/******/ return getter;
|
||||||
|
/******/ };
|
||||||
|
/******/ })();
|
||||||
|
/******/
|
||||||
|
/******/ /* webpack/runtime/create fake namespace object */
|
||||||
|
/******/ (() => {
|
||||||
|
/******/ var getProto = Object.getPrototypeOf ? (obj) => (Object.getPrototypeOf(obj)) : (obj) => (obj.__proto__);
|
||||||
|
/******/ var leafPrototypes;
|
||||||
|
/******/ // create a fake namespace object
|
||||||
|
/******/ // mode & 1: value is a module id, require it
|
||||||
|
/******/ // mode & 2: merge all properties of value into the ns
|
||||||
|
/******/ // mode & 4: return value when already ns object
|
||||||
|
/******/ // mode & 16: return value when it's Promise-like
|
||||||
|
/******/ // mode & 8|1: behave like require
|
||||||
|
/******/ __webpack_require__.t = function(value, mode) {
|
||||||
|
/******/ if(mode & 1) value = this(value);
|
||||||
|
/******/ if(mode & 8) return value;
|
||||||
|
/******/ if(typeof value === 'object' && value) {
|
||||||
|
/******/ if((mode & 4) && value.__esModule) return value;
|
||||||
|
/******/ if((mode & 16) && typeof value.then === 'function') return value;
|
||||||
|
/******/ }
|
||||||
|
/******/ var ns = Object.create(null);
|
||||||
|
/******/ __webpack_require__.r(ns);
|
||||||
|
/******/ var def = {};
|
||||||
|
/******/ leafPrototypes = leafPrototypes || [null, getProto({}), getProto([]), getProto(getProto)];
|
||||||
|
/******/ for(var current = mode & 2 && value; typeof current == 'object' && !~leafPrototypes.indexOf(current); current = getProto(current)) {
|
||||||
|
/******/ Object.getOwnPropertyNames(current).forEach((key) => (def[key] = () => (value[key])));
|
||||||
|
/******/ }
|
||||||
|
/******/ def['default'] = () => (value);
|
||||||
|
/******/ __webpack_require__.d(ns, def);
|
||||||
|
/******/ return ns;
|
||||||
|
/******/ };
|
||||||
|
/******/ })();
|
||||||
|
/******/
|
||||||
|
/******/ /* webpack/runtime/define property getters */
|
||||||
|
/******/ (() => {
|
||||||
|
/******/ // define getter functions for harmony exports
|
||||||
|
/******/ __webpack_require__.d = (exports, definition) => {
|
||||||
|
/******/ for(var key in definition) {
|
||||||
|
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
|
||||||
|
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
|
||||||
|
/******/ }
|
||||||
|
/******/ }
|
||||||
|
/******/ };
|
||||||
|
/******/ })();
|
||||||
|
/******/
|
||||||
|
/******/ /* webpack/runtime/ensure chunk */
|
||||||
|
/******/ (() => {
|
||||||
|
/******/ __webpack_require__.f = {};
|
||||||
|
/******/ // This file contains only the entry chunk.
|
||||||
|
/******/ // The chunk loading function for additional chunks
|
||||||
|
/******/ __webpack_require__.e = (chunkId) => {
|
||||||
|
/******/ return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
|
||||||
|
/******/ __webpack_require__.f[key](chunkId, promises);
|
||||||
|
/******/ return promises;
|
||||||
|
/******/ }, []));
|
||||||
|
/******/ };
|
||||||
|
/******/ })();
|
||||||
|
/******/
|
||||||
|
/******/ /* webpack/runtime/get javascript chunk filename */
|
||||||
|
/******/ (() => {
|
||||||
|
/******/ // This function allow to reference async chunks and sibling chunks for the entrypoint
|
||||||
|
/******/ __webpack_require__.u = (chunkId) => {
|
||||||
|
/******/ // return url for filenames based on template
|
||||||
|
/******/ return "" + chunkId + ".js";
|
||||||
|
/******/ };
|
||||||
|
/******/ })();
|
||||||
|
/******/
|
||||||
|
/******/ /* webpack/runtime/getFullHash */
|
||||||
|
/******/ (() => {
|
||||||
|
/******/ __webpack_require__.h = () => ("89440127ad6e3f87")
|
||||||
|
/******/ })();
|
||||||
|
/******/
|
||||||
|
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
||||||
|
/******/ (() => {
|
||||||
|
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
|
||||||
|
/******/ })();
|
||||||
|
/******/
|
||||||
|
/******/ /* webpack/runtime/make namespace object */
|
||||||
|
/******/ (() => {
|
||||||
|
/******/ // define __esModule on exports
|
||||||
|
/******/ __webpack_require__.r = (exports) => {
|
||||||
|
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
||||||
|
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
||||||
|
/******/ }
|
||||||
|
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
||||||
|
/******/ };
|
||||||
|
/******/ })();
|
||||||
|
/******/
|
||||||
|
/******/ /* webpack/runtime/node module decorator */
|
||||||
|
/******/ (() => {
|
||||||
|
/******/ __webpack_require__.nmd = (module) => {
|
||||||
|
/******/ module.paths = [];
|
||||||
|
/******/ if (!module.children) module.children = [];
|
||||||
|
/******/ return module;
|
||||||
|
/******/ };
|
||||||
|
/******/ })();
|
||||||
|
/******/
|
||||||
|
/******/ /* webpack/runtime/startup entrypoint */
|
||||||
|
/******/ (() => {
|
||||||
|
/******/ __webpack_require__.X = (result, chunkIds, fn) => {
|
||||||
|
/******/ // arguments: chunkIds, moduleId are deprecated
|
||||||
|
/******/ var moduleId = chunkIds;
|
||||||
|
/******/ if(!fn) chunkIds = result, fn = () => (__webpack_require__(__webpack_require__.s = moduleId));
|
||||||
|
/******/ chunkIds.map(__webpack_require__.e, __webpack_require__)
|
||||||
|
/******/ var r = fn();
|
||||||
|
/******/ return r === undefined ? result : r;
|
||||||
|
/******/ }
|
||||||
|
/******/ })();
|
||||||
|
/******/
|
||||||
|
/******/ /* webpack/runtime/require chunk loading */
|
||||||
|
/******/ (() => {
|
||||||
|
/******/ // no baseURI
|
||||||
|
/******/
|
||||||
|
/******/ // object to store loaded chunks
|
||||||
|
/******/ // "1" means "loaded", otherwise not loaded yet
|
||||||
|
/******/ var installedChunks = {
|
||||||
|
/******/ "webpack-runtime": 1
|
||||||
|
/******/ };
|
||||||
|
/******/
|
||||||
|
/******/ // no on chunks loaded
|
||||||
|
/******/
|
||||||
|
/******/ var installChunk = (chunk) => {
|
||||||
|
/******/ var moreModules = chunk.modules, chunkIds = chunk.ids, runtime = chunk.runtime;
|
||||||
|
/******/ for(var moduleId in moreModules) {
|
||||||
|
/******/ if(__webpack_require__.o(moreModules, moduleId)) {
|
||||||
|
/******/ __webpack_require__.m[moduleId] = moreModules[moduleId];
|
||||||
|
/******/ }
|
||||||
|
/******/ }
|
||||||
|
/******/ if(runtime) runtime(__webpack_require__);
|
||||||
|
/******/ for(var i = 0; i < chunkIds.length; i++)
|
||||||
|
/******/ installedChunks[chunkIds[i]] = 1;
|
||||||
|
/******/
|
||||||
|
/******/ };
|
||||||
|
/******/
|
||||||
|
/******/ // require() chunk loading for javascript
|
||||||
|
/******/ __webpack_require__.f.require = (chunkId, promises) => {
|
||||||
|
/******/ // "1" is the signal for "already loaded"
|
||||||
|
/******/ if(!installedChunks[chunkId]) {
|
||||||
|
/******/ if("webpack-runtime" != chunkId) {
|
||||||
|
/******/ installChunk(require("./" + __webpack_require__.u(chunkId)));
|
||||||
|
/******/ } else installedChunks[chunkId] = 1;
|
||||||
|
/******/ }
|
||||||
|
/******/ };
|
||||||
|
/******/
|
||||||
|
/******/ module.exports = __webpack_require__;
|
||||||
|
/******/ __webpack_require__.C = installChunk;
|
||||||
|
/******/
|
||||||
|
/******/ // no HMR
|
||||||
|
/******/
|
||||||
|
/******/ // no HMR manifest
|
||||||
|
/******/ })();
|
||||||
|
/******/
|
||||||
|
/************************************************************************/
|
||||||
|
/******/
|
||||||
|
/******/
|
||||||
|
/******/ })()
|
||||||
|
;
|
||||||
138
app/frontend/.next/static/chunks/app-pages-internals.js
Normal file
138
app/frontend/.next/static/chunks/app-pages-internals.js
Normal file
File diff suppressed because one or more lines are too long
39
app/frontend/.next/static/chunks/app/_not-found/page.js
Normal file
39
app/frontend/.next/static/chunks/app/_not-found/page.js
Normal file
File diff suppressed because one or more lines are too long
127
app/frontend/.next/static/chunks/app/admin/curriculum/page.js
Normal file
127
app/frontend/.next/static/chunks/app/admin/curriculum/page.js
Normal file
File diff suppressed because one or more lines are too long
171
app/frontend/.next/static/chunks/app/admin/documents/page.js
Normal file
171
app/frontend/.next/static/chunks/app/admin/documents/page.js
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user