Teaching: hide-notes-until share, session length check, slides export

- A study share link can hide the owner's notes until a chosen time: the server
  sends only passages and discussion questions until then, with a banner in the
  shared view (new share_reveal column, migrated on start).
- Outline: a session length with "N of M min planned, X min over" and a marker
  on the point where the plan runs past it.
- Export > Slides: printable 16:9 slides (title, passage, outline points with
  their verse, discussion questions) for one session or all; light or dark.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-14 13:08:21 -04:00
1 parent 67645cf1df
commit 2c7c7145c4
14 files changed
+303 -33

No files matched your search

+11 -9
View File
@@ -2,7 +2,7 @@
import { randomUUID } from 'crypto';
import { acceptEditInvite, clearOutlineShareToken, clearShareToken, countOpenEditInvites, createEditInvite, getSharePassHash, getShareSettings, getSharedByToken, isProjectOwner, listCollaborators, removeCollaborator, setOutlineShareToken, setShareSettings, setShareToken } from '../db.js';
import { requireAuth } from '../auth.js';
import { checkShareAccess, expiryFromDays, hashPasscode } from '../shareLinks.js';
import { checkShareAccess, expiryFromDays, hashPasscode, hideUntilReveal, revealFromInput } from '../shareLinks.js';
import { buildOutlinePayload } from '../outlineShare.js';
import { TOO_MANY, isLimited, recordFailure } from '../limiter.js';
@@ -16,7 +16,7 @@ export function registerSharingRoutes(app) {
try {
const settings = getShareSettings(req.params.id, req.session.userId);
if (!settings) return res.status(404).json({ error: 'Project not found.' });
res.json({ shareToken: settings.token, expiresAt: settings.expiresAt, hasPasscode: settings.hasPasscode });
res.json({ shareToken: settings.token, expiresAt: settings.expiresAt, hasPasscode: settings.hasPasscode, revealAt: settings.revealAt });
} catch (err) {
console.error('GET /api/projects/:id/share error:', err);
res.status(500).json({ error: 'Failed to load share status.' });
@@ -37,8 +37,10 @@ export function registerSharingRoutes(app) {
// a blank passcode field means "keep it"; null clears; a string sets
const current = getSharePassHash(req.params.id, userId, outline);
const passHash = 'passcode' in body ? (body.passcode === null ? null : await hashPasscode(body.passcode) ?? current) : current;
setShareSettings(req.params.id, userId, { expiresAt, passHash }, outline);
res.json({ shareToken: token, expiresAt, hasPasscode: !!passHash });
// only a study link can hide its notes until a time; a past or empty time turns hiding off
const revealAt = outline ? null : ('revealAt' in body ? revealFromInput(body.revealAt) : settings.revealAt);
setShareSettings(req.params.id, userId, { expiresAt, passHash, revealAt }, outline);
res.json({ shareToken: token, expiresAt, hasPasscode: !!passHash, revealAt });
} catch (err) {
console.error(`POST share (${outline ? 'outline' : 'study'}) error:`, err);
res.status(500).json({ error: 'Failed to enable sharing.' });
@@ -123,14 +125,14 @@ export function registerSharingRoutes(app) {
res.status(access.status).json({ error: access.error, needsPasscode: !!access.needsPasscode, expired: access.status === 410 });
return null;
}
return found.project;
return found;
}
// GET /api/share-outline/:token — PUBLIC, no login required (a passcode may be needed)
app.get('/api/share-outline/:token', async (req, res) => {
try {
const project = await openSharedLink(req, res, true);
if (project) res.json(buildOutlinePayload(project));
const found = await openSharedLink(req, res, true);
if (found) res.json(buildOutlinePayload(found.project));
} catch (err) {
console.error('GET /api/share-outline/:token error:', err);
res.status(500).json({ error: 'Failed to load the shared outline.' });
@@ -140,8 +142,8 @@ export function registerSharingRoutes(app) {
// GET /api/share/:token — PUBLIC, no login required: fetch a shared project read-only (a passcode may be needed)
app.get('/api/share/:token', async (req, res) => {
try {
const project = await openSharedLink(req, res, false);
if (project) res.json(project);
const found = await openSharedLink(req, res, false);
if (found) res.json(hideUntilReveal(found.project, found.revealAt));
} catch (err) {
console.error('GET /api/share/:token error:', err);
res.status(500).json({ error: 'Failed to load shared project.' });