Skip to content

v1.23 - AI-assisted report template generation

Choose a tag to compare

@hermanekt hermanekt released this 17 Apr 00:03

v1.23 - 2026-04-17

AI-assisted report template generation graduates from beta1 to the
stable 1.23 release, combining the original template wizard with a
second iteration that fixed everything real testers hit: template
mangling on save, limited provider choice, hard-coded timeouts, and
missing operator UI. The "AI-assisted" feature itself still wears a
"beta" label in the UI so operators know the LLM-generated output
needs human review, but everything around it (the Settings editor
for [admin.ai], the Tool Exposure bubble for the extensions group,
the Shortcuts widget category, the Report Header widget fix) is
stable and ready for general use.

Highlights

  • Visual editor gains a Shortcuts widget category (Logo + nine
    one-click variable chips) replacing the "Insert variable..."
    dropdown.
  • Seven LLM providers supported end-to-end: Anthropic, OpenAI,
    Google Gemini, Azure OpenAI, Ollama (self-hosted, API key
    optional), Mistral, Groq. Configurable from a new "AI Template
    Generation" section in /settings instead of hand-editing
    config.toml.
  • Server-side Jinja validation on every template save so a broken
    template never reaches /etc/zabbix-mcp/templates - the operator
    gets an actionable error in the editor instead of a preview that
    silently dies.
  • Auto-header removed from base.html; each builtin report
    (availability / capacity_host / capacity_network / backup / new
    showcase) owns its own header block, so custom templates have
    full control via the Shortcuts widgets.

Changes since v1.23b1 (beta1 -> stable)

Iteration on the v1.23b1 reporting beta. Focuses on the gaps real
testers hit in the AI template wizard (mangled Jinja on save,
unsupported providers, hard-coded timeouts) and on making the visual
editor emit templates that actually render.

Added

  • Admin portal UI for [admin.ai] - new "AI Template Generation" section at the bottom of /settings that mirrors the TOML config so operators no longer need to hand-edit config.toml to enable the AI wizard. Exposes Enabled toggle (drives a new [admin.ai].enabled key, defaults to True for backward compatibility), Provider dropdown, API Base URL (auto-locked for providers with a canonical endpoint so the field cannot be filled in by mistake, editable for Azure OpenAI + Ollama), API Key (masked password input with a Show toggle; leaving the field blank on save preserves the existing secret via a new SECRET_KEEP_EMPTY rule so operators do not have to re-paste on every save), Model (empty = provider default), Timeout (30-600 s), Max tokens (1000-32000). Settings writer now walks dotted section names (admin.ai), so deeper config sub-tables can reuse the same pattern in the future.
  • Five additional LLM providers - on top of Anthropic + OpenAI, the wizard now supports Google Gemini, Azure OpenAI (via operator-supplied deployment URL + api-version query param), Ollama (self-hosted; API key optional, driven by PROVIDERS_KEY_OPTIONAL), Mistral, and Groq. Anthropic and Gemini each get their own provider class; the OpenAI wire format is reused for Mistral/Groq/Ollama by swapping the base_url. Every new class is integration-tested against the real upstream endpoint (fake keys return provider-specific 401/403/400 errors, confirming the class is wired end-to-end). A PROVIDER_DEFAULTS registry is the single source of truth for default base URL + model per provider.
  • Shortcuts category in the visual editor - new draggable widget category alongside Zabbix / Layout containing a Logo block (wraps <img src="{{ logo_base64 }}"> with the HTML-comment if-trick so the Jinja control flow survives GrapesJS re-serialization) and one-click chips for every common template variable: Company, Subtitle, Period label, Period from/to, Availability %, Hosts count, Events count, Generated at. Replaces the old "Insert variable..." dropdown - widgets are first-class citizens now instead of a side pull-down menu.
  • "Use logo" toolbar button on image components - selecting any <img> in the visual editor now exposes a logo icon in the component toolbar that replaces the image with the Logo shortcut widget (full {% if logo_base64 %}<img src="{{ logo_base64 }}">{% endif %} block, not a mangled-src trait). Safer than a src attribute swap because GrapesJS's image component synchronously validates src against URL format and strips non-URL values like Jinja placeholders.
  • Showcase builtin report template - new showcase.html lives next to availability / capacity / backup and demonstrates every widget that ships with the v1.23 visual editor (gauge, metric cards, summary row, two/three-column layout, note callout, page breaks, host table, capacity bars, inline hosts loop, backup matrix, network interfaces). Intended as a starting point operators duplicate and trim down to the sections they actually need.
  • Server-side Jinja validation on template save - POST /templates/create and POST /templates/<id> now run the submitted HTML through the same SandboxedEnvironment + sample context render the AI wizard already used. A template with a syntax error is refused and the operator is returned to the editor with a specific error ("line 3: expected token ')', got 'integer'") instead of silently writing a broken file that explodes at every subsequent preview / PDF attempt. Legacy behavior preserved when the reporting extras are not installed.
  • Proper error page for preview failures - /templates/preview (both the POST-with-html form and the GET-by-id form) used to return a bare <p style="color:red">Template error: ...</p> on Jinja failures, which looked broken inside the preview iframe. Now returns a full HTML document with a styled error card (red title, error type, highlighted error message, and a hint listing the three most common causes: unbalanced {% if %}/{% endif %}, ternary written as (x y z) instead of x if cond else z, loop variable used outside its {% for %} block).

Changed

  • Default [admin.ai].timeout bumped from 60 s to 180 s - large reasoning models (Claude Opus, GPT-5) regularly take 90-150 s for a full template and the 60 s default reliably produced "The read operation timed out" errors in testing. 180 s gives headroom without making the UI wait indefinitely on a genuinely stuck call. Applies to all seven provider classes + the get_provider() fallback. Operators who want more can now bump it in the Settings UI (30-600 s range).
  • Auto-header removed from base.html - the automatic <div class="header">...subtitle + logo...</div> that every report inherited is gone. Each builtin template (availability / capacity_host / capacity_network / backup / showcase) now includes its own header block so operators have full control via the Shortcuts widgets when designing custom templates. Existing custom templates that extended base.html and relied on the auto-header will need an explicit header block - either drop the Report Header widget from the Zabbix category or paste the markup from any builtin template.
  • Report Header widget actually renders the logo - the v1.23b1 widget had <img src=""> (empty) because binding a Jinja placeholder to the attribute mangled through GrapesJS's URL validator. Switched to the HTML-comment if-trick (<!--{% if logo_base64 %}--><img src="{{ logo_base64 }}">...<!--{% endif %}-->) so the directive survives the visual editor round-trip and the img ends up with the actual base64 data URI at PDF render time.
  • AI generator no longer mangles the generated template on load - the old flow called editor.setComponents(generatedHtml) and then switchTab('html'), which round-tripped the Jinja through the GrapesJS HTML parser (moving <tr> out of {% for %} blocks, stripping inline styles, etc.) and synced the mangled output BACK into the textarea the operator would save. Now the generated HTML is written straight to the textarea and HTML mode is forced directly, bypassing GrapesJS entirely. When the operator later clicks Visual Editor on a template with Jinja control flow, a confirm dialog warns them that the parse can destroy working syntax.
  • AI system prompt tightened - explicit rules added so the LLM stops producing {{ 'yellow' 97 'red' }} style malformed ternaries (must be A if cond else B, nested for multi-way), empty {% for %}{% endfor %} shells with the <tr> body outside the loop, and unterminated {% if %} / {% endif %} pairs. These were the three patterns that kept showing up in v1.23b1 OpenAI GPT-5 output and made the saved template fail to render.
  • Provider dropdown copy rewritten for clarity - replaced "(none - require BYO)" (meaningless to anyone who does not know the BYO jargon) with "None - each admin pastes own key in modal". "(custom key)" variants in the AI modal became "(paste your key)". Section description rewrites explain that a server-side default is shared across admins while leaving the field as "None" forces every admin to paste their own key on every use.

Fixed

  • ReportEngine mutated shared module-level template registry - load_custom_templates() used to write into _REPORT_TEMPLATES (the module-level dict), so adding a custom template in one engine instance leaked into every other instance and made custom templates show up under "Built-in" in the dashboard. Now scoped to self._templates = dict(_REPORT_TEMPLATES) per engine instance.
  • Tool Exposure UI missing the extensions group - the Settings page had TOOL_DATA hardcoded with only the five Zabbix-API groups (monitoring / data_collection / alerts / users / administration), so the initMAX extension tools (graph_render, anomaly_detect, capacity_forecast, report_generate, action_prepare, action_confirm, zabbix_raw_api_call, health_check) were invisible to the bubble editor. Operators who wanted to disable reporting but keep monitoring had no way to do it from the portal. Added the extensions group to the UI; the tool allowlist hint and config.example.toml comments now list it too. Also corrected the stale config.example.toml claim that health_check / zabbix_raw_api_call were "always registered" - they have been in the extensions group and gated by _ext_allowed() since v1.16.

Initial v1.23 beta1 scope (2026-04-16)

The feature that made v1.23 and the reason the release exists in the first place:

  • AI-assisted report template generation (beta) - a new "Generate with AI" button on /templates/create and /templates/<id> opens a dialog where the operator describes the report they want in plain English ("Weekly SRE review with a big availability gauge, total-hosts/total-events summary row, and a table of hosts sorted by event count"). The dialog calls an LLM via [admin.ai] config, receives a valid Jinja2 template, validates it in the SandboxedEnvironment with the same sample context the preview uses, and loads it into the HTML editor. No valid template = clear validation error back to the operator so they can refine the prompt. Feature is off by default - add [admin.ai] to config.toml with provider + api_key to enable. Admin + operator roles only (viewer cannot generate).
  • POST /templates/generate endpoint - JSON API backing the UI, CSRF-protected, audit-logged (provider, model, request/response character counts, elapsed ms - not the request content to avoid leaking NDA'd descriptions). Returns 412 when AI is disabled, 400 on validation failure (malformed Jinja, unknown variables, sandbox denied operation), 502 on upstream LLM failure, 200 with {html, provider, model, elapsed_ms} on success.
  • [admin.ai] config section - provider, api_key (supports ${ENV_VAR} expansion so the key never hits the audit log or UI), model (empty = provider default), max_tokens, timeout. Documented in config.example.toml. Missing section or empty key = feature silently disabled, no error at startup.
  • src/zabbix_mcp/admin/ai_template.py module - provider-agnostic LLM abstraction (LLMProvider protocol + per-provider concrete classes using stdlib urllib so no new pinned dependency is needed), prompt builder with full variable catalog derived from reporting.data_fetcher return shapes, CSS class list from base.html, one worked example (availability.html), and validation via jinja2.sandbox.SandboxedEnvironment before returning to the UI. Sandboxed rendering means a malicious/hallucinated template cannot escape through Python introspection even if the operator tries to save it.