Skip to main content
Jahia Store
EN

customGPT.ai - Jahia indexer

community
Download 1.0.10

Information

Module ID
customgpt-ai
Group ID
org.jahia.modules.community
Status
community
Category
Connectors
Author
fbourasse
Developer website
http://www.jahia.com
Requires Jahia
8.2.3.0
Released
2026-10-03
Source
scm:git:git@github.com:Jahia/customgpt-ai.git
Tags
  • ai
  • chat
  • chatbot

Jahia module that integrates with the CustomGPT.ai API to index Jahia site content (pages and files) into a CustomGPT project, keeping it in sync with JCR publish/unpublish events.

Dependencies

Depended on by

Nothing depends on this module.

Versions

1.0.10Download

Resolves the Search Generative Experience deployment details for the Academy search page, and closes the feature when the CustomGPT query quota is spent.

Added

Service.getSgeDeployment() returns an SgeDeployment: {projectId, shareableKey, available}, with isUsable() as the single question a caller asks.

source

projectId

module configuration

shareableKey

shareable_slug on the GET /projects/{id} payload this module already reads project_name from

available

GET /limits/usage — current_queries against max_queries for the billing cycle

Why the key is resolved here rather than typed in

academy-augmented-search-ui 1.2.5 takes the project id and public deployment key as component properties, filled in by an editor. That is a credential hazard, not a chore: the field sits next to a project id in an editor form, and the obvious wrong thing to paste into it is this module's API token — a real secret, which would then be rendered into page source, because rendering the key into page source is what the key is for.

SgeDeployment has no field that can carry the token. A test asserts the key does not reach toString() either.

Quota

max_queries against current_queries makes "the quota is spent" a read rather than an inference. A spent quota now hides the button, instead of letting a visitor click and wait for CustomGPT's own error inside the widget.

Note

max_queries is per team, per billing cycle — not per agent. The button disappears from the Academy when anything else on the same CustomGPT team exhausts the quota.

Caching, and why it is mandatory

Without it, every render of a search page makes two authenticated calls to CustomGPT, one of them to an endpoint that is itself rate limited: page views become API spend and then 429s.

Five minutes, chosen against what the data does — a team's quota moves slowly, so the cost of being up to five minutes stale is a handful of visitors at the boundary. The cache is dropped when the configuration changes, or a new project id would appear not to have taken effect for five minutes. "Not configured" is cached too: a site that has not set this up must not pay for a lookup per page view either.

Fail open

Availability goes false only when the quota is positively known to be spent. A timeout, a 429, a missing field, or a non-positive max_queries — which is how an unmetered plan reads — all leave the feature on.

Hiding a working feature because of a transient error is the worse of the two failures: the visitor loses a route that would have worked, and nothing explains why. Letting one doomed query through costs a single error message they can see and act on. A definite "spent" is the one case where closing the feature is right, and it is the only case that does.

Verification

mvn clean install BUILD SUCCESS · 259 tests pass, 15 of them new.

The quota decision is split from the HTTP request so it is testable without a network, and most of the new tests pin the fail-open branches rather than the happy path — those are the ones that invert quietly if someone later tidies up the null handling.

Not exercised against a live exhausted quota. The decision logic is tested exhaustively; that a spent quota actually reports current_queries >= max_queries on this plan is taken from CustomGPT's API reference, not observed.

Requires Jahia 8.2.3.0Released 2026-10-03

A patch release on top of 1.0.8. Upgrade if you are running 1.0.8 - a first publish of content whose public URL redirects fails to index on that version.

Requires Jahia 8.2.3.0 or later.

Fixed - a first publish failed when the page URL redirects

Impossible to retrieve content from https://<host>/contents/knowledge-base/2026/....html (HTTP 301)

 

Jahia answers the raw .html path with a 301 to its canonical form. The render client is built followRedirects(false), so the fetch failed and the node was never indexed.

The error message is correct behaviour - 1.0.8 made this condition raise instead of logging a warning and returning, which is why it is visible at all. On earlier versions the same publish reported success over a page that was never uploaded. The defect is refusing the redirect, not reporting it.

The security control is preserved rather than traded away. followRedirects(false) stays: the request carries Basic credentials, and OkHttp would replay them to wherever a redirect pointed. Redirects are now followed explicitly and only within the same scheme, host and port, so credentials cannot leave the host they were issued for. A different host, a subdomain, a downgrade to http, a different port, or a missing Location header is not followed, and the page fails as before. Hops are capped at 3.

The URL actually fetched becomes the stored citation URL. Storing the pre-redirect one gives the reader a citation that costs them a hop, and is how raw .html paths came to sit in a corpus. Pages indexed from this version on store the canonical URL by construction, rather than needing repairPageUrls to correct them afterwards.

A redirect is also no longer retried as if transient. It is a routing fact, and three attempts with 500ms sleeps burned a second per failing page without changing the answer.

Fixed - a corroborating read was not independent of the read it corroborated

1.0.8 re-reads page metadata before believing a missing URL, because this API can return a correct envelope with a dropped payload. Both reads were issued back to back, so a second sample taken in the same burst inherits the same cause as the first - and the code then records "absent from every read" with false confidence.

The drop this guards against is sustained-volume dependent, so the decorrelating variable is time rather than attempt count. Re-reads are now spaced. Only a missing value is re-sampled, so a page whose URL is present still costs one read and no delay.

Upgrading

Nothing to do beyond deploying the module. The 1.0.7 note still applies if you are coming from a version before that: installing a new version does not repair a node already running a stranded listener, so restart affected processing nodes once.

If 1.0.8 left content unindexed with the HTTP 301 error above, re-publish those nodes or run startNodeIndex against them after upgrading.

Tests

244 unit tests. Every component of the redirect origin check is mutation-verified independently.

Requires Jahia 8.2.3.0Released 2026-10-01

Requires Jahia 8.2.3.0 or later.

Upgrade note carried forward from 1.0.7: installing a new version does not repair a node already running a stranded listener. Restart affected processing nodes once after upgrading.

Fixed - content from unregistered sites was indexed

acceptablePathToIndex checked the shape of a path and nothing else, so any publication anywhere under /sites/ produced an index operation whether or not an administrator had registered the site with addSite. The jmix:customGptIndexableSite mixin was consulted when listing sites and nowhere on the publish path.

This was measurable: a page from an unregistered site sat in a corpus intended to hold one site's content, and a CustomGPT corpus cannot be filtered by origin afterwards.

The gate now sits at dispatchOperation, the choke point all four index-producing operations already route through. Deletes deliberately stay ungated - removing a page that should not be in the corpus is always safe. It fails closed: failing to index is repaired by the next publication, while indexing wrongly is not.

Fixed - vanity URLs never resolved, and the failure was invisible

Jahia resolves vanity URLs inside rewriteOutbound through a JCR lookup that reads the thread-local current user. Indexation runs on a pooled executor thread where doExecuteWithSystemSessionAsUser binds the session's user, not that thread-local. The lookup degraded to the raw .html path, Jahia answered with a 302, and this module does not follow redirects - so the page was never fetched.

The second half is why it went unnoticed: indexJahiaPage logged a warning and returned without recording anything, so the run still reported success over content that was never uploaded. It now raises into the failure recording that already existed.

Pages indexed after this change store the canonical vanity URL. Pages indexed before it do not, and are corrected by repairPageUrls below.

Fixed - a 2xx on a metadata write was not evidence the write took

Under sustained load this API answers status: success with an empty body while reporting zero errors, and the write is dropped. The verification now reads the value back and retries until it matches, checking the echoed page id as well as the URL - a response that does not name the page asked for is a dropped write wearing a 2xx.

A single read is not enough either: the API has been observed returning a correct envelope with a correct echoed id and a null payload for a page that has a URL. Reads are corroborated - a value seen in any read is real, only a value absent from every read is believed absent.

Fixed - a failed site indexation is no longer reported as success

A site whose sitemapIndexURL is missing or malformed used to index nothing at all and still report success. It now records a failure.

New - repairPageUrls(siteKey, dryRun, pageIds)

Re-issues only the metadata write for indexed pages whose stored URL is missing or no longer canonical. The pages and their mapping nodes are intact, so nothing is re-rendered or re-uploaded: one request per affected page rather than a full re-index. The URL is recomputed through the same code path indexation uses, so a repaired URL is by construction the URL indexation would produce.

Operational notes:

  • dryRun is required. Run it true first and read the planned rewrites.
  • pageIds scopes the run. It is required in practice for a large site - this runs synchronously on the request thread and refuses more than 100 pages, because a client timing out does not stop the server, and an unbounded run would rewrite an unknown subset with no summary.
  • A computed URL is validated before it overwrites anything: a URL that does not parse, has no host, contains a null path segment, or disagrees with the stored URL on the host is refused for that page.
  • Without pageIds, a plan covering more than 25% of examined pages is refused as implausible.

New - sweepOrphanedPages(dryRun, pageIds, siteKey)

Reports pages in the project that no mapping node claims. Mapping nodes are children of the content node, so deleting content deletes its mapping; a page nothing in Jahia accounts for is one whose delete never propagated.

An orphan is not necessarily redundant, and this tool will not guess. The same state arises when a mapping node is lost while the content survives, in which case the page is the only copy of that content and the remedy is to re-index the node, not to delete it. On a production corpus, measured by content, roughly three quarters of orphans were older renders of still-claimed pages and the rest were unique.

So deletion requires an explicit pageIds list a human has read and passed back, and naming an id that is not an orphan does not make it deletable. The report prints each orphan's real URL from the metadata endpoint, its size, the single claimed page sharing its title with that page's size, and - where the URL is a raw JCR path rather than a vanity URL - a clearly labelled guess at the node. Sizes are printed side by side deliberately: a constant byte delta repeating down a cluster is what distinguishes an older render from different content, and no summary reproduces it.

The sweep refuses to run at all if Jahia holds no mapping nodes, since every page would then look orphaned.

Other

  • A mapping node pointing at a page deleted out of band is reported as its own category rather than as a repair failure.
  • A failed CustomGPT delete no longer leaves an orphaned page behind.
  • Dry-run mode says so in the log instead of returning silently.
Requires Jahia 8.2.3.0Released 2026-10-01

Fixed - a stale JCR listener could abort a publication

A publication failed with NoClassDefFoundError on IndexOperations$CustomGptOperationType, thrown from a listener that had outlived the bundle classloader which created it. A wiring refresh did not clear it; only restarting the processing node did.

JCRObservationManager keeps listeners in a JVM-wide static list and removes them by instance identity, while TemplatePackageRegistry de-duplicates by Class identity - so a listener that stop() failed to detach is unreachable forever, and a redeploy registers a second listener beside the stale one rather than replacing it. Jahia contains Exception around listeners but not Error, which is why a NoClassDefFoundError killed the publication job while ordinary exceptions never did.

  • stop() now detaches the listener unconditionally and first, with each step isolated so one failure cannot skip another.
  • init() registers the listener last, after everything that can throw. A component whose activate() throws is never deactivated, so registering early stranded the listener on a path no shutdown hardening could reach.
  • onEvent() contains LinkageError, so a stale listener can no longer abort someone else's publication.
  • A deactivated component refuses to allocate new thread pools.

Upgrade note: installing this does not repair a node that is already in the bad state. The stranded listener belongs to the previous bundle generation and has no such handling. Restart affected processing nodes once after upgrading.

Fixed - silent failures that made the module look healthy while doing nothing

  • A listener with an empty node-type filter was registered and permanently deaf. Jahia rejects every event against an empty filter, so the module reported started and indexed nothing for the life of the component. Registration is now refused and retried when configuration arrives - and a change to the indexed node types now takes effect instead of being frozen at first registration.
  • A failed site indexation reported COMPLETED. Per-node failures were swallowed, so a run in which nothing indexed was indistinguishable from success. Failures are now recorded with node path and language, and a run that had any is reported FAILED.
  • Dry run skipped indexation silently. dryRun defaults to true in the shipped configuration, and that file is not overwritten on redeploy - so this was the likeliest cause of "the module is running but nothing is indexed", with no evidence in the logs. It now says so.

Fixed - re-indexing could leave the previous page behind

A corpus audit found 223 of 3055 pages (7.3%) redundant, concentrated in the security-advisory tree, with one URL holding 17 copies. Superseded copies of advisories stayed retrievable, so the chatbot could answer from a withdrawn one.

removeExistingPage() discarded the result of the page delete and added a replacement regardless, so every re-publish whose DELETE failed orphaned the previous page - with no log line at any level. The delete result is now honoured: a page confirmed gone proceeds, and any status that may have left it in place aborts rather than creating a duplicate.

Also coalesces repeat index operations. One publication performs several JCR saves, each delivering its own event batch, so the same node was dispatched up to 10 times per publication - each repeat costing a delete and a post against an API that degrades under sustained volume. Repeats are now dropped while equivalent work is queued.

Behaviour change: a second change to a node published while its first index is still pending is dropped rather than queued, so that content is indexed on the next publication.

Also since 1.0.5

  • customGptAdmin permission for fine-grained RBAC, with regression tests
  • Stored secrets no longer exposed; credential transport hardened; https enforced on the token-bearing base URL
  • Duplicate EventHandler registration removed; asynchronous errors surfaced; HTTP clients bounded
  • 86 SonarQube issues resolved; WCAG accessibility fixes across the admin UI
  • Vulnerable transitive test dependencies pinned

Verification

157 unit tests, and the end-to-end Cypress suite green at 33/33 against Jahia 8.2.3.2. The suite had been failing 20 of 33 before this release - for a harness reason rather than a module one - and that is fixed here too: it now resolves module dependencies from a releases-only repository and runs against a released Jahia.

Requires Jahia 8.2.3.0Released 2026-09-30

Highlights

  • Resolved all 86 pre-existing SonarQube issues across the codebase, significantly improving code quality.
  • Fixed WCAG 2.1 AA accessibility issues throughout the admin UI and CustomGptSettings component.
  • Hardened the rate-limit interceptor by moving Thread.sleep outside of the synchronized block to reduce contention.

Changes

Features

  • None in this range.

Fixes

  • ecd752e fix: resolve all 86 pre-existing SonarQube issues.
  • d711461 fix(rate-limit): move Thread.sleep outside synchronized block and reduce lock-hold time.
  • 1edf8a0 fix(ui): stick scrollbar to right edge by removing max-width from scroll container.
  • c45528e fix(a11y): resolve WCAG 2.1 AA issues in CustomGptSettings.
  • f34db7e fix: resolve WCAG 2.1 AA accessibility issues in admin UI.

Refactoring / chores

  • None in this range. (Touched files include broad refactors across Service.java, IndexerJCRListener.java, CustomGptIndexerNodeHandler.java, etc., bundled inside the SonarQube fix commit.)

Tests / CI

  • 86876a6 test(cypress): assert scheduleJobASAP resets to false after save.
  • d962126 test(cypress): increase defaultCommandTimeout to 10000ms.

Full Changelog: 1_0_4...1_0_5

Requires Jahia 8.2.3.0Released 2026-05-19

New features

  • Token-bucket rate limiting — the RateLimitInterceptor now enforces a configurable maximum request rate (default 10 req/s) using a token-bucket algorithm. Tokens are refilled lazily from elapsed wall-clock time; no background thread is needed. All API calls — indexing and purge alike — share the same bucket.
  • Configurable rate via Admin UI and OSGi config — the new rateLimit.requestsPerSecond property (org.jahia.community.modules.customgpt.rateLimit.requestsPerSecond) is exposed in the settings panel next to Batch Size. It can also be set directly in the .cfg file. Note: the OkHttp client reads the value at startup; a module restart is required for changes to take effect.

Improvements

  • Exponential back-off on HTTP 429 — replaced the previous single fixed-delay retry (1 s, one attempt) with up to 3 retries using full-jitter exponential back-off (base 1 s, cap 30 s). The Retry-After response header is honoured when present, overriding the computed delay.
  • Streaming-batch purge — purgeAllPages no longer loads every page ID into memory before starting deletions. It now fetches one API result page, deletes those IDs concurrently, then re-fetches until the first page comes back empty. Memory usage is bounded to one result page (typically 15–20 IDs) regardless of project size.
  • Capped purge thread pool — the deleteAllPages executor is sized to min(batchSize, requestsPerSecond) instead of batchSize, preventing the creation of hundreds of idle threads when the batch size is large.

Bug fixes

  • Fixed pagination drift during purge: the previous implementation followed next_page_url while deleting, which silently skipped items when offset-based pagination shifted after deletions. The new loop always re-queries the first page URL after each deletion round, guaranteeing no items are missed.

Upgrade notes

  • The new rateLimit.requestsPerSecond property defaults to 10. Existing deployments that relied on the previous fixed 500–1000 ms jitter between requests will now send up to 10 requests per second; adjust the property if the CustomGPT.ai plan has stricter limits.
  • No JCR schema changes. No migration script required.

Full Changelog: 1_0_3...1_0_4

Requires Jahia 8.2.3.0Released 2026-05-13

New features

  • Purge All Pages — new danger-zone button in the admin UI that deletes every page registered in the CustomGPT project. The operation runs concurrently in configurable batches (operations.batch.size) and returns the exact count of pages deleted. A confirmation dialog prevents accidental use.
  • Project name display — the admin UI now shows the CustomGPT project name (resolved live from the API) next to the Project ID field, making it easy to verify the correct project is configured.
  • Configurable API base URL — the CustomGPT API base URL is now a first-class configuration property (org.jahia.community.modules.customgpt.apiBaseUrl, default https://app.customgpt.ai/api/v1/). This replaces the previously hardcoded value and allows connecting to custom or on-premise CustomGPT deployments.
  • React admin UI — a full settings panel is now available under Jahia Administration → CustomGPT.ai (/jahia/administration/customgptAiSettings). All configuration properties can be viewed and saved directly from the UI without editing .cfg files.
  • End-to-end test suite — Cypress tests covering the full indexing lifecycle (add site, index, update, delete page) with Docker Compose setup and CI scripts.

Improvements

  • Concurrent batch deletion — purgeAllPages now fetches all page IDs first (following next_page_url pagination) and then deletes them in parallel batches using a dedicated ExecutorService, significantly reducing purge time on large projects.
  • Detailed purge logging — each batch deletion logs the page ID, HTTP status, and running count so progress is visible in Jahia logs.
  • scheduleJobASAP auto-reset — after indexing jobs are scheduled with scheduleJobASAP=true, the flag is automatically written back to false via ConfigurationAdmin. This prevents repeated re-indexing on every module restart.
  • EVENT_TYPE_CONFIG_UPDATED_REQUIRE_REINDEX wired — changing indexable node types in the config now correctly triggers a full re-index of affected sites.
  • i18n — purge/danger-zone UI labels added for English, French, and German.
  • Vertical scrolling — the settings panel now correctly scrolls when the content overflows the available height.
  • Build automation — frontend-maven-plugin compiles the React UI automatically during mvn install (no separate yarn build step required).

Refactoring

  • JCR mapping node redesign — customGptPageId is now stored in a customgptIndex child node of type jnt:customGptIndexEntry directly under each indexed node. This replaces the previous approach of storing mapping nodes in a separate repository subtree (/sites/…/customgpt-index/…), which caused conflicts with Jahia's publication workflow.
  • Credential scoping — Basic Auth credentials for Jahia page rendering are now added per-request instead of via a global OkHttp3 interceptor, preventing accidental credential leakage on redirects or shared client instances.
  • Removed static executors — Service.java no longer uses static ExecutorService fields; executors are instance-managed and properly shut down on OSGi component deactivation.
  • GWT Guava removed — com.google.gwt.thirdparty.guava imports replaced with standard java.util.Objects and Java string utilities throughout.

Bug fixes

  • Fixed GraphQL admin query self-reference (admin.customGpt resolving to itself in introspection).
  • Fixed jmix:customGptIndexable mixin not being present on site nodes, causing customgptIndex child node creation to fail.
  • Fixed customgpt-index mapping node name collision with the jnt:customGptIndexContainer node type; renamed to customgptIndex.
  • Fixed unhandled NullPointerException on malformed CustomGPT API responses by adding safe JSON validation in extractPageId().
  • Fixed InterruptedException catches that swallowed the interrupt signal without calling Thread.currentThread().interrupt(), causing thread pool misbehaviour under shutdown.

Code quality

  • SonarQube quality gate passing on new code: 0 open issues, A ratings on maintainability, reliability, and security.
  • All public Java classes and methods documented with Javadoc.
  • GqlSettings DTO refactored to Builder pattern (eliminates 16-parameter constructor).
  • purgeAllPages decomposed into focused private helpers to meet cognitive-complexity limits.

Migration

If upgrading from a version that used the jmix:customGptIndexed or jmix:customGptFileIndexed mixins, run the provided Groovy cleanup script from the Jahia Groovy console:

scripts/cleanup-legacy-customgpt-mixins.groovy

This removes the legacy mixins and the customGptPageId property from all nodes in both EDIT and LIVE workspaces. A full re-index is required after migration.


Full Changelog: 1_0_2...1_0_3

Requires Jahia 8.2.3.0Released 2026-05-13
Requires Jahia 8.2.1.0Released 2025-06-12