docs: a Help page for the people who use the site, a Handbook for whoever runs it
Some checks failed
Tests / backend (push) Failing after 9s
Tests / frontend (push) Successful in 29s
Tests / e2e (push) Failing after 27s

They were one page, and it served neither. Somebody wanting to know what
"Studying for" does had to scroll past what the tutor is prompted with
and what re-embedding breaks.

/help — every signed-in account. Signing in through PedsHub SSO, what
"Studying for" scopes, Qbank against Sessions against Collections, study
and exam mode, where performance comes from, Reading and Cards and study
plans, "Make a deck", and how to report a bad question. An educator also
gets a section of their own: what a moderator has, what a grant gives
and what it does not, that nothing in the bank belongs to anybody, how a
draft becomes an article, how a plan is built, and what a moderator
cannot do. A learner never sees that half.

/handbook — administrators only now, with the same FAQ plus the rest.
The Settings card that pointed at it is admin-only to match, and
RequireAuth learned an admin door, which it did not have.

Help is in the account menu rather than the footer: a question you have
while working is answered from where you are.

And the repo docs describe the site that exists. CLAUDE.md lost the LMS
section — courses, modules, lessons, enrolments, all removed months ago
and still documented — and gained the permission model and the sign-in
flow. ADMIN.md's role table said moderators create courses; it now says
what the three roles actually reach, where roles come from, and that the
bank has no owners.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
This commit is contained in:
Daniel 2026-09-13 14:12:47 +02:00
parent 12453ba473
commit 1f2770257f
10 changed files with 496 additions and 46 deletions

View file

@ -61,12 +61,9 @@ docker compose exec backend python -m app.cli reprocess-doc 32
**Why documents get stuck:** If the Celery worker restarts while processing a PDF, the task is lost and the document stays at "processing" forever. `fix-stuck-docs` resets these to "ready" so they can be reprocessed.
### Courses & Quizzes
### Quizzes
```bash
# List all courses
docker compose exec backend python -m app.cli list-courses
# List quizzes (active only)
docker compose exec backend python -m app.cli list-quizzes
@ -115,9 +112,32 @@ cat backups/daily/pedquiz-YYYYMMDD-HHMMSS.sql.gz | gunzip | \
| Role | Capabilities |
|------|-------------|
| `user` | Take quizzes, study flashcards, enroll in courses, create quizzes from question bank |
| `moderator` | All user abilities + create courses, upload PDFs, manage documents |
| `admin` | All moderator abilities + admin dashboard, user management, model config, system settings |
| `user` | Sit sessions, study shared decks, read articles, keep collections and folders. No bank content unless a `CategoryGrant` says otherwise. |
| `moderator` | The whole bank — every question, article, category, deck, image library — plus the editorial queue. Nothing that configures the site. |
| `admin` | Everything, including models, people, site policy and the Handbook. |
**Roles come from the identity provider.** With `OIDC_ROLE_CLAIM` set, group
membership at `sso.pedshub.com` decides the role and is reapplied at every
sign-in; `pedshub-admins` and `pedshub-moderators` are the groups. The in-app
role endpoints answer 409 while that is on, because a role set here would be
overwritten at the next sign-in. The sync refuses to demote the last admin.
**Grants are per-user and in-app.** `/access` gives somebody a category branch,
an image library or a folder; it makes them an editor of what is inside and
adds the Questions and Images menu entries. Never self-assignable, never from a
claim.
**The bank has no owners.** Questions, articles, categories, decks, documents
and shared quizzes carry `user_id = NULL`. Authorship confers no rights.
Attempts, notes, favourites, collections, folders and personal sittings stay
with their person.
## Signing in
SSO only, via Authentik at `sso.pedshub.com`. No sign-up form, no invite codes,
no email sign-in codes — the provider does all three. `settings:sso_only` in
Redis closes every password door. Accounts are created on first sign-in, matched
by email address; a different address is a different account.
## Common Issues
@ -316,5 +336,5 @@ OIDC_PROVIDER_NAME=Authentik
- JWT tokens auto-refresh via sliding expiration (12h age or <1h remaining)
- Rate limiting on login (10 attempts per IP per 15 min) via Redis
- Email verification required for new accounts (SSO users are auto-verified)
- Quiz reminders skip deleted quizzes and course quizzes
- Quiz reminders skip deleted quizzes
- Users can opt out of reminders in Settings > Notifications

View file

@ -1,7 +1,15 @@
# PedsHub — AI Synopsis for Codebase Work
## What this is
PedsHub is a pediatric medical learning platform with an integrated LMS. Admins upload PREP exam PDFs, AI extracts MCQ questions (or generates flashcards), and users study them with an AI tutor. Any user can create courses, quizzes, and questions. Everything runs in Docker.
PedsHub is a pediatric question bank and reading library. Educators upload PREP
exam PDFs, AI proposes MCQs and flashcards from them, a person reviews what is
worth keeping, and learners sit it as sessions with an AI tutor beside them.
Sign-in is through Authentik at `sso.pedshub.com`; the companion scribe app is
at `app.pedshub.com`. Everything runs in Docker.
The LMS — courses, modules, lessons, enrolments — was removed. Anything in this
file or the code that still mentions a course is stale; say so rather than
building against it.
## Critical rules
- **Never restart services while a Celery task is running** — check `docker compose logs celery --tail=5` first
@ -28,21 +36,59 @@ Browser → Nginx (frontend) → FastAPI (4 uvicorn workers)
└── Celery (2 fork workers: PDF processing, quiz extraction, flashcard generation, classification, embedding regeneration)
└── db-backup (daily PostgreSQL dumps, 14 daily / 4 weekly / 6 monthly retention, ./backups/)
## Course/LMS system
Any user can create courses (not just moderators). Course structure: Course → Modules → Lessons.
- **Lesson types**: text (markdown), video (Vimeo/YouTube/local), document, quiz (from question bank), live_session (BBB/Zoom/Meet)
- **Enrollment**: users enroll in published courses, progress tracked per-lesson
- **Video**: auto-detect provider from URL (vimeo.com → vimeo, youtube.com → youtube)
- **BBB**: full API integration (create/join/end). Config: `BBB_SERVER_URL` + `BBB_SECRET` in .env
- **AI**: generate/refine lesson text via `POST /courses/{id}/lessons/{id}/ai-generate`
- **Status**: draft (creator-only) → published (visible to all) → archived
- **Subscription gate**: `requires_subscription` flag on courses (402 on enroll if set — Stripe integration placeholder)
- **Course quizzes**: fully decoupled from main quiz system. `POST /courses/{id}/quiz` copies questions and creates an independent quiz with `course_id` set. Hidden from main quizzes page, search, dashboard stats, and attempt history. Creator sets mode (timed/study), time limit, max attempts, questions per attempt (random pool), and `allow_review` (whether students can review answers). Results page is course-aware — shows "Back to Course" instead of retake/all quizzes/delete. Users see attempt history + review links on the course page only.
- **User roles**: `admin`, `moderator`, `user`. Only moderators and admins can create courses. Any user can create quizzes from the question bank.
- **Enrollee analytics**: `GET /courses/{id}/enrollees` returns progress + quiz scores. `GET /courses/{id}/enrollees/export` exports CSV.
- **Question ownership**: questions have `user_id` and `is_shared`. Users see shared + own questions. `PATCH /questions/{id}/share` toggles visibility.
- **Rich editor**: Milkdown (ProseMirror-based markdown WYSIWYG) for lesson content. Supports GFM tables, code blocks, LaTeX math (`$formula$`). No JSX parsing issues with `<` or `{`.
```
## Who may do what
Three roles, and one kind of grant.
- **admin** — everything, including Settings: models, people, site policy.
- **moderator** — the whole bank: every question, article, category, card
deck, image library, and the editorial queue. Nothing that configures the
site.
- **user** — their own sittings, notes, starred questions and folders. No
bank content at all unless a grant says otherwise.
Roles come from the identity provider's groups, never from this app. With
`OIDC_ROLE_CLAIM` set, `services/sso_roles.apply()` runs on every SSO sign-in
and brings the role into line with the person's groups — so removal at the
provider takes the role away here too. The two in-app role endpoints answer 409
while that mapping is on, because a role set here would be silently overwritten
at the next sign-in. The one demotion the sync refuses is the last
administrator.
`CategoryGrant` is the per-user half, given inside this app by a moderator or
admin (`/access`). It names a category branch, an image library or a folder,
and makes the holder an editor of what is inside it — which also makes the
Questions and Images menu entries appear. It never comes from a claim and is
never self-assignable.
**Nothing in the bank has an owner.** Questions, articles, categories, decks,
documents, media and shared quizzes all carry `user_id = NULL`, and every
creation path writes NULL. Authorship confers no rights anywhere: `may_edit_question`
and `can_edit_article` ask the role and the grants and nothing else. What keeps
an owner is what is genuinely one person's — attempts, notes, favourites,
collections, folders, study-plan progress, and the unshared quizzes that are
somebody's own sittings. See migration `q6a7b8c9d0e1`.
## Signing in
SSO only, through Authentik at `sso.pedshub.com`. There is no sign-up form, no
invite codes and no email sign-in codes — all three were this app doing the
provider's job, and all three were removed (migration `r7b8c9d0e1f2` drops
their tables).
- `GET /auth/sso/login` → 302 to the provider. It must stay `async` and the
redirect must be awaited; authlib's Starlette client returns a coroutine, and
returning it unawaited is a 500 on every click.
- `GET /auth/sso/callback` matches on the email claim, refuses an explicit
`email_verified: false`, applies the role from groups, then parks the access
token in Redis under a one-time code (`sso:exchange:<code>`, 60s) and
redirects with `?code=`. The token never travels in a URL — nginx logs the
request line, so it would be written to disk on every sign-in.
- `POST /auth/sso/exchange` spends the code once (GETDEL) and returns the token.
- `settings:sso_only` in Redis closes every password door: login, register,
forgot/reset password, resend verification, and setting a password through
`PUT /auth/me`. `GET /auth/signup-policy` reports it so the forms decline to
draw themselves rather than being refused after the fact.
## Key directories
```
@ -73,9 +119,6 @@ frontend/src/
FlashcardsPage.jsx — Browse decks + card browser with search
FlashcardStudyPage.jsx — Flip cards, got-it/review, keyboard nav, progress
QuizPage.jsx — Take quiz (exam/study mode), timer, progress save to Redis
CoursesPage.jsx — Browse/enroll courses, my courses, create courses
CourseDetailPage.jsx — Student view: modules, lessons, video player, progress
CourseEditorPage.jsx — Course creator: modules, lessons, AI content, question bank browser
AdminPage.jsx — Model config, user management, settings
components/
Navbar.jsx — Auth-aware nav with jobs badge
@ -89,8 +132,8 @@ frontend/src/
| users | Accounts with role (admin/moderator/user) | — |
| pdf_documents | Uploaded PDFs | user_id → users |
| sections | Page ranges within a document | document_id → pdf_documents |
| quizzes | Quiz metadata (course_id set = course-only, allow_review controls student access) | section_id → sections (nullable), user_id → users, course_id → courses (nullable) |
| questions | MCQ questions with pgvector embedding | source_quiz_id → quizzes (nullable), user_id → users (nullable) |
| quizzes | A sitting or a shared bank test. `is_shared=1` is the bank's and ownerless; `is_shared=0` is somebody's own session | section_id → sections (nullable), user_id → users (nullable) |
| questions | MCQ questions with pgvector embedding. `user_id` is always NULL — the bank has no owners | source_quiz_id → quizzes (nullable) |
| quiz_question_links | Quiz ↔ Question many-to-many | quiz_id, question_id |
| flashcard_decks | Flashcard deck metadata | section_id → sections, user_id → users |
| flashcards | Individual cards (front/back) | deck_id → flashcard_decks |
@ -98,11 +141,6 @@ frontend/src/
| question_tag_links | Question ↔ Tag | question_id, tag_id |
| flashcard_tag_links | Flashcard ↔ Tag | flashcard_id, tag_id |
| quiz_attempts | User quiz sessions with score | quiz_id, user_id |
| courses | LMS courses (draft/published/archived) | user_id → users |
| course_modules | Sections within a course | course_id → courses |
| course_lessons | Items within a module (text/video/quiz/live) | module_id → course_modules, quiz_id → quizzes |
| course_enrollments | User enrollment + progress tracking | course_id, user_id (unique) |
| course_lesson_progress | Per-lesson completion status | enrollment_id, lesson_id (unique) |
## Common patterns
- **Tag filtering SQL**: `WHERE tag_id = ANY(:tag_ids) GROUP BY ... HAVING COUNT(DISTINCT tag_id) = :cnt` — AND logic across tags
@ -161,8 +199,8 @@ docker compose exec backend alembic downgrade -1
- Don't `docker compose restart` expecting code changes to apply — must rebuild
- Don't use `window.confirm()` — user hates browser popups, use inline confirmation or the Dialog component
- Don't use MDXEditor — it's an MDX parser that chokes on `<` and `{` in medical content. Milkdown (CommonMark) is used instead.
- Don't create quizzes via `POST /questions/from-bank` for courses — use `POST /courses/{id}/quiz` which copies questions and hides the quiz from the main page.
- Don't show course quiz data on the main quizzes page, dashboard stats, or attempt history — course quizzes are fully decoupled. Filter with `Quiz.course_id.is_(None)`.
- Don't show "from pool of N" to users on course quiz display — just show the number of questions per attempt.
- Don't allow users to delete course quiz attempts — the backend returns 403.
- Don't put documents listing on the dashboard — it's in Settings page under Nextcloud.
- Don't gate anything on `x.user_id == current_user.id` for bank content. Authorship confers nothing; ask the role or a grant.
- Don't stamp a creator on new bank content — write `user_id=None`.
- Don't reintroduce invite codes or email sign-in codes. The identity provider does that.
- Don't put a token in a URL. nginx logs the request line.

View file

@ -578,10 +578,16 @@ def sso_config():
from app.config import settings as cfg
sso_enabled = bool(cfg.OIDC_PROVIDER_URL and cfg.OIDC_CLIENT_ID)
sso_settings = _get_sso_settings()
from app.services import sso_roles
return {
"sso_enabled": sso_enabled,
"sso_only": sso_settings["sso_only"],
"provider_name": cfg.OIDC_PROVIDER_NAME if sso_enabled else None,
# Whether roles come from the provider's groups. Says only that they
# do, never which groups — enough for the access page to stop offering
# a control that can now only answer 409.
"roles_from_provider": sso_roles.is_configured(cfg),
}

View file

@ -44,6 +44,7 @@ const CollectionsPage = lazyPage(() => import('./pages/CollectionsPage'))
const EditorialPage = lazyPage(() => import('./pages/EditorialPage'))
const AccessPage = lazyPage(() => import('./pages/AccessPage'))
const HandbookPage = lazyPage(() => import('./pages/HandbookPage'))
const HelpPage = lazyPage(() => import('./pages/HelpPage'))
const ToolsPage = lazyPage(() => import('./pages/ToolsPage'))
const StudyPlansPage = lazyPage(() => import('./pages/StudyPlansPage'))
const StudyPlanPage = lazyPage(() => import('./pages/StudyPlanPage'))
@ -151,10 +152,11 @@ function Forbidden() {
// are not signed in. A role you do not have is worth saying out loud.
export function RequireAuthForTest(props) { return RequireAuth(props) }
function RequireAuth({ moderator = false }) {
function RequireAuth({ moderator = false, admin = false }) {
const { user, loading } = useAuth()
if (loading) return <LoadingFallback />
if (!user) return <Navigate to="/home" replace />
if (admin && user.role !== 'admin') return <Forbidden />
if (moderator && user.role !== 'admin' && user.role !== 'moderator') return <Forbidden />
return <Outlet />
}
@ -240,9 +242,20 @@ function AppRoutes() {
Settings now; the old address still works for anyone who
bookmarked it. */}
<Route path="/admin" element={<Navigate to="/settings?s=people" replace />} />
{/* How the site works, for anyone who maintains it an educator
with one branch needs it as much as an administrator does, so
it is not moderator-only. */}
{/* What a learner or an educator actually asks. The Handbook is
the same material plus how the tutor is prompted and what
changing the embedding model breaks reference for whoever
runs the place, noise for everybody else so it moved behind
the admin door and this stayed in front of it. */}
<Route path="/help" element={<HelpPage />} />
</Route>
</Route>
{/* Administrators only. The Handbook is how the site is run the
tutor's prompt, what changing the embedding model breaks and
the part of it people actually ask about is at /help. */}
<Route element={<RequireAuth admin />}>
<Route element={<AppLayout />}>
<Route path="/handbook" element={<HandbookPage />} />
</Route>
</Route>

View file

@ -138,6 +138,10 @@ function AccountMenu({ user, onLogout }) {
administrator, so a second entry pointing into the same page was
two names for one place. */}
<Link role="menuitem" to="/settings" onClick={() => setOpen(false)}>Settings</Link>
{/* How the site works, for the people who use it. Not in the footer:
a question you have while working is answered from where you are,
not from the bottom of the page. */}
<Link role="menuitem" to="/help" onClick={() => setOpen(false)}>Help</Link>
<button type="button" role="menuitem" className="acct-signout"
onClick={() => { setOpen(false); onLogout() }}>Sign out</button>
</div>

View file

@ -144,6 +144,19 @@ export default function AccessPage() {
: api.delete(`/access/${selectedId}/grants/${kind}/${targetId}`)),
on ? 'Could not grant that' : 'Could not remove that')
//: Whether the identity provider decides roles. Asked once; the endpoint is
//: public and says only that it does, never which groups.
const [rolesFromProvider, setRolesFromProvider] = useState(false)
const [providerName, setProviderName] = useState('your identity provider')
useEffect(() => {
api.get('/auth/sso/config')
.then(res => {
setRolesFromProvider(res.data?.roles_from_provider === true)
setProviderName(res.data?.provider_name || 'your identity provider')
})
.catch(() => {})
}, [])
const setRole = (role) => change(
() => api.put(`/access/${selectedId}/role`, { role }), 'Could not change that')
@ -233,6 +246,15 @@ export default function AccessPage() {
<p className="ac-note">
An administrator already manages everything, and that is not changed here.
</p>
) : rolesFromProvider ? (
/* The switch is at the provider. A control here would set a
role that is overwritten from their groups at their next
sign-in, and a control that can only fail is worse than no
control so it says where to go instead. */
<p className="ac-note">
Whether this person manages everything is decided by their groups at
{' '}{providerName}. The branches below are set here.
</p>
) : (
<section className="ac-section">
<h3>Everything</h3>

View file

@ -184,3 +184,23 @@ describe('question folders as a grantable thing', () => {
expect(await screen.findByText('0 branches, 1 folder')).toBeInTheDocument()
})
})
it('does not offer a role switch when the provider decides roles', async () => {
// A control that can only answer 409 is worse than no control: it invites
// somebody to make a change that appears to work, holds for an afternoon,
// and is overwritten from their groups at their next sign-in.
vi.clearAllMocks()
api.get.mockImplementation(url => {
if (url === '/access/') return Promise.resolve({ data: users() })
if (url === '/access/tree') return Promise.resolve({ data: TREE })
if (url === '/auth/sso/config') {
return Promise.resolve({ data: { sso_enabled: true, roles_from_provider: true,
provider_name: 'PedsHub SSO' } })
}
return Promise.resolve({ data: {} })
})
mount()
await pick('Ada')
expect(screen.queryByLabelText('Give access to everything')).toBeNull()
expect(await screen.findByText(/decided by their groups/)).toBeInTheDocument()
})

View file

@ -15,6 +15,8 @@ import BackLink from '../components/BackLink'
*/
const SECTIONS = [
{ id: 'faq', label: 'Common questions' },
{ id: 'educators', label: 'For educators' },
{ id: 'links', label: 'Article links' },
{ id: 'tips', label: 'Tips in the prose' },
{ id: 'tutor', label: 'The AI tutor' },
@ -70,6 +72,139 @@ export default function HandbookPage() {
</nav>
</div>
<section id="faq">
<h2>Common questions</h2>
<p className="hb-note">
For everybody who uses PedsHub. The sections after this one are for
people who write the material.
</p>
<h3>How do I sign in?</h3>
<p>
Through PedsHub SSO. The Sign in button hands you to{' '}
<code>sso.pedshub.com</code>, which asks for your address and emails
you a code; typing it there brings you back signed in. There is no
separate PedsHub password, and no sign-up form an administrator
sends you an invitation link from the sign-in service itself.
</p>
<h3>What does "Studying for" at the top do?</h3>
<p>
It names your exam, and it scopes the site to it: which questions the
bank offers, which filters appear, and what your performance page is
measured against. Material tied to no exam stays visible whichever
you pick, so choosing one never hides the general reading.
</p>
<h3>Qbank, Sessions, Collections which is which?</h3>
<p>
<strong>Qbank</strong> is where a session is built: pick topics, a
length and a mode, and start. <strong>Sessions</strong> is every
sitting you have done, with the analysis of how each went.{' '}
<strong>Collections</strong> is what you have put aside starred
questions and any folders you keep.
</p>
<h3>Study mode or exam mode?</h3>
<p>
Study mode marks each question as you answer it and shows the
explanation there and then. Exam mode holds everything back until the
block is finished, and runs a clock at the pace of a real paper.
</p>
<h3>Where does my performance come from?</h3>
<p>
Your own answers, and nobody else's. It is on the Sessions page,
alongside a per-topic reading of where you stand and what to study
next. Sittings marked as repetitions are analysed but left out of the
figures that say how much of the bank you know.
</p>
<h3>Reading, Cards and Study plans</h3>
<p>
<strong>Reading</strong> is the article library, browsed by topic; a
question's explanation links into it. <strong>Cards</strong> are the
flashcard decks shared with you, studied one at a time with a verdict
on each. <strong>Study plans</strong> are ordered blocks of questions
and reading, published by an educator, which you work through in
order.
</p>
<h3>What is "Make a deck"?</h3>
<p>
A link out to the companion app at <code>app.pedshub.com</code>, where
a deck is written from your own material. The same sign-in works on
both sides.
</p>
<h3>Something in a question looks wrong.</h3>
<p>
Report it from the question itself. It reaches the people who
maintain the bank with the question attached, which is quicker than
describing which one you meant.
</p>
</section>
<section id="educators">
<h2>For educators</h2>
<p>
Two things decide what you may change: your <strong>role</strong>,
which comes from your groups at PedsHub SSO, and your{' '}
<strong>grants</strong>, which an administrator gives you inside this
app.
</p>
<h3>What a moderator has</h3>
<p>
Every question, article, category, card deck and image library, and
the editorial queue. Nothing under Settings that configures the site
itself models, people, site policy which is an administrator's.
</p>
<h3>What a grant gives</h3>
<p>
A grant names a branch of the topic tree, an image library, or a
folder. Holding one makes you an editor of what is inside it: the
questions filed there, the articles filed there, and the images in
that library. It appears as menu entries Questions and Images
the moment it is given, and takes them away again when it is
withdrawn. It does not open Editorial, which is the whole library's
review queue.
</p>
<h3>What nothing in the bank belongs to</h3>
<p>
Anybody. Questions, articles, categories, decks and uploaded
documents have no owner: writing one does not make it yours, and
losing a colleague does not orphan it. What is yours is your own
sittings, your notes, your starred questions and your folders.
</p>
<h3>Writing an article</h3>
<p>
Editorial holds the queue and every draft. A draft is opened from
there, at its own address, and stays out of Reading until it is
published Reading is what a learner sees, and shows no drafts to
anybody. Drafting with AI is offered there too; the result is a draft
like any other, for a person to read before it goes out.
</p>
<h3>Making a study plan</h3>
<p>
Study plans, then <em>New plan</em>. Add blocks, attach reading to
them, and publish when it is worth working through. A plan can also
be built from an examining board's published outline: as papers, each
block drawn to the board's weights, or as domains, one block per
content area in the board's order.
</p>
<h3>What a moderator cannot do</h3>
<p>
Change anybody's role that is decided by groups at PedsHub SSO
or reach the model configuration, the people list, or site policy.
</p>
</section>
<section id="links">
<h2>How a question comes to link to an article</h2>
<p>
@ -251,8 +386,24 @@ export default function HandbookPage() {
<section id="joining">
<h2>Joining and sharing</h2>
<p>
Registration can require an invite code issued by an administrator.
Turning that on does not affect anyone who already has an account.
Accounts come from PedsHub SSO at <code>sso.pedshub.com</code>: an
administrator mints an invitation link there, the person sets
themselves up, and this app creates their account the first time they
sign in. There is no sign-up form here and no invite codes both
were this app doing the sign-in service's job, and both are gone.
</p>
<p>
Roles come from groups at the same place:{' '}
<code>pedshub-admins</code> makes an administrator,{' '}
<code>pedshub-moderators</code> a moderator, and anybody else is a
learner. They are applied at every sign-in, so removing somebody from
a group takes the role away without anybody remembering to. That is
also why the role control in Access is not offered while the mapping
is on: a role set here would be overwritten at their next sign-in.
</p>
<p>
Matching is by email address. The same address signs into the same
account; a different address is a different account.
</p>
<p>
A learner can make a public link to a session. Turning sharing off

View file

@ -0,0 +1,174 @@
import { useAuth } from '../context/AuthContext'
import './HandbookPage.css'
import BackLink from '../components/BackLink'
/**
* Help, for the people who use the site rather than run it.
*
* The same material used to live at the top of the Handbook, which also
* carries how the tutor is prompted and what changing the embedding model
* breaks reference for whoever maintains the place, and noise for everybody
* else. So the two are separate pages: this one answers what a learner or an
* educator actually asks, and the Handbook keeps the rest.
*/
const SECTIONS = [
{ id: 'faq', label: 'Common questions' },
{ id: 'educators', label: 'For educators' },
]
export default function HelpPage() {
const { user } = useAuth()
const educator = !!user?.is_moderator
return (
<div className="hb">
<BackLink to="/">Dashboard</BackLink>
<div className="hb-head">
<h1>Help</h1>
<p>
How PedsHub works, in the order people ask.
</p>
<nav aria-label="Help sections">
{SECTIONS.filter(s => s.id !== 'educators' || educator).map(section => (
<a key={section.id} href={`#${section.id}`}>{section.label}</a>
))}
</nav>
</div>
<section id="faq">
<h2>Common questions</h2>
<h3>How do I sign in?</h3>
<p>
Through PedsHub SSO. The Sign in button hands you to{' '}
<code>sso.pedshub.com</code>, which asks for your address and emails
you a code; typing it there brings you back signed in. There is no
separate PedsHub password, and no sign-up form an administrator
sends you an invitation link from the sign-in service itself.
</p>
<h3>What does "Studying for" at the top do?</h3>
<p>
It names your exam, and it scopes the site to it: which questions the
bank offers, which filters appear, and what your performance page is
measured against. Material tied to no exam stays visible whichever
you pick, so choosing one never hides the general reading.
</p>
<h3>Qbank, Sessions, Collections which is which?</h3>
<p>
<strong>Qbank</strong> is where a session is built: pick topics, a
length and a mode, and start. <strong>Sessions</strong> is every
sitting you have done, with the analysis of how each went.{' '}
<strong>Collections</strong> is what you have put aside starred
questions and any folders you keep.
</p>
<h3>Study mode or exam mode?</h3>
<p>
Study mode marks each question as you answer it and shows the
explanation there and then. Exam mode holds everything back until the
block is finished, and runs a clock at the pace of a real paper.
</p>
<h3>Where does my performance come from?</h3>
<p>
Your own answers, and nobody else's. It is on the Sessions page,
alongside a per-topic reading of where you stand and what to study
next. Sittings marked as repetitions are analysed but left out of the
figures that say how much of the bank you know.
</p>
<h3>Reading, Cards and Study plans</h3>
<p>
<strong>Reading</strong> is the article library, browsed by topic; a
question's explanation links into it. <strong>Cards</strong> are the
flashcard decks shared with you, studied one at a time with a verdict
on each. <strong>Study plans</strong> are ordered blocks of questions
and reading, published by an educator, which you work through in
order.
</p>
<h3>What is "Make a deck"?</h3>
<p>
A link out to the companion app at <code>app.pedshub.com</code>, where
a deck is written from your own material. The same sign-in works on
both sides.
</p>
<h3>Something in a question looks wrong.</h3>
<p>
Report it from the question itself. It reaches the people who
maintain the bank with the question attached, which is quicker than
describing which one you meant.
</p>
</section>
{educator && (
<section id="educators">
<h2>For educators</h2>
<p>
Two things decide what you may change: your <strong>role</strong>,
which comes from your groups at PedsHub SSO, and your{' '}
<strong>grants</strong>, which an administrator gives you inside this
app.
</p>
<h3>What a moderator has</h3>
<p>
Every question, article, category, card deck and image library, and
the editorial queue. Nothing under Settings that configures the site
itself models, people, site policy which is an administrator's.
</p>
<h3>What a grant gives</h3>
<p>
A grant names a branch of the topic tree, an image library, or a
folder. Holding one makes you an editor of what is inside it: the
questions filed there, the articles filed there, and the images in
that library. It appears as menu entries Questions and Images
the moment it is given, and takes them away again when it is
withdrawn. It does not open Editorial, which is the whole library's
review queue.
</p>
<h3>What nothing in the bank belongs to</h3>
<p>
Anybody. Questions, articles, categories, decks and uploaded
documents have no owner: writing one does not make it yours, and
losing a colleague does not orphan it. What is yours is your own
sittings, your notes, your starred questions and your folders.
</p>
<h3>Writing an article</h3>
<p>
Editorial holds the queue and every draft. A draft is opened from
there, at its own address, and stays out of Reading until it is
published Reading is what a learner sees, and shows no drafts to
anybody. Drafting with AI is offered there too; the result is a draft
like any other, for a person to read before it goes out.
</p>
<h3>Making a study plan</h3>
<p>
Study plans, then <em>New plan</em>. Add blocks, attach reading to
them, and publish when it is worth working through. A plan can also
be built from an examining board's published outline: as papers, each
block drawn to the board's weights, or as domains, one block per
content area in the board's order.
</p>
<h3>What a moderator cannot do</h3>
<p>
Change anybody's role that is decided by groups at PedsHub SSO
or reach the model configuration, the people list, or site policy.
</p>
<p className="hb-note">
The Handbook, in Settings, goes further into how the parts that are
not obvious work. It is for administrators.
</p>
</section>
)}
</div>
)
}

View file

@ -242,11 +242,13 @@ function NextcloudSection() {
function ToolsSection() {
// Editorial is not here: it has its own entry in the section bar, and a card
// pointing at it would be a second door to the same room.
const { user } = useAuth()
const isAdmin = user?.role === 'admin'
return (
<Section title="Tools" description="Editing surfaces that have a page of their own.">
<div className="set-cards">
{[
{ to: '/handbook', icon: '📖', label: 'Handbook', desc: 'How the parts that are not obvious work' },
{ to: '/handbook', icon: '📖', label: 'Handbook', desc: 'How the parts that are not obvious work', admin: true },
/* Not the workbench and not the taxonomy. Both are question work,
both are one press from Questions in the main menu, and listing
them here as well is a second door into the same room which is
@ -254,7 +256,7 @@ function ToolsSection() {
{ to: '/access', icon: '🔑', label: 'Access', desc: 'Who may edit what' },
{ to: '/trash', icon: '🗑️', label: 'Trash', desc: 'Restore deleted questions' },
{ to: '/jobs', icon: '📋', label: 'Jobs', desc: 'Extractions, drafts and card runs, with their logs' },
].map(item => (
].filter(item => !item.admin || isAdmin).map(item => (
<Link key={item.to} to={item.to} className="set-card">
<div className="set-card-icon" aria-hidden="true">{item.icon}</div>
<div className="set-card-name">{item.label}</div>