Embedded Analytics Developer Guide (JDK)

Version 0.4.4 | Last Updated: June 2026

This document covers all public callable functions and event-bound CSS classes across the QuantumLayers frontend JavaScript Development Kit. Every AJAX call injects an Authorization: Bearer <token> header via QLAuth.getSessionToken(). All endpoints expect a WordPress AJAX POST to qlAuth.ajaxurl and return {success: true|false, data: {...}}.


Table of Contents

  • QLAuth — Authentication, session management, login/register/logout
  • QLUpload — CSV file upload and dataset update
  • QLDashboard — Dashboard data, dataset management, scheduled report controls
  • QLDatasetDetail — Single-dataset detail view, column stats, deletion
  • QLAnalytics — Chart rendering, statistical analysis, saved charts
  • QLInsights — AI-powered dataset insights
  • QLMergeDatasets — Dataset join/merge builder
  • QLReportScheduler — Scheduled report creation and configuration
  • QLOrganizations — Org/team management and billing
  • QLUserProfile — User profile management (inline IIFE)
  • QLAgent — Multi-turn AI agent conversation

QLAuth

QLAuth is the authentication backbone used across every QuantumLayers page. It manages the full lifecycle of a standalone (non-WordPress) user account: email/password sign-in and registration, logout, forgot/reset password, Google Sign-In via Google Identity Services, and a third-party JWT bridge that lets an external site authenticate one of its own users into QL without ever showing them the QL login form. Every other QL module — dashboards, analytics, uploads, organizations, and so on — depends on QLAuth because they all call QLAuth.getSessionToken() to attach the Authorization: Bearer <token> header to their own AJAX requests. Because of this dependency, auth.js must always be the first QuantumLayers script loaded on a page. Defined in jdk/auth.js and exposed globally as window.QLAuth.

How to Include

Load jQuery first, then auth.js. Because every other QuantumLayers module calls QLAuth.getSessionToken(), this must be the first QL script on the page, with every other QL script listed after it:

A global qlAuth object with ajaxurl and nonce keys must also exist on the page before auth.js runs its own init(); see Authentication for how to obtain and populate these values from a third-party page.

Callable Functions

getSessionToken()

Description: Reads the JWT stored under the ql_session_token key in localStorage, wrapped in a try/catch so it fails safe (returns null) in environments where storage access throws, such as strict privacy modes or sandboxed iframes. Every other module calls this before making an AJAX request so it can attach the token as a Bearer header; if it returns null, requests fall back to same-origin nonce authentication instead.

Parameters: None.

Returns: string|null — the token, or null if none is stored or localStorage is unavailable. No AJAX call.

setSessionToken(token)

Description: Persists a session token to localStorage so the user stays signed in across page reloads and browser restarts, which is what makes third-party embedding possible without relying on a WordPress session cookie. It is called after every successful sign-in flow — email/password, Google, and third-party JWT — immediately after the server returns a fresh session_token.

Parameters:

  • token (string) — the JWT to store.

Returns: void — writes to localStorage under ql_session_token; silently no-ops if localStorage is unavailable or token is falsy. No AJAX call.

clearSessionToken()

Description: Removes the stored token from localStorage, logging the user out on this device only; it does not invalidate the token server-side. It is called by handleLogout() immediately after the ql_user_signout request succeeds, so a stale token can never be reused to make further requests from this browser.

Parameters: None.

Returns: void — removes ql_session_token from localStorage. No AJAX call.

checkAuth()

Description: Verifies the current session against the server as soon as the page loads, since a stored token can expire or be revoked between visits. If the server reports the user is logged in, it calls updateUI(response.data.user) to populate header/nav elements with the user’s name, email, and avatar; if not, the page is left untouched rather than force-redirected, unlike the checkAuth() variants in most other modules.

Parameters: None.

Returns: void — calls updateUI(response.data.user) if the user is authenticated; otherwise leaves the page untouched.

Endpoint: ql_check_auth

POST params:

Success: If response.data.logged_in is false, redirects to /ql-login. If true, calls updateUI(response.data.user) to populate the page with user data.

handleLogin(e)

Description: Submits the login form’s email, password, and remember-me flag to the server, disabling the submit button and swapping its label to a loading state first so a slow network can’t be mistaken for a missed click. Any error banner from a previous failed attempt is cleared before the new request goes out. On success the returned session token is stored and the browser redirects either to the dashboard or to whatever page was requested via the redirect query parameter.

Parameters:

  • e (Event) — the form submit event; e.preventDefault() is called internally.

Returns: void — on success calls setSessionToken() and redirects after 1 second.

Endpoint: ql_user_signin

Success: Calls setSessionToken(response.session_token) then redirects to the dashboard or the redirect query param.

handleRegister(e)

Description: Validates that the password and confirm-password fields match before submitting the registration form, since the server does not perform that particular check itself. It also reads a referral code — captured earlier from the page’s ?ref= query parameter, session storage, or a persistent cookie — and passes it along so referral attribution still works even if the user lands on the registration page well after first clicking a referral link.

Parameters:

  • e (Event) — the form submit event.

Returns: void — validates that password and confirm-password match before submitting; redirects to email verification after 2 seconds on success.

Endpoint: ql_user_signup

Success: Stores session token and redirects to the dashboard.

handleLogout(e)

Description: Signs the user out after asking for confirmation via a native confirm() dialog, since logging out discards the local session immediately with no undo. On success it clears the stored token and redirects either to the login page or to whatever redirect query parameter was present, mirroring the same redirect-preservation behavior used by handleLogin().

Parameters:

  • e (Event) — the click event.

Returns: void — shows a confirm() dialog first; clears the session token and redirects on success.

Endpoint: ql_user_signout

Success: Calls clearSessionToken() then redirects to /ql-login.

handleForgotPassword(e)

Description: Requests a password-reset email for the address entered in the form. The server intentionally returns the same generic success message regardless of whether that address actually has an account, which avoids leaking which emails are registered; this function simply relays whatever message the server responds with and resets the form afterward.

Parameters:

  • e (Event) — the form submit event.

Returns: void.

Endpoint: ql_forgot_password

Success: Displays response.data.message to the user.

handleResetPassword(e)

Description: Sets a new password using the one-time reset key and email address embedded in the link the user received by email, both of which are read from the current page’s URL query string rather than from any form field. It validates the new password locally first (minimum 8 characters, must match the confirmation field) and shows an immediate error without contacting the server if the key or email is missing from the URL, for example if the link was mistyped or truncated.

Parameters:

  • e (Event) — the form submit event.

Returns: void — validates password length (min. 8 characters) and match, then reads key/email from the URL query string before submitting.

Endpoint: ql_reset_password

Success: Shows a success message and redirects to login.

handleGoogleResponse(response)

Description: Callback invoked by Google’s Identity Services library once the user completes the Google Sign-In popup or One Tap flow. It exchanges the Google ID token for a QuantumLayers session by posting the credential to the server, which creates a new QL account on first sign-in or logs into an existing one if that Google email is already registered; the two outcomes redirect to different destinations — a welcome page for brand-new users, the dashboard (or the pending redirect target) for returning ones.

Parameters:

  • response (object) — the credential response object passed by the Google Identity Services callback.

Returns: void — stores the returned session token and redirects to the dashboard (or welcome page for new users).

Endpoint: ql_google_login

Success: Calls setSessionToken(response.data.session_token) and redirects to the dashboard.

thirdPartySignin(jwt, onSuccess, onError)

Description: Lets a third-party site silently authenticate one of its own users into QuantumLayers using a JWT the third party’s own backend already issued, without ever showing that user a QL login form — this is the mechanism behind the third-party provider registration (shadow users) approach described in the Authentication section. The third party is responsible for calling this once per session and handling the success/error callbacks itself; QLAuth does not automatically retry the request or redirect the page on either outcome.

Parameters:

  • jwt (string) — a JWT issued by the third-party site’s own server.
  • onSuccess (function) — invoked with the QL user object on success.
  • onError (function) — invoked with an error message string on failure.

Returns: void — stores the returned session token via setSessionToken() before invoking the callback.

Endpoint: ql_third_party_signin

Success: Stores response.data.session_token via setSessionToken(), then calls onSuccess(response.data.user). On failure calls onError().

Event Bindings

  • #ql-login-form submit — Fires handleLogin(e) whenever the login form is submitted. The submit button is disabled and its label swapped to a loading state for the duration of the request so a slow network can’t be mistaken for a missed click, and any error banner from a previous failed attempt is cleared before the new request goes out.
  • #ql-register-form submit — Fires handleRegister(e) when the registration form is submitted. Because the submit button stays disabled until the terms checkbox is checked (see the .terms-agreed change binding below), simply reaching this handler already implies the user has agreed to the terms; the handler itself performs the remaining password-match validation.
  • .ql-logout-btn click — Fires handleLogout(e) for any element carrying this class, anywhere on the page; the binding is delegated via $(document).on() so it also works for logout buttons injected into the DOM after page load.
  • .ql-google-signin click — Triggers Google’s google.accounts.id.prompt() to open the One Tap or account chooser flow. If the Google Identity Services script hasn’t finished loading yet (it’s fetched asynchronously during init()), the click shows an inline error instead of failing silently.
  • .ql-toggle-password click — Toggles the sibling password input’s type attribute between password and text so the user can momentarily reveal what they typed, and swaps the button’s own label between “Show” and “Hide” to reflect the current state. Purely a client-side UI convenience with no network request involved.
  • #ql-forgot-password-form submit — Fires handleForgotPassword(e) when the forgot-password form is submitted, disabling the submit button while the request is in flight and re-enabling it once the server responds, regardless of whether the request succeeded.
  • #ql-reset-password-form submit — Fires handleResetPassword(e) when the reset-password form is submitted. The reset key and email are not read from this form at all — they come from the page’s URL query string, since that’s how the emailed reset link identifies the request — so this binding only supplies the new password value.
  • .terms-agreed change — Enables the registration form’s submit button the moment the terms-of-service checkbox is checked, and disables it again if the user unchecks it, giving an immediate visual gate on registration without waiting for a form submission attempt to fail server-side.

QLUpload

This module powers the CSV upload page, the entry point where a user turns a raw spreadsheet into a QuantumLayers dataset. It solves the problem of getting tabular data into the platform with minimal friction: users can either click to browse or drag-and-drop a .csv file onto a drop zone, watch a live progress bar as the file streams to the server, and land on a populated dashboard as soon as processing finishes. The same page and script double as an editor — when opened with a numeric dataset ID in the query string, the form switches into edit mode, letting a user rename a dataset, change its visibility, or swap in a fresh CSV to replace the underlying data without deleting and recreating the dataset. On success, both new uploads and edits redirect into QLDashboard (/ql-dashboard?uploaded=1&dataset_id=<id> or /ql-dashboard?updated=1), which is where the newly created or updated dataset row becomes visible with links out to QLDatasetDetail, QLAnalytics, and QLInsights. Defined in jdk/upload.js and exposed globally as window.QLUpload.

How to Include

This module requires jQuery and QLAuth (auth.js) loaded first: it reads qlAuth.ajaxurl and qlAuth.nonce from the localized script object and calls QLAuth.getSessionToken() to build the Authorization: Bearer header on every AJAX request. It has no other special external dependency (no Chart.js, no Paddle.js) — its only third-party touchpoint is an optional Google Ads gtag() conversion call, guarded so it degrades gracefully if that tag manager script isn’t present.

Callable Functions

checkAuth()

Description: Runs during init() as a client-side gate against unauthenticated access to the upload form. It first checks that the localized qlAuth object exists at all — if a page is served without it, it logs an error and redirects to /ql-login immediately rather than letting a broken form render. If qlAuth is present, it fires an asynchronous ql_check_auth request; because this check is async, the form is technically visible before the response returns, so this is a UX safeguard rather than the real security boundary. If the response reports the user is not logged in, it redirects to /ql-login with a redirect query parameter pointing back at the current path; on an outright AJAX error it deliberately does not redirect, leaving the user free to attempt the upload since the server-side check on the upload handler is the real enforcement point.

Parameters: None.

Returns: void — redirects to /ql-login if not authenticated; otherwise leaves the form usable.

Endpoint: ql_check_auth

Success: If not logged in, redirects to /ql-login?redirect=<current path>.

handleSubmit(e) — new upload

Description: This is the primary ingestion path, invoked when the form is submitted and this.editMode is false. It first validates that a file has been selected and that a non-empty dataset name was typed into #dataset_name; either failure shows an inline error and aborts before any network call. Once validated, it builds a FormData from the raw form, tags it with action=ql_upload_dataset, then disables the upload button and switches its label to a spinner state so the user can’t double-submit while the request is in flight. The actual request uses a custom XHR so upload progress events can drive the progress bar, and delegates to separate success/error handlers to keep the transport logic separate from the outcome UI logic.

Parameters:

  • e (Event) — the form submit event.

Returns: void — validates that a file and dataset name are present, builds a FormData payload, and uploads it via XMLHttpRequest with progress tracking.

Endpoint: ql_upload_dataset (when editMode is false)

Success: Fires Google Ads conversion tag (gtag), then redirects to /ql-dashboard?uploaded=1&dataset_id=<id> after 2 seconds. Uses XHR upload progress events to update the progress bar.

handleSubmit(e) — edit mode

Description: This is the same physical handleSubmit function as the new-upload path, but it takes the opposite branch when this.editMode is true (set earlier by checkEditMode()/loadDatasetData()). Because a user editing a dataset may only want to rename it or change its visibility, the file-presence validation is skipped entirely — the CSV file is optional here, which is why the file input’s required attribute is stripped when entering edit mode. The FormData payload is tagged with action=ql_update_upload_dataset and additionally carries dataset_id so the backend knows which row to update rather than creating a new one; on success it redirects to /ql-dashboard?updated=1 instead of the uploaded=1 variant, and does not fire the Google Ads conversion tag, since that event is meant to track first-time uploads only.

Parameters:

  • e (Event) — the form submit event; same handler as above, branches internally on this.editMode.

Returns: void — the CSV file is optional in this branch, since only the metadata may be changing.

Endpoint: ql_update_upload_dataset (when editMode is true)

Success: Redirects to /ql-dashboard?updated=1 after 2 seconds.

loadDatasetData()

Description: Called by checkEditMode() as soon as a numeric ID is parsed out of the query string, this function fetches the dataset’s current record so the edit form isn’t blank. Before populating anything it defensively checks that the dataset’s source_type is actually "upload"; if the ID belongs to a connection-based or merged dataset instead, it shows an error explaining this page can’t edit that kind of dataset and bounces the user back to /ql-dashboard, preventing a confusing half-populated form. On a valid upload dataset it fills in the name and visibility fields directly from the response, then relabels the button, page heading, and drop-zone copy so the page visually communicates “you are editing,” not “you are uploading new.”

Parameters: None.

Returns: void — reads this.editDatasetId, set earlier by checkEditMode() from the URL.

Called automatically when a numeric dataset ID is detected in the URL query string (edit mode). Endpoint: ql_get_dataset_detail

Success: Verifies response.data.dataset.source_type === "upload"; if not, redirects to dashboard. Otherwise populates #dataset_name and the visibility <select>, then calls updateUIForEditMode(dataset) to relabel the page heading and make the file input optional.

Event Bindings

  • #file-drop-zone click — Lets the entire drop zone act as a large, forgiving click target for browsing files, rather than requiring the user to hit a small native “choose file” button. It checks that the click target isn’t the file input itself, so clicking directly on the (usually invisible/overlaid) input doesn’t trigger a double file-picker invocation; anywhere else in the zone forwards the click to the real input.
  • #csv_file change — Fires whenever the browser’s native file picker returns a selection, and is the single choke point both this handler and the drag-and-drop handler funnel through. It rejects anything not ending in .csv and anything over the hard-coded 50MB ceiling, clearing the input in both cases so a rejected file can’t be silently submitted; on success it writes an HTML-escaped summary (name, size, type) into #file-info and reveals it.
  • #file-drop-zone dragover — Triggered continuously while a dragged file hovers over the zone; prevents the browser’s default handling (which would otherwise try to open the file) and toggles on the drag-over class purely as a visual cue so the user gets immediate feedback that dropping here is a valid target.
  • #file-drop-zone dragleave — The mirror image of dragover, firing when the cursor (or dragged file) leaves the zone’s bounds; it removes the drag-over highlight so the affordance doesn’t stay lit once the drag target is no longer this element.
  • #file-drop-zone drop — Handles the actual file drop: it prevents the browser’s default (which would otherwise navigate to/open the file) and clears the drag-over styling, then reads the dropped file list and assigns it directly onto the real file input element, so the eventual form submission behaves exactly as if the user had picked the file manually. It then runs the same size/type validation used by the change handler.
  • #ql-upload-form submit — Bound once during bindEvents() and shared by both the new-upload and edit-mode flows, since both branch out of the same handleSubmit based on this.editMode. Submitting the form prevents the native page navigation, runs the file/name validation appropriate to the current mode, and — once validation passes — locks the UI down by disabling the submit button and revealing the progress bar.

QLDashboard

This module drives the main dashboard page, which is the hub a user lands on after logging in, after a successful upload from QLUpload, after connecting a new data source, or after most other cross-page actions redirect back with a status flag (?uploaded=1, ?deleted=1, ?synced=1, ?report_saved=1). It gives users a single, at-a-glance inventory of everything they’ve brought into QuantumLayers: profile info, a dataset count, a row-per-dataset list with status/row counts/visibility controls, and a separate scheduled-reports list. From each dataset row it links out to more specialized pages — QLDatasetDetail via “Details”, QLAnalytics and QLInsights via their own links, and back into QLUpload/connect pages via the edit button — making the dashboard the connective tissue between all the dataset-specific modules rather than a page that does deep analysis itself. It also owns dataset lifecycle actions that don’t belong on a detail page — deleting a dataset, toggling visibility, and resynchronizing it against its original source (upload, database/API, Google Sheets, or SFTP) — plus a lightweight scheduled-reports panel (run now, delete) that links out to QLReportScheduler for editing. Defined in jdk/dashboard.js and exposed globally as window.QLDashboard.

How to Include

This module requires jQuery and QLAuth (auth.js) loaded first, since virtually every function calls QLAuth.getSessionToken() for the bearer header. It has one conditional soft dependency: if window.QLConnect isn’t present, initGoogleAuth() dynamically injects the Google Identity Services script itself so that re-authenticating a Google Sheets connection still works from the dashboard. Aside from that self-loaded script, there is no other special external dependency (no Chart.js, no Paddle.js) required.

Callable Functions

loadDashboard()

Description: Called first from init(), this is the function that turns an empty page into a populated dashboard. It guards against a missing qlAuth object, redirecting to /ql-login if the auth script never loaded. Before the request even fires it swaps the dataset list for a spinner via showLoading(), so the user never sees a flash of empty content. On a successful response it delegates everything to updateDashboard(response.data); on an explicit failure response it redirects to /ql-login, treating a failed data fetch as effectively an expired session, while a raw network error is currently only logged to the console.

Parameters: None.

Returns: void — populates user info, stats, and the dataset table, then triggers loadScheduledReports().

Endpoint: ql_get_dashboard_data

Response: response.data contains user, datasets_count, and datasets[]. Populates user info, dataset count stats, and the dataset table. Also triggers loadScheduledReports().

handlePrivacyToggle(e)

Description: Bound to the change event of the per-row visibility dropdown, this lets a user reclassify a dataset as private, public, org-wide, or group-shared without leaving the dashboard. It immediately disables the select to prevent rapid repeated changes, then sends the new value to the server. On success it commits the new value as the select’s “previous” value so a later failed change can still roll back correctly; on failure it reverts the visible selection and surfaces the server’s error message via alert, ensuring the UI never silently displays a visibility setting that wasn’t actually persisted.

Parameters:

  • e (Event) — the change event on a .ql-privacy-toggle <select>.

Returns: void — reverts the select to its previous value if the request fails.

Endpoint: ql_set_dataset_visibility

Success: Disables the select during the request; reverts to old value on error.

handleDelete(e)

Description: Wired to the trash-can button on each dataset row; because that button lives inside a clickable row, the handler stops the click from bubbling into any row-level navigation. It requires an explicit confirmation dialog given the action is destructive and irreversible, and only after confirmation does it disable the button and relabel it “Deleting…” before making the request. On success it performs a full page redirect to the dashboard with a deleted flag rather than an in-place DOM removal, which also guarantees the dataset count stays consistent with the server; on failure it re-enables the button so the user can retry.

Parameters:

  • e (Event) — the click event.

Returns: void — shows a confirm() dialog first; redirects to ?deleted=1 on success.

Endpoint: ql_delete_dataset

Success: Redirects to ?deleted=1.

resyncConnection(datasetId, btn, accessToken, refreshToken)

Description: This is the generic re-sync path used for any dataset whose source type isn’t upload or SFTP — plain database/API connections call it directly, while Google Sheets connections route through it only after a valid (possibly freshly refreshed or re-authenticated) OAuth token has been secured. After a confirmation prompt, it disables the button and relabels it “Syncing…”, then sends the sync request, conditionally attaching access/refresh tokens only when they were supplied so plain DB/API connections send a leaner request. A successful sync triggers a full redirect with a synced flag; a failure re-enables the button and alerts the user with the server’s message or a generic fallback.

Parameters:

  • datasetId (int).
  • btn (jQuery element) — the sync button; its label is updated while the request is in flight.
  • accessToken (string, optional) — used to re-authenticate a Google Sheets connection.
  • refreshToken (string, optional).

Returns: void — shows a confirm() dialog first; redirects to ?synced=1 on success.

Endpoint: ql_resync_connection — re-fetches data from an external connection (e.g. Google Sheets, API source).

resyncSFTP(datasetId, btn)

Description: Invoked when a row’s source type is SFTP, this pulls the latest file directly from the configured SFTP server rather than requiring a manual re-upload. Like the other resync paths it gates on a confirmation dialog and disables/relabels the button during the request, but its error handling is slightly more defensive, extracting a server-supplied error message even from a non-200 response where possible. On success it redirects with a synced flag, identical to the other sync flows, keeping the post-sync UX consistent regardless of source type.

Parameters:

  • datasetId (int).
  • btn (jQuery element).

Returns: void — shows a confirm() dialog first.

Endpoint: ql_sync_sftp_dataset

handleSyncUpload(e)

Description: Fired when the user clicks “Upload & Sync” inside the modal opened for an upload-type dataset row. It reads the file directly from the raw file input and shows an alert if no file was actually chosen, guarding against the case where the modal’s upload button was somehow enabled without a selection. Assuming a file is present, it disables the button, relabels it “Uploading…”, and constructs a multipart form payload carrying the dataset ID and the file itself. On success it closes the modal and redirects with a synced flag; on failure it re-enables the button and shows the server’s error message, leaving the modal open so the user can retry.

Parameters:

  • e (Event) — the click event on the modal’s upload button.

Returns: void — reads the selected file from #sync-file-input and submits it as FormData; closes the modal and redirects on success.

Endpoint: ql_resync_upload_dataset — replaces the CSV data for an upload-type dataset via a modal file picker. Uses FormData.

loadScheduledReports()

Description: Called at the end of updating the dashboard (and again after a report is deleted) to populate the separate scheduled-reports panel independently of the main dataset list, since reports have a materially different data shape and lifecycle than datasets. On success it hands the array off to a renderer for display; on either an explicit failure or a network error it only logs to the console, leaving the reports panel in whatever state it was previously in rather than surfacing a user-facing error, a softer failure mode appropriate to a secondary feature of the page.

Parameters: None.

Returns: void.

Endpoint: ql_get_scheduled_reports

Response: response.data.reports[]. Populates the scheduled reports section of the dashboard.

handleRunReportNow(e)

Description: Bound to each report row’s “Run” button, this lets a user force an out-of-cycle send of a report to all its configured recipients rather than waiting for its next scheduled run. It requires confirmation since it has a real side effect (recipients receive an email/report immediately), then disables the button and shows “Running…” while the request executes, since report generation likely involves querying datasets and formatting output server-side. On completion it always restores the button and communicates the outcome via alert, since there’s no persistent state change to reflect in the row itself until the page or reports list is reloaded.

Parameters:

  • e (Event).

Returns: void — shows a confirm() dialog first.

Endpoint: ql_run_report_now

handleDeleteReport(e)

Description: Bound to each report row’s delete button, this permanently removes a scheduled report definition after a confirmation dialog. Unlike dataset deletion, which does a full page redirect, this success path shows an inline success banner and re-fetches just the reports list in place — a lighter-weight update appropriate since deleting a report doesn’t affect the rest of the dashboard’s data. On failure it surfaces the server’s error message via alert and leaves the list untouched.

Parameters:

  • e (Event).

Returns: void — shows a confirm() dialog first; reloads the report list on success.

Endpoint: ql_delete_scheduled_report

Success: Reloads the scheduled reports list.

handleLogout(e)

Description: Bound to the dashboard’s logout control, this ends the user’s session so sign-out is available directly from the hub page alongside other account actions. It first confirms intent, then calls the sign-out endpoint; notably this handler unconditionally redirects to the login page whether the request succeeds or errors out, ensuring the user is never left on the dashboard after choosing to log out even if the sign-out request itself fails.

Parameters:

  • e (Event).

Returns: void — shows a confirm() dialog first.

Endpoint: ql_user_signout

Success: Clears session token and redirects to /ql-login.

Event Bindings

  • .ql-logout-btn click — Delegated so it keeps working even if the logout button is re-rendered dynamically; clicking it hands off entirely to handleLogout(), which gates the actual sign-out behind a confirmation dialog before hitting the sign-out endpoint and redirecting to the login page.
  • .ql-btn-delete click — Bound with delegation since dataset rows are rendered dynamically after the AJAX dashboard load. It stops the click from bubbling before delegating to handleDelete(e), which reads the dataset ID off the button’s data-id attribute and shows a native confirmation dialog; only on confirmation does it disable the button, show “Deleting…”, and fire the delete request, redirecting the whole page on success.
  • .ql-btn-sync click — Delegated for the same dynamically-rendered-rows reason as the delete button; it stops the click from bubbling and branches four ways on the row’s source type: upload datasets open the in-page sync modal so the user can pick a replacement CSV; Google Sheets datasets go through a token-aware resync path that may transparently reuse a cached token, silently refresh an expired one, or prompt a full Google re-authentication popup; SFTP datasets call the SFTP resync directly; and any other type (plain database/API connections) calls the generic resync directly with no tokens.
  • .ql-privacy-toggle change — Delegated on the dynamically rendered visibility select in each dataset row; the handler stops event propagation so the change doesn’t trigger any row-level click behavior, then forwards to handlePrivacyToggle(e), which disables the select, persists the newly chosen visibility, and reverts the selection with an alert if the server rejects the change.
  • .ql-modal-close click / .ql-modal-btn-cancel click — Both the header’s close button and the footer’s cancel button in the sync modal are wired to the same close call, giving the user two equivalent ways to back out of a sync without uploading a file; closing hides the modal via CSS and clears the tracked syncing dataset ID so a stale ID can’t leak into a later action.
  • .ql-modal-overlay click — Bound on the full-screen overlay behind the modal dialog; because the modal content is a child of the overlay and clicks bubble up, the handler checks that the click landed on the overlay itself (not the dialog box inside it) before closing — this is what gives users the familiar “click outside to dismiss” affordance without accidentally closing the modal when clicking inside it.
  • #sync-file-input change — Fires when the user picks a file in the sync modal’s file input; it displays the chosen file’s name and enables the “Upload & Sync” button, which starts out disabled specifically to prevent submitting with no file attached. If the selection is cleared, it hides the label again and re-disables the upload button.
  • .ql-modal-btn-upload click — Bound to the modal’s confirm/upload button, this is the trigger for actually submitting the replacement CSV chosen via the file input above; it delegates straight to handleSyncUpload(), which packages the file into a multipart form payload and posts it, closing the modal and redirecting on success.
  • .ql-run-report-btn click — Delegated because the scheduled-reports list, like the dataset list, is rendered dynamically from an AJAX response; clicking it reads the report ID off the button and forwards to handleRunReportNow(), which confirms with the user before immediately triggering generation and delivery of that report outside its normal schedule.
  • .ql-delete-report-btn click — Also delegated for the same dynamic-rendering reason; triggers handleDeleteReport(), which confirms the destructive action, deletes the report server-side, and then refreshes just the scheduled-reports panel in place rather than reloading the whole dashboard.

QLDatasetDetail

This module powers the single-dataset detail page, which a user typically reaches by clicking “Details” on a dataset row in QLDashboard; the dataset’s numeric ID is passed in the URL query string (e.g. /ql-dataset?42) rather than as a named parameter. It solves the problem of letting a user inspect the shape and quality of an ingested dataset before committing to building charts or running AI insights on it: it surfaces dataset-level metadata (name, status, row count, source type, creation date) alongside a per-column statistics grid showing inferred type, distinct/null counts, min/max/mean/standard deviation for numeric columns, and a handful of sample values for a quick sanity check of the data. Compared to QLDashboard, which only lists datasets at a glance, and QLAnalytics/QLInsights, which build visualizations and AI-generated narratives on top of the data, this page sits in between as a lightweight schema/quality inspector. It also exposes the same delete action available from the dashboard, letting a user remove a dataset from within its own detail view. Defined in jdk/dataset-detail.js and exposed globally as window.QLDatasetDetail.

How to Include

This module requires jQuery and QLAuth (auth.js) loaded first, since both of its AJAX calls call QLAuth.getSessionToken() for the bearer header. Unlike QLUpload and QLDashboard, it does not perform an explicit auth-missing redirect — if the auth script fails to load, requests will simply error out. It has no other special external dependency (no Chart.js, no Paddle.js) despite rendering statistical data, since all rendering here is plain HTML string building rather than charting.

Callable Functions

loadDataset()

Description: Called from init() on every page load and again whenever the filter buttons are clicked, this is the sole data-fetching function for the page — everything the user sees is derived from its response. It reads this.datasetId, parsed out of the URL query string during initialization. On a successful response it hands the full payload to a renderer to populate the header stats and column cards; on an application-level failure it shows the server’s error message (or a generic fallback), and a raw network error triggers the same error path, replacing the whole page body with an error state and a “Back to Dashboard” link rather than leaving stale or partial content on screen.

Parameters: None — reads this.datasetId, set from the URL during init().

Returns: void — calls renderDataset(response.data) on success or showError() on failure.

Endpoint: ql_get_dataset_detail

POST params:

Response: response.data.dataset (name, status, row_count, source_type, created_at) and response.data.columns[] (each with col_name, inferred_type, distinct_count, null_count, min_val, max_val, mean_val, stddev_val, sample_values). Renders the header stats and a card grid of columns.

handleFilter(e)

Description: Bound to clicks on any filter button, this reads the clicked button’s filter value into this.currentFilter and updates which button carries the active CSS class so the UI reflects the current selection. However, this is effectively a UI-only toggle at present: it calls loadDataset() again immediately, but that function’s AJAX payload never actually includes currentFilter, so the re-fetch returns the exact same unfiltered column set from the server every time. This is worth flagging to anyone extending the page, since the intent (filtering columns by some status) is visible in the code structure but not actually wired through to the backend request.

Parameters:

  • e (Event) — the click event; the clicked button’s data-filter attribute is read to set this.currentFilter.

Returns: void — toggles the active CSS class on the clicked button, then calls loadDataset(). Makes no AJAX call of its own.

handleDelete(e)

Description: Bound to the page’s delete control, this mirrors QLDashboard‘s delete behavior but operates on the dataset currently being viewed rather than a row in a list. It requires a confirmation dialog before proceeding, since the action is irreversible, then sends the current dataset ID to the delete endpoint. On success it performs a full-page redirect back to the dashboard, since there’s nothing left to display on a detail page for a dataset that no longer exists; on failure it surfaces the server’s error message via alert and leaves the page as-is so the user can retry.

Parameters:

  • e (Event) — the click event.

Returns: void — shows a confirm() dialog first; redirects to /ql-dashboard?deleted=1 on success, shows an alert() on failure.

Endpoint: ql_delete_dataset

POST params:

Event Bindings

  • .ql-filter-btn click — Delegated so it applies even if the filter buttons are re-rendered; clicking any filter button hands off to handleFilter(e), which visually marks that button active and triggers a fresh loadDataset() call — though the selected filter value is not currently passed through to the server, so the visible column list doesn’t actually change based on which filter is active.
  • .ql-delete-dataset click — Delegated the same way, this is the page’s single destructive action; clicking it routes to handleDelete(e), which confirms with the user, deletes the dataset, and redirects back to the dashboard on success or shows an alert on failure.

QLAnalytics

The QLAnalytics module powers the interactive chart builder and statistical analysis workbench that end users see on a dataset’s analytics page: it lets a user pick a chart type and column mapping and get back a live Chart.js visualization, run one-click statistical tests (correlation, PCA, ANOVA, descriptive statistics), save chart configurations for later, and browse AI-recommended charts. It supports 14 distinct chart types — histogram, time_series, stacked_area, scatter, pie, doughnut, regression, bar, horizontal_bar, violin, box_plot, bubble, matrix, and heatmap — each with its own column-mapping requirements and its own parameter shape sent to the shared ql_get_chart_data endpoint. QLDatasetDetail links users into the analytics page for a given dataset, and once there, QLAnalytics.init() reads the dataset ID from the URL and wires up all the chart controls. QLInsights (the AI narrative-insights panel) does not duplicate chart-rendering logic — instead it calls window.QLAnalytics.renderChart() directly to draw the same Chart.js visualizations inside its own generated insight cards, so a bug fix or visual change to renderChart() affects both modules. Similarly, QLAgent calls into this module’s chart-rendering machinery when it wants to show the user a chart as part of a conversational response. Because of this shared-rendering design, QLAnalytics is effectively the single source of truth for how a chart gets drawn across the whole plugin. Defined in jdk/analytics.js and exposed globally as window.QLAnalytics.

How to Include

This module has a heavier dependency chain than the others. It requires jQuery and QLAuth (auth.js) loaded first, exactly like every other frontend module. In addition, because chart and statistical-analysis rendering all call new Chart(...) directly, and several chart types (matrix/heatmap, box_plot/violin) map onto Chart.js types that are not part of core Chart.js, this file requires Chart.js 4.4.0 plus three plugins to be present on the page before analytics.js runs: a boxplot/violin plugin, a date-fns time-scale adapter (for time_series/stacked_area axes), and a matrix/heatmap plugin. If any of these are missing or loaded after analytics.js, chart rendering for the affected types will fail or render incorrectly.

Callable Functions

loadDatasetDetails()

Description: This is the first data call the module makes on page load, invoked from init() right after bindEvents(). It reads the dataset ID out of the URL query string, stores it on this.currentDatasetId, and requests the dataset’s column list and row count. On success it calls populateColumnSelectors(), which classifies every column as numeric, categorical, or date/datetime — using a distinctness threshold relative to row count to decide whether a low-cardinality numeric column should be treated as categorical — and fans that classification out into all the different selector dropdowns (X, Y, string, date, filter, Z, color). If the AJAX call fails or the response lacks columns, it only logs to the console and leaves the selectors empty; there is no user-facing error state for this failure.

Parameters: None.

Returns: void — populates all chart control dropdowns and calls loadInitialCharts().

Endpoint: ql_get_dataset_detail

Response: response.data.columns[] and response.data.row_count. Populates all column selector dropdowns and calls loadInitialCharts().

loadChart(containerId, chartType, xcolumnName, ycolumnName, zcolumnName, colorName)

Description: This is the central data-fetching function for every chart on the page, called both in response to control changes and directly for the fixed diagnostic charts (column_types, missing_data). It normalizes the X/Y column arguments into arrays when they arrive as arrays (for regression’s multi-column X or time-series/bar’s multi-column Y), reads the currently selected category and date-range filter values, and branches on chart type in a large switch statement to build a chart-type-specific payload — for example histogram sends column_name, scatter sends x_column/y_column, time_series/stacked_area add aggregation, interval, and a cumulative flag, bubble adds size_column/color_column, and matrix/heatmap send x_column/y_column/z_column plus aggregation. While the request is in flight it shows a loading placeholder in the target container; on success it draws the Chart.js instance, and for the custom-chart slot it also attaches the save/copy-link buttons; on failure it replaces the container’s content with an error message.

Parameters:

  • containerId (string) — the DOM element ID to render the chart into.
  • chartType (string) — one of the 14 supported chart types.
  • xcolumnName (string|array, optional, default null) — X-axis/category column(s).
  • ycolumnName (string|array, optional, default null) — Y-axis/value column(s).
  • zcolumnName (string, optional, default null) — Z-axis or size column (bubble/matrix charts).
  • colorName (string, optional, default null) — color-dimension column (bubble charts).

Returns: void — renders a Chart.js instance into containerId.

Endpoint: ql_get_chart_data

Common params present for all chart types:

Additional params by chart type:

showCorrelationMatrix()

Description: Bound to the correlation button, this opens a modal, immediately shows a loading state inside it, and requests correlation data for the dataset. On success it renders an HTML table of pairwise Pearson coefficients color-coded from red (negative) to blue (positive), then additionally draws a bubble-chart heatmap of the full matrix plus a bar chart of the top 10 strongest correlations, excluding near-perfect ones assumed to be trivial self-correlations or duplicates. If the backend returns an error, or the request itself fails, the modal content is replaced with an error message instead of the table.

Parameters: None.

Returns: void — opens a modal showing both a numeric table and a heatmap chart.

Endpoint: ql_get_correlation_matrix

Response: response.data.columns[] and response.data.matrix[][]. Renders in a modal with both a numeric table and a heatmap chart.

showPCAAnalysis()

Description: Bound to the PCA button, this opens a modal with a loading message and requests a 3-component PCA decomposition. On success it builds a summary of explained variance with a running cumulative percentage, a component-loadings table showing each feature’s weight on each principal component, and a scree plot of variance explained per component. If two or more components and transformed per-row coordinates are returned, it also plots the first two components as a scatter chart, sampling down to at most 500 points for performance. A backend error or request failure replaces the modal body with an error message instead of results.

Parameters: None.

Returns: void — opens a modal showing explained variance, a scree plot, and a scatter plot of the first two components.

Endpoint: ql_get_pca_analysis

Response: response.data has n_components, explained_variance[], explained_variance_ratio[], components[][], feature_names[], transformed_data[][].

showAnovaAnalysis()

Description: Bound to the ANOVA button, this opens a modal with a loading message and requests ANOVA results for every categorical/numerical column pair. On success it builds a significance-level legend, a compact matrix table where each cell shows confidence percentage and a significance badge derived from the p-value, plus an expanded per-category detail table showing F-statistic, p-value, group count, and degrees of freedom for every pair. It also draws a bubble-chart visualization of confidence levels across the same matrix. Cells where ANOVA could not be computed (e.g. insufficient groups) render an error badge instead of statistics, and a backend error or request failure again swaps in an error message.

Parameters: None.

Returns: void — opens a modal showing a confidence-level heatmap and detailed result tables.

Endpoint: ql_get_anova_analysis

Response: response.data has categorical_columns[], numerical_columns[], anova_matrix{}{}, sample_size.

showStatisticalSummary()

Description: Bound to the statistics button, this opens a modal with a loading message and requests statistics for every column. On success it builds a single wide table with one row per column, adapting its fields depending on whether the column is numeric (mean, standard deviation) or categorical/string (unique count, most frequent value); numeric-only fields like min, quartiles, median, and max fall back to “N/A” when not applicable. This is the simplest of the four analysis modals — it renders only a table, no chart — and like the others, a backend error or request failure replaces the modal content with an error message.

Parameters: None.

Returns: void — opens a modal with the summary table.

Endpoint: ql_get_statistical_summary

Response: response.data is an array of per-column stat objects (min, max, mean, median, std, etc.).

loadSavedCharts()

Description: Called on page init and again after every successful save or delete, this guards against running before a dataset ID is known and requests the user’s saved charts. On success it clears and rebuilds the saved-charts grid, creating one card per saved chart with a title, a delete button, and a chart container, caching each chart object by ID so click handlers can look it up later without re-fetching. Each card’s chart is then re-fetched and rendered individually; a top-level request failure only logs to the console, though individual chart loads that fail do show their own error message.

Parameters: None.

Returns: void — renders them into the saved-charts panel.

Endpoint: ql_get_saved_charts

Response: response.data.charts[]. Renders each saved chart in the saved charts panel.

handleSaveChart(e)

Description: Bound (via delegation) to clicks on the save button, this looks up the matching chart parameters for the container the button belongs to, tracked earlier when the chart was last loaded; if no params are found it shows a plain alert and stops. It then prompts the user for an optional chart name and posts the chart type and full parameter payload to the save endpoint. On success it refreshes the saved-charts panel with the new entry; on failure it surfaces either the backend’s error message or a generic fallback via alert. This function only operates on charts built through the custom chart builder, since that is the only slot with tracked parameters.

Parameters:

  • e (Event).

Returns: void — calls prompt() for an optional chart name before submitting; reloads the saved-charts list on success.

Endpoint: ql_save_chart

handleDeleteSavedChart(e)

Description: Bound (via delegation) to clicks on a saved chart’s delete button, this reads the chart’s ID and immediately shows a confirmation dialog; if the user cancels, no request is made. On confirmation it posts the chart ID to the delete endpoint, and on success refreshes the saved-charts panel to reflect the removal rather than manually removing the DOM node. On failure it shows an alert with either the backend’s error message or a generic fallback.

Parameters:

  • e (Event).

Returns: void — shows a confirm() dialog first.

Endpoint: ql_delete_saved_chart

createRecommendedCharts()

Description: Called during page init, this first checks for the recommended-charts container and silently no-ops if it isn’t present, making it safe to call on pages that don’t have that panel. It resolves the dataset ID and requests up to 20 AI-recommended charts. On success it builds a grid where each recommended chart gets a card with a reason/title, an insight-score badge, and a canvas, caching each chart object by index so a click handler can retrieve the full definition later, then draws each recommendation inline immediately. If zero charts are returned it shows a “no high-insight charts found” message instead of an empty grid; if the request fails it shows an error message with whatever detail the backend provided.

Parameters: None.

Returns: void — renders up to 20 clickable chart cards; no-ops silently if the #recommended-charts container is not present on the page.

Endpoint: ql_get_recommended_charts

Response: response.data.charts[] — each item has reason, insight_score, chart_data, params, chart_type.

Event Bindings

  • .ql-chart-selector change — Fires when the value of a chart-selector dropdown changes, which updates the current chart-type visibility and re-runs the chart request against the same container with the newly selected configuration.
  • .ql-chart-type-selector change — Fires when the primary chart-type dropdown changes (used by the custom chart builder). It toggles which of the roughly ten different column/aggregation/cumulative controls are visible for the newly chosen chart type, then re-issues the chart request — effectively a full chart type switch with a full redraw.
  • .ql-x-column-selector change — Fires when the numeric X-axis dropdown changes (used by histogram, scatter, and bubble), which re-reads all relevant selector values and re-issues the chart request with the updated X column.
  • .ql-reg-x-columns-selector change — Fires when the multi-select backing the regression X-columns picker changes, which collects the array of selected column names, updates the multi-select trigger button’s label/count badge, and re-issues the chart request with the new x_columns array.
  • .ql-y-column-selector change — Fires when the single numeric Y-axis dropdown changes (used by scatter, pie/doughnut, regression, box_plot/violin, and bubble), which reads the current chart type to decide what the Y value represents and re-issues the chart request.
  • .ql-multi-y-column-selector change — Fires when the multi-select Y-columns control changes — used by time_series/stacked_area (as value columns) and bar/horizontal_bar (as aggregated value columns) — which updates the trigger button’s label and re-issues the chart request with the new array of Y columns.
  • .ql-date-column-selector change — Fires when the date/datetime column dropdown changes, relevant only to time_series and stacked_area chart types where it supplies the time column parameter; requires both a date column and at least one Y column to be selected before the chart request fires.
  • .ql-string-column-selector change — Fires when the categorical/string column dropdown changes, used as the category axis for pie/doughnut, bar/horizontal_bar, and as the categorical X-axis for box_plot/violin; the chart request re-issues once both this and the matching Y selector are populated.
  • .ql-z-column-selector change — Fires when the Z-axis dropdown changes; for bubble charts this supplies the size column (bubble radius), and for matrix/heatmap charts it supplies the aggregated numeric value visualized as color intensity. The chart re-draws once the chart type’s required X/Y columns are also present.
  • .ql-color-selector change — Fires when the color-dimension dropdown changes, used only by bubble charts to set a categorical column used to color-code bubble points; passed through directly to the chart request.
  • .ql-filter-column-selector change — Fires when the shared category-filter column dropdown changes — this control is independent of chart type and applies to nearly every chart’s category filter parameters. Changing it re-draws the active chart against the new filter column, and populating the matching filter-value options happens as part of the broader column-selector population flow.
  • .ql-filter-value change — Fires when the user picks a specific value to filter by, populated from the selected filter column’s distinct values; the chosen value is included in every chart type’s request payload, redrawing the active chart restricted to matching rows.
  • .ql-filter-date-column-selector change — Fires when the date-filter column dropdown changes, determining which date/datetime column the from/to bounds apply against; the chart re-issues so the new date filter column is sent alongside whatever from/to values are currently set.
  • .ql-filter-date-from-input change — Fires when the “From” preset dropdown changes. It first shows or hides the custom date input (visible only when the selected value is “custom”), then redraws the chart with the new from-date filter applied.
  • .ql-filter-date-to-input change — Fires when the “To” preset dropdown changes, mirroring the “From” handler: it toggles the custom date-to input’s visibility, then redraws the current chart with the updated to-date value.
  • .ql-filter-date-from-custom change — Fires when the user picks a specific date in the custom “From” date input, which only becomes visible after selecting “custom” in the preset dropdown; this field’s value is read as the from bound whenever the preset selector is set to custom.
  • .ql-filter-date-to-custom change — Fires when the user picks a specific date in the custom “To” date input (visible only when the preset is set to custom); read as the to bound for the date range filter before the chart is redrawn.
  • .ql-aggregation-selector change — Fires when the aggregation-method dropdown changes, relevant to time_series/stacked_area, bar/horizontal_bar, and matrix/heatmap chart types, all of which send an aggregation parameter (defaulting to mean if the selector is absent); the aggregation selector’s value is read fresh each time the chart request is built.
  • .ql-cumul-selector change — Fires when the cumulative checkbox is toggled, relevant only to time_series/stacked_area charts, which read the checkbox state to set a cumulative flag in the request payload, redrawing the chart as a running cumulative total or raw per-interval values.
  • #ql-correlation-btn click — Fires when the “Correlation Matrix” analysis button is clicked, directly invoking showCorrelationMatrix(), which opens a modal, shows a loading state, and fetches Pearson correlations for all numeric columns, ultimately rendering both a colored coefficient table and a bubble-chart heatmap plus a top-10 correlations bar chart inside the modal.
  • #ql-pca-btn click — Fires when the “PCA” analysis button is clicked, invoking showPCAAnalysis(), which opens a modal, shows a loading state, and requests a 3-component PCA decomposition, rendering explained-variance tables, a scree plot, a component-loadings table, and (when at least 2 components are returned) a 2D scatter plot of the transformed data.
  • #ql-anova-btn click — Fires when the “ANOVA” analysis button is clicked, invoking showAnovaAnalysis(), which opens a modal, shows a loading state, and requests one-way ANOVA results for every categorical-versus-numerical column pair, rendering a significance-coded matrix table, detailed per-pair statistics, and a confidence-level bubble-chart heatmap.
  • #ql-stats-btn click — Fires when the “Statistics” button is clicked, invoking showStatisticalSummary(), which opens a modal, shows a loading state, and requests descriptive statistics for every column, rendering a single table whose columns adapt based on whether each row represents a numeric or categorical column.
  • .ql-multiselect-trigger click — Fires when a multi-select trigger button (used for the regression X-columns and time-series/bar Y-columns pickers) is clicked, revealing the underlying multi-select list for the user to choose columns from.
  • .ql-multiselect-modal-close click / .ql-multiselect-modal-backdrop click — Fires when either the modal’s close button or its backdrop is clicked, both dismissing the picker without explicitly re-triggering a chart redraw — any selections already made via the underlying change events, however, persist.
  • .ql-multiselect-done click — Fires when the “Done” button inside a multi-select modal is clicked, closing the modal; the actual column-selection state was already captured earlier by the underlying change handlers, so this click primarily just dismisses the modal.
  • .ql-save-chart-btn click (delegated) — Fires when a save (floppy-disk icon) button — dynamically added to the custom-chart container after a chart loads — is clicked; delegated on the document since the button doesn’t exist at page-load time. It invokes handleSaveChart(), which prompts for an optional chart name and posts the current chart’s type and parameters, refreshing the saved-charts panel on success.
  • .ql-copy-link-btn click (delegated) — Fires when the link (chain icon) button next to the save button is clicked, also delegated on the document; it builds a shareable chart URL by serializing the current chart’s parameters into query parameters, then copies it via the Clipboard API (falling back to a hidden textarea for older browsers) and briefly swaps the button’s icon to a checkmark as feedback.
  • .ql-delete-saved-chart-btn click (delegated) — Fires when a saved chart’s delete button is clicked, delegated on the document since saved-chart cards are rendered dynamically; it shows a confirmation dialog and, if accepted, deletes the chart and then re-fetches and re-renders the whole saved-charts grid without the deleted entry.
  • .ql-saved-chart-wrapper click (delegated) — Fires when a user clicks anywhere on a saved-chart card (excluding the delete button, which is explicitly excluded), delegated on the document; it looks up the cached chart object and loads that chart’s parameters into the custom-chart container, updates the chart-type selector and all relevant column selectors to match, re-fetches and re-renders the chart, and re-attaches the save/copy-link buttons.
  • .ql-recommended-chart-wrapper click (delegated) — Fires when a user clicks a recommended-chart card, delegated on the document; it looks up the cached recommendation, reshapes it into a saved-chart-like object, and passes it to the same loader used by saved charts — updating the chart-type/column selectors, redrawing into the custom-chart container, and adding the save button — before smoothly scrolling the page so the custom-chart container is centered in view.

QLInsights

QLInsights powers the AI-narrated insights panel that end users see on a dataset’s detail page: it lets a user pick which columns and date/category filters to analyze, sends that configuration to the backend, and renders the Claude-generated narrative back as a mix of holistic analysis and individual insight cards (correlations, trends, outliers, ANOVA results, and so on). It solves the problem of making raw statistical output approachable, turning backend analysis into readable prose with severity/importance badges and “View Chart” actions. Structurally it departs from the rest of the codebase’s object-literal-plus-IIFE convention: it’s an ES6 class instantiated once as const insightsPanel = new QLInsights(), with all state held as instance properties rather than in a shared literal. It is tightly coupled to QLAnalytics: whenever the AI narrative embeds a chart block, QLInsights fetches chart data and hands it to window.QLAnalytics.renderChart() for in-page rendering, so it cannot render embedded charts correctly unless analytics.js has already executed and registered window.QLAnalytics. Defined in jdk/insights.js, instantiated as const insightsPanel = new QLInsights(), and not exposed on window.

How to Include

QLInsights requires jQuery and QLAuth (auth.js) loaded first. In addition, because renderInsightCharts() hands off rendering to window.QLAnalytics.renderChart(), Chart.js 4.4.0 and its three plugins must be loaded, followed by analytics.js itself, all before insights.js executes — otherwise embedded charts fall back to a “Chart unavailable” placeholder. The correct load order is: jQuery, auth.js, Chart.js + plugins, analytics.js, then insights.js, since insights.js instantiates QLInsights immediately on load and begins wiring DOM listeners and fetching columns right away.

Callable Functions

loadAvailableColumns()

Description: Called once from the constructor via initControls()initColumnSelector(), this async method fetches the dataset’s columns using the dataset ID parsed from the URL query string. On success it rebuilds the column-selector option list from scratch; any thrown error is only logged to the console, with no user-facing error surfaced. It runs independently of loadInsights() and only feeds the column multi-select UI, not the insights themselves.

Parameters: None.

Returns: Promise<void> — declared async; awaits the response before calling populateColumnSelector().

Endpoint: ql_get_dataset_detail

Response: Populates #ql-insights-column-selector from response.data.columns[].

loadInsights()

Description: Triggered by the “Load Insights” button click, this async method first blanks the container with a loading spinner, then assembles a request by reading several optional DOM controls: the category filter column/value pair, resolved date filter values (either preset or custom from/to dates), a clamped max-insights integer (default 30), and a trimmed custom prompt. Each optional field is only added to the payload if present, keeping the request minimal. On success it renders the returned insights; on a failure or thrown/network error it shows an error banner instead of insight cards.

Parameters: None.

Returns: Promise<void> — declared async; reads filter, date-filter, max-insights, and custom-prompt values directly from the DOM before the request.

Endpoint: ql_get_insights

Response: response.data.insights[] — each item has type, title, message, recommendation, importance, severity, chart_suggestion, ai_analysis. After rendering, calls renderInsightCharts(container).

renderInsightCharts(container)

Description: Called right after the holistic analysis body’s HTML is set from markdown rendering, this method queries the container for every chart-embed placeholder left behind by markdown processing. For each embed it JSON-parses the embedded chart parameters (catching malformed JSON and showing an “Invalid chart config” message), assigns the embed a synthetic ID if it lacks one, and fetches chart data for those params. On success, if window.QLAnalytics.renderChart exists it delegates rendering to it by embed ID; if that global isn’t available or the response lacks data, it falls back to a “Chart unavailable” message — making this function’s correctness fully dependent on analytics.js having loaded first.

Parameters:

  • container (HTMLElement) — the DOM subtree to scan for .ql-insight-chart-embed elements.

Returns: void — delegates the actual Chart.js rendering to window.QLAnalytics.renderChart().

Endpoint: ql_get_chart_data

Scans the rendered insights HTML for .ql-insight-chart-embed elements. For each one, reads its data-params attribute and fires a chart data request. Delegates actual Chart.js rendering to window.QLAnalytics.renderChart(). This mirrors the ql-chart fenced block pattern used by the AI agent.

Event Bindings

  • #ql-load-insights-btn click — Bound via a plain addEventListener('click', ...) on the raw DOM element (not jQuery delegation), this fires loadInsights() directly, which itself blanks the container with an “Analyzing data…” message before the AJAX round trip begins.
  • .ql-filter-date-column-selector change — This listener invokes a no-op placeholder handler; the actual effect of changing the date column selector is deferred entirely until loadInsights() is next called and re-reads the live DOM, so the binding exists mainly to keep the event-wiring symmetric with the other filter controls.
  • .ql-filter-date-from-input change — Reads the selected preset value (e.g. “1 month”, “custom”) and shows or hides the adjacent custom date input accordingly; the chosen value itself is only consumed later when insights are actually requested.
  • .ql-filter-date-to-input change — The symmetric counterpart to the “from” handler: it toggles the custom “to” date input’s visibility based on whether “custom” was chosen.
  • .ql-filter-date-from-custom change — Bound to a no-op handler; the custom date value entered here is only read on demand later, when loadInsights() builds its request payload — this binding exists purely so future logic has a hook point.
  • .ql-filter-date-to-custom change — Also bound to a no-op handler; like its “from” counterpart, the actual custom “to” date is picked up lazily at request time rather than reacting immediately to this change event.
  • .ql-insights-column-trigger click — Prevents the default action and adds a visibility class to the column-selection modal, making it visible without touching any selection state.
  • .ql-insights-modal-close click / .ql-insights-modal-backdrop click — Both selectors close the modal by removing its visibility class, hiding it without applying or discarding any pending column selection.
  • .ql-insights-columns-done click — Updates the trigger button’s label/count based on the current selection state (showing “all Columns” when empty, the single column name when one is picked, or the first name plus an ellipsis when multiple), then closes the modal — effectively treating the “Done” click as a commit-and-close action.
  • #ql-insights-column-selector change — Reads the selected options off the multi-select into the tracked column selection and immediately updates the trigger button text to reflect the live selection even before the “Done” button is clicked.

QLMergeDatasets

QLMergeDatasets drives the dataset-merge builder page, letting a user combine two or more of their existing uploaded datasets into a single virtual “merged dataset” by selecting join columns and join types (inner/left/right/outer) between pairs, similarly to configuring SQL joins through a checkbox-and-dropdown UI instead of writing queries. Once a merge is created, the resulting merged dataset can be treated like any other dataset elsewhere in QuantumLayers — fed into QLInsights for AI narrative analysis, charted via QLAnalytics, or scheduled into a report via QLReportScheduler. Unlike the ES6-class QLInsights, this module keeps the standard object-literal pattern but diverges from the usual bare-IIFE wrapper: the whole module, including its init() call and the window.QLMergeDatasets export, is wrapped in jQuery(document).ready(...), so none of its code runs until the DOM is fully parsed. It supports the full lifecycle of a merged dataset — creating one, loading an existing one for editing, viewing a read-only join-flow diagram, and deleting one entirely. Defined in jdk/merge-datasets.js and exposed globally as window.QLMergeDatasets.

How to Include

QLMergeDatasets needs jQuery and QLAuth (auth.js) loaded beforehand, since it is defined inside a jQuery(document).ready() callback and every AJAX call authenticates with QLAuth.getSessionToken(). It has no other external dependencies (no Chart.js, no analytics.js) because it only manipulates form controls and modal HTML, never renders charts. Initialization is gated on the presence of the merge container element, so the script can safely be included on pages without that container with no side effects.

Callable Functions

loadAvailableDatasets()

Description: Called both on page load and from the refresh button’s click handler, this first toggles a loading spinner, then requests the available dataset list. On success it stores the returned array, rebuilds the checkbox list from it, and calls the edit-mode check afterward, since edit-mode detection depends on the dataset checkboxes already existing in the DOM. The request’s completion handler always re-shows the dataset list and hides the spinner regardless of success or failure.

Parameters: None.

Returns: void — renders the dataset checklist, then calls checkEditMode().

Endpoint: ql_get_available_datasets

Response: response.data.datasets[] — each has id, name, column_count, row_count. Renders the dataset checklist.

loadMergedDataset()

Description: Invoked once edit mode has been detected and a dataset ID parsed from the URL, this fetches the existing merged dataset’s configuration. On success it populates the merge-name field, relabels the submit button to “Update Merged Dataset”, and derives the full set of involved dataset IDs from the returned joins (deduplicating both the left and right side of each join). It then checks each corresponding checkbox programmatically and, once every checkbox has been ticked, schedules re-applying the stored join parameters after a short delay to let the column-loading requests triggered by those checkbox changes settle first.

Parameters: None.

Returns: void — reads this.editDatasetId, set earlier by checkEditMode(); selects the involved dataset checkboxes and re-applies the stored join parameters.

Endpoint: ql_get_merged_dataset — loads an existing merged dataset for display or editing.

Response: response.data has merged_dataset{name} and joins[] each with left_dataset_id, right_dataset_id, join_column, join_type.

loadDatasetColumns(datasetId)

Description: Triggered whenever a dataset checkbox is checked, this fetches that dataset’s columns. On success it caches the result and populates that dataset row’s join-column dropdown; if more than one dataset is currently selected it additionally filters every non-base dataset’s column options down to only those shared with the first (“base”) selected dataset, since joins can only be made on common columns.

Parameters:

  • datasetId (int).

Returns: void — populates the join-column <select> for that dataset row.

Endpoint: ql_get_dataset_columns_for_merge

Response: response.data.columns[] — each has col_name and inferred_type. Populates the join column selector for a specific dataset row.

viewMergeConfig(e)

Description: Reads the target dataset ID off the clicked row and fetches the full join configuration. On success it dynamically builds an HTML summary describing each join step (left dataset name/ID, join type and column, right dataset name/ID, with connectors between multi-hop joins), appends it as a floating modal, and wires a close handler that removes the modal entirely from the DOM.

Parameters:

  • e (Event) — reads the target dataset ID from data-dataset-id on e.target.

Returns: void — opens a modal with the join flow diagram.

Endpoint: ql_get_merge_config — fetches the full join definition of an existing merged dataset for read-only display in a modal.

Response: response.data.joins[] with full join details per pair. Renders a config summary modal.

createMergedDataset(e)

Description: First validates that a non-empty merge name was entered and that at least two datasets are selected; either failure shows an error and aborts before any network call. It then builds a datasets array by iterating the selected datasets in selection order, reading each row’s join column and join type from the DOM — forcing the first (base) dataset’s join type to “inner” regardless of its dropdown value. It disables and relabels the submit button, then posts to the create or update endpoint depending on edit mode, using an identical payload shape for both. On success it redirects to the dashboard after a short delay; any failure re-enables the button and shows an error.

Parameters:

  • e (Event).

Returns: void — validates the merge name and dataset count (≥2) before submitting; redirects to the dashboard on success.

Endpoint: ql_create_merged_dataset (new) or ql_update_merged_dataset (edit mode)

deleteMergedDataset(e)

Description: Shows a confirmation dialog first and aborts immediately if the user cancels. If confirmed, it reads the dataset ID from the clicked row and sends the delete request. On success it fades out and removes the closest table row from the DOM in place, with no page reload; a failed or unsuccessful response shows an error with the server’s message or a generic fallback.

Parameters:

  • e (Event).

Returns: void — shows a confirm() dialog first; removes the row from the DOM on success without a page reload.

Endpoint: ql_delete_merged_dataset

Success: Removes the dataset row from the UI DOM without a page reload.

Event Bindings

  • .dataset-checkbox change — Branches on the checkbox’s checked state: when checked it tracks the dataset ID and fetches that row’s join-column options; when unchecked it removes the ID from tracking, slides the join-config panel closed, re-filters remaining datasets’ column selectors if any are still selected, and strips any “Base Dataset” badge from the row. Every invocation re-flags which remaining dataset is the base and re-evaluates whether the create button should be enabled.
  • .join-column-select change — Does nothing beyond re-evaluating whether the create button should be enabled — the join column’s actual value is read live from the DOM later when the merge is created, rather than being cached on change.
  • #create-merge-btn click — Bound directly (not delegated), clicking the button runs the full validation-then-submit flow: it checks for a non-empty name and the two-dataset minimum before ever making the create/update request.
  • .view-merge-config-btn click — Delegated so it applies to dynamically rendered rows; extracts the dataset ID from the clicked element and fetches the join configuration to render the join-flow modal.
  • .delete-merge-btn click — Delegated for dynamically rendered rows; immediately shows a confirmation prompt before doing anything else, and only on confirmation proceeds to read the row’s dataset ID and issue the delete request.
  • #refresh-datasets-btn click — Bound directly, this re-runs the same dataset-loading flow used on initial page load, including its follow-on edit-mode check — meaning a manual refresh while in edit mode will re-select the previously joined datasets and re-apply their join parameters again.

QLReportScheduler

QLReportScheduler backs the create/edit scheduled-report page, letting users configure a recurring PDF and/or HTML email report — frequency, delivery time and timezone, recipients, and format — and attach one or more dataset “sections” to it, each with its own independent AI-insights configuration, suggested-chart configuration, saved-chart selection, and date/category filters. It solves the problem of turning ad-hoc dashboard analysis into a recurring, hands-off digest: instead of a user manually revisiting QLInsights or QLAnalytics, the scheduled backend job regenerates the same insights and charts automatically and emails them out. It wraps the whole module in a standalone self-invoking function, with initialization deferred inside a nested document-ready call gated on the presence of the reports page container. The most structurally complex piece is the nested per-dataset-section JSON object saveReport() assembles before submission — each section bundles its dataset ID, an insights sub-config, a suggested-charts sub-config, and a list of selected saved chart IDs, all serialized together. It relies on the same dataset-detail and saved-charts endpoints used elsewhere, tying it indirectly to the same data model that QLDatasetDetail and QLAnalytics operate on. Defined in jdk/report-scheduler.js, object literal pattern, not exposed on window.

How to Include

QLReportScheduler requires jQuery and QLAuth (auth.js) loaded first: it is wrapped in a jQuery IIFE, and checkAuth() explicitly checks whether the global auth object is undefined before doing anything else, redirecting to /ql-login if the auth script never loaded. It has no other external script dependencies — it does not render any charts itself (it only lets users pick chart counts and saved-chart IDs for the backend job to use later), so neither Chart.js nor analytics.js is required.

Callable Functions

checkAuth()

Description: Called first inside init(), this guards against the auth script not being loaded at all by checking whether the global auth object is undefined, redirecting to /ql-login immediately if so, skipping the AJAX call entirely in that case. Otherwise it checks the session with the server and, if the response is unsuccessful or the user isn’t logged in, redirects with a return-path query parameter; a transport-level failure is only logged to the console without redirecting.

Parameters: None.

Returns: void — redirects to /ql-login if not authenticated.

Endpoint: ql_check_auth

loadReportData()

Description: Called only when a numeric report ID was found in the URL, this first replaces the form area with a loading spinner, then fetches the report’s details. On success it rebuilds the entire settings form HTML from the returned data, appends one dataset section per entry in the response, and re-invokes the event-binding setup since the old form’s handlers were destroyed along with its DOM. Both an unsuccessful response and a network failure show an error and redirect back to the dashboard after a delay, preventing the user from being stuck on a broken edit form.

Parameters: None.

Returns: void — reads this.editReportId; calls rebuildFormWithData() to reconstruct the entire form from the response.

Called in edit mode when report_id is present in the URL. Endpoint: ql_get_report_details

Response: response.data has report{report_name, frequency, schedule_day_of_week, schedule_day_of_month, schedule_time, timezone, recipients, format} and datasets[]. Populates the entire form.

loadAvailableDatasets(index, selectedId)

Description: Reuses the dashboard’s own dataset-list endpoint rather than a report-specific one, caching the result on success. If a section index is given it populates just that section’s dropdown; if the index is null (used on initial page load for a brand-new report) it instead iterates every existing dataset dropdown on the page and populates each by its own index, with no pre-selection. Populating a selector with a pre-selected ID also automatically chains into fetching that dataset’s columns.

Parameters:

  • index (int, optional, default null) — the dataset section to populate; when null, all sections are populated.
  • selectedId (int, optional, default null) — a dataset ID to pre-select.

Returns: void.

Endpoint: ql_get_dashboard_data — reuses the dashboard endpoint to get the user’s dataset list.

Response: response.data.datasets[]. Populates the dataset <select> at position index in the section list, pre-selecting selectedId if provided.

loadDatasetColumns(index, datasetId)

Description: Fetches the dataset’s columns and performs three separate population passes against the given section: filling the insight-columns multi-select with every column, the category filter-column select with every column plus a “No filter” default, and the date-filter-column select with only columns whose inferred type is date or datetime. After populating each, it re-applies any values previously cached on the section (set earlier when reconstructing an edit-mode form), including special-casing custom date values by selecting “custom” and revealing the adjacent date input. Finally it chains into fetching that section’s saved charts.

Parameters:

  • index (int) — the dataset section index.
  • datasetId (int).

Returns: void — populates the insight-columns, filter-column, and date-filter-column selectors for that section.

Endpoint: ql_get_dataset_detail

Response: response.data.columns[]. Populates insight column and date filter column selectors for the section at index.

loadSavedCharts(index, datasetId)

Description: Fetches the dataset’s saved charts and rebuilds the saved-charts multi-select for the given section, using each chart’s name if set or falling back to its chart type as the option label. It then re-applies any previously cached chart selection for that section so that in edit mode the charts originally attached to it remain checked after the list is rebuilt from scratch.

Parameters:

  • index (int).
  • datasetId (int).

Returns: void.

Endpoint: ql_get_saved_charts

Response: Populates the .saved-charts multi-select for the section at index.

saveReport()

Description: Gathers the top-level report fields directly from form controls (name, frequency, schedule time, conditionally day-of-week or day-of-month depending on frequency, timezone, recipients, format), then iterates every dataset section in the DOM to build one config object per dataset. Each per-section object nests an insights block, a suggested-charts block, a flat array of saved chart IDs, and, only if a date filter column was chosen, a date-filter block resolved to either a custom date value or the selected preset string. If any section is missing a selected dataset, it shows an error and aborts the whole save before any request is sent; an empty section list is likewise rejected. Once validated, the sections are serialized into a single JSON string field, the submit button is disabled and relabeled, and the request posts to the create or update endpoint depending on mode — success redirects to the dashboard after a delay, failure re-enables the button and shows an error.

Parameters: None.

Returns: void — reads the whole form via jQuery selectors and serializes the dataset section configs to a JSON string before submitting; redirects to the dashboard on success.

Endpoint: ql_create_scheduled_report (new) or ql_update_scheduled_report (edit mode)

Event Bindings

  • #add-dataset-btn click — Unbound first and re-bound on every form rebuild to avoid duplicate handlers, this appends a fresh, empty section at the next index, immediately populates its dataset dropdown, and triggers the insights/suggested-charts checkboxes’ change handlers to initialize their sub-panel visibility.
  • .remove-dataset-btn click — Delegated and stops the click from bubbling into the section’s collapsible header; removes the section from the DOM entirely and renumbers every remaining section’s index and visible heading text to stay contiguous.
  • .ql-dataset-header click — Slide-toggles the section’s body content and toggles a collapsed class on the header itself, purely a presentational accordion effect with no data side effects.
  • .dataset-select change — Reads the section index and the chosen dataset ID and, only if a non-empty dataset was actually chosen, fetches that dataset’s columns; the columns request’s own success handler is what subsequently triggers the saved-charts fetch, so the two calls are chained rather than fired independently from this handler.
  • .ql-collapsible-header click — Slide-toggles the following content block (used for the AI Insights, Suggested Charts, and Saved Charts sub-panels within each section) and toggles a collapsed class on the header, independent of the outer dataset-section accordion.
  • .include-insights change — Toggles the nested insights-config block’s visibility to match the checkbox’s checked state; this same handler is also invoked programmatically right after a new section is added, ensuring the visibility starts in sync with the checkbox’s default checked state.
  • .include-suggested-charts change — Functions identically to the insights toggle but targets the suggested-charts config block instead; it too is triggered programmatically once when a section is first added so the “Number of Charts” field’s visibility matches the checkbox’s initial state.
  • #frequency change — Shows the day-of-week group only when frequency is “weekly” and the day-of-month group only when it’s “monthly”, hiding both otherwise for “daily”; these groups are injected into the DOM ahead of time either for a new report or when rebuilding the form in edit mode.
  • .filter-date-from-input change — Locates the enclosing form row and shows the adjacent custom date-from input only when the selected value is “custom”, hiding it for any preset option; the underlying filter value itself is only read later, at save time.
  • .filter-date-to-input change — Mirrors the “from” handler exactly but targets the custom date-to input: it reveals the field only when “custom” is selected, with the actual value again only consumed later at save time.
  • #report-form submit — Unbound first to prevent duplicate bindings across form rebuilds, then bound to intercept the native form submission and run the save flow, which performs all of its own field-gathering, per-section validation, and JSON serialization before making the request.

QLOrganizations

QLOrganizations powers the multi-seat team management screen where a user can create an organization, invite and manage members with distinct roles, and handle billing for the whole team as a single paid entity. It solves the problem of seat-based, per-team subscriptions sitting on top of what is otherwise a single-user analytics product: an org’s manager pays for a pool of seats via Paddle rather than each teammate needing an individual plan, and member roles (member, admin, manager) gate what each teammate can do inside the org. On init(), if window.Paddle and a configured client token are both present, it initializes Paddle so Paddle.Checkout.open() can later be invoked client-side for both initial subscription purchase and resuming a pending payment. It shares the same JWT/qlAuth authentication pattern used by every other QL module. Where QLUserProfile manages an individual’s personal settings, QLOrganizations manages the team/billing layer that sits above individual accounts. Defined in jdk/organizations.js and exposed globally as window.QLOrganizations.

How to Include

QLOrganizations requires jQuery and QLAuth (auth.js) loaded first. In addition, because billing is handled client-side through Paddle’s overlay checkout, the page must load Paddle.js v2 before organizations.js, and a global Paddle client token value must be present for Paddle initialization to run — without it, org/member management still functions, but the Paddle-backed checkout calls will silently no-op.

Callable Functions

loadMyOrganizations()

Description: Issues a request for the current user’s organizations and, on success, rebuilds the organizations list container from scratch — each organization is rendered as a card showing its license badge, member count, the caller’s role, and status. If the response indicates failure, it surfaces a generic notification; unlike most other calls in this module it does not attach a network-error callback. This is the function that re-populates the list view whenever the user returns to it, including after navigating back and after deleting an organization from the list.

Parameters: None.

Returns: void — uses $.post() rather than $.ajax()/_ajax().

Endpoint: ql_get_my_organizations

Response: response.data.organizations[] — each has id, name, license_type, member_count, role, status, manager_id.

viewOrganization(e)

Description: Reads the target organization’s ID from the clicked element, then fetches the organization. On success it stores the full organization object, populates the header, settings form, and overview stats, and immediately fires three follow-up requests — members, billing history, and payment status — before swapping the list view out for the detail view. If the server call fails, it shows the server-provided error message instead of navigating, leaving the user on the list view.

Parameters:

  • e (Event) — reads data-org-id from e.currentTarget.

Returns: void — on success also triggers loadMembers(), loadBillingHistory(), and loadPaymentStatus().

Endpoint: ql_get_organization

Response: response.data.organization with full org details. Renders the org detail view and calls loadMembers(), loadBillingHistory(), and loadPaymentStatus().

loadMembers(orgId)

Description: Requests only active members into the current member list. On success it renders the member table, which computes whether the current user is the org’s manager and, for each member, whether that row is editable (a manager can edit anyone except the primary owner) — this determines whether a role dropdown and a Remove button are shown or a static role badge is shown instead. This function is re-invoked after adding or removing a member, and after a failed role update, to resynchronize the UI with server state.

Parameters:

  • orgId (int).

Returns: void.

Endpoint: ql_get_organization_members

Response: response.data.members[]. Renders the member table.

loadBillingHistory(orgId)

Description: Requests up to 12 billing records, then builds one table row per invoice showing the invoice ID (falling back to a generated placeholder if absent), billing period, seat count, formatted dollar amount, status badge, and paid date. There is no error branch in the callback — if the request fails or the response indicates failure, the billing table is silently left in whatever state it was in before the call, typically empty since it’s invoked right after the detail view is opened.

Parameters:

  • orgId (int).

Returns: void.

Endpoint: ql_get_organization_billing

Response: response.data.billing_history[]. Renders the billing history table.

createOrganization(e)

Description: Reads the create-org form fields (name, license type, max users, billing email) and submits them. On success it branches three ways: if the response includes a checkout transaction ID and Paddle is available, it opens the Paddle checkout overlay with a callback that reloads the page once checkout completes; otherwise, if a checkout URL is present, it does a full-page redirect there instead; and if neither is present, it treats the organization as created but payment setup as failed, shows a warning, hides the create form, and refreshes the organization list. On failure it surfaces the server’s error message and leaves the create form open for correction.

Parameters:

  • e (Event) — the form submit event.

Returns: void — opens the Paddle checkout overlay if a checkout_transaction_id is returned; otherwise reloads the organization list.

Endpoint: ql_create_organization

Success: If response.data.checkout_transaction_id is present, opens the Paddle checkout overlay for payment. Otherwise reloads the org list.

updateOrganization(e)

Description: Gathers the full settings/billing form (name, license type, max users, billing email/address/country/tax id) into a payload and submits it. On success it shows a success notification, replaces the cached organization object with the updated one returned by the server, and re-renders the detail view so the header, badges, and stat boxes immediately reflect the new plan/seat pricing. On failure, the form is left as-is and the server’s error message is shown, so no local state is mutated until the server confirms the change.

Parameters:

  • e (Event).

Returns: void.

Endpoint: ql_update_organization

deleteOrganization(e) / deleteOrganizationFromList(e)

Description: Both handlers first show a confirmation dialog warning that all members will be removed, and do nothing further if the user cancels. Both then read the organization ID from the clicked element and post to the same delete endpoint; they differ only in what happens after success — the detail-view variant navigates back and refreshes the list, while the list-view variant simply re-renders the list in place without changing views. Both surface the server’s error message if the deletion is rejected, for example because the caller isn’t the primary manager.

Parameters:

  • e (Event) — both handlers read data-org-id from e.currentTarget.

Returns: void — both show a confirm() dialog first.

Endpoint: ql_delete_organization — called from the detail view (deleteOrganization) or the list view (deleteOrganizationFromList).

addMember(e)

Description: Reads the invite email and selected role from the add-member form, posting them along with the current organization’s ID. On success it clears the email input and refreshes the roster with the newly added member; on failure it shows the server’s error message, for example if the invited email doesn’t correspond to an existing account or the org is at its seat limit, without clearing the input so the user can retry.

Parameters:

  • e (Event) — the form submit event.

Returns: void — reloads the member list on success.

Endpoint: ql_add_organization_member

removeMember(e)

Description: Shows a confirmation dialog before doing anything; if confirmed, it reads the target user ID from the clicked button and posts the removal request. On success it shows a confirmation notification and reloads the member list; on failure it surfaces the server’s error message and leaves the member in place, since the UI is only ever refreshed from a fresh fetch rather than being optimistically updated.

Parameters:

  • e (Event).

Returns: void — shows a confirm() dialog first.

Endpoint: ql_remove_organization_member

updateMemberRole(e)

Description: Fires immediately on the change event of a role dropdown with no confirmation step, reading the new role value and the affected member’s ID, then submitting the change. On success it shows a brief success notification but does not re-render the member list, since the select’s own value already reflects the change; on failure it shows the error message and explicitly reloads the member list, reverting the dropdown back to the member’s actual role if the update was rejected.

Parameters:

  • e (Event) — the change event on a .ql-member-role-select.

Returns: void — submitted immediately on change, with no confirmation dialog.

Endpoint: ql_update_member_role

subscribe(e)

Description: Resolves the target organization ID from either the clicked element’s data attribute or, if absent, the currently loaded organization — this dual lookup lets the same handler work both from a “Complete Payment” button on a list card and from the payment-pending banner shown inside the detail view. It posts a subscribe request, and on success opens the Paddle checkout overlay with the returned transaction ID (reloading the page on completion) if Paddle is available, or redirects to the returned URL otherwise. On failure it shows either the server’s message or a generic fallback.

Parameters:

  • e (Event).

Returns: void — opens the Paddle checkout overlay using the returned transaction ID.

Endpoint: ql_org_subscribe

Success: Opens Paddle checkout overlay using response.data.transaction_id.

loadPaymentStatus(orgId)

Description: Requests the payment status and, on success, renders the Paddle subscription status and current period end date, along with a computed billing-rate breakdown derived from the org’s license type and member count. If the target container isn’t present in the DOM, rendering exits immediately as a no-op guard. This call runs alongside the members and billing-history fetches every time the detail view opens, keeping the payment/subscription state fresh whenever an org is opened.

Parameters:

  • orgId (int).

Returns: void — updates the billing status badge in the org detail view.

Endpoint: ql_org_payment_status

Response: response.data has paddle_status and current_period_end. Updates the billing status badge in the org detail view.

Event Bindings

  • .ql-show-create-org click — Prevents the default anchor/button action and toggles visibility by hiding the org list and showing the create panel, giving the user an empty form to fill in without navigating away from the page.
  • .ql-cancel-create click — Prevents the default action, hides the create panel, shows the list again, and explicitly resets the create form’s fields so that any partially entered values don’t linger if the panel is reopened later.
  • .ql-view-org click — Delegated for any matching element, this fetches the organization, its members, billing history, and payment status in sequence, then swaps the list view for the detail view once the primary organization fetch succeeds.
  • .ql-back-to-list click — Prevents default, hides the detail view, shows the list, clears the cached current organization, and reloads the list — this is also the shared handler that a successful delete calls internally.
  • #ql-create-org-form submit — Prevents the native page reload and instead submits the new org’s name, license type, seat count, and billing email, then routes the user into Paddle checkout, an external checkout URL, or an error state depending on the response shape.
  • #ql-update-org-form submit — Prevents default submission and persists every settings and billing field currently in the form, re-rendering the detail view header/stats from the server’s response on success.
  • .ql-delete-org click — Only ever shown/enabled for the organization’s primary manager, this click triggers the shared confirm-then-delete flow and, on success, navigates back to the list view.
  • .ql-delete-org-list click — Attached to the delete button rendered on each org card (only for cards where the current user is the manager), this shows the same confirmation dialog but, on success, stays on the list view and simply reloads it instead of navigating.
  • #ql-add-member-form submit — Prevents default submission of the invite form and posts the email/role pair scoped to the current organization; a successful response clears the email field and reloads the roster to show the new member immediately.
  • .ql-remove-member click — Requires confirmation, then submits the member’s ID for removal; rather than manipulating the DOM row directly, the success path re-fetches the full member list so the table is rebuilt from authoritative server data.
  • .ql-member-role-select change — Fires as soon as the dropdown’s value changes, with no confirmation step, persisting the new role for that member against the current organization; a rejected update triggers a reload of the member list to visually revert the dropdown.
  • .ql-tab click — Prevents default, reads the target tab off the clicked element, then toggles the active class on both the tab buttons and their corresponding content panels, giving a simple client-side tabbed interface with no additional data fetching per tab.
  • .ql-subscribe click — Bound for both the “Complete Payment” button on list cards and the payment-pending banner inside the detail view, this resolves the organization ID from either source, then opens Paddle checkout or redirects to a checkout URL depending on what the server returns.

QLUserProfile

QLUserProfile powers the personal account-settings screen where an individual user views and edits their own profile — name, company, email, country, marketing consent, password, and light/dark UI preference — and can permanently delete their account. It solves the need for self-service account management separate from organization-level administration: where QLOrganizations lets a manager administer a team’s seats and billing, QLUserProfile is scoped entirely to the single logged-in user’s own record. Structurally it is the odd module out: instead of the usual object-literal pattern exposed on window, it is written as a single anonymous document-ready IIFE containing private local functions, with nothing attached to the global scope, so no other script can call into it directly. It depends on a separate ql-helpers.js file for populating the country dropdown, logging a console error and leaving the dropdown empty if that helper isn’t loaded. It also contains a live theme-preview feature that swaps stylesheet hrefs across the page immediately when the user toggles the UI-mode radio buttons, giving instant visual feedback before the preference is actually saved. Defined in jdk/user-profile.js, uses an inline IIFE, and is not exposed on window.

How to Include

QLUserProfile requires jQuery and QLAuth (auth.js) loaded first, exactly like the other modules. Because it has no exported object, there is nothing to reference from other scripts, so load ordering only matters relative to its own dependencies. Uniquely among the modules covered here, it also requires ql-helpers.js to be loaded before user-profile.js, since the country dropdown delegates directly to that helper — if it’s missing, the dropdown silently fails to populate and an error is logged rather than the page breaking outright.

Callable Functions

loadUserProfile()

Description: Runs immediately inside the document-ready callback (not bound to any event), first toggling a loading indicator and hiding the content, then fetching the profile. On success it populates every editable field (including delegating the country dropdown to the helper module) and renders the read-only summary panel — badges for email verification and marketing consent, formatted membership/last-login dates, and conditional password-section visibility based on the auth provider. On failure it shows an error message and, for an unsuccessful response, redirects to /ql-login after a two-second delay, treating a failed profile fetch as an expired or invalid session.

Parameters: None.

Returns: void — populates the profile form and the read-only info display; redirects to /ql-login after 2 seconds if the user is not authenticated.

Endpoint: ql_get_profile

Response: response.user has first_name, last_name, company, email, country, ui_mode, marketing_consent, marketing_consent_at, auth_provider, email_verified, created_at, last_login, avatar_url, role. Populates all form fields.

updateUserProfile()

Description: Reads every profile field directly from the DOM (name, company, email, marketing consent checkbox, country, and the selected UI-mode radio) into a payload, including the new password only if that field is non-empty. It disables and relabels the submit button for the duration of the request, then on success shows a success message, additionally warns about email verification if the email was changed, refreshes the displayed info and form from the returned user object if present, clears both password fields, and finally redirects to the dashboard after a delay regardless of what changed. On failure it shows the server’s error message and re-enables the button either way; this function is only ever called after passing client-side validation.

Parameters: None.

Returns: void — reads all form field values directly via jQuery selectors; the password field is only included in the request if non-empty.

Endpoint: ql_update_profile

Success: If response.email_changed is true, displays an email verification notice.

deleteUserAccount()

Description: Requires two sequential confirmations before making any network request: a dialog listing exactly what will be destroyed (account, datasets, connections, files), and then a prompt asking the user to type their password; if either is cancelled or the password prompt returns empty, the function returns early with no request sent. Once both gates pass, it submits the password, disabling the delete button meanwhile. On success it starts a three-second countdown, updating the displayed message each second, before redirecting to the homepage; on failure it shows the server’s message and re-enables the button so the user can retry, for example after mistyping their password.

Parameters: None.

Returns: void — requires a confirm() dialog followed by a prompt() for the account password before submitting; redirects to / after a countdown on success.

Endpoint: ql_delete_account

Guards: Requires a browser confirm() dialog first, then prompts for password via prompt(). Success clears session and redirects to /ql-login.

Event Bindings

  • #profile-form submit / #update-profile-btn click — Both handlers independently prevent the default action and run a validation pass before saving — checking that a changed email is well-formed and that a new password, if provided, is at least 8 characters and matches the confirm field, blocking the request entirely and showing an inline error if either check fails. This dual binding means the update can be triggered either by pressing Enter inside the form or by explicitly clicking the button, both funneling through the same validated path.
  • #delete-account-btn click — Prevents the default action and immediately calls the delete function, which itself owns the two confirmation gates (the dialog and the password prompt) — this binding does no validation of its own beyond dispatching to that function.
  • #new_password / #confirm_password keyup — Fires on every keystroke in either password field, and only evaluates once both fields are non-empty; it toggles the match indicator between a green “match” state and a red “do not match” state, and hides the indicator entirely if either field is emptied.
  • #new_password keyup — Computes a strength score by checking length thresholds, presence of both lowercase and uppercase letters, presence of a digit, and presence of a non-alphanumeric character; a low score shows “Weak” in red, a middling score shows “Medium” in yellow, and a high score shows “Strong” in green, with the indicator hidden entirely when the field is empty.
  • #email change — Compares the field’s current value against the original email cached when the profile was first loaded; if they differ, it reveals a warning explaining that a verification link will be sent to the new address, and hides the warning again if the value is reverted back to the original.
  • input[name=”ui_mode”] change — When a different radio is selected than the theme currently active, determined by inspecting loaded stylesheet hrefs for a dark-mode suffix, it shows an informational toast telling the user to click “Update Profile” to persist the change, then rewrites the href of every matching light/dark stylesheet pair to the newly selected mode — this preview is purely visual and is not saved until the profile form is actually submitted.

QLAgent

QLAgent implements the popup chat window where a user converses with a Claude-backed multi-turn AI agent capable of listing datasets, generating insights, building charts, and setting up scheduled reports, all through natural-language requests rather than the discrete point-and-click flows used elsewhere in the app. It ties together many of the plugin’s individual features behind a single conversational entry point, maintaining the full message history client-side so each new turn can be sent back to the server for context. Because Claude is instructed to respond in HTML directly, the module includes pre-processing logic that detects chart specifications the model may emit in several inconsistent forms — properly fenced blocks, tag-wrapped JSON, entity-escaped JSON, or bare inline JSON — and normalizes them all into a single canonical form before extraction. Unlike QLInsights, which renders its charts by delegating to the shared QLAnalytics chart-drawing code, QLAgent renders its own inline charts independently, fetching chart data and instantiating Chart.js directly — meaning the agent’s chart rendering can drift from QLAnalytics’s rendering logic since the two are not shared. It opens as a standalone chrome-less popup window rather than being embedded in the main dashboard layout. Defined in jdk/agent.js, object literal inside an IIFE, not exposed on window.

How to Include

QLAgent requires jQuery and QLAuth (auth.js) loaded first. It additionally requires Chart.js 4.4.0 to be loaded before agent.js, because renderAgentCharts() references the global Chart constructor directly (guarded so it silently skips chart rendering if the library is absent). Notably, QLAgent does NOT require analytics.js/QLAnalytics to be loaded at all — unlike QLInsights, it never delegates to that module and builds its charts entirely from its own logic using Chart.js directly.

Callable Functions

checkAuth()

Description: Checks the session using the bearer token, and redirects to the login page (preserving a return path) only if the response explicitly indicates the session is invalid or not logged in. Notably, if the auth-check request itself errors out at the transport level, the error callback only logs a console warning and lets the UI continue running rather than forcing a redirect — a deliberate “fail open” choice so a flaky auth-check request doesn’t lock a legitimately logged-in user out of the chat.

Parameters: None.

Returns: void — redirects to /ql-login if not authenticated; continues silently if the auth-check request itself fails.

Endpoint: ql_check_auth

send()

Description: Guards against concurrent submissions and against empty input by trimming and checking the textarea’s value before proceeding. It immediately appends the user’s message as a bubble, clears and resets the input’s height, and sets the loading/thinking-indicator state before firing the request. The conversation history mechanism sends the entire prior history array alongside the new message on every call, and on a successful response replaces the history wholesale with the array the server returns — meaning the server, not the client, is the source of truth for how the conversation is structured turn to turn. The request uses an explicit 180-second timeout because agent turns can involve multi-step tool-calling loops on the backend (dataset queries, insight generation, chart building) that take substantially longer than a typical AJAX call. On a genuine timeout it tells the user the agent may still be running and to wait before retrying, distinct from a generic connection-error message shown for other transport failures.

Parameters: None.

Returns: void — reads the current value of #ql-agent-input; request timeout is 180000 ms (3 minutes). On success, updates this.history with the full conversation and appends the assistant’s reply to the UI.

Endpoint: ql_run_agent. Timeout: 3 minutes (180 000 ms).

Response: response.data has messages[] (updated conversation history array) and final_text (HTML string that may contain .ql-agent-chart-embed placeholder elements for inline charts). After rendering, calls renderAgentCharts($container).

renderAgentCharts($container)

Description: Immediately no-ops if the global Chart constructor isn’t defined, then scans the given message bubble for every chart-embed placeholder element produced earlier during extraction of fenced chart blocks from the assistant’s raw response text — that extraction logic decodes HTML entities, unwraps chart specs the model may have placed inside tag-wrapped text, protects already-fenced blocks from double-wrapping, and wraps any remaining bare JSON objects, so that by the time this function runs, each embed’s parameters reliably hold parsed JSON regardless of how inconsistently the model originally formatted them. For each embed it parses the parameters, flattens any values the model nested under an extra key (a defensive workaround for a common malformed shape), and fetches chart data with the flattened parameters. On success it creates a canvas element and instantiates Chart.js inside a try/catch, showing a render-error message if Chart.js throws; on a failed or errored request it shows a load-failure message, and a malformed parameters attribute is caught even earlier with an invalid-data message before any network call is made.

Parameters:

  • $container (jQuery object) — scanned for .ql-agent-chart-embed elements.

Returns: void — for each embed, creates a <canvas> element and calls new Chart(canvas, response.data) directly, bypassing QLAnalytics.

Endpoint: ql_get_chart_data

Scans the rendered response HTML for .ql-agent-chart-embed elements. For each one, reads its data-params attribute (JSON), sends a chart data request to ql_get_chart_data, then creates a <canvas> element and calls new Chart(canvas, response.data) directly — bypassing QLAnalytics.

Event Bindings

  • #ql-agent-send click — Simply invokes the send function on click; that function itself is responsible for reading and validating the input, appending the user bubble, and disabling the send button for the duration of the request.
  • #ql-agent-input keydown — Checks for a control or command key combined with an Enter keypress, preventing the default newline insertion and invoking send — this lets plain Enter still insert a newline for multi-line messages while the modifier combination submits. On every keydown it also auto-grows the textarea, resetting its height and then capping it at a fixed maximum so the box grows with content but doesn’t expand indefinitely.
  • #ql-agent-input input — Fires on the standard input event (covering paste, autocomplete, and other non-keydown changes that keydown wouldn’t catch) and calls the same auto-grow helper used by the keydown handler, ensuring the textarea resizes correctly even when text enters the field through means other than direct typing.
  • .ql-agent-new-btn click — Empties the conversation history, forces the loading flag back to false and re-enables the send button (guarding against a stuck disabled state if the button was reset mid-request), removes any lingering thinking indicator, and replaces the entire message container’s contents with the welcome screen including the suggestion chips.
  • .ql-agent-suggestion click — Reads the trimmed text content of the clicked suggestion chip (one of the canned prompts in the welcome screen, e.g. “List my available datasets”), sets it as the input’s value, and immediately calls send — effectively simulating the user having typed and submitted that exact suggestion themselves, without requiring a second click.