Files
chatwoot/enterprise/app/services/onboarding/help_center_creation_service.rb
Shivam MishraandGitHub d9c07fe2e9 feat: expose onboarding help center generation status (#14671)
This PR adds a reload-safe onboarding Help Center generation status
path. Previously, generation progress was pushed through ActionCable,
which meant the onboarding UI could lose context after a page reload or
missed websocket event. The new endpoint exposes the current generation
id, raw Redis generation state, and Help Center article/category counts
so clients can recover state by fetching from the backend.

**What changed**

- Added an onboarding Help Center generation status endpoint.
- Persisted `help_center_generation_id` when generation is enqueued.
- Removed Help Center generation ActionCable broadcasts completely.
- Kept generation progress in Redis as the backend source of truth.
- Kept Help Center generation out of the onboarding hot path; the
existing onboarding controller does not start generation yet.

**How to test**

1. Start Help Center generation for an account.
2. Fetch the onboarding generation status endpoint and verify it returns
the generation id, Redis state, and article/category counts.
3. Reload the onboarding UI or client state and fetch again to confirm
progress can be recovered without relying on websocket events.
4. Verify skipped/completed generation states are reflected from Redis.

## Related PRs

- https://github.com/chatwoot/chatwoot/pull/14569
- https://github.com/chatwoot/chatwoot/pull/14568
- https://github.com/chatwoot/chatwoot/pull/14567
- https://github.com/chatwoot/chatwoot/pull/14619
- https://github.com/chatwoot/chatwoot/pull/14565 (Primary onboarding
PR)
2026-06-10 16:24:37 +05:30

131 lines
3.5 KiB
Ruby

class Onboarding::HelpCenterCreationService
DEFAULT_PORTAL_COLOR = '#1f93ff'.freeze
LOGO_MAX_DOWNLOAD_SIZE = 5.megabytes
def initialize(account, user)
@account = account
@user = user
end
def perform
existing = existing_portal
return reuse_existing_portal(existing) if existing
@account.portals.create!(portal_attributes).tap do |portal|
attach_brand_logo(portal)
enqueue_article_generation(portal)
end
end
private
def existing_portal
@account.portals.first
end
def reuse_existing_portal(portal)
Rails.logger.info "[HelpCenterCreation] Reusing existing portal #{portal.id} for account #{@account.id}"
portal
end
def portal_attributes
{
name: portal_name,
slug: generate_slug,
color: portal_color,
page_title: portal_name,
header_text: header_text,
homepage_link: homepage_link,
channel_web_widget_id: web_widget_channel_id,
config: { default_locale: locale, allowed_locales: [locale] }
}.compact
end
def brand_info
@brand_info ||= (@account.custom_attributes['brand_info'] || {}).deep_symbolize_keys
end
def portal_name
brand_info[:title].presence || @account.name
end
def portal_color
hex = brand_info[:colors]&.first&.dig(:hex)
hex.to_s.match?(/\A#\h{6}\z/) ? hex : DEFAULT_PORTAL_COLOR
end
def header_text
brand_info[:slogan].presence || brand_info[:description].presence
end
def homepage_link
with_scheme(custom_attributes_website.presence || brand_info[:domain].presence)
end
def with_scheme(raw)
return raw if raw.blank?
raw.match?(%r{\Ahttps?://}i) ? raw : "https://#{raw}"
end
def custom_attributes_website
@account.custom_attributes['website']
end
def enqueue_article_generation(portal)
return if homepage_link.blank?
generation_id = SecureRandom.uuid
Onboarding::HelpCenterArticleGenerationJob.perform_later(@account.id, portal.id, @user.id, generation_id)
@account.update!(custom_attributes: @account.custom_attributes.merge('help_center_generation_id' => generation_id))
rescue StandardError => e
Rails.logger.error "[HelpCenterCreation] Failed to enqueue article generation for account #{@account.id}: #{e.class} - #{e.message}"
end
def attach_brand_logo(portal)
logo_url = brand_logo_url
return if logo_url.blank?
SafeFetch.fetch(logo_url, max_bytes: LOGO_MAX_DOWNLOAD_SIZE, allowed_content_type_prefixes: ['image/']) do |logo_file|
portal.logo.attach(
io: logo_file.tempfile,
filename: logo_file.original_filename,
content_type: logo_file.content_type
)
end
rescue StandardError => e
Rails.logger.error "[HelpCenterCreation] Logo attachment failed for account #{@account.id}: #{e.class} - #{e.message}"
end
def brand_logo_url
Array(brand_info[:logos]).filter_map do |logo|
logo.is_a?(Hash) ? logo[:url] : logo
end.find(&:present?)
end
def web_widget_channel_id
@account.inboxes.find_by(channel_type: 'Channel::WebWidget')&.channel_id
end
def locale
@account.locale.presence || 'en'
end
def generate_slug
slug_candidates.find { |slug| !Portal.exists?(slug: slug) } || fallback_slug
end
def slug_candidates
base = @account.name.to_s.parameterize.presence
return [] if base.blank?
first_token = base.split('-').first
[base, first_token, "#{first_token}-docs", "#{first_token}-help"].uniq
end
def fallback_slug
base = @account.name.to_s.parameterize.presence || 'portal'
"#{base}-#{SecureRandom.hex(4)}"
end
end