Files
classroomio/packages/core
Rotimi BestandRotimi Best 2ebb544a81 fix(dashboard): cache org stats in Redis with versioned invalidation (#883)
* fix(dashboard): cache org stats in Redis with versioned invalidation

Replace the never-expiring process-local cache in the org dashboard page
load with API-side Redis caching keyed per org. A version stamp prevents
slow reads from writing pre-purge data back into the cache.

Invalidate on course create/update/delete, student enroll/remove, certificate
claim, and progress reset. Remove the dashboard SSR module cache so every
page load reads through the API cache layer.

Fixes #852

Co-authored-by: Rotimi Best <[email protected]>

* fix(dashboard): address PR review on stats cache invalidation

- Validate bust via ZDashStats instead of raw query access
- Use import type for OrganisationAnalytics
- Resolve orgId before course delete so invalidation is reliable
- Invalidate immediately after persisted writes, before fallible side effects
- Invalidate on member role changes and program course access enrollment

Co-authored-by: Rotimi Best <[email protected]>

* fix(dashboard): complete stats cache invalidation coverage

Wire org stats cache purge on lesson completion (manual toggle and
video-watch auto-complete) so topCourses completion percentages stay
fresh. Harden bulk member adds with sequential inserts and a finally
invalidation boundary, and only purge org invite enrollments when new
group memberships were actually inserted.

Co-authored-by: Rotimi Best <[email protected]>

* fix(core): make lesson completion stats purge best-effort

Wrap org stats cache invalidation in try/catch so a lookup or Redis
failure cannot turn a successful lesson completion into a 500.

Co-authored-by: Rotimi Best <[email protected]>

* fix(dashboard): keep org stats cache through lesson completion

Lesson complete is a high-frequency event and usually does not change
the rounded dashboard percentages. Rely on a 10 minute TTL instead of
purging the snapshot on every completion.

Co-authored-by: Rotimi Best <[email protected]>

* docs: explain how admin dashboard numbers are calculated

Add a user-facing guide covering headline counts, top courses,
certificates, login activity, and when the 10 minute snapshot refreshes.

Co-authored-by: Rotimi Best <[email protected]>

---------
2026-08-16 22:15:34 +01:00
..

@cio/core — shared business services

This package holds the business-logic layer that both apps/api (HTTP) and apps/jobs (BullMQ worker) need to call. Anything in here is transport-agnostic: no Hono Context, no ContentfulStatusCode, no c.json, no zValidator middleware. Just functions that compose database queries, apply business rules, and raise AppError on failure.

The name is intentional: this is the core of the platform. The HTTP routes in apps/api and the worker processors in apps/jobs are thin transports over this layer. Anything they share has to live here — otherwise one app ends up depending on the other (apps/jobs → apps/api was the original sin that motivated extracting this package).

What lives here

Folder Contents
src/services/agent The durable agent runner, chat-mode tool implementations, planning helpers.
src/services/course Course / section / content / landing-page business logic.
src/services/exercise Exercise + question CRUD and orchestration.
src/services/lesson Lesson read/write helpers.
src/services/assets Asset upload, attach, detach, presign — used by both API routes and worker.
src/services/jobs BullMQ enqueue helpers (e.g. startMediaJob) callable from any service.
src/services/lesson-language Localized lesson content helpers.
src/utils Storage, presigning, sanitization, tinybird, redis helpers.
src/config Env schema (env), storage config, dashboard URL helpers.

Placement rule

            ┌─────────────────────────────────────────────────────┐
            │  Does the function need a Hono Context, or call     │
            │  `c.json`, or validate a request shape?             │
            │                                                     │
            │      YES  →  it's a route handler, stays in         │
            │              `apps/api/src/routes/{domain}/…`       │
            │                                                     │
            │      NO   →  it's a business service, goes in       │
            │              `packages/core/src/services/{domain}/…`│
            └─────────────────────────────────────────────────────┘

If the function only touches the database and has no business logic, it doesn't belong here either — it goes in packages/db/src/queries/{domain}/. The split is:

  • Query layer (@cio/db/queries): pure SQL/Drizzle helpers, no business rules.
  • Service layer (@cio/core/services): composes queries, applies rules, throws AppError.
  • Route layer (apps/api/src/routes): HTTP transport — auth, validation, status codes, response shapes.
  • Worker layer (apps/jobs/src/workers): BullMQ transport — pulls a job, calls services, persists outcome.

What does NOT belong here

  • HTTP-shaped code (Hono types, response helpers, content-type handling) — keep in apps/api.
  • BullMQ worker bootstrapping (Redis connection, worker lifecycle) — keep in apps/jobs.
  • Frontend / Svelte / UI primitives — those live in apps/dashboard or packages/ui.
  • Raw Drizzle query functions with no business logic — those go in packages/db/queries.

Why it isn't called @cio/services

@cio/core is shorter and reads cleaner at import sites (from '@cio/core/services/course/content'). The folder structure makes it obvious that it's a services layer; the package name doesn't need to repeat that.

History

This package was originally extracted as @cio/agent-runtime to hold the durable agent runner and its transitive closure. That closure turned out to be ~80% generic business services that apps/api and the worker both need, so the package was renamed to @cio/core to match its contents. The agent runner (services/agent/course-generation-runner.ts) remains the top-level export for apps/jobs, but it's just one of many services living here now.