Drop sell-online and teach-online from the "What brings you here" list so onboarding only offers employee training, customer courses, and expanding from another platform.
* fix(course): use shield-check icon for compliance nav
Certificates and Compliance both used CertificateIcon in the course
sidebar; point Compliance at Lucide's shield-check instead.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(ui): add ShieldCheckIcon moving icon for compliance nav
Replace the Lucide shield-check import with a moving-icons component
matching sibling hover-animation patterns, and use it for Compliance.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(course): use moving BotIcon for AI tutor nav
Switch the course sidebar AI Assistant item from Lucide bot to the
shared moving-icons BotIcon so hover animation matches other nav items.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix: sequential progression bug in course with ordered contents
* test(utils): add course progression tests and unify navigation ordering
Reuse flattenNavigableItems in getOrderedNavigableContent so next/previous
navigation follows the same section-aware order as progression locks.
Add vitest coverage for grouped multi-section flattening and sequential
access rules.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix: address PR review comments for course progression
Simplify grouped flatten ordering, only block progression for lessons and
exercises explicitly, integrate flattenNavigableItems in the sequential access
test, and include @cio/core watch in api:dev:full.
---------
* fix: handle flakiness in railway deployment logs by verifying actual deployment status
* fix: improve handling of railway deployment log-streaming errors to ensure accurate deployment status
* fix: enhance deployment status verification to address log-streaming glitches in railway
* fix: allow running of railway workflow for forked version
* Apply suggestions from code review
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
---------
Co-authored-by: Rotimi Best <best@classroomio.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Specify cache-first caption fetch for lesson YouTube embeds so AI chat and the existing transcript UI can reuse stored text. Cross-link related research and study-from-source PRDs and add the draft to the tracker.
Co-authored-by: best <best@classroomio.com>
Add a PRD and three prototype directions (Gallery, Command Bar, Guided) behind the prototype picker harness for the lesson Slide embed flow. Covers the top 10 embeddable slide platforms with per-platform detection, link/embed-code normalization, live preview, and How-to-embed docs links.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
Fixes#870 by updating outdated documentation URLs across Part 1, Part 2, and Part 14 in ONBOARDING.md to their correct paths.
Co-authored-by: richardkenneth <richard-kenneth@users.noreply.github.com>
* feat(ui): restyle unsaved-changes bar as a compact inverted card
Show Page.SettingsActions only when the form is dirty, as a centered
dark card with a secondary Discard button and a primary Save button.
Co-authored-by: best <best@classroomio.com>
* fix(ui): announce settings actions from a persistent live region
Keep role=status mounted outside the dirty-state card so screen readers
still hear the status when the bar appears, without live-region wrapping
the Discard/Save buttons.
Co-authored-by: best <best@classroomio.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
* fix(dashboard): hide admin callout banner and actions on cohort courses page for students
* fix(dashboard): update empty state description for student users
---------
Co-authored-by: richardkenneth <richard-kenneth@users.noreply.github.com>
Adds `db:create-org` which provisions an organization, adds the given
email as a verified admin, and attaches an active plan (BASIC by
default). Creates the user + profile first when the email is unknown,
derives the site name from the org name unless overridden, and supports
a --dry-run preview mode.
* Back button in Login page
Modified the back button to have two options, href option and onlick
The onClick bceomes usable when there's no href prop
and the onClick takes you directly to the previous page
While the href takes you to the url you indicated
* added a showBack condition to show the button
* modified the onClick function
* Removed the back button, made the Image act as the back button
* Changed the OAuth place holder
* Update back-button.svelte
* remove f
---------
Co-authored-by: Rotimi Best <best@classroomio.com>
* fix: update z-index utility class for Select component across tag category and tutor selection views
* refactor: standardize z-index management using ui:z-menu-elevated, ui:z-app-bar, ui:z-app-bar-elevated and ui:z-modal classes across UI components
---------
Co-authored-by: richardkenneth <richard-kenneth@users.noreply.github.com>
* preserve section content when toggled off
* Update types.ts
* Remove comment on LandingPageLinks enabled property
Removed comment about the default visibility of the section.
---------
Co-authored-by: Rotimi Best <best@classroomio.com>
* Remove the run-docker-full-stack.sh script to streamline the deployment process and eliminate unnecessary complexity.
* fix: update documentation to clarify script behavior for building and pulling images
* update docs
* fix: update script URL to use classroomio.com for downloads
* fix: humanize selfhosting docs and minor amendment
* fix: attend to comment
* fix: humanize docs
* update docs
* fix: adjusted the Restarting ClassroomIO... function
* fix: update commands and documentation for environment variable handling and upgrades
* chore: added space for rebuild
Isolate npm publish from PR CI, enforce frozen lockfiles and a 24h
minimumReleaseAge, and document safe pull_request_target usage so fork
PRs cannot reach publish credentials.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
Normalize power users spelling on customer education, and align pricing
hero copy with Basic plan student and AI credit limits.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
Replace the vague "Certified training for every team" headline with
clearer plan-selection copy that matches the academy positioning.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
Force "customers actually love." onto its own line under "Launch an
academy" so the blue accent does not wrap with an orphaned last word.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
Update the customer education hero to "Launch an academy customers
actually love", rewrite landing and solutions marketing copy with the
humanizer skill, and remove em/en dashes from those pages while leaving
blog and legal content untouched.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
Full spec for ordered, sequentially unlocked, bundle-priced learning
paths, plus 16 clickable prototypes (public org site, learner LMS, and
teacher workspace) built on the packages/ui tokens and the minimal
org-landing theme. The prototypes are the UX source of truth; start at
prototypes/learning-paths/index.html. Also adds the write-prd skill
codifying the clarify -> write -> prototype workflow.
* fixed error of not showing the org name API response error
* Fixed the error also for organization settings page
* modified
* Keep field-specific mapping in OrgApi
---------
Co-authored-by: Rotimi Best <best@classroomio.com>
* fix(ui): restore theme pill radius on course landing CTAs
LandingButton hardcoded rounded-md, so hero Enroll and pricing CTAs
lost the per-theme pill radius after CoursePrimaryAction switched to
LandingButton. Use --landing-radius-pill and cn() so theme overrides
still win.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(ui): preserve landing theme button radii
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(ui): restore hero CTA Button sizing with disabled parity
Revert CoursePrimaryAction to the shared Button so hero enroll matches
View curriculum sizing (h-10 text-sm). When enrollment is closed, apply
landing-button CSS vars with forced opacity so theme color overrides do
not keep the hero CTA looking active while pricing stays muted.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(ui): align LandingButton sizing and use for hero CTA
Match LandingButton sm/md/lg heights to the shared Button scale (h-8/h-9/h-10,
text-sm) so hero enroll can use landing-theme tokens without oversized CTAs.
CoursePrimaryAction returns to LandingButton for enabled color parity with
pricing; disabled overrides are preserved. Terminal hero drops hardcoded hex
colors in favor of --landing-button-* tokens.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
---------
* fix(course): coerce metadata.discount to number before save
Normalize string discount values from the landing page pricing form and
coerce legacy string metadata on ZCourseUpdate validation so course
settings saves do not fail with a Zod type error.
Fixes classroomio/classroomio#828
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* refactor(utils): add shared toFiniteNumber for metadata discount coercion
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(utils): only coerce string discount values in metadata preprocess
Reject broad Number() coercion on booleans and arrays. String and finite
number discounts are normalized; invalid types are left for Zod to reject.
Addresses Greptile review on #830
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* refactor(dashboard): use writable $derived in pricing form again
Keep toFiniteNumber coercion in setter effect; derived fields stay in
sync when course loads or updates (Svelte 5 writable derived).
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(utils): reject invalid metadata.discount instead of clearing it
Only coerce numeric strings in preprocess; leave invalid strings,
non-finite numbers, and other types for Zod to reject.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
The bar was nested in an extra shrink-0 div together with the dock
sentinel. A sticky element is constrained to its containing block, and
that wrapper is only as tall as the bar plus a 1px sentinel, so the bar
had roughly 1px of travel. It could never float over the page content and
only ever appeared where it naturally sits, at the end of the page.
Render the bar and the sentinel as siblings so the containing block is
Page.Root and the bar has the full page to travel. Measured in headless
Chrome at scrollY=600 in a 713px viewport: wrapped bar bottom was 2484
(off screen), sibling bar bottom is 713, pinned to the viewport.
Also raise the bar from z-10 to z-50. The editor root is relative z-50,
so it painted over the bar on any settings page with an editor, such as
the course welcome email. Matching 50 rather than exceeding it wins the
tie on DOM order while keeping body-portaled dialogs and sheets above the
bar.
ref is now bound to the sticky element rather than the removed wrapper.
No dashboard caller binds ref today.
* fix(dashboard): persist notification preferences after save
Personal notification toggles were reset when a stale GET response
arrived after save. Org toggles also triggered a spurious org save
because change detection compared against stale currentOrg settings,
and the UI reset because currentOrg was not updated after org save.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* refactor(dashboard): send only changed settings key and tidy load guard
The org update endpoint deep-merges settings server-side, so spreading
local settings into the payload risked writing stale values over
unrelated keys. Also drop a redundant load-version bump and let only
the latest in-flight preferences load clear the loading flag.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* refactor(dashboard): hydrate org notification defaults on org id transition
The sync effect read orgHasChanges and savedOrgToggles while writing them
through applyOrgToggleState, causing an extra effect pass after hydration.
/account returns settings together with the org id and role, so gating
hydration on org id transitions is sufficient and also discards unsaved
edits when switching orgs instead of saving them against the wrong org.
Addresses CodeRabbit review on PR #824.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
---------
* fix(workflows): retry MinIO bucket creation to survive post-boot format window
A freshly duplicated cio-minio's HTTP listener accepts connections
before it finishes formatting its storage backend (confirmed via its
own boot logs: "Formatting 1st pool, 1 set(s), 1 drives per set."),
so a bucket-creation call landing in that window can transiently fail
with a misleading "specified bucket does not exist" response even
though auth and connectivity (mc alias set) already succeeded. Retry
bucket creation up to 10 times with a 5s backoff instead of failing
the whole run on what's usually just a not-ready-yet response.
* fix(workflows): truncate env name slug to fit Railway's length limit
railway environment new failed with "Error in name - Invalid input"
for a 48-char name (pr-cursor-notes-workspace-feedback-6d09-0e6aa7c9)
derived from a long auto-generated branch name. Railway docs cite a
32-char limit for service names; environment names hit the same wall.
The collision-resistant hash suffix (added for the earlier CodeRabbit
finding) is what actually guarantees uniqueness -- the slug is only
there for readability -- so cap the slug at 18 chars instead of
dropping the hash. pr-<slug><=18>-<hash8> stays at or under 30 chars.
* fix(workflows): retry mc anonymous set for the same boot-race window
The mb retry loop worked as intended in the latest test run (attempt
1 failed on all three buckets during cio-minio's post-boot format
window, attempt 2 succeeded), but the very next call -- anonymous set
download on cio/media -- hit the identical "specified bucket does not
exist" error right after mb had just succeeded. Same underlying race,
one command later. Wrap it in the same retry pattern.
* update workflow
* fix: silen error on db:seed
* update workflow
* update workflow
* update workflow
* fix(newsfeed): tighten card spacing and reposition pinned icon
- Move pinned icon to top-left corner with a tilted rotation
- Remove the extra pin row that added excess top padding
- Reduce spacing between post content, reactions, and comments
- Remove card box shadow
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(newsfeed): place pinned icon on card corner instead of avatar
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(newsfeed): stop dropdown menus locking body scroll
bits-ui defaults DropdownMenu.Content's preventScroll to true, so every
open ran a body scroll lock: a forced full-document style recalc and
reflow, a horizontal shift by the scrollbar width, and pointer-events:
none on the body. On the newsfeed page — a long list of cards each with
a scrollable comment thread — that made the comment ellipsis menu feel
sluggish to open. Menus don't need scroll locking; only modal layers do.
Also tighten card spacing, use the primary color for the pinned icon,
give the card menu trigger a pointer cursor, and switch the comment
save and submit buttons to the loading and secondary variants.
* refactor(newsfeed): store reply targets as data, not markup
Replies used to prepend a presentational <blockquote> — Tailwind classes,
hardcoded English and a copied snippet of the parent — onto the comment
body, and that whole string was persisted to course_newsfeed_comment.content.
Editing a reply therefore showed raw markup in the textarea, the quoted
snippet froze at write time, the label could never be localized, and two of
its ui: utilities never compiled because they appeared only in dashboard code.
Add a replyToCommentId column, distinct from parentId (the thread root), so
a reply can name the comment it answers. The service verifies the target
belongs to the same feed and the same thread; the paginated query joins it
to return replyToAuthorFullname, so the label is rendered from live data by
the new CommentTree.ReplyingTo component using the existing replying_to key.
Rows written the old way still display their stored quote, and
stripLegacyReplyQuote keeps that markup out of the edit textarea.
* refactor(newsfeed): drop redundant comments on reply target plumbing
* fix(newsfeed): address review feedback on pinned icon and quote stripping
- The pin icon carried an unprefixed `text-muted-foreground` plus
`ui:stroke-primary`, which is emitted nowhere because it appears only in
dashboard code, so the outline fell back to the muted colour. Use
`ui:text-primary` and let the stroke inherit currentColor.
- Give the pinned state an accessible name: the icon is `aria-hidden`, and
header.svelte no longer carries an indicator, so screen readers had nothing.
- Filter feeds on `isPinned` alone. `is_pinned` is only the database column
name; drizzle maps it to `isPinned` and the API never returns the snake_case
form, so the `as any` fallback was dead and let the page and card disagree.
- `ui:transition-color` is not a Tailwind utility and emitted nothing; the
plural `ui:transition-colors` is what applies the transition.
- Strip a legacy reply quote only when the blockquote carries the `reply-quote`
class the old code always wrote, so a blockquote the author typed is kept.
* docs(agents): forbid presentational markup in persisted content columns
---------
* feat(ui): add AttachmentList component for lesson file attachments
Introduce a reusable AttachmentList molecule with view and edit modes.
View mode shows a header with file count and view/download actions; edit
mode supports drag-to-reorder, view, and delete. Includes Storybook
stories and package exports.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(ui): address AttachmentList PR review feedback
- Sync edit-mode file state using full metadata signature, not IDs only
- Clamp formatFileSize unit index and guard non-finite inputs
- Recognize common MIME types for file-type icon styling
- Scope role=list to row container; use dragHandleZone for reordering
- Add reorder label for drag handle accessibility
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(ui): guard AttachmentList against undefined files prop
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(ui): render AttachmentList stories and guard missing props
Use Storybook template snippets and default args so stories mount
correctly. Default mode and labels in the component for autodocs, and
align dragHandleZone items with rendered rows.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* feat(dashboard): use AttachmentList on lesson documents tab
Replace the horizontal DocumentCard grid with AttachmentList in view and
edit modes, including drag-to-reorder in edit. Tighten the list header to
p-2 with xs text, add document mapping helpers, and translate new labels.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(dashboard): reset document upload modal state on close
Clear selected file, success message, and upload progress when the add
document modal closes so reopening starts fresh.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(dashboard): raise PDF viewer above sidebar and AI chat
Use z-modal (200) instead of z-100 so the full-screen PDF overlay stacks
above z-app-bar-elevated (150) course sidebar and z-app-bar (100) AI panel.
Add the missing z-modal utility to dashboard app.css to mirror the UI scale.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* chore: minor changes
* feat(dashboard): show spinner and snackbar on document download
AttachmentList download button now shows the Button loading spinner
immediately while the async download runs. On success, a snackbar confirms
the download; on failure, an error snackbar is shown instead of opening
a new tab.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(ui): address PR review feedback on AttachmentList
- Reject negative byte counts in formatFileSize instead of using Math.abs
- Make AttachmentList mode/files/labels props optional; disable cross-list drops
- Include file name in drag-handle aria-label for screen readers
- Prevent upload modal from closing while an upload is in progress
- Use ICU plural rules for attachment file_count across all locales
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* feat(ui): add Page.SettingsActions for settings save bar
Introduce a sticky bottom save/discard bar on Page and migrate settings
pages to use it instead of header or inline save buttons. Includes
profile, org, customize-lms, notifications, auth, AI tutor, course
settings, certificates, and cohort settings.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(ui): always show Page.SettingsActions, disable when clean
Keep the settings save bar visible at all times and disable Save and
Discard when hasChanges is false instead of hiding the bar entirely.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(ui): add bottom spacing and shadow to Page.SettingsActions
Wrap the actions card in a sticky container with bottom padding so it
does not sit flush against the viewport edge, and use shadow-md on the
card for clearer elevation.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(ui): stop viewport-sticking Page.SettingsActions on scroll
Remove sticky bottom positioning so the save bar stays at the end of the
page content and scrolls away when viewing upper sections.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(ui): use sticky bottom for Page.SettingsActions dock behavior
Replace the IntersectionObserver fixed/dock toggle that caused layout
thrashing and flipping with native position: sticky; bottom: 0. Move
overflow-x-hidden from Page.Root to Page.Body so sticky is not broken.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(ui): keep Page.SettingsActions sticky and above editor content
Page.Root carried md:overflow-x-auto, which makes overflow-y compute to
auto, turning the root into the sticky bar's nearest scrolling ancestor.
Since the root sizes to its content it never scrolls internally, so the
bar rendered at its natural flow position instead of pinning to the
viewport. Drop the overflow so sticky resolves against the viewport;
horizontal containment stays on Page.Body.
Raise the bar to z-50 so it is not painted over by the editor root,
which is relative z-50. Matching rather than exceeding 50 keeps
body-portaled dialogs and sheets above the bar.
Show the shadow only while the bar is pinned away from its flow
position, tracked with an IntersectionObserver on a 1px sentinel
sibling. Unlike the previous observer this toggles a box-shadow only, so
it cannot feed back into layout.
* fix(ui): tighten settings actions spacing and tab visibility
- Remove mt-6 and lower z-index so popovers sit above the bar
- Wrap sentinel in a single root to avoid extra Page.Root flex gap
- Remove Page.Body mb-4 redundant with settings actions spacing
- Show certificate save bar only on the settings tab
- Raise theme color picker z-index via --picker-z-index
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(dashboard): address PR review feedback for settings actions
- Capture profile/org saved baselines only on initial load, not on edits
- Reset course settings errors with explicit field shape in handleDiscard
- Restore VisitOrgSiteButton on org profile settings header
- Derive AI tutor dirty state from stores directly instead of mirrored state
- Use discard-specific French label and document SettingsActions props
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* docs(ui): clarify Page.Root ownership for Page.SettingsActions
"Last child of Page.Root" is a runtime relationship. Several settings
pages get their Page.Root from a +layout.svelte, so the page file holds
only Page.Header, Page.Body, and Page.SettingsActions. Document that this
is correct and list the pages, since adding a nested Page.Root stacks two
min-height containers and shortens the sticky bar's containing block.
Also document that Page.SettingsActions must never be rendered from
inside Page.Body: overflow-x-hidden makes Page.Body a scroll container
that never scrolls, so a sticky bar in it has no travel.
---------
* feat(workflows): add Railway preview environment and teardown workflows
* fix(workflows): enhance security by binding branch names to env variables in Railway workflows
* fix(workflows): ensure preview URLs are live by blocking on service healthchecks during deployment
* temp: auto-run railway-preview.yml on push for demo recording
workflow_dispatch requires the file on the default branch before "Run
workflow" is available; add a scoped push trigger + input fallbacks so
this branch can produce a real run to record. Revert before merge.
* fix(workflows): pass --workspace to railway link for non-interactive CI
Railway CLI now requires --workspace when running `railway link`
non-interactively; without it the preview workflow fails at the link
step. Defaults to the "cio projects" workspace the staging project
lives in, overridable via a RAILWAY_WORKSPACE repo variable.
* fix(workflows): read RAILWAY_PROJECT_ID from secrets, not vars
It's configured as a repository secret (has been for weeks), but the
workflow read it via the vars context, which is a separate namespace
and always resolved empty -> railway link failed with Project "" not
found. Read it via secrets to match how it's actually stored.
* fix(workflows): pass --workspace to environment new/delete, stop masking failures
railway environment new needed --workspace in non-interactive mode too
(same as railway link), but its failure was unconditionally swallowed
by `|| echo`, so the workflow proceeded to "Link" as if the preview
environment existed when it never got created. Add --workspace to
both environment new and environment delete, and only swallow the
"already exists" case -- any other failure now fails the run where it
actually happened instead of surfacing as a confusing error two steps
later.
* fix(workflows): stop passing --project/--workspace to railway environment new/delete
Verified against the actual CLI (railway environment new --help / delete
--help): those subcommands take NO --project/--workspace/--environment
flags at all, and were silently rejecting them, which is why environment
creation kept failing and getting masked by the fallback echo. Only
`railway link` accepts --project/--workspace/--environment, so link to
the base project first, then create/delete environments by name, then
re-link to switch into the new one for deploy.
* fix(workflows): force path-style addressing for mc against Railway's MinIO domain
mc alias set defaults to --path auto, which can pick virtual-host-style
bucket addressing (bucket.host/key). Railway's edge only routes the
exact configured domain, not wildcard bucket subdomains, so mb then
fails with a misleading "specified bucket does not exist" even though
the alias/auth check (a GET) succeeds. Force --path on, matching the
app's own OBJECT_STORAGE_FORCE_PATH_STYLE=true for this same endpoint.
* chore: retrigger preview workflow after fixing cio-minio start command
staging's cio-minio now has the correct Custom Start Command, and the
stale pr-chore-staging-setup preview env (duplicated before that fix)
has been deleted. Empty commit to trigger a fresh deploy run.
* fix(workflows): generate cio-minio's public domain in the preview env
Confirmed staging's cio-minio has its own domain, but environment
duplication doesn't carry it to the new preview environment, so
RAILWAY_PUBLIC_DOMAIN came back empty there and produced an invalid
"https://" alias URL. Generate one in CI, same as the existing
cio-dashboard domain step, pinned to port 9000 since the minio/minio
image exposes both the S3 API (9000) and console (9001).
* chore(workflows): revert temp push-trigger testing scaffold, trim comments
Remove the push trigger and || fallback expressions added solely to
get a run before this file exists on main (workflow_dispatch requires
that for the "Run workflow" UI). Back to workflow_dispatch-only, with
comments trimmed from a debugging narrative down to the durable facts
future readers need. All real fixes found during testing (link
--workspace, secrets vs vars, environment new/delete flag handling,
mc --path on, cio-minio domain generation) are unchanged.
* fix(workflows): fix teardown link/secrets bugs, harden domain reads
railway-preview-teardown.yml had the same class of bugs already found
and fixed in railway-preview.yml during testing:
- RAILWAY_PROJECT_ID read from vars.* while the deploy workflow (and
the actual GitHub config) uses secrets.* — always resolved empty.
- environment delete was called without linking first, and passed an
unsupported --project flag; the resulting CLI error was swallowed
by `|| echo`, so teardown silently no-opped on every run and every
preview environment leaked indefinitely.
Also hardens railway-preview.yml against a repeat of the cio-minio
domain bug: two other "generate domain, then read it back" spots
(cio-minio bucket seeding, cio-dashboard's final URL report) had the
same unguarded `|| true` swallow with no check that the resulting
host is non-empty. Both now fail loudly instead of silently
proceeding — the dashboard one matters most since it's the final
step and would otherwise report success with a broken link.
Resolves the now-verified SPIKE comment on environment duplication,
and trims a few comments that restated code instead of explaining
non-obvious behavior.
* fix(workflows): address CodeRabbit findings on preview lifecycle
- Unify concurrency groups between deploy and teardown (was
railway-preview-<branch> vs railway-preview-teardown-<branch>, so
they never coordinated). A PR-close teardown and a manual deploy
for the same branch could otherwise race, letting a deploy recreate
an environment right after teardown deleted it.
- Pin the Railway CLI to 5.30.1 (the version verified against all
session) in both workflows, instead of an unpinned `npm install -g`
that could silently pick up different undocumented CLI behavior
later, same class of bug this session hit repeatedly.
- Append a short hash of the original branch string to the derived
environment name in both workflows. The slug alone is lossy (/, _,
and . all collapse to the same separator), so e.g. "foo/bar" and
"foo_bar" previously collided onto the same environment name.
* fix(workflows): scope teardown job to same-repo PRs only
The header comment already documented that this only applies to
same-repo branches (secrets are withheld from fork PR runs), but
nothing enforced it -- the job ran on every closed PR including
forks, where the empty RAILWAY_API_TOKEN/RAILWAY_PROJECT_ID made
railway link fail with an auth error and show a red run for a PR
that never had a preview environment to clean up.
* docs: migrate RAILWAY_PREVIEW.md to PREVIEW_ENV.md
Replace docker/docs/RAILWAY_PREVIEW.md with PREVIEW_ENV.md at the repo
root, restructured per PR review feedback around the two things a
reader actually needs: how to deploy to staging, and how to deploy a
PR to a preview environment. Drops the one-time Railway project setup
section (not needed on this page, per review).
---------
Co-authored-by: Rotimi Best <best@classroomio.com>
Change the Customize design button on the certificate design page from
primary to secondary styling for better visual hierarchy.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
CoursePrimaryAction used the base Button with per-theme color classes
that overrode disabled styling. When enrollment was closed, the pricing
CTA appeared correctly muted while the hero Buy Now button still looked
active.
Switch the hero CTA to LandingButton (same primitive as pricing) and
reset theme color overrides when disabled so both CTAs look and behave
consistently.