| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
feat(billing): active-user tracking and overage billing Bills organizations for active users beyond their plan's included member limit, end to end in this repo instead of split across apps/api and a separate platform repo. Tracking - A member counts as active for a month once seen on 2+ distinct UTC days. Recorded by a best-effort touch in get_current_user, deduped through Redis so it costs at most one DB write per user/org/day, and fully server-side so ad and tracker blockers cannot skip it. - Overage is $1 per active user beyond the limit (Standard 200, Pro 500), exposed through an org endpoint, an internal platform-key endpoint, a CLI command, and a badge on the Users page. Drops the obsolete seats_200 add-on pack. Billing - apps/web adds the overage as a line item on the org's existing renewal invoice via the invoice.created webhook, before Stripe finalizes it. One invoice, one charge, no metered billing config. - Each renewal invoice reconciles every complete prior month it covers, derived from the subscription's own interval (12 months for annual, 1 for monthly) plus a 2-month buffer. A missed webhook is caught up on the next renewal invoice, so there is no cron to schedule. - The subscription id is read from parent.subscription_details as well as the invoice's top level, since Stripe's Basil API version moved it and the SDK is on v22. - Idempotency: an org/month is skipped when an au_overage item for that period already exists, looked up with pagination and a created >= billed-month filter. The Stripe idempotency key only lasts 24h, so that lookup is the durable guard. - A failed webhook clears its own in-flight marker so Stripe's retry is not mistaken for a duplicate and silently dropped. Plan changes - Overage bills in arrears, up to a year back on annual subscriptions, so the member limit has to come from the plan that applied during the billed month, not the plan the org is on when the invoice is cut. organization_plan_history records each transition from update_org_with_config_no_auth, the one path every plan change goes through, and resolve_plan_for_month prices a month against the most generous limit the org held during it. Orgs with no history covering a month fall back to their current plan. Tested: apps/api security suite green with new coverage for the activity logic, the Redis cache fallbacks, the auth hooks and the plan-history resolution; activity.py at 100%. apps/web has unit tests for the billing period math and both invoice payload shapes. Not exercised against live Stripe. Before this bills anyone, invoice.created must be enabled on the Stripe webhook endpoint; it is not subscribed yet. The first billable month is the first complete month after rollout, and only standard/pro plans are ever billed. | 1 个月前 | |
fix(web): don't paint the denial page before authorization is decided isAuthorized started at `false` while the decision is made in an effect, which runs after the commit — so page mode rendered the denial surface for a frame on every dashboard load, admins included. As a bare <h1> that went unnoticed; as a full error card it would not. Tri-state instead: `null` means undecided and holds the loader, `false` means denied. The unauthenticated branch leaves it undecided too, so a signed-out visitor gets the loader while the /login navigation is in flight rather than being told they lack permission on the way out. Scoped to page mode — component mode renders inline (the sidebar, the dashboard home), where a full-screen spinner in place of the component would be worse than rendering nothing. | 1 个月前 | |
fix(security): make the superadmin surface Enterprise-only The superadmin dashboard shipped in OSS builds. Its backend routes are Enterprise-only, so what an OSS operator got was admin chrome over endpoints that were not there, plus a login form with nothing behind it. The only check that said "not available in OSS" ran client-side, on a signal that cannot be trusted. Add a shared guard in security/superadmin.py that returns 403 with a machine-readable ee_required detail. It is applied to require_superadmin, the three AI-credit mutators, and superadmin token authentication. The read-only AI-credits endpoint is deliberately left alone — org billing and usage pages read it in every mode. On the web side, app/admin/layout.tsx now decides server-side and renders a licence screen before the admin providers mount. Being a root layout it also covers /admin/login, which sat outside the gated route group. The untrusted client-side check is removed rather than kept alongside the new one, so there is a single gate with one failure direction. EELicenseError gains a detector for the new 403, kept distinct from the existing 503 case, which means something different and is worth retrying. Also tightens deployment-mode resolution to fail closed, and fixes a loading-state bug in SuperadminAuthorization where logged-out visitors never reached the right screen. | 1 个月前 | |
fix: stop startup, email and list-render failures from cascading API: - auto_install no longer opens its own engine; a second pool per pod doubled the connection footprint against the Postgres pooler, and once the pooler ran out of clients that extra pool raised during boot, aborting startup into a crash loop that opened even more connections. - connect_to_db retries transient connect failures with backoff and still fails fast on permanent ones (bad credentials). - Welcome/lifecycle emails are best-effort: a rate-limited or timing-out mail provider no longer turns a completed signup into a 503. Password reset, invitation and verification mail still raise. Transient Resend failures get one retry. - HLS jobs use a scratch dir whose cleanup cannot fail the job (ffmpeg racing the rmtree surfaced as "Directory not empty" after a successful transcode). - Background queue pollers and reapers only log at error after repeated failures, not on a single Redis read timeout. - Missing org config logs at warning; health-probe 503s and OpenTelemetry context-detach noise no longer report as application errors. Web: - asArray() coerces API results before list rendering. getResponseMetadata returns the error body under `data`, so a failed request handed components an object where a list was expected and they died on `.map`/`.filter is not a function`. getResponseMetadata also survives a response with no JSON body. - safePlay() handles rejected play() promises (autoplay policy, aborted by a source swap) instead of leaking unhandled rejections. - Dashboard opts out of built-in page translation, which mutated the DOM under React and killed the editor with "insertBefore ... not a child of this node". - Course editor gets a segment error boundary so a crash in one tab no longer blanks the whole dashboard. - Sentry client drops browser-extension errors that are attributed to us. | 1 个月前 | |
fix(billing): resolve the internal key from either env name, restore fulfillment fallbacks The plan write read the internal key under a single env name and fell back to an empty string, so a deployment that provisioned the other supported name sent an empty key and got a 403 that looked like a key mismatch rather than a missing one. Read either name, preferring the one the API validates, and throw when neither is set. Restores the redundancy that a stale-base rebase removed: the webhook-independent POST /api/billing/fulfill path, the customer.subscription.created handler, and the retry for a subscription Stripe has not materialized yet. An unresolvable plan on a session that carries an org id now retries instead of acking, since that means a missing price env rather than a foreign checkout. Only a completed event counts as a duplicate now. Marking before processing and not clearing it meant a delivery that died mid-flight had its retry answered "duplicate" and dropped. The post-checkout page announced success unconditionally; it now waits for fulfillment to confirm, and invalidates the org query so the plan badge does not serve a stale value for five minutes. Tests cover the key contract and the webhook paths. | 1 个月前 | |
fix(config): fail loud on missing SaaS credentials instead of degrading quietly Pack activation and active-user overage billing read the platform key with an empty-string fallback, so an unset key sent an empty header and the backend answered "not configured on the server" — a message that points at the API when the credential is actually missing on the caller. Both call sites now resolve it through one helper that throws. Google audience verification read only one of the two names this value is provisioned under, so a deployment that had the client id all along still ran with verification disabled. Accept either name. The missing-credential report ran on every config load, repeating the same lines thousands of times a day and burying the one-shot warnings between them — which is how a disabled audience check went unnoticed. It is now a single aggregated line per process, and it covers the Google client id too. | 1 个月前 | |
fix(billing): resolve the internal key from either env name, restore fulfillment fallbacks The plan write read the internal key under a single env name and fell back to an empty string, so a deployment that provisioned the other supported name sent an empty key and got a 403 that looked like a key mismatch rather than a missing one. Read either name, preferring the one the API validates, and throw when neither is set. Restores the redundancy that a stale-base rebase removed: the webhook-independent POST /api/billing/fulfill path, the customer.subscription.created handler, and the retry for a subscription Stripe has not materialized yet. An unresolvable plan on a session that carries an org id now retries instead of acking, since that means a missing price env rather than a foreign checkout. Only a completed event counts as a duplicate now. Marking before processing and not clearing it meant a delivery that died mid-flight had its retry answered "duplicate" and dropped. The post-checkout page announced success unconditionally; it now waits for fulfillment to confirm, and invalidates the org query so the plan badge does not serve a stale value for five minutes. Tests cover the key contract and the webhook paths. | 1 个月前 | |
fix(security): harden containers, dependencies, and URL handling - Move Docker images to python:3.14.7-alpine3.24 (apk instead of apt), uv/bun copied in from their images instead of pip install/curl, frozen installs, pip removed from runtime - Drop llama-index-core for a small tiktoken chunker, cutting ~30 transitive deps - Pin pillow, refresh JS lockfiles, add stricter URL/redirect validation on web - Canonicalize SQLite storage paths before authorizing course access, use full SHA-256 for the analytics cache key - Add regression tests for chunking, URL guards, and path containment | 8 天前 | |
fix: sort folders by org mode and hide certificate copy Folder content sorted by position only, ignoring org sort_mode, so dashboard drag order didn't match the public library page. Certificate copy was shown based on the learner's certificate row rather than whether the course has certification enabled, so uncertified courses displayed misleading "no certificate" warnings. Shared sort comparator: apps/web/lib/library/sort.ts. New gate: useCourseCertification hook. | 25 天前 | |
feat(communities): allow opt-in YouTube embeds in discussion posts Communities can enable rich content in discussions, off by default. When enabled, students can embed a YouTube video in a post; the API checks the tiptap doc against an allowlist of node and mark types and only accepts a YouTube embed with a valid video id. Disabling the flag renders existing embeds as plain links instead of dropping the post. | 6 天前 | |
fix(web): give course-structure labels their own element so translation can't crash the page Chrome and Edge page translation swaps a text node for a <font> wrapper it inserts itself. Where that text node is a bare sibling of an element, React's next commit tries to insert before a node that is no longer a child of the container and throws "NotFoundError: Failed to execute 'insertBefore'", unmounting the course content page while the user is editing it. The activity row had four of those: the publish pill and three dropdown items, each an icon element followed by a bare label. ChapterElement already wraps its labels in spans, which is why the crash was isolated to the activity rows. Wrapping the labels keeps the text translatable — the translator swaps what is inside the span and React's sibling structure survives. The dashboard already sets notranslate on its own subtree, but the dropdown renders through a Radix portal to document.body, outside that wrapper, so the attribute never covered these nodes. A test pins the shape for the three components in this directory. Fixes LEARNHOUSE-WEB-5M | 1 个月前 | |
fix(editor,assignments): follow-up fixes for H5P resize, due dates - Re-chain due_date migration onto dev's real head; the old down_revision forked the graph, so `alembic upgrade head` aborted with MultipleHeads instead of applying it. - Fix unsaved-changes guard: it stripped `height` from H5P nodes unconditionally, so a custom-mode resize (which touches no other attribute) vanished with no save prompt. - Add a 3px drag threshold, stop arrow keys from committing past the clamp, and drop pointercancel instead of committing it; add aria-valuemin/max/text to the resize handle. - Trim stored deadlines with a time component to YYYY-MM-DD so the date input isn't blank and doesn't get silently overwritten on save. - Pin the H5P SIZE_MODES test to an ordered list; the old assertion missed a dropped 'auto'. - Mark due_date optional/nullable in the offline OpenAPI snapshot. | 17 天前 | |
fix(feature-gate): let admins re-enable AI after disabling it Turning off "Enable AI Features" in Org Settings -> AI trapped admins on a "Feature Disabled" card with no way back: the toggle lived inside FeatureGate and vanished along with everything else it gated. On self-hosted the only way out was hitting the config API directly. FeatureGate now takes an opt-in allowWhenDisabled prop that suppresses just the admin-disabled card; plan gating still shows the upgrade card as before. OrgEditAI opts in and shows a notice when AI is off instead of hiding the switch. Gate branching moved into a pure resolveGateReason() so it's testable under bun, since apps/web tests have no React runtime. Also stopped useResolvedFeature from applying its plan fallback while the org is still loading: it was firing upgrade-shown analytics with a guessed plan before real entitlements arrived, including on public org pages. Added feature-gate-lockout.test.mjs (15 tests) and one i18n key in en.json/ar.json. eslint and tsc clean on changed files; not tested in a live browser. Fixes #1061 | 17 天前 | |
fix(editor,assignments): follow-up fixes for H5P resize, due dates - Re-chain due_date migration onto dev's real head; the old down_revision forked the graph, so `alembic upgrade head` aborted with MultipleHeads instead of applying it. - Fix unsaved-changes guard: it stripped `height` from H5P nodes unconditionally, so a custom-mode resize (which touches no other attribute) vanished with no save prompt. - Add a 3px drag threshold, stop arrow keys from committing past the clamp, and drop pointercancel instead of committing it; add aria-valuemin/max/text to the resize handle. - Trim stored deadlines with a time component to YYYY-MM-DD so the date input isn't blank and doesn't get silently overwritten on save. - Pin the H5P SIZE_MODES test to an ordered list; the old assertion missed a dropped 'auto'. - Mark due_date optional/nullable in the offline OpenAPI snapshot. | 17 天前 | |
fix(editor): keep H5P height out of undo history, harden preview and divider Follow-up to the review of this branch: - The height reported by an embed was written as an ordinary transaction, so it landed in the undo stack and, because the frame follows the attribute, undoing it made the content report the height straight back and wipe the redo branch. It is now written with addToHistory disabled. - prepareResize writes the frame height to the DOM before replying; a React state update is not guaranteed to commit before the content re-measures. - Raised the height ceiling: once the handshake completes the content stops managing its own scrolling, so a 4000px cap silently truncated long activities. - Moved the wire protocol to lib/media/h5pProtocol.ts, with tests. The component keeps only the parts that need the DOM. - Applying a different URL resets the height instead of inheriting the previous activity's. - ActivityPreview and the unsaved-changes snapshot look up node types as own properties only; content typed "__proto__" reached Object.prototype and crashed the preview. The preview also skips an H5P block whose URL was removed, matching the editor and the indexer. - The snapshot walks the document once via a stringify replacer instead of cloning it; it runs on every keystroke. - Schemeless host:port now needs a path, so prose like "step:1" is rejected again instead of being saved as a dead embed. - Divider: styled the version-history and merge-conflict preview surface too (that one synthesizes an hr), gave the line a real hit area so the drag handle can reach it, and a selected-state colour. - Tests for the H5P branch of the content indexer. | 20 天前 | |
fix: sort folders by org mode and hide certificate copy Folder content sorted by position only, ignoring org sort_mode, so dashboard drag order didn't match the public library page. Certificate copy was shown based on the learner's certificate row rather than whether the course has certification enabled, so uncertified courses displayed misleading "no certificate" warnings. Shared sort comparator: apps/web/lib/library/sort.ts. New gate: useCourseCertification hook. | 25 天前 | |
feat(quiz): explicit single- and multiple-response questions The quiz builder had no way to say whether a question is pick-one or select-all-that-apply. Storage already allowed several correct options and the grader already did an exact-set match, so multi-answer worked by accident: nothing enforced single-select when authoring, and the learner got no affordance telling the two apart. - Questions carry response_type (single|multiple) and tasks carry grading_mode (all_or_nothing|partial_credit). Both live in the opaque contents JSON, so there is no migration. - Content authored before this has neither field. resolve_response_type is the single place inference lives: 2+ correct options was always a de-facto select-all. Stored questions are never rewritten, and an unrecognised value falls back to inference so a typo cannot grade a 3-correct question as a pick-one. - Single-response marks and selects like a radio group; multiple keeps toggling and says so. Both get role/aria-checked and keyboard activation, and a circle-vs-square indicator. - Partial credit awards (correct - incorrect) / total_correct, clamped, so selecting everything scores 0 rather than full marks. Default stays all-or-nothing, which reduces to the previous formula exactly. - The server grader and the learner's preview grader run the same shared helper on each side, since a divergence there shows one score and stores another. - _strip_answer_key stamps the resolved response_type on the outgoing copy: the learner needs the affordance, and the key it would otherwise be inferred from has just been removed. | 25 天前 | |
feat(editor,assignments): add H5P size modes and optional due dates - H5P blocks get a size picker (Auto/16:9/4:3/Short/Medium/Tall) plus a manual drag handle, so a clipped embed no longer needs fullscreen to be readable. - Assignment due_date is now nullable across the migration, service layer, and both web forms; an explicit null clears an existing deadline instead of being ignored. - Hoist the Arabic key set out of rtl-guard's filter loop, cutting that test from ~5.2s to ~0.4s. | 17 天前 | |
perf(scorm): recompress proxied content since fetch strips gzip The API gzips responses over 1000 bytes, but fetch decodes them transparently and Next.js route handlers don't compress streams either, so packages crossed the last mile uncompressed. Re-gzip in the route when the body was decoded, the response is a 200, and the client accepts gzip; leave 206 ranges alone. | 20 天前 | |
fix(security): harden containers, dependencies, and URL handling - Move Docker images to python:3.14.7-alpine3.24 (apk instead of apt), uv/bun copied in from their images instead of pip install/curl, frozen installs, pip removed from runtime - Drop llama-index-core for a small tiktoken chunker, cutting ~30 transitive deps - Pin pillow, refresh JS lockfiles, add stricter URL/redirect validation on web - Canonicalize SQLite storage paths before authorizing course access, use full SHA-256 for the analytics cache key - Add regression tests for chunking, URL guards, and path containment | 8 天前 | |
fix(security): harden containers, dependencies, and URL handling - Move Docker images to python:3.14.7-alpine3.24 (apk instead of apt), uv/bun copied in from their images instead of pip install/curl, frozen installs, pip removed from runtime - Drop llama-index-core for a small tiktoken chunker, cutting ~30 transitive deps - Pin pillow, refresh JS lockfiles, add stricter URL/redirect validation on web - Canonicalize SQLite storage paths before authorizing course access, use full SHA-256 for the analytics cache key - Add regression tests for chunking, URL guards, and path containment | 8 天前 | |
feat(web): Video.js player — HLS, MP4 fallback, ±15s, captions, thumbnails Video.js 8 player: adaptive HLS with quality selector + hover-scrub thumbnails, default skin with fill sizing so controls always render, layered recovery (HLS->MP4 fallback, load watchdog, Retry), PiP, ±15s seek buttons, and caption text-track loading. Source/caption/thumbnail resolvers + media URL helpers + the activity service calls. Adds the video.js deps (both lockfiles). | 2 个月前 |
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 8 天前 | ||
| 25 天前 | ||
| 6 天前 | ||
| 1 个月前 | ||
| 17 天前 | ||
| 17 天前 | ||
| 17 天前 | ||
| 20 天前 | ||
| 25 天前 | ||
| 25 天前 | ||
| 17 天前 | ||
| 20 天前 | ||
| 8 天前 | ||
| 8 天前 | ||
| 2 个月前 |