| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
fix(web): make copilot math survive streaming, lists and RTL Five things the plain plugin wiring got wrong: - The model does not always use dollar delimiters. \( \) and \[ \] reached the reader as literal text with the backslashes stripped, so rewrite them before parsing — on one line when the formula sits inside a list item, table cell or blockquote, since a blank line would close the block it belongs to. - A display fence is only half-written while the answer streams. Left open, it swallows every chunk after it and paints the rest of the message as one parse error, so hold the equation back until it closes. - copilot.tsx is reachable from the org menu, which put KaTeX and its stylesheet in the bundle of every page. Load them on demand the first time a message actually contains math. - KaTeX positions its atoms as inline-blocks and sets no direction of its own, so an RTL page rendered equations backwards. Lock it the same way the editor's math block does. - Every streamed chunk re-rendered the whole transcript, re-parsing finished answers. Memoise the assistant message and give it a stable empty sources array. Long display math now scrolls inside the bubble instead of widening it. | 14 天前 | |
Merge pull request #1062 from learnhouse/feat/h5p-sizing-and-optional-due-dates Add H5P size modes and optional assignment due dates | 9 天前 | |
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. | 13 天前 | |
test(i18n): guard RTL against regression Three layers, because each catches what the others cannot see. An ESLint rule rejects physical-direction utilities in class strings. It is an error rather than a warning: the codebase was converted in one pass, so there is no backlog to stage around and anything it catches is new. It knows the two exceptions the codemod knows — 50% centering insets, and Radix data-[side=*] pairs, which report the resolved physical side and are already correct. Surfaces that are deliberately LTR are listed once, next to the reason. A bun test covers raw CSS and the constants ESLint cannot reach: every physical declaration in globals.css must carry an rtl-ok comment explaining itself, the language registry must declare a direction for every locale and agree with the lazy loaders, ar.json must cover every en.json key, and the RTL list duplicated into public/dir-init.js must match lib/direction.ts. The test also compiles globals.css and asserts the RTL rules survive. That is not paranoia: the icon-mirroring block was silently dropped once because a comment above it contained `*/`, which closed the comment early and took the rule with it. No build error, no lint error — the app just stopped mirroring icons, and the source looked correct the whole time. The Playwright specs cover the three failures that are invisible in review: direction wrong on first paint, direction not following a live language switch, and horizontal overflow — the last being the cheapest way to catch a physical margin that escaped the sweep, on any route, with no screenshots to maintain. Two ESLint changes were needed for the changed-files gate to pass. Four real errors are fixed — a redundant regex escape, a React import missing where it is used in a type position, an interface shadowing its component, an unscoped const in a case block. And no-unused-vars drops to warn, joining the React Compiler rules already there for the same #800 backlog: the gate lints whole changed files, so a repo-wide change fails on debt it merely walked past. Base ESLint also cannot read TypeScript and reports parameter names inside type annotations as unused variables — those are documentation, not dead code. The real fix is adopting @typescript-eslint/no-unused-vars, which is its own change. | 29 天前 | |
feat(web): add PostHog product analytics + learnhouse-analytics event layer Adds PostHog to the web frontend behind a single opt-in env var (NEXT_PUBLIC_POSTHOG_KEY — absent = fully off). Session replay with masked inputs/text, reverse-proxied via /ingest so adblockers can't strip it (works on custom domains too). - New services/analytics mini-lib: one typed useLHAnalytics().track() that fans out to PostHog and the existing backend analytics hook, with auto-injected standard props (org, plan, surface, locale, role, membership) and central identify/group/reset wired across all auth paths via a session watcher. - Instrument ~140 explicit product events across every feature area (auth, courses, activities, communities, podcasts, playgrounds, boards, library, search/copilot, store/checkout, course-mgmt, editor, assignments, org settings, billing/upgrade, admin, navigation), including impression and drop-off funnel events. - Remove Umami (layout script, /umami proxy rewrites, telemetry getter). - Declare @codemirror/language as a direct dependency (it is imported directly and was relying on transitive resolution). | 2 个月前 | |
Merge pull request #1062 from learnhouse/feat/h5p-sizing-and-optional-due-dates Add H5P size modes and optional assignment due dates | 9 天前 | |
Merge pull request #1062 from learnhouse/feat/h5p-sizing-and-optional-due-dates Add H5P size modes and optional assignment due dates | 9 天前 | |
feat(i18n): resolve text direction from the active locale Adds the plumbing that makes direction a property of the language rather than something hardcoded, plus the codemod used to convert the codebase. Direction has to be resolved on the client. i18next's highest-priority detection source is localStorage, which the server cannot read, and reading the cookie in the root layout would deopt every page to dynamic rendering and still be wrong whenever the two disagree. So dir-init.js runs synchronously in <head> and writes lang/dir before the body paints — the same approach embed-bg.js already uses to avoid a flash. Its detection order mirrors lib/i18n.ts exactly, and the RTL list it carries is asserted against lib/direction.ts by a test, since it's a constant duplicated into a file the bundler never sees. AVAILABLE_LANGUAGES gains a `dir` field, so Persian works today and a future locale is a one-line change; nothing branches on "is Arabic". useDirection() exposes it to components along with a multiplier for physical offsets, and Radix's DirectionProvider makes align="end", collision flipping and arrow-key navigation direction-aware across ~40 call sites with no per-component edits. The codemod converts physical Tailwind utilities to logical ones. It parses with the TypeScript compiler API rather than running a regex over the file, because `pr-`, `mr-` and `pl-` appear in URLs, i18n keys and identifiers. space-x-*, divide-x* and inset-x-* are skipped — they already emit logical properties in v4 — and so is any 50% inset, which is a centering idiom whose paired translate may live in a sibling class or an inline style, and which lands off-centre in RTL if converted alone. Also drops tailwind.config.js. Tailwind v4 only loads it via an @config directive this project doesn't have, so it was dead — and a trap for anyone later adding an RTL plugin there. | 29 天前 | |
feat(i18n): resolve text direction from the active locale Adds the plumbing that makes direction a property of the language rather than something hardcoded, plus the codemod used to convert the codebase. Direction has to be resolved on the client. i18next's highest-priority detection source is localStorage, which the server cannot read, and reading the cookie in the root layout would deopt every page to dynamic rendering and still be wrong whenever the two disagree. So dir-init.js runs synchronously in <head> and writes lang/dir before the body paints — the same approach embed-bg.js already uses to avoid a flash. Its detection order mirrors lib/i18n.ts exactly, and the RTL list it carries is asserted against lib/direction.ts by a test, since it's a constant duplicated into a file the bundler never sees. AVAILABLE_LANGUAGES gains a `dir` field, so Persian works today and a future locale is a one-line change; nothing branches on "is Arabic". useDirection() exposes it to components along with a multiplier for physical offsets, and Radix's DirectionProvider makes align="end", collision flipping and arrow-key navigation direction-aware across ~40 call sites with no per-component edits. The codemod converts physical Tailwind utilities to logical ones. It parses with the TypeScript compiler API rather than running a regex over the file, because `pr-`, `mr-` and `pl-` appear in URLs, i18n keys and identifiers. space-x-*, divide-x* and inset-x-* are skipped — they already emit logical properties in v4 — and so is any 50% inset, which is a centering idiom whose paired translate may live in a sibling class or an inline style, and which lands off-centre in RTL if converted alone. Also drops tailwind.config.js. Tailwind v4 only loads it via an @config directive this project doesn't have, so it was dead — and a trap for anyone later adding an RTL plugin there. | 29 天前 | |
fix(ai): make course-plan tags a list, join to pipe at finalize Course tags are stored pipe-separated and the web TagInput splits on `|`, but the planning prompt told the model to return tags "comma- separated" and finalize stored that string as-is. Result: a plan with `"a, b, c"` landed in the DB as `a, b, c` and the editor rendered ONE chip instead of three. Fixing the prompt to say "pipe-separated" would just move the fragility: models drift back to commas, spaces, or "and" no matter what odd delimiter you name, so a prose contract for a machine boundary stays brittle. Instead, stop making the model emit a delimited string at all. - Prompt asks for a JSON array (`["python", "beginners"]`); arrays are the one shape LLMs serialize reliably. - CoursePlan.tags is now list[str] with a mode="before" validator that still accepts a stray delimited string (comma or pipe), trims, and drops blanks. A regressed generation can't 422 finalize over tags. - finalize owns the storage encoding: `"|".join(plan.tags)`. The pipe separator lives in one place that knows the DB format and never appears in a prompt again. - Web CoursePlan.tags typed string[] to match; nothing on the web reads the field, it only rides through to finalize. Tests pin the guarantee (list trims + drops blanks, stored form is pipe-joined) and the negative case (a comma string still splits into separate tags, not one chip) so a deleted validator fails loudly. No new ruff findings vs HEAD. | 16 天前 | |
Merge pull request #1058 from learnhouse/fix/h5p-editor-block-and-divider Fix H5P embed resizing, editor state bugs, and add a divider block | 11 天前 | |
Merge pull request #1062 from learnhouse/feat/h5p-sizing-and-optional-due-dates Add H5P size modes and optional assignment due dates | 9 天前 | |
feat(web): analytics + Loops onboarding, Resend/lifecycle emails, org/folders services, delete-account service, error helpers | 2 个月前 | |
feat: enable sentry | 6 个月前 | |
chore: Add .prettierrc.yaml in apps/web directory | 2 年前 | |
fix(docker): move every image to bun 1.4.0 and drop the node build stages The frontend image build has been dying at `bun run build`: the Next build finishes, the output is written, and then bun 1.3.14 segfaults during process teardown, which docker reports as exit 139. Upstream already fixed it. oven-sh/bun#36866 and #37031 are a NAPI threadsafe-function use-after-free during worker teardown, both closed against oven-sh/bun#34067, which shipped in 1.4.0. There is no 1.3.15, so moving off the bug means moving to 1.4.0. Reverts the two node builder stages that were stopgaps for this, and moves the frontend runtime onto bun as well. Measured on linux/amd64 with the real Dockerfiles: 1.3.14 segfaults at 0x13CB0 every time, 1.4.0 builds clean, and the resulting image serves /api/health, server-renders, and still optimises images through sharp. Two things this drags along: - 1.4.0 writes lockfileVersion 3, so apps/web/bun.lock is regenerated and 1.3.14 can no longer read it. .bun-version, packageManager and the image tags all move together or --frozen-lockfile fails. - apps/collab keeps its node runner. @hocuspocus/server builds crossws' node adapter unconditionally and that adapter throws when it sees Bun, before the server listens. The Dockerfile now says so. Two gaps that let this ship in the first place: - apps/web/Dockerfile and apps/collab/Dockerfile had no image build in this repo. Only the root Dockerfile was covered, so the same fix had to be written twice and the broken file kept shipping. Both now build on every PR, without pushing. - Nothing checked the bun tag in a Dockerfile against .bun-version. The lockfile script now fails when they disagree. | 16 天前 | |
feat: refactor the entire learnhouse project | 2 年前 | |
fix(deps): update all non-major dependencies to v3.30.5 | 9 天前 | |
feat(i18n): resolve text direction from the active locale Adds the plumbing that makes direction a property of the language rather than something hardcoded, plus the codemod used to convert the codebase. Direction has to be resolved on the client. i18next's highest-priority detection source is localStorage, which the server cannot read, and reading the cookie in the root layout would deopt every page to dynamic rendering and still be wrong whenever the two disagree. So dir-init.js runs synchronously in <head> and writes lang/dir before the body paints — the same approach embed-bg.js already uses to avoid a flash. Its detection order mirrors lib/i18n.ts exactly, and the RTL list it carries is asserted against lib/direction.ts by a test, since it's a constant duplicated into a file the bundler never sees. AVAILABLE_LANGUAGES gains a `dir` field, so Persian works today and a future locale is a one-line change; nothing branches on "is Arabic". useDirection() exposes it to components along with a multiplier for physical offsets, and Radix's DirectionProvider makes align="end", collision flipping and arrow-key navigation direction-aware across ~40 call sites with no per-component edits. The codemod converts physical Tailwind utilities to logical ones. It parses with the TypeScript compiler API rather than running a regex over the file, because `pr-`, `mr-` and `pl-` appear in URLs, i18n keys and identifiers. space-x-*, divide-x* and inset-x-* are skipped — they already emit logical properties in v4 — and so is any 50% inset, which is a centering idiom whose paired translate may live in a sibling class or an inline style, and which lands off-centre in RTL if converted alone. Also drops tailwind.config.js. Tailwind v4 only loads it via an @config directive this project doesn't have, so it was dead — and a trap for anyone later adding an RTL plugin there. | 29 天前 | |
fix(docker): move every image to bun 1.4.0 and drop the node build stages The frontend image build has been dying at `bun run build`: the Next build finishes, the output is written, and then bun 1.3.14 segfaults during process teardown, which docker reports as exit 139. Upstream already fixed it. oven-sh/bun#36866 and #37031 are a NAPI threadsafe-function use-after-free during worker teardown, both closed against oven-sh/bun#34067, which shipped in 1.4.0. There is no 1.3.15, so moving off the bug means moving to 1.4.0. Reverts the two node builder stages that were stopgaps for this, and moves the frontend runtime onto bun as well. Measured on linux/amd64 with the real Dockerfiles: 1.3.14 segfaults at 0x13CB0 every time, 1.4.0 builds clean, and the resulting image serves /api/health, server-renders, and still optimises images through sharp. Two things this drags along: - 1.4.0 writes lockfileVersion 3, so apps/web/bun.lock is regenerated and 1.3.14 can no longer read it. .bun-version, packageManager and the image tags all move together or --frozen-lockfile fails. - apps/collab keeps its node runner. @hocuspocus/server builds crossws' node adapter unconditionally and that adapter throws when it sees Bun, before the server listens. The Dockerfile now says so. Two gaps that let this ship in the first place: - apps/web/Dockerfile and apps/collab/Dockerfile had no image build in this repo. Only the root Dockerfile was covered, so the same fix had to be written twice and the broken file kept shipping. Both now build on every PR, without pushing. - Nothing checked the bun tag in a Dockerfile against .bun-version. The lockfile script now fails when they disagree. | 16 天前 | |
test(i18n): guard RTL against regression Three layers, because each catches what the others cannot see. An ESLint rule rejects physical-direction utilities in class strings. It is an error rather than a warning: the codebase was converted in one pass, so there is no backlog to stage around and anything it catches is new. It knows the two exceptions the codemod knows — 50% centering insets, and Radix data-[side=*] pairs, which report the resolved physical side and are already correct. Surfaces that are deliberately LTR are listed once, next to the reason. A bun test covers raw CSS and the constants ESLint cannot reach: every physical declaration in globals.css must carry an rtl-ok comment explaining itself, the language registry must declare a direction for every locale and agree with the lazy loaders, ar.json must cover every en.json key, and the RTL list duplicated into public/dir-init.js must match lib/direction.ts. The test also compiles globals.css and asserts the RTL rules survive. That is not paranoia: the icon-mirroring block was silently dropped once because a comment above it contained `*/`, which closed the comment early and took the rule with it. No build error, no lint error — the app just stopped mirroring icons, and the source looked correct the whole time. The Playwright specs cover the three failures that are invisible in review: direction wrong on first paint, direction not following a live language switch, and horizontal overflow — the last being the cheapest way to catch a physical margin that escaped the sweep, on any route, with no screenshots to maintain. Two ESLint changes were needed for the changed-files gate to pass. Four real errors are fixed — a redundant regex escape, a React import missing where it is used in a type position, an interface shadowing its component, an unscoped const in a case block. And no-unused-vars drops to warn, joining the React Compiler rules already there for the same #800 backlog: the gate lints whole changed files, so a repo-wide change fails on debt it merely walked past. Base ESLint also cannot read TypeScript and reports parameter names inside type annotations as unused variables — those are documentation, not dead code. The real fix is adopting @typescript-eslint/no-unused-vars, which is its own change. | 29 天前 | |
feat: final improvements | 6 个月前 | |
feat: enable sentry | 6 个月前 | |
fix(activities): allow same-origin framing for resource routes Resource activities embed a Library resource (board/course/podcast/community/ playground) in a same-origin iframe, but the global frame-ancestors 'none' / X-Frame-Options: DENY blocked it ("Framing ... violates ... frame-ancestors 'none'"). Add a header override letting these resource routes be framed by their own origin, mirroring the existing SCORM override. | 2 个月前 | |
fix(deps): update all non-major dependencies to v3.30.5 | 9 天前 | |
feat: migrate tailwind to v4 | 1 年前 | |
fix(security): scope user-account authorization to shared organizations A user row has no org column, so the resource-org resolver answers None for a user UUID. That fell into the placeholder branch of _load_applicable_roles, which returns every role the caller holds anywhere — so an admin of one organization could update or delete an account belonging only to another, just by supplying its UUID. Role loading for a user target is now restricted to the organizations the caller and the target actually share, and a caller with no shared organization is refused outright. The placeholder used by the create paths still resolves to no target and is unchanged. Two more from the same sweep: - /redirect_from_auth landed everyone on "/" while carrying the ?next it was given, so any deep link that prompted a sign-in returned to the org picker instead of the page asked for. It now honours a same-origin ?next and forwards the remaining params. - Deleting a course refreshed the course list but not the org's usage numbers, which are what drive the "limit reached" banner and the disabled New Course button — so an org at its cap stayed locked out after freeing a slot. | 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 个月前 | |
chore: update Sentry configuration to reduce sample rates and add beforeSend filtering for specific error messages | 5 个月前 | |
refactor(rbac): enhance resource access checks with memoization and context handling for unpublished courses | 4 个月前 | |
feat(config): enhance platform URL handling for client-side access and backward compatibility | 4 个月前 | |
fix(web): drop deprecated tsconfig baseUrl so the image build type-checks The community image build fails type checking on both architectures: tsconfig.json(25,5): error TS5101: Option 'baseUrl' is deprecated and will stop functioning in TypeScript 7.0. TypeScript 6 has been pinned since April, so this was latent the whole time — the frontend install layer was Docker-cached from before the bump, and the cached node_modules carried a TypeScript that did not enforce the deprecation. Regenerating the lockfiles invalidated that layer, bun installed 6.0.3 for real, and the error surfaced. Silencing it with ignoreDeprecations only moves the failure to TypeScript 7, where baseUrl is removed outright. Dropping it is the durable fix: path mappings are resolved relative to the tsconfig directory, so every alias just becomes explicitly relative. baseUrl also let 21 files import bare from the app root (app/... and public/...), which no alias covered. Those two prefixes are now explicit path entries, so the imports resolve exactly as before without needing baseUrl. Verified by diffing the full tsc error set before and after: identical, and the build now gets past type checking. | 16 天前 |
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 14 天前 | ||
| 9 天前 | ||
| 13 天前 | ||
| 29 天前 | ||
| 2 个月前 | ||
| 9 天前 | ||
| 9 天前 | ||
| 29 天前 | ||
| 29 天前 | ||
| 16 天前 | ||
| 11 天前 | ||
| 9 天前 | ||
| 2 个月前 | ||
| 6 个月前 | ||
| 2 年前 | ||
| 16 天前 | ||
| 2 年前 | ||
| 9 天前 | ||
| 29 天前 | ||
| 16 天前 | ||
| 29 天前 | ||
| 6 个月前 | ||
| 6 个月前 | ||
| 2 个月前 | ||
| 9 天前 | ||
| 1 年前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 5 个月前 | ||
| 4 个月前 | ||
| 4 个月前 | ||
| 16 天前 |