Fix CoordinatorJob infinite loop caused by non-advancing StackOne pagination cursor
Summary
Fix two bugs in FuseliteStackOne::CoordinatorJob that cause an infinite loop. This happens when StackOne returns a non-advancing pagination cursor, flooding the Sidekiq queue with thousands of duplicate PageJobs.
Context
The external_courses sync job has been failing since April 9. This was due to the cursor column being too small (varchar(255)) for StackOne's base64-encoded cursors. The job fails with Mysql2::Error: Data too long for column 'cursor' and ends up in the Sidekiq dead set. As a result, courses added to Go1 after the last successful sync were not picked up. After widening the column to varchar(1024) (PD-12840) and re-running the sync, it successfully picked up 325 active courses.
The infinite loop occurs because StackOne cursors are base64-encoded JSON blobs. The ts (timestamp) field changes on every response, making simple string equality ineffective for detecting a non-advancing cursor. The actual page position is in the pc field. If StackOne's Go1 connector fails to paginate correctly, it returns p: -1 and the same pc value, leading to an indefinite loop of enqueuing duplicate PageJobs.
Acceptance criteria
CoordinatorJob#discover_and_enqueue! detects a non-advancing cursor by comparing the decoded pc field inside the base64 cursor JSON.
The coordinator raises a clear error message when a non-advancing cursor is detected, instead of looping.
sidekiq_retries_exhausted clears the cursor to nil (with pages_total: 0, pages_done: 0).
Graceful shutdown resumability is preserved; a Sidekiq-interrupted coordinator resumes from the last stored cursor.
Falls back to string equality if cursor decoding fails.
FE: Change to Icon and Title for main overview page
In order to support translation and make a smoother and clearer user journey, the ‘Main overview page’ button needs to be changed, along with the icon used.
Story…
As a user…
I click on the overview overlay, I can see the button, ‘Learning plan overview’.
I can see the icon next to the ‘Learning plan overview’.
Design note: For the icon, use the Learning Plans icon as found in the left side bar for Learning Plans on Pink Fuse.
Given I am on a page with the overview overlay
When I open the overview overlay
Then I see the button labeled, ‘Learning plan overview’
Given I am on a page with the overview overlay
When I open the overview overlay
Then I see the icon next to the, ‘Learning plan overview’ button
Bump stackone_client gem and replace Faraday bypass for list_assignments with SDK
Summary
The task is to update the stackone_client gem and replace the Faraday bypass for list_assignments and list_user_assignments with proper SDK calls. This is due to a recent fix in the StackOne Ruby SDK that addresses a type crash issue.
Context
The list_assignments method was using a raw Faraday bypass because the API returned "source_value": 0 (Integer), which conflicted with the SDK's Sorbet type. This led to a TypeError in Crystalline. The existing workaround can be found in LmsGateway#list_assignments.
Acceptance criteria
Bump stackone_client to the version with the fix for source_value: 0.
Replace the raw Faraday bypass in LmsGateway#list_assignments and LmsGateway#list_user_assignments with SDK calls:
Use @client.lms.list_assignments
Use @client.lms.list_user_assignments
Remove the RawResponse struct and lms_faraday helper from LmsGateway if they are no longer needed.
Update ResponseParser if the SDK response shape for assignments differs from the raw Faraday shape.
Verify and document the intentional omission of updated_after for assignments at the SDK call level.
Widen cursor column to varchar(1024) in content_provider_sync_cursors
Summary
The issue involves the cursor column in the content_provider_sync_cursors table. It is currently defined as varchar(255), which leads to a Mysql2::Error: Data too long for column 'cursor' error in production. This occurs when StackOne returns pagination cursors longer than 255 characters.
Context
The cursor column was originally defined with a length that is insufficient for the data being stored. The error arises from base64-encoded JSON blobs exceeding the 255-character limit. The table was introduced in the current branch and has not yet been merged into the main branch.
Acceptance criteria
Update the cursor column definition to varchar(1024) in the content_provider_sync_cursors table.
Ensure no separate migration is needed since the table is new and not yet in the main branch.
Other information
Original migration file: 20260327200000_create_content_provider_sync_tables.rb
Error message: Mysql2::Error: Data too long for column 'cursor'
As a user I can see a progress indicator as I move through the formal assessment
As a user I must be able to clearly see my progress as I move through a formal assessment.
Story
As a user…
Above each question I can see a progress bar indicating now far I am through the assessment.
The bar should be grey by default, and as I progress through the assessment it updates and displays progress in accordance to the percentage I am through the assessment. For example, in a 10 question assessment, if I am on the first question, 10% of the progress bar is populated with company colours. If I am on the 5th question, then 50% of the bar is populated with company colours.
Progress should be depicted with company colours.
If I am on the last question, then the whole bar should be populated with company colours, as I am 100% through the assessment.
QA Note: Please note that the design shows a segmented progress bar. Ignore this, we are expecting to see a solid bar.
AC1: Progress bar is visible above each question
Given I am a user taking a formal assessment
When I view any question in the assessment
Then I can see a progress bar displayed above the question
AC2: Progress bar default state
Given I have not yet started progressing through the assessment
When the progress bar is rendered
Then the bar displays the company colours of the percentage comprised of the first question
AC3: Progress bar updates based on current position
Given I am on question N of a total of T questions in the assessment
When the progress bar is displayed
Then the bar is filled to (N / T) × 100% using the company colours, and the remaining portion remains grey
AC4: Progress bar reflects correct percentage (example: first question)
Given I am taking a 10-question assessment
When I am on the 1st question
Then 10% of the progress bar is filled with company colours
AC5: Progress bar reflects correct percentage (example: midway)
Given I am taking a 10-question assessment
When I am on the 5th question
Then 50% of the progress bar is filled with company colours
AC6: Progress bar at 100% on the last question
Given I am taking a formal assessment
When I am on the last question of the assessment
Then the entire progress bar (100%) is filled with company colours
AC7: Company colours are used for progress
Given the organisation has company colours configured
When the progress bar fills as I advance through the assessment
Then the filled portion of the bar is rendered using the company colours
As a user I can clearly see which question I am on out of x amount
As a user I must be able to see clearly which question I am on out of the number of questions within the formal assessment.
Story
As a user…
I can clearly see in the top left the information regarding which question I am on out of the number of questions within the formal assessment. This will be displayed in the format ‘Question ‘x’ of ‘y’’ (this 'y' being the total number of questions in the formal assessment). For example ‘Question 1 of 9’.
Given I am taking a formal assessment that contains multiple questions
When I view any question in the assessment
Then I can see a question progress indicator in the top left of the screen in the format "Question X of Y" (e.g. "Question 1 of 9")
Given I am on question, for example 3 of a 9-question formal assessment
When I navigate to the next question
Then the progress indicator updates, for example to "Question 4 of 9"
Given a formal assessment has been configured with a specific number of questions
When I begin the assessment and view any question
Then the total number shown in the indicator matches the actual number of questions in the assessment
Given I have started a formal assessment
When the first question is displayed
Then the progress indicator reads "Question 1 of Y", where Y is the total number of questions
Given I am taking a formal assessment with Y questions
When I reach the final question
Then the progress indicator reads "Question Y of Y"
Task 12 - Sync content metadata (tags, description, thumbnail) from Stack One into fuselite
Summary
Sync content metadata from Stack One into fuselite. This includes tags, descriptions, and thumbnails for mirror Content records.
Context
This task is part of the metadata improvements epic (PD-10626). The Stack One API offers richer metadata than is currently stored. This implementation will sync that metadata into the shared database for FuseTube consumers. Language mapping is not included in this task.
Acceptance criteria
Tags from Stack One (categories, skills, tags fields) are stored on the mirror Content record and update correctly on re-sync.
Description and thumbnail sync correctly on both create and update.
ExternalContent.languages stores a clean JSON array of locale codes.
All specs pass.
Other information
Included:
Sync tags, categories, and skills from Stack One into Fuse content tags via a new ContentTagSync service.
Sync description from Stack One into the mirror Content record.
Sync thumbnail from Stack One into the mirror Content record via ContentTagSync.
Add Tag and Tagging AR models to fuselite.
Full RSpec coverage.
Not included:
Syncing languages to the contents table (pending clarification).
Any FuseTube changes.
Implementation notes:
Tags are a union of row["tags"] + row["categories"] + row["skills"].
ContentTagSync writes directly to the shared tags/taggings tables without acts-as-taggable-on gem.
[Growth Squad][Transcription]Copy transcription message fails to appear in user set language.
When the user sets their language to something other than English and clicks on the copy button to copy the transcripts. The message “Transcript copied to clipboard” appears in English.
Ideally, it should appear in the same language that the user has set.
Tested these scenarios on the German, French, and Spanish languages.
Once I answer the last question in a formal assessment and click ‘Submit’, then I am navigated to the results screen.
On the results screen, I can see my score, and the pass criteria.
I can see the buttons 'Retake assessment' - provided further attempts are available.
I can see the relevant messaging indicating I have failed the assessment.
I can only see the results from the session I have completed. Prior scores are not displayed, e.g., If I pass an assessment, then retake it and fail, I will see the result of the fail as it is the most recent session I completed.
Given I have answered the last question in a formal assessment, and failed to meet the pass criteria
When I click "Submit"
Then I am navigated to the results screen
And I can see my score, and the pass criteria.
And I can see the “Retake assessment" button - provided further attempts are available.
And I can see the relevant messaging Informing me I have failed.
Given I have NOT met the pass criteria and failed the assessment
When I see the relevant messaging
Then it says ‘Not quite there. You did not meet the pass criteria this time'
Given I am on the results screen
When there are no other attempts available
Then I see the ‘no attempts left banner already utilised on the splash screen’. (PD-12345 )
Given I have just completed a formal assessment session
And I am viewing the results screen
When I view my assessment results
Then I can only see the results from the session I have just completed
And prior scores from earlier sessions are not displayed
And if I previously passed the assessment but then retake it and fail, I see the result of the failed attempt because it is the most recent completed session
The top bar navigation gets stuck in a loop. This occurs when there are duplicate contents in the learning plan. Users are unable to navigate effectively.
Expected behaviour
The top bar navigation should function smoothly without looping. Users should be able to access different sections of the learning plan without issues.
Actual behaviour
The top bar navigation is stuck in a loop when duplicate contents are present in the learning plan.
[FE][Article] Images revert to 100% size on publish — editor resize not retained
Description
When an article is published, images that have been manually resized in the article editor revert to 100% width. The resized dimensions set by the author are not retained on the published content page.
Expected behaviour
Images should retain the size they were adjusted to in the article editor view when the article is published. The published article should reflect the same image dimensions the author set during editing.
Acceptance Criteria
When an author resizes an image in the article editor (e.g. to 50% or a specific pixel width), the published article displays the image at that same size
Image size persists correctly across save, publish, and subsequent page loads
Steps to reproduce
Create or edit an article
Insert an image
Resize the image to a size smaller than 100% (e.g. 50%)
Task 11 — Download and store Stack One content thumbnails into shared assets table
Summary
This issue involves downloading and storing Stack One content thumbnails into a shared assets table used by FuseTube and fuselite. The goal is to ensure that fuselite can write to the same table so that FuseTube can serve images seamlessly.
Context
FuseTube and fuselite utilize the same MySQL database and S3 assets bucket (ASSETS_BUCKET). Currently, LearningObjectsSync stores image URLs directly without creating the necessary asset rows. This leads to missing thumbnails, as noted in a previous comment on PD-12639.
Acceptance criteria
contents.thumbnail_id is populated for Stack One content rows with a cover_url/thumbnail_url.
The assets row has:
type = 'Asset::ContentAsset'
Correct asset_uid S3 key
Valid asset_data JSON
FuseTube serves the image through its existing pipeline without changes.
Re-syncing the same content with the same image URL is idempotent.
Content without an image URL results in no asset row and thumbnail_id = NULL.
Other information
Approach
New service: FuseliteStackOne::ContentThumbnailSync
Accepts a thumbnail_url string and company_id.
Downloads the image via Net::HTTP (following redirects).
Uploads binary to S3 under uploads/{company_id}/stack_one/{sha256_of_url}.{ext}.
Finds or creates an Asset::ContentAsset with asset_uid = S3 key and asset_data JSON (matching the format used by ThumbnailHelper).
Returns the asset.
LearningObjectsSync#upsert_mirror_content calls ContentThumbnailSync and assigns content.thumbnail = asset.
Skip if thumbnail_url is blank or S3 key already exists (idempotent).
Errors are logged and swallowed; a failed image download must not abort the content sync.
Once I answer the last question in a formal assessment and click ‘Submit’, then I am navigated to the results screen.
On the results screen, I can see my score, and the pass criteria.
I can see the button 'Retake assessment' - provided further attempts are available.
I can see the relevant messaging indicating I have passed the assessment.
I can only see the results from the session I have completed. Prior scores are not displayed, e.g., If I pass an assessment, then retake it and fail, I will see the result of the fail as it is the most recent session I completed.
Given I have answered the last question in a formal assessment
When I click "Submit"
Then I am navigated to the results screen
And I can see my score, and the pass criteria.
And I can see the "Retake assessment" button - provided further attempts are available
And I can see the relevant messaging informing me I have passed
Given I am on the results screen
When there are no other attempts available
Then I see the ‘no attempts left banner already utilised on the splash screen’. (PD-12345 )
Given I am on the results screen
When I see the results
Then they should be the latest results, not the highest result I have ever scored for this formal assessment
Given I have met the pass criteria and passed the assessment
When I see the relevant messaging
Then it says ‘Congratulations! You passed the formal assessment’
Given I have just completed a formal assessment session
And I am viewing the results screen
When I view my assessment results
Then I can only see the results from the session I have just completed
And prior scores from earlier sessions are not displayed
And if I previously passed the assessment but then retake it and fail, I see the result of the failed attempt because it is the most recent completed session
Note: Ignore the View Breakdown button in the design. This will be future development work, and is not part of this story.
FE: Update Assessment Button State Based on Retake Eligibility
Description:
As a user, I want the assessment action button to clearly reflect whether I can retake an assessment, so that I understand my available options without confusion.
Acceptance Criteria:
Retake Label:
If the user has already completed the formal assessment but is still eligible for additional attempts, the button label should display “Retake Assessment.”
Disabled State:
If the user is not eligible to retake the assessment, then they will see the information box detailing as such, and the “Retake Assessment” button is not visible.
Given I have completed the formal assessment, but I am eligible for additional attempts
When I see the button
Then the button will state ‘Retake Assessment’
Given I am not eligible to retake the assessment (e.g., no attempts remaining or restricted)
When I see page
Then the button will not be visible and an information box is present.
Change the "you" German translation on instance from Sie to "Ich"
Impact
The German translation of "you" in the side menu is incorrect. Users see "Sie" instead of "Ich". This affects user experience for German-speaking clients.
Expected behaviour
The side menu should display "Ich" as the translation for "you" in German.
Actual behaviour
Currently, the side menu shows "Sie" instead of "Ich".
Steps to reproduce
Navigate to the side menu on the affected instance.
This issue focuses on documenting the FuseLite Stack One SDK sync feature. The goal is to provide comprehensive documentation that helps engineers understand, operate, and troubleshoot the feature.
Context
The documentation will be based on a reference PR and implementation. It aims to clearly explain the end-to-end functionality of the Stack One sync feature and how it differs from the previous FuseTube integration.
Acceptance criteria
Feature overview clearly explains:
What the Stack One sync does.
The problems it solves.
How it differs from the previous FuseTube integration.
Approach: Port spec/integration/stack_one_sync_pipeline_spec.rb from the reference branch as a final validation pass once Stories 1–7 are complete. This spec exercises the full pipeline end-to-end against a real test database and should pass without modification if the implementation is correct.
Goal: All sync behaviour is covered by automated tests and the three correctness bugs found during development cannot regress.
Acceptance criteria:
All sync services, jobs, and dashboard endpoints have unit specs
End-to-end integration spec exercises the full pipeline against a real test database
Explicit regression cases for:
Concurrent workers on the same content row do not raise RecordNotUnique on CommunityItem
Re-syncing an already-known completion does not enqueue redundant downstream jobs
SyncUserCompletionsJob finds a completion that falls beyond the first page of results
Approach: TDD. Port spec/requests/api/v10/sync_dashboard_spec.rb and sync_dashboard_stale_coordinator_spec.rb first.
Goal: Operators have real-time visibility into sync health and can trigger syncs without touching the Rails console or Sidekiq UI.
Acceptance criteria:
GET /api/v10/sync_dashboard — summary stats + per-provider status; shows in-progress syncs with ETA based on remaining pages and rolling average page duration
GET /api/v10/sync_dashboard/logs — filterable by provider, status, and sync type
GET /api/v10/sync_dashboard/queue — live Sidekiq queue, retry, and dead job counts
POST /api/v10/sync_dashboard/enqueue — triggers a sync for a specific provider + sync type
All endpoints require authentication and verify the user belongs to the company
Documented in swagger/v10/swagger_internal.json, accessible via Swagger UI
Approach: TDD. Port spec/jobs/fuselite_stack_one/sync_user_completions_job_spec.rb first. Pay particular attention to the pagination regression case — the spec explicitly covers a completion that falls beyond the first page of results.
Goal: When a user engages with external content, their completion status is checked against the LMS immediately rather than waiting for the next scheduled batch.
Acceptance criteria:
ExternalContentProgressJob triggers SyncUserCompletionsJob for content backed by ExternalContent
SyncUserCompletionsJob paginates all pages of list_user_assignments until the matching assignment is found or all pages are exhausted
Writes ExternalContentCompleteness only when a completed assignment is confirmed
No-op if the user has no ExternalMembership for the provider or integration is disabled
Approach: TDD. Port spec/jobs/fuselite_stack_one/recurring_sync_spec.rb first. These specs are simple delegation checks and will drive the four job subclasses and schedule config cleanly.
Goal: Each sync type runs automatically on a fixed cadence matching FuseTube's existing schedule, with no manual intervention required.
Acceptance criteria:
Memberships daily at 06:00, external contents at 06:30, courses at 07:30, completions hourly at :02
Schedule defined in config/sidekiq-schedule.yml, loaded at Sidekiq server startup
Not loaded in the test environment
Each RecurringSync job class delegates entirely to ScheduleSyncJob and contains no sync logic
Approach: TDD. Port spec/services/fuselite_stack_one/sync/ from the reference branch first. The specs are behavioural and cover all three sync types, the resumable mixin, and the three correctness bugs — use them to drive the implementation.
Goal: Each sync type is a self-contained, testable service that can be run, resumed after interruption, and safely re-run without duplicating or corrupting data.
Acceptance criteria:
MembershipsSync upserts ExternalMembership rows keyed by (source_id, content_provider_id)
LearningObjectsSync upserts ExternalContent + mirror Content + CommunityItem; publishes or deactivates the mirror content to match the active flag; concurrent execution by multiple workers on the same row does not raise or produce duplicates
CompletionsSync writes ExternalContentCompleteness using completed_at (not updated_at); triggers ExternalContentProgressJob only for newly created records, not re-syncs
All sync classes share a Resumable mixin: cursor persisted after every page, expired cursors (HTTP 400/404/410) detected and restarted from page 1, max_pages guard prevents runaway
Approach: Implementation-first. Port the factories from the reference branch (spec/factories/) once models are defined — they will be needed by Stories 3–8.
Goal: All data produced by the sync has a well-defined, validated home in the database that is safe to run against the shared FuseTube/FuseLite DB.
Acceptance criteria:
content_provider_sync_cursors — one row per (provider, sync_class), stores pagination cursor, coordinator JID, and progress metadata for dashboard ETA
content_provider_sync_logs — append-only audit log per sync run with success / failed / skipped status and error detail
ExternalContent, ExternalMembership, ExternalContentCompleteness, ExternalContentTarget are fully modelled with validations and associations
LearningPlanItem STI hierarchy covers Content and ExternalContent item types
Company exposes stack_one_integration_enabled?; Company::Settings stores stack_one_integration_enabled and stack_one_external_content_owner_id
User exposes stack_one_learning_plan_ids covering community, audience, and assigned-membership visibility types
Migrations use if_not_exists — safe to run against a DB FuseTube may have already migrated
Approach: Implementation-first. SDK behaviour and compatibility issues can only be discovered by running against the real API. Consult docs/stack_one_sdk_issues.md in the reference branch for known issues and their workarounds before starting.
Goal: Provide a stable internal interface to the Stack One LMS API so the rest of the app is decoupled from the SDK and HTTP transport details.
Acceptance criteria:
All Stack One API calls go through FuseliteStackOne::LmsGateway — no direct SDK usage outside it
[New Content][Hotfix]Clickable links formatting is not in order once the content is published.
It is observed that when the user adds multiple links with proper spacing between them in the description of new content pages, the formatting is disrupted and the links are published without any spaces in the description box.
FE: Place top bar navigation onto monolith SCORM page
Story
As a user I can see the top bar navigation at the top of the existing monolith pages for SCORM.
As a user I am able to see these when I navigate from the Learning Plan Overview page, or navigate to the SCORM page utilising the Next and Previous button in the top bar navigation
Given I am viewing a SCORM page
And I have navigated to the page from the Learning Plan Overview page
When the page loads
Then I can see the new top bar navigation rendered at the top of the page above the main SCORM content
Given I am viewing a SCORM page
And I have navigated to the page using the top bar navigation - either next or previous buttons
When the page loads
Then I can see the new top bar navigation rendered at the top of the page above the main SCORM content
As a user I want to be able to answer a multiple choice question - Image Answer type
User story
As a user, when I am answering a question that allows multiple answers, I want to be able to select more than one option so that I can accurately reflect all correct responses.
As a user, I want the UI to clearly show that I can choose multiple answers so that I am not misled into selecting only one.
Context
This issue involves the implementation of a multiple answer question format for Many correct multiple choice questions in assessments. The goal is to ensure that users can select multiple answers for a single question, with clear UI indications and consistent behaviour across different multiple choice answer formats, as defined in the Question creator in the Assessment tab:
Adding questions to the question pool in assessments
This story excludes assessment tools and only covers Image single answer questions.
Acceptance criteria
For Many correct multiple choice questions, the user can select one, or multiple answers at the same time (checkbox-style behaviour).
Each answer option is independently selectable and deselectable without affecting other selections.
The UI must clearly indicate that multiple answers may be selected (e.g., checkbox controls).
The behaviour must be distinct from One correct questions where only a single answer is allowed.
The question must behave correctly regardless of the multiple choice answer format - Image answers: shown as an image with a multiple-choice selector.
Aspect rations should be respected. The UI should be responsive, and where necessary, answers should be stacked.
As a user I want to be able to take a single answer question - Fraction Answer type
User story
As a user, when I am answering a question, I want to select only one answer so that I can provide a clear response.
As a user, when I change my answer, I want the previous selection to be automatically deselected so that there is no confusion about my choice.
Context
This issue involves the implementation of a single answer question format. The goal is to ensure that users can only select one answer at a time, with clear UI indications and consistent behaviour across different answer formats.
This story excludes assessment tools and only covers Fraction single answer questions.
Acceptance criteria
Only one answer can be selected at a time (radio-style behaviour).
Changing the selection automatically deselects any previously selected answer.
The behaviour must be distinct from Many correct questions.
The question must behave correctly - Fraction answers: shown as a fraction with a single-choice selector.
Aspect rations should be respected. The UI should be responsive, and where necessary, answers should be stacked.
As a learner viewing a video transcript, I want to click a chapter marker and jump to that section of the transcript, So that I can quickly navigate to relevant parts of the content.
AC1 — Create Chapters
Given an author of a piece of content has added ‘jump to’ positions in a video
When the transcript panel is open
Then the chapter headings are visible at the top of the transcript tab as per the design
AC2— Click scrolls to chapter
Given a learner clicks a chapter marker
When the transcript panel is open
Then the panel smooth-scrolls so that the matching chapter heading is positioned at the top of the visible area
AC3 — Edit Chapters
Given an author of a piece of content has edited the ‘jump to sections’
When the transcript panel is open
Then the updated chapter headings are visible at the top of the transcript tab as per the design
As a user I want to be able to answer a variation question
User story
As a user, when I am answering a variation question, I am able to see the question and then a free text field beneath it.
As a user I am able to click into the free text field and input my answer.
As a user, I am unable to click the ‘Next question’ button until I have input something into the free text field.
Context
The issue involves the implementation of a variation answer question format for Variation choice questions in assessments. The goal is to ensure that users can input their answer into the free text field, with clear UI indications and consistent behaviour, as defined in the Question Creator in the Assessments tab: Adding questions to the question pool in assessments
Variation assessments only allow text answer types - fractions and images are not included.
Users can click into the free text field and input an answer.
If a user has yet to click into the answer box, then they will see the words ‘Enter answer here’ within the free text field box.
If a user clicks into the free text field box, then the words ‘Enter answer here’ move above the box, and the text ‘Enter answer here’ and framing of the box become the company colours.
Should the user clear an answer in the free text field, then click outside the box, then the ‘Enter answer here’ text will display within the box again.
As a user I want to be able to answer a multiple choice question - Text answer type
User story
As a user, when I am answering a question that allows multiple answers, I want to be able to select more than one option so that I can accurately reflect all correct responses.
As a user, I want the UI to clearly show that I can choose multiple answers so that I am not misled into selecting only one.
Context
This issue involves the implementation of a multiple answer question format for Many correct multiple choice questions in assessments. The goal is to ensure that users can select multiple answers for a single question, with clear UI indications and consistent behaviour across different multiple choice answer formats, as defined in the Question creator in the Assessment tab:
Adding questions to the question pool in assessments
This story excludes assessment tools and only covers Text multiple choice answer questions.
Acceptance criteria
For Many correct multiple choice questions, the user can select one, or multiple answers at the same time (checkbox-style behaviour).
Each answer option is independently selectable and deselectable without affecting other selections.
The UI must clearly indicate that multiple answers may be selected (e.g., checkbox controls and/or helper text such as “Select all that apply”).
The behaviour must be distinct from One correct questions where only a single answer is allowed.
The question must behave correctly regardless of the multiple choice answer format configured in the Question creator:
Text answers: shown as text with a multiple-choice selector.
As a user I want to be able to take a single answer question - Text Answer type
User story
As a user, when I am answering a question, I want to select only one answer so that I can provide a clear response.
As a user, when I change my answer, I want the previous selection to be automatically deselected so that there is no confusion about my choice.
Context
This issue involves the implementation of a single answer question format. The goal is to ensure that users can only select one answer at a time, with clear UI indications and consistent behaviour across different answer formats.
This story excludes assessment tools
Acceptance criteria
Only one answer can be selected at a time (radio-style behaviour).
Changing the selection automatically deselects any previously selected answer.
The behaviour must be distinct from Many correct questions.
The question must behave correctly regardless of the answer format - Text answers: shown as text with a single-choice selector.
As a user taking a formal assessment in a learning plan I want to land on a splash page to introduce the assessment.
Description / Context (expanded)
When a learner navigates to a formal assessment from a learning plan, they should first see a dedicated splash page before any questions are shown.
This splash page should:
Clearly confirm that this is a formal assessment (not just casual practice/quiz).
Provide key information about the assessment (e.g. title, number of questions, pass criteria, time expectations where applicable).
Set a professional, consistent tone across all formal assessments in learning plans.
The page is read-only context; the only primary action is to take the assessment. Any secondary actions (e.g. return to learning plan) should be clearly labelled so that the learner doesn’t accidentally exit.
If the user has already completed the assessment they are able to retake (if the settings applies) or move to the next piece of content.
This story is focused on the user-facing experience of that splash page; data and configuration (e.g. pass mark, attempt limits) are assumed to already exist in backend / settings and are only being surfaced here.
Acceptance Criteria
Entry point & triggering
When a learner navigates to the formal assessment from a learning plan
Via the learning plan overview page
Via the next button on a previous content in the learning plan
Via a direct link
They are taken to the splash page before any question is displayed
Required content on the splash page
The splash page displays:
The assessment title (Right now the API throws always null as there’s no way to change the Assessment title, we will use “Formal Assessment” as default title) @User
The number of questions or an indication of approximate length (e.g. “10 questions”). - Only if available in the api. Cross out if not available.
The pass criteria (e.g. required score/percentage) if configured for the assessment. - Only if available in the api. Cross out if not available.
A short explanation of what happens on completion, aligned with the wiki:
That completion contributes to learning plan progress.
Whether retakes are allowed (if this information is already available/configured). Out of scope
Actions on the splash page
The splash page provides a primary action button (e.g. “Take assessment”) that: → This is provided by API
Starts the assessment and displays the first question.
The splash page provides a clear way to go back without starting (Top Bar Navigation) which:
Returns the learner to the previous learning plan view.
Skips assessment (if sequential learning not enabled)
Does not create a partially-started attempt.
Visual and UX consistency
The styling of the splash page is consistent with the current learning plan / assessment UI guidelines documented in Confluence (typography, spacing, button style, etc.).
The page clearly communicates that this is a formal assessment (e.g. using a label or subtitle), maintaining the “professional welcome” tone described in the epic.
FE: Place top bar navigation onto monolith Events & Occurrence pages
Story
As a user I can see the top bar navigation at the top of the existing monolith pages for Events.
As a user I can see the top bar navigation at the top of the existing monolith pages for Occurrences.
As a user I am able to see these when i navigate from the Learning Plan Overview page, or navigate to the Event and Occurrences pages utilising the Next and Previous button in the top bar navigation.
Given I am viewing an Event page
And I have navigated to the page from the Learning Plan Overview page
When the page loads
Then I can see the new top bar navigation rendered at the top of the page above the main Event content
Given I am viewing an Occurrence page
And I have navigated to the page from the Learning Plan Overview page
When the page loads
Then I can see the new top bar navigation rendered at the top of the page above the main Occurrence content
Given I am viewing an Event page
And I have navigated to the page using the top bar navigation - either next or previous buttons
When the page loads
Then I can see the new top bar navigation rendered at the top of the page above the main Event content
Given I am viewing an Occurrence page
And I have navigated to the page using the top bar navigation - either next or previous buttons
When the page loads
Then I can see the new top bar navigation rendered at the top of the page above the main Occurrence content
UA2 - Automatic creation of users in GDC who don't already exist
As an analytics users
I want to be automatically created in GoodData Cloud with the permissions relevant to my role at the point that I try and access analytics
So that I do not need to be manually created
Given the user is a community admin of the community, if the user does not already exist in GDC when they try and access analytics from the community settings page > Analytics option, then we create them in GDC with the right access and the right workspace permissions i.e community admin = view communities dashboards only
Given the user is a community admin of the community, if the user already exists in GDC when they try and access analytics from the community settings page > Analytics option, then we simply permit them to access the page in accordance with their permissions
Given the user is a site admin, when the user accesses analytics from the community settings page > Analytics option, The UI should behave exactly the same for a site admin as it would for a standard user who is a community admin
FE: Update Start/Continue Buttons to Use Next Content Item API
Description:
Update the frontend to leverage the new nextContentItemId field from the GET /learning/plans/{id} API. This will ensure the Start and Continue buttons always navigate the user to the correct next content item in the learning plan.
Requirements:
Fetch the learning plan details using GET /learning/plans/{id}.
Use the nextContentItemId field to determine the target content item for navigation.
Button behavior:
Get started – Navigate to the nextContentItemId when the user hasn’t started the plan.
Continue – Navigate to the nextContentItemId when the user has partially completed the plan.
If all items are completed, the API will return the first item; buttons should navigate there.
Ensure navigation integrates with the existing content routing logic.
Notes:
Coordinate with the backend team to confirm API field name (nextContentItemId).
Ensure proper loading states while fetching the learning plan data.
Consider caching or updating local state to prevent unnecessary API calls on repeated button clicks.
Get started button navigates to the next content item for users who haven’t started the plan.
Continue button navigates to the next uncompleted content item, or first item if all completed.
Existing button UI and styling remain unchanged.
No regressions in navigation for other learning plan workflows.
BE: Add “Next Item” Field to GET Learning Plan API
Description:
Enhance the /learning_plans/{id} API to include information about the next item in the learning plan sequence. This will allow the client to identify what the user should engage with next.
Requirements:
Add a new field in the API response for the next item in the sequence of learning plan items.
The field should return the ID of the next item.
Add the item type.
Logic:
If there are uncompleted items, return the next uncompleted item based on sequence order.
If all items are completed, return the first item in the learning plan sequence.
Next item can be:
- Event
- Event Occurrence
- SCORM (within a topic)
- SCORM
- All normal content types
- Formal Assessment
- External content
Do NOT return the topic or any other types of assessment
Acceptance Criteria:
API correctly returns the next uncompleted item when there are pending items, and only items from the list above.
API returns the first item if all items are completed.
Existing API functionality remains unchanged.
If a Learning Plan has reset, then the user will navigate to the first item in the learning plan sequence.
Release 26.18 (4.2 Launch) delivers three main streams of value:
Formal Assessments UX & Logic – A complete, production-ready experience for taking formal assessments in learning plans: a proper splash page, clear progress indicators, flexible question/answer types (text, image, fraction, variation), and robust pass/fail results handling including retakes.
Stack One / FuseLite integration & sync pipeline – A fully documented, production‑grade LMS content and completion sync layer (Stack One → FuseLite/FuseTube) with resilient scheduling, observability, metadata/thumbnails sync, and multiple correctness fixes.
Experience & localisation fixes – A set of targeted UI/UX and localisation improvements across content pages, top bar navigation, GoodData analytics access, search filters, and translations.
This release contains a mix of net-new capabilities, significant improvements to existing features, and a number of bug fixes that remove friction for learners, admins, and operators.
New Features
1. Formal Assessments: End‑to‑End Experience
Assessment splash page in learning plans
PD-12345 – Introduces a dedicated formal assessment splash page when accessed from a learning plan (via overview, Next button, or deep link).
The splash screen:
Confirms this is a formal assessment, not a casual quiz.
Shows the assessment title (defaulting to “Formal Assessment” where necessary).
Where available via API, shows:
Number of questions.
Pass criteria (e.g. required %).
Provides a single primary action to start/take the assessment, plus a safe route back via top‑bar navigation (no partial attempts created).
Question progress indicators & progress bar
PD-12831 – Adds a clear question counter:
Shows “Question X of Y” in the top left on every question.
Updates as the learner navigates between questions.
Ensures the total number matches the actual number of questions.
PD-12833 – Adds a visual progress bar for formal assessments:
Displayed above each question.
Filled proportionally to progress, e.g. 10% on Q1 of 10, 50% on Q5 of 10.
Uses company colours for the filled portion, grey for the remainder.
Shows 100% filled on the last question.
Behaviour is continuous (solid bar), not segmented (design segmentation is intentionally ignored).
Question & answer types: single- and multi‑select
PD-12346 – Single-answer (Text):
Radio-style behaviour: only one text answer can be selected at a time.
Changing selection deselects the previous option.
PD-12493 – Single‑answer (Fraction):
Same radio‑style behaviour, but for fraction-based answer UI.
Ensures only one fraction answer is active at any time.
PD-12347 – Many‑correct (Text):
Checkbox-style behaviour for multiple correct text answers.
Users can select and deselect multiple options independently.
UI clearly indicates multi‑select (checkboxes and/or helper text like “Select all that apply”).
PD-12497 – Many‑correct (Image):
Checkbox-style multi‑select behaviour for image answer options.
Answer images respect aspect ratio, and layout is responsive/stacked where necessary.
Behaviour clearly distinguished from single‑answer flows.
PD-12882 – Image-based questions (general):
Ensures when a question includes an image:
Question text is clearly visible and legible.
Image renders cleanly at an appropriate size without blocking text or options.
All answer options remain visible and interactable.
Selected answers are clearly highlighted; users can change selection before submission.
Variation question free‑text answer type
PD-12433 – Adds variation question support:
Learner sees the question with a free‑text field beneath.
“Enter answer here” placeholder behaves like a modern floating‑label:
Shows inside the field initially.
Moves above in company colours when the field is focused.
Returns to placeholder position if the field is cleared and blurred.
The Next question button is disabled until the learner has entered a response.
Results screens (pass and fail) and retake behaviour
PD-12763 – Results Screen – Pass:
After submitting the final question and meeting pass criteria:
User is redirected to a results screen showing score and pass criteria.
A “Retake assessment” button is shown if more attempts are available.
Messaging confirms success: “Congratulations! You passed the formal assessment”.
Results always show the latest session:
If a user passes once and later fails, the fail attempt is shown, not the historic pass.
Only the current session’s result is visible; previous scores are hidden.
“No attempts left” banner reuses existing UI from PD-12345 when attempts are exhausted.
PD-12790 – Results Screen – Fail:
After submitting the last question and not meeting pass criteria:
User is taken to a fail results screen with score and pass criteria.
“Retake assessment” is shown only if additional attempts are available.
Failure message: “Not quite there. You did not meet the pass criteria this time”.
Same “latest attempt only” logic applies as above.
Reuses the “no attempts left” banner when no further attempts remain.
PD-12737 – Assessment button state / retake eligibility:
If the user has completed the assessment and still has attempts left:
The button reads “Retake Assessment”.
If the user is not eligible to retake (no attempts remaining or restricted):
The Retake button is hidden, and an information box is shown instead.
2. Stack One / FuseLite Integration & Sync Pipeline
This release substantially matures the Stack One → FuseLite/FuseTube integration, turning it into a resilient, observable sync platform.
API integration layer and data model foundations
PD-12640 – Stack One API integration layer:
Centralises all Stack One API access through FuseliteStackOne::LmsGateway.
Supports:
list_users
list_content
list_courses
list_assignments
list_user_assignments
Correctly extracts and forwards pagination cursors and normalises provider‑unsupported endpoints (403/501) into consistent error types.
All configuration is environment-driven (API URL, credentials, batch size, delta thresholds).
PD-12641 – Data model + schema:
Introduces core tables and models to safely store external LMS data in the shared DB:
content_provider_sync_cursors – one row per (provider, sync_class) with cursor, Sidekiq JID, and progress metadata.
content_provider_sync_logs – append‑only audit log of sync runs with status and error detail.
ExternalContent, ExternalMembership, ExternalContentCompleteness, ExternalContentTarget – fully modelled with validations and associations.
LearningPlanItem STI hierarchy covers content and external content.
Company/Company::Settings exposes stack_one_integration_enabled? and stack_one_external_content_owner_id.
User exposes stack_one_learning_plan_ids to reflect visibility via communities, audiences, and memberships.
Migrations are defensive (if_not_exists) to co‑exist safely with existing FuseTube migrations.
Sync services, resumability, and correctness
PD-12642 – Sync services:
Introduces dedicated sync services with a shared Resumable mixin:
MembershipsSync – upserts ExternalMembership keyed by (source_id, content_provider_id).
LearningObjectsSync – upserts ExternalContent, mirror Content, and CommunityItem; aligns publish/deactivate state to remote active flag; avoids duplicates even under concurrent workers.
CompletionsSync – writes ExternalContentCompleteness using completed_at (not updated_at), and triggers ExternalContentProgressJob only for new completions.
Resumable behaviour:
Cursor persisted after every page.
Expired cursors (HTTP 400/404/410) detected and reset to page 1.
max_pages guard prevents runaway jobs.
All upserts are idempotent to support safe re‑runs.
PD-12840 – Cursor column widening:
In content_provider_sync_cursors, cursor is expanded from varchar(255) to varchar(1024) to avoid Data too long for column 'cursor' when storing long, base64‑encoded cursors.
PD-12945 – CoordinatorJob infinite loop fix:
Fixes an infinite loop in FuseliteStackOne::CoordinatorJob when Stack One returns a non‑advancing cursor:
Correctly decodes cursors (base64‑encoded JSON) and compares the pc (page cursor) field, not raw strings.
Detects and raises clear errors for non‑advancing cursors instead of re‑enqueuing endlessly.
sidekiq_retries_exhausted now clears the cursor to nil with reset progress metadata.
Preserves graceful shutdown resumability (jobs resume from last stored cursor).
Falls back to simple string equality if decoding fails.
This closes a real incident where external_courses sync had been failing and flooding Sidekiq with duplicate PageJobs after cursor truncation issues (fixed separately in PD‑12840).
Adds Sidekiq schedule with cadences mirroring FuseTube:
Memberships – daily at 06:00.
External contents – daily at 06:30.
Courses – daily at 07:30.
Completions – hourly at :02.
Jobs under fuselite_stack_one/recurring_sync delegate entirely to ScheduleSyncJob and contain no sync logic.
Schedule is not loaded in test to keep tests deterministic.
PD-12646 – Sync dashboard API:
New internal endpoints provide real‑time visibility and control:
GET /api/v10/sync_dashboard – summary stats and per‑provider status, including in‑progress syncs with ETA based on remaining pages and rolling average page duration.
GET /api/v10/sync_dashboard/logs – filterable view of content_provider_sync_logs by provider, status, sync type.
GET /api/v10/sync_dashboard/queue – live Sidekiq queue/retry/dead job counts.
POST /api/v10/sync_dashboard/enqueue – on‑demand triggering of a sync for a given provider and sync type.
All endpoints are authenticated and company‑scoped and are documented in Swagger (swagger_internal.json and HTML docs).
PD-12648 – SDK compatibility documentation:
Comprehensive internal documentation of:
Feature overview and differences vs legacy FuseTube integration.