docs: a Help page for the people who use the site, a Handbook for whoever runs it
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:
parent
12453ba473
commit
1f2770257f
10 changed files with 496 additions and 46 deletions
36
ADMIN.md
36
ADMIN.md
|
|
@ -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
|
||||
|
|
|
|||
98
CLAUDE.md
98
CLAUDE.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
174
frontend/src/pages/HelpPage.jsx
Normal file
174
frontend/src/pages/HelpPage.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue