NETReporter.Client 3.0.0

NETReporter.Client

Tenký klient pro ReportServer: stejné rozhraní jako knihovna NETReporter, ale report se vykreslí na serveru. Aplikace nemusí nést QuestPDF, Roslyn, OxyPlot ani ZXing; přihlašuje se API tokenem, který si vygenerujete v UI ReportServeru.

Klient implementuje IReportRenderer a ITemplateTools z balíčku NETReporter.Abstractions, takže mezi lokálním a vzdáleným renderem přepíná jen registrace v DI. Kód, který renderuje, se nemění.

Instalace

dotnet nuget add source https://nuget.advisor-soft.com/v3/index.json --name AdvisorSoft
dotnet add package NETReporter.Client

Požadavky: .NET 10. Balíčky NETReporter, NETReporter.Abstractions a NETReporter.Client se vydávají společně se stejnou verzí.

API token

  1. Přihlaste se do UI ReportServeru a otevřete stránku API tokeny.
  2. Zadejte název (a případně dobu platnosti) a token vytvořte. Otevřený text se ukáže jen jednou, server si pamatuje jen hash; uložte ho do tajných hodnot (proměnná prostředí, user secrets, trezor), ne do repozitáře.
  3. Token má stejná práva jako jeho vlastník. Odvolat ho může vlastník i správce; deaktivace vlastníka ho vyřadí hned.

Rychlý start s DI

using Microsoft.Extensions.DependencyInjection;
using NetReporter;

services.AddNetReporterClient(options =>
{
    options.BaseAddress = new Uri("https://reports.example.com/api");   // adresa API včetně předpony cesty
    options.ApiToken = configuration["NetReporter:ApiToken"];           // nrp_…
});

// kdekoli v aplikaci
public sealed class InvoiceService(IReportRenderer renderer)
{
    public async Task<byte[]> CreatePdfAsync(Stream template, Stream data)
    {
        await using var pdf = await renderer.RenderAsync(template, data);
        using var buffer = new MemoryStream();
        await pdf.CopyToAsync(buffer);
        return buffer.ToArray();
    }
}

Nastavení jde číst z konfigurace: services.AddNetReporterClient(configuration) čte sekci NetReporterClient s klíči BaseAddress, ApiToken a Timeout. Chybějící adresa nebo token zastaví aplikaci při startu srozumitelnou chybou.

Bez DI

using NetReporter.Client;

using var http = NetReporterClient.CreateHttpClient(new NetReporterClientOptions
{
    BaseAddress = new Uri("https://reports.example.com/api"),
    ApiToken = Environment.GetEnvironmentVariable("NETREPORTER_API_TOKEN")
});

IReportRenderer renderer = new ReportServerRenderer(http);
ITemplateTools tools = new ReportServerTemplateTools(http);

Co klient umí

  • Render libovolné šablony (ad-hoc) do všech formátů: PDF, HTML, JSON, SVG, WebP, PNG, JPEG, i po stránkách (RenderSvgPagesAsync, RenderWebpPagesAsync, …) a se všemi volbami RenderOptions (PDF/A, přílohy, šifrování, vodoznak, rozlišení…).
  • Diagnostiky bindingů přes RenderOptions.OnBindingDiagnostics a pozorovatele pipeline.
  • Nástroje nad šablonou (ITemplateTools): validace šablony a dat, datový kontrakt a ukázková data.
  • Mimo rozsah: správa uložených šablon, render podle uložené šablony a historie jobů (ty zůstávají v UI a REST API).

Čím se vzdálený render liší od lokálního

  • Soubory z disku se na serveru nenačítají: obrázky, SVG ani podklady PDF z cesty k souboru nefungují (použijte data URI nebo adresu http/https).
  • Server omezuje počet stránek, rozlišení obrazů a velikost příloh; diagnostik bindingů vrací nejvýše 50 různých.
  • RenderOptions.Icons se nepoužije: server vykresluje ikony vlastním katalogem.
  • Pozorovatel pipeline dostane zjednodušené události (parsování proběhlo na serveru).

Chyby

Situace Výjimka
Chyba v šabloně ReportServerException (kód TEMPLATE_VALIDATION_FAILED, Diagnostics s JSON cestami)
Vadné volby ArgumentException (ParamName == "options"), u lokálních kontrol bez volání serveru
Překročený limit stránek RenderPageLimitExceededException
Zrušení volajícím OperationCanceledException
Neplatný, odvolaný nebo prošlý token ReportServerException (AUTH_UNAUTHORIZED)
Server nedostupný nebo překročený Timeout ReportServerException (REPORTSERVER_UNAVAILABLE nebo REPORTSERVER_TIMEOUT)

ReportServerException dědí z InvalidOperationException, takže ji zachytí i kód napsaný pro lokální render. Seznam změn je v souboru CHANGELOG.md v balíčku.

No packages depend on NETReporter.Client.

- Template fonts (`meta.fonts`): hardening against malformed font files, which native code (Skia, FreeType, HarfBuzz) reads in the process. Before a font from any source (asset, data URI, URL, file) is registered, its structure is checked (`NetReporter.Resources.FontFileCheck.TryValidate`, a public method hosts can call too, for example when a font is uploaded): the table directory, `loca` and `glyf` (every glyph: points, flags, coordinates, composite glyphs), `cmap` subtables, `name`, CFF indexes and the counts in `maxp`, `hhea` and `hmtx` must be consistent and lie inside the file. A damaged font is the warning `NRP045` and the text uses the fallback font; the ReportServer rejects such an upload with `415` and the reason. The check is lenient about what the readers cope with (bad checksums, unsorted or unknown tables), so fonts that work in practice are still accepted. The copy of the font that is registered no longer contains embedded bitmaps, colour layers and SVG glyphs (`sbix`, `CBDT`/`CBLC`, `EBDT`/`EBLC`/`EBSC`, `COLR`/`CPAL`, `SVG `): a document is drawn from outlines and these tables feed the riskiest parts of the readers, so a colour or bitmap font is drawn from its outlines only. - `ResourcePolicy.Limits.MaxFontBytes = 0` switches custom fonts off: `meta.fonts` is ignored with the warning `NRP045` and nothing is loaded or registered (`SECURITY_RESOURCES_MAX_FONT_BYTES=0` for the ReportServer renders, `ReportServer:Content:Assets:MaxFontBytes=0` stops font uploads with `415`). A negative limit is still invalid. Reading fonts in a separate process is not offered (ToDo #164). - Template editor: the button **Balíček** exports a template stored in the library together with the images, PDFs and fonts it refers to as a zip bundle (the working copy, or a published version with the assets pinned when it was published) and imports such a bundle as a new private, unpublished template (a name clash adds the suffix `-2`, assets with the same content you already have are reused). The dialog **Assety** has the new section **Obrázky zapsané přímo v šabloně**: *Zkontrolovat* counts the `data:…;base64,…` values of at least 64 kB in the template, *Přesunout do assetů* replaces them with `asset:{id}` and loads the template in its new form. Both ask to save unsaved changes first, because the server works with the saved working copy. - MCP: new tools `export_template_bundle` (a template with its assets as a base64 zip, up to 5 MB; the working copy or a published `version`) and `import_template_bundle` (writer role; creates a new private unpublished template, reuses assets by content, never overwrites). - `NetReporter.ReportServer.Client`: `ContentApiClient.ExportTemplateBundleAsync` returns the bundle together with the file name the API suggests (`ExportTemplateAsync` is unchanged). - **Behavior change** MCP: user API tokens (`nrp_…`, `MCP_AUTH_USER_TOKENS=true`) are no longer checked in the ReportServer database. The MCP asks the ReportServer API instead (`GET /auth/me` with the token being checked, so the API applies the same rules as for every request and records the last use), which means the MCP needs no database access: `MCP_AUTH_USER_TOKENS_CONNECTION_STRING` is ignored, the script `deploy/k8s/sql/mcp-user-tokens-role.sql` and the check `user-token-db` of `/healthz` are gone, and the Secret entry can be removed (the DB role `netreporter_mcp`, if it was created, can be dropped). `MCP_REPORTSERVER_URL` is now **required** with the switch on (the MCP does not start without it). A valid token is remembered for `MCP_AUTH_USER_TOKENS_CACHE_SECONDS` (default 5, 0 = ask the API on every request), so a revoked token or a deactivated account stops working in the MCP at the latest after that time; the tools that call ReportServer check the token with the API on every call. The MCP passes the client address in `X-Forwarded-For`, so the API counts failed checks for the real client and not for the MCP as a whole. The MCP image no longer carries EF Core, Npgsql, the AWS SDK and FluentStorage. - ReportServer API: `GET /auth/me` also returns `tokenId` (the id of the API token the caller used, `null` for a JWT). - ReportServer: `POST /admin/content/drop-legacy?target=all|legacy|editor&confirm=false&acceptSkipped=false` (Admin) deletes the old storages that the shared content store replaced: the schema `legacy` (the old tables `ReportTemplates` and `TemplateVersions`, which the migration `AddContentSchema` moved aside) and the schema `editor` (the database of the old editor, read from `EDITOR_CONNECTION_STRING`). Without `confirm=true` it only reports (`present`, `rows`, `canDrop`, `blockers`, `dropped`). `legacy` is dropped only after the API imported the templates (the flag `LegacyTemplatesImportedUtc`); `editor` only when a dry run of `import-editor` finds nothing left to import (an item that would be skipped, for example of a deleted account, needs `acceptSkipped=true`). The drop is irreversible (`DROP SCHEMA … CASCADE`): make a `pg_dump` first. `ContentApiClient.DropLegacyAsync` calls it. The import at start and the deletion of an account skip the old tables once they are gone. - **Behavior change** ReportServer: deleting an account (`DELETE /users/{id}`) no longer deletes rows from the database of the old editor (`editor."Templates"`, `"Snippets"`, `"Themes"`); the editor has no database any more. Rows of a deleted account that remain there are skipped by `import-editor` and must be confirmed with `acceptSkipped=true` before the schema `editor` is dropped. - **Behavior change** MCP Docker image: it no longer carries the bundled samples, snippets and themes (`dotnet publish -p:BundleSampleContent=false`); the container reads them from the shared content store, so it needs `MCP_REPORTSERVER_URL` (k3s already sets it). The local MCP over stdio without a ReportServer still bundles them. - ReportServer: template bundles. `GET /content/templates/{id}/export?version=` returns a zip (`netreporter-bundle` v1: `manifest.json`, `template.json` and `assets/{sha256}{ext}`, each content once) with the template and the assets it refers to that the caller may read (never an icon package); with `version` the content pinned at publication is used. `POST /content/templates/import?name=` (the zip as the request body) creates a private, unpublished template of the caller: an asset the caller already owns with the same SHA-256 is reused, otherwise a new one is created and the references in the template are rewritten; a name that exists gets a suffix (`Invoice-2`). The zip is untrusted input: only the files the manifest lists with the exact path `assets/{64 hex}{.ext}` and the matching hash are read (names are never used as paths), the actually unpacked bytes and the number of entries are limited (`ReportServer:Content:Bundles:*`: 60 MB body, 120 MB unpacked, 300 entries, 100 assets), every asset goes through the checks of an upload (type from content, SVG check, limit per kind), nothing is saved on an error. `ContentApiClient.ExportTemplateAsync`, `ImportTemplateAsync`, `ExtractAssetsAsync` and `ExtractDataUrisAsync`. - ReportServer: large data URIs of a template become assets. `POST /content/templates/{id}/extract-assets?minBytes=&dryRun=` replaces every whole-string value `data:…;base64,…` of at least `minBytes` (default 64 KiB) of the working copy by `asset:{id}` of an asset of the caller (the same content is reused); the JSON is not rewritten, only the strings are replaced, so the formatting stays. Unknown or too large content and unsafe SVG are skipped. `POST /admin/content/extract-data-uris?minBytes=&dryRun=` (Admin) does it for the working copies of all templates, the asset always belonging to the owner of the template; published versions are immutable and stay as they are. Start with `dryRun=true`. - MCP: the description of `upload_asset` said that uploading the same content again returns the existing asset; it does not (every upload creates an asset, the content is stored once). - **Behavior change** FontAwesome Pro icons come from the shared content store. The template editor no longer downloads FontAwesome from npm and no longer reads `FONTAWESOME_PRO_TOKEN` / `FONTAWESOME_VERSIONS` (the install service and the install-at-start service are gone); the licence key is now only a setting of the ReportServer API. An administrator installs a package with `POST /admin/content/iconpacks/install` (`{ "edition": "pro", "version": "7" }`; the buttons of the dialog *Emoji a ikony* in the editor call it): the API downloads it from npm (Pro with `FONTAWESOME_PRO_TOKEN`, which is never returned) and stores it as a system asset of the kind `iconpack` (the blob store, never inline). `GET /content/iconpacks` and `…/{id}/content` (roles Editor and Admin only, `ETag` = SHA-256, `Cache-Control: private, no-store`) serve it to the hosts; it does not show in `/content/assets`, `asset:{id}` cannot point to it and it is never part of an export. `GET /admin/content/iconpacks` shows the state (installed packages, whether the key is set) and `DELETE /admin/content/iconpacks/{id}` removes a package. - ReportServer: icons of FontAwesome Pro in every render. The API unpacks the installed packages to `NETREPORTER_ICONS_DIR` at start and every 5 minutes (`IconPackSyncWorker`, the other replicas follow within 5 minutes) and passes its `IconCatalog` as `RenderOptions.Icons`, so `light`, `duotone` and `sharp-*` icons are drawn instead of their Free substitute (warning `NRP041`) for all templates. The editor takes the packages over from the server when a user opens the page (at most once per 5 minutes, only for the roles Editor and Admin; with `EDITOR_REPORTSERVER_API_TOKEN` also in the background) and MCP takes them over in the background under its service account (which then needs the role Editor or Admin); `render_report` of MCP draws Pro icons too. A package takes about 50 MB per instance that loads it. - `NetReporter.ReportServer.Client`: `IconPackSynchronizer` (keeps a directory of icon packages in step with a source by SHA-256, replaces files atomically, removes only what it wrote, then reloads the `IconCatalog`), `IIconPackSource`, `HttpIconPackSource` and `IconPackSyncLoop`; `ContentApiClient.ListIconPacksAsync`, `DownloadIconPackAsync`, `GetIconPackStatusAsync`, `InstallIconPackAsync` and `DeleteIconPackAsync`. The project now references `NetReporter`. - Template format: custom fonts, `meta.fonts`. An object `alias -> source` (or an array of sources, one per face of the same family: regular, bold, italic...) names a TTF or OTF font that the template brings with it; the alias is then written to `font` and `fontFallbacks` of a style like any other font. A source is an `asset:{id}`, a data URI, an http(s) URL or a file, loaded through the same gate as images and PDFs (the resolver first, then `ResourcePolicy`: files, addresses, `Limits.MaxFontBytes` 10 MB, time limit), or `{{$.Assets.Name}}` to reuse a `meta.assets` entry. Weight and slant of a face are read from the file, so `bold` and `italic` pick the right face. At most 16 fonts with 8 faces each. QuestPDF's font registry is global to the process and cannot be emptied, so a font is registered under a name made of its content (`nrf-` and 16 hex digits of the SHA-256 of its faces; the registered copy of the file has its family and PostScript names replaced, the outlines are untouched): the same fonts of different templates share one registration, different fonts of the same family never mix, and a font of one user never lands in a document of another. The process keeps at most 256 faces and 256 MB; further fonts are refused. PDF and the image formats embed the font, HTML gets `@font-face` rules with data URIs (a face up to 2 MB), SVG only names it. A font that cannot be used (not found, not readable, not TTF or OTF, a TTC or WOFF file, two faces with the same weight and slant, over a limit, a malformed definition) is the warning `NRP045` (`DiagnosticCodes.FontUnavailable`) with the JSON path, the text is rendered with a fallback font and the render does not fail. A font source that fails is not reported a second time as `NRP044`. - ReportServer: fonts as assets. `POST /content/assets` accepts TTF and OTF (kind `font`, content type `font/ttf` or `font/otf`, recognized from the content: the table directory is checked, a collection (TTC) or WOFF is `415`), limited by `ReportServer:Content:Assets:MaxFontBytes` (10 MB). A font always goes to the blob store (S3 or disk), even below `InlineThresholdBytes`, when a store is configured. A reference in `meta.fonts` counts as a use of the asset like any other `asset:{id}` and is pinned to a published version. The template editor's dialog **Assety** and MCP (`upload_asset`, `list_assets` with `kind=font`) accept fonts too. - **Behavior change** MCP with `MCP_REPORTSERVER_URL`: the samples, snippets and themes (`list_samples`, `get_sample`, `list_snippets`, `get_snippet`, `list_themes`, `get_theme` and the resources `netreporter://samples/...`, `snippets/...`, `themes/...`) come from the shared content store of ReportServer, read with the caller's token, instead of the files shipped with the MCP server. Besides the system (shipped) items the lists now include the caller's own snippets and themes (made in the template editor) and those other users made public; other users' private items are never visible. Each item has `source` (`system` | `own` | `shared`; `bundled` without ReportServer) and `uid`; when several items share an id, their `resourceUri` carries the `uid` and `get_snippet` / `get_theme` accept an `id` or a `uid` (own first, then system, then shared). `resources/list` is built per caller. Without ReportServer (typically the local stdio mode) the server still uses the shipped files. An unreachable ReportServer is an `InternalError` with a clear message. - MCP: working copy and published versions of templates. `update_template` has the new parameter `publish` (default `true`, as before: validates and publishes a new version); `publish=false` only saves the working copy without validation. New tools `publish_template` (validates in Strict mode, creates the next version, a diagnostics list with the error), `list_template_versions` (versions with notes, `publishedVersion`, `hasUnpublishedChanges`) and `get_template` with `draft=true` (the working copy instead of the published version; before, a template saved in the template editor but not published had empty content). `render_template` renders the last published version. - MCP: assets in templates. New tools `list_assets` (own, other users' public and system images and PDFs, each with its `asset:{id}` reference) and `upload_asset` (writer role, base64 or a data URI, up to 5 MB; the type is detected from the content). `render_report` resolves `asset:{id}` references in the template with the caller's rights (an asset the caller cannot read renders as a placeholder with the warning `NRP044`); `render_template` does it on the server. - `NetReporter.ReportServer.Client`: `ContentApiException.Diagnostics` carries the validation diagnostics the API returns when a template cannot be published. - **Behavior change** Template editor: it no longer has its own storage. Templates, snippets, themes, sample data and assets are read and written only through the ReportServer API (`/content/*`), in the shared content store. `EDITOR_REPORTSERVER_URL` (the API address with its path, for example `http://netreporter-api:8080/api`) is **required**, the editor does not start without it. With the shared sign-in the editor calls the API on behalf of the signed-in user (a short JWT issued from the cookie with the same `JWT_SIGNING_KEY`, `JWT_ISSUER` and `JWT_AUDIENCE` as the API), without it with `EDITOR_REPORTSERVER_API_TOKEN`. Removed: the folder mode and the database mode (`EDITOR_CONNECTION_STRING`, `ConnectionStrings:EditorDb`, `EDITOR_TEMPLATES_PATH`, `TemplatesPath`, `SampleDataPath`, `SnippetsPath`, `ThemesPath`, `TemplateLibrary:SamplesFolder`, `TemplateLibrary:WarmupThumbnails`), the copies of `sampleTemplates`, `sampleData`, `snippets` and `themes` in the image (the API serves them as system content). The editor pod is stateless: k3s and the Docker Compose sample no longer have its volume (`deploy/k8s/02-pvc.yaml` is removed) and it is rolled out with a rolling update. Upgrade: import the old content once with `POST /admin/content/import-editor` or `import-folder` (see the docs) before the new editor goes live. - Template editor: **Publish** saves and publishes the template as a new version (the server renders the last published version; the toolbar shows "Not published", "Published v3" or "Published v3, unpublished changes"), the dialog **Verze** lists the published versions and restores one as the working copy, the dialog **Assety** uploads images and PDFs to the shared store, copies the reference `asset:{id}` and deletes assets. Preview and thumbnails resolve `asset:{id}` through the API as the signed-in user. - ReportServer API: `GET /content/{kind}?includeContent=true` returns the content of snippets and themes in the list (one call for the whole library); `GET /content/{kind}/{id}` and `/lookup` accept `includeContent=false`. `NetReporter.Identity`: `NetReporterUserTokenCache` (a short JWT for calls to the API on behalf of the signed-in user, shared by the ReportServer UI and the editor). - ReportServer: import of the old editor storage into the shared content store, `POST /admin/content/import-editor` (the editor database, schema `editor`, read from `EDITOR_CONNECTION_STRING` in the API configuration; templates saved without sign-in go to `localOwner`) and `POST /admin/content/import-folder` (the editor folder as a zip: templates in the root, `snippets/`, `themes/`). Admin only, repeatable, with a `dryRun`; imported items are working copies without publication, an item whose key already exists with the same content is merged, with different content it is imported next to it as "{name} (editor)"; nothing is overwritten. - **Upgrade note** ReportServer: the migration `AddContentAssets` adds the tables `content.Blobs`, `Assets`, `ItemAssetRefs` and `VersionAssetPins` (nothing existing changes). - ReportServer: assets in the shared content store, `/content/assets`. Images (PNG, JPEG, GIF, WebP, BMP, SVG) and PDFs are uploaded as the request body (the type is recognized from the content, SVG is checked for scripts, events and external references), limits per kind (10 MB images, 25 MB PDF; `ReportServer:Content:Assets:*`), identical content is stored once (SHA-256): small blobs in the database, larger ones in the blob storage (S3 or disk), all in the database when no blob storage is configured. A template refers to an asset by `asset:{id}`. Download is served with `nosniff` and a strict CSP (SVG and PDF as attachments), `ETag` is the content hash, replacing the content needs `If-Match`, deleting an asset that items refer to needs `force` (`409 ASSET_IN_USE`). Unused blobs are removed after a retention period (`ContentBlobGcWorker`). - ReportServer: rendering resolves `asset:{id}` from the content store. Publishing a template version pins the content of the assets it refers to, so a render of that version is reproducible after the asset changes and works for a reader who cannot read the asset himself; a missing or inaccessible asset gives the same `NRP044` diagnostic. The content hash of the assets is part of the render cache key. `POST /render/by-template/{id}` now also renders public and system templates (the working copy `draft` only the own one). - `NetReporter.ReportServer.Client`: asset methods of `ContentApiClient` and `ContentResourceResolver` (resolves `asset:{id}` through the API with the caller's token, caches metadata for 30 s and content by hash). - ReportServer: bundled system content. At start the API fills the shared content store with the sample templates and their data, the ready-made snippets and the themes (the folder `content/` next to the application, `ReportServer:Content:BundledPath` to change it): items of the system owner, public and read-only (a user copies one to edit it), with stable ids and the templates published as version 1. The seed is idempotent (a fingerprint of the bundle in `ConfigEntries`), updates changed files, removes items whose file is gone and does not overwrite the content of a newer API version during a rolling update; a failure is logged and does not stop the API. New internal project `NetReporter.ReportServer.Client` with `ContentApiClient`, `ContentApiException` and `BearerTokenHandler` for the editor and the MCP server. - ReportServer: the shared content store has an HTTP API `/content/{kind}` (`templates`, `snippets`, `themes`, `datasets`) for the roles User, Editor and Admin (the new policy `ContentAuthor`; JWT or API token). List with `scope` (own, shared, system, all), `search`, `category` and `linkedTemplateId`; detail with `ETag: "r{revision}"` and `304`; create (a duplicate key is `409 CONTENT_KEY_DUPLICATE` with the existing id); copy; metadata and visibility changes that do not touch the revision; delete. Saving the working copy (`PUT /content/{kind}/{id}/content`) requires `If-Match` with the revision or `*` (`428` when missing, `412` with `currentRevision` on a mismatch, `413` over 2,000,000 characters) and does not validate the content. Templates also have `publish` (Strict validation, idempotent when the working copy equals the last version), the list and detail of immutable versions (`ETag: "v{n}"`) and `restore` (a version becomes the working copy). Own, public and system items are readable, only own ones are writable; an inaccessible item is `404`. - **Upgrade note** ReportServer: templates move to the shared content store. The migration `AddContentSchema` moves the tables `ReportTemplates` and `TemplateVersions` to the schema `legacy` (nothing is deleted) and creates the schema `content` (`content."Items"` and `"ItemVersions"`); at start the API takes the templates over once (`LegacyTemplateImporter`: the ids of templates and versions, the owner and the times stay, the last version becomes both the working copy and the published version, deleted templates are skipped, a template whose name already exists for the owner in another letter case gets a key with the suffix `-2`, `-3`…). The import runs in one transaction under a database lock, writes a flag to `ConfigEntries` (scope `Content`) and later starts skip it; if it fails the API stays not ready (`/readyz` 503) instead of running without templates. **Back up the database and do not run old and new API replicas together** (old ones look for the tables in `public`): scale the API to 0, deploy, scale to 1, wait for `/readyz`, then scale up (see `deploy/k8s/README.md`). The database role of the MCP server has no access to the new schemas. - ReportServer: a template now has a working copy and published versions. `POST /templates` creates it as version 1 and `PUT /templates/{id}` saves and publishes a new version (as before); `GET /templates/{id}` returns the published version (an unpublished template that exists only as an editor draft has version 0 and no content). `POST /render/by-template/{id}` renders the last published version, and the new request fields `version` (an older published version) and `draft` (the working copy) choose another one (together: `400 RENDER_INVALID_REQUEST`; an unknown version `404`; a template that was never published `409 CONTENT_NOT_PUBLISHED`). The render job records the rendered version (`RenderJob.TemplateVersionNumber`, `templateVersionNumber` in the job detail; `null` for ad-hoc and `draft`). **Behavior change:** template names are compared case-insensitively (`Faktura` and `faktura` are one name), and `PUT` answers `409 CONTENT_REVISION_CONFLICT` when the template was changed by someone else in the meantime. - Asset references and a resource resolver: a template can name an image, a PDF or a font by `asset:{id}` (a GUID, 32 hex digits or with hyphens) in `meta.assets`, `Image.src`, `watermark.image`, `page.letterhead.src`, `meta.document.append` or in the data (`{{$.LogoRef}}`), and the host looks it up with the new `IResourceResolver` passed in `RenderOptions.ResourceResolver` (`ResolveAsync(reference)` returns `ResourceResult.Success(data, contentType)`, `Failed(reason)` for a source it knows and will not give, or `NotHandled`, which lets the render look further as before; `AssetReference` parses, formats and finds the references). Without a resolver `asset:...` is plain text as before. The resolver is a trusted source: `ResourcePolicy` does not apply to it (it has to guard what the caller may read itself), the size limits per kind, the limit of sources and bytes per render and `ResourceLimits.DownloadTimeout` do. A reference is looked up at most once per render and never for a data URI; the references in the template and in the data are fetched up front and in parallel (at most 4 at a time, at most 64 references), the others when the layout needs them. A reference that cannot be loaded (missing, not allowed, too large, no answer, the resolver threw) is the new warning `NRP044` with the JSON path of the place (`$.meta.assets.Logo`, `$data.LogoRef` in the data), the image is replaced by the placeholder and a PDF source is left out (PDF sources also report `NRP036`); the reason a resolver gives is shown to the template author, an exception is only logged. HTML inlines an image from a resolver as a data URI up to 2 MB, the JSON output reports `resolved` or `resolver-failed`. The resolver is not sent over the wire (the thin client logs a warning). - ReportServer: blob storage with a choice of backend. New `IBlobStore` (`FluentBlobStore` over FluentStorage) keeps files either in S3/MinIO or in a directory on disk (a PVC), selected by `ReportServer:Storage:Provider`: `auto` (the new default - S3 when `Bucket` and `Region` or `ServiceUrl` are set, otherwise disk when `DiskPath` is set), `s3` (the old names `aws.s3` and `minio.s3` still mean S3) or `disk`. The render output store (the PDF behind the render cache) runs on it, so the render cache no longer needs S3: set `ReportServer:Storage:Enabled=true` and `ReportServer:Storage:DiskPath` (env `REPORTSERVER_STORAGE_DISK_PATH`, an absolute directory such as a mounted volume; it is read from the process configuration only, never from the database/`/admin/config`, so the web UI cannot choose where the server writes). With several API replicas the volume must be shared (`ReadWriteMany`), otherwise keep S3. A storage `Prefix` containing `.` or `..` segments is refused, so it cannot lead out of the directory. The health check is now `blob-storage` (was `s3-storage`; it also writes and removes a probe file on disk). **Breaking** for monitoring that matches the check name `s3-storage` in `/healthz`. - ReportServer: saving a template (`POST /templates`, `PUT /templates/{id}`) no longer writes a copy of the version to the storage - nothing ever read those copies (the database is the source of truth). Copies left by older versions are not read; deleting an account (`DELETE /users/{id}`) removes them together with the render outputs of that user. - **Behavior change** Resource policy: the new `RenderOptions.ResourcePolicy` (`NetReporter.Resources.ResourcePolicy`) controls in one place how a render loads the sources a template names - images and SVG (`Image.src`, watermark), PDF (`page.letterhead`, `meta.document.append`) and, later, fonts. `LocalFiles` (`Any`, `AllowedDirectories`, `Deny`) decides where on disk a render may read (the path is canonicalised, a file must lie under one of `AllowedDirectories` and no symlink or junction may lead out of it), `Remote` (`Any`, `PublicOnly`, `AllowedHosts`, `Deny`) where it may download from: `PublicOnly` checks the IP address the render really connects to (after DNS resolution, on every new connection), so loopback, private networks, link-local including the cloud metadata address `169.254.169.254`, CGNAT, multicast and reserved IPv4 and IPv6 ranges are refused even after a redirect or through a name that points to them (DNS rebinding); `AllowedHosts` allows only the listed hosts (`cdn.firma.cz`, `*.firma.cz`, an IP address) including every redirect; both run without the system proxy, follow at most 3 redirects and refuse a URL with credentials. `Limits` sets the largest image (10 MB), PDF (25 MB) and font (10 MB), the download timeout (30 s) and how many sources and bytes one render may download (64, 128 MB). A refused file, a missing file and a file outside the allowed directories give the same result (the NRP036 text, the placeholder image, the JSON reason `file-not-found` or `local-sources-disabled`), and so do a refused address and a network error: nothing is revealed about the environment, the reasons go to the host log only. The cache of downloaded images is split by policy, so data downloaded by an unrestricted render never reaches a stricter one. `ResourcePolicy.Default` (used when no policy is set) keeps the old freedom, `ResourcePolicy.Server` (files denied, `PublicOnly`) is meant for hosts that render foreign templates; `ResourcePolicyOptions.ToPolicy` builds a policy from host configuration. `AllowLocalImageSources` and `AllowLocalPdfSources` stay as shortcuts (`false` turns files off for that kind regardless of the policy). **What changes for existing code:** even with no policy a render now applies the limits - an image larger than 10 MB or a download longer than 30 s (before: no size limit, the 100 s default of the HTTP client) is refused; raise them with `Limits`. The JSON output has the new availability reason `remote-sources-disabled`, and HTML output replaces an image address the policy does not allow with the placeholder image. - ReportServer, MCP and editor use the resource policy: the API applies `ResourcePolicy.Server` to every render (files denied, downloads only from public addresses; before: files were denied but any address could be downloaded, so a template could reach the internal network) and the administrator widens it only on purpose with `Security:Resources:*` (env `SECURITY_RESOURCES_*`; an invalid value stops the start). The MCP server over HTTP uses `ResourcePolicy.Server` too (before, images could be read from files of the server and PDF sources only were denied), over stdio `ResourcePolicy.Default`; settings `NetReporterMcp:Resources:*` (env `MCP_RESOURCES_*`). The template editor uses `ResourcePolicy.Server` when sign-in is on (a shared server with foreign templates) and `ResourcePolicy.Default` as a single-user local tool; the section `Render:Resources` (env `Render__Resources__*`) overrides single values. Not sent over the wire: the thin client and the API never accept a policy from a caller. - Quick fixes of validation diagnostics: new `NetReporter.Template.Fixes.DiagnosticFixProvider` (`GetFixes` offers `QuickFix` objects for a diagnostic and a template as `JsonNode`, `ApplyFixes` applies them in order and returns a `QuickFixReport`). It renames a misspelled property named by the diagnostic's hint (`NRP012`, `NRP013`), replaces an invalid enum value by the closest allowed one (`NRP015`), points an unknown style reference at the closest style or creates the missing style empty (`NRP026`), adds an empty `styles.base` (`NRP027`) and the missing table cell of a column (`NRP022`), puts `#` before a chart hex color (`NRP029`), spreads `marginMm` / `paddingMm` over the sides without a value of their own (`NRP020`, `NRP021`) and removes margins from a paragraph span style (`NRP030`, the only fix that is not safe). The messages of `NRP012` (an unsupported node property) and of an unsupported property in a nested object now end with the hint `Myslíte 'x'?` when a similar name exists, as `NRP013` in styles already did. MCP: new tool `apply_quick_fixes` (`templateJson`, `validationMode`, `includeUnsafe`) returns the corrected template, the applied fixes, the skipped diagnostics and the validation of the result; the prompt `fix_template_diagnostics` and the server instructions mention it. Template editor: the diagnostics panel has a **Fix** button per diagnostic and **Fix all safe** (also in the command palette), each one undo step. - Template editor: the **Data** panel under the structure tree shows the fields of the preview data (sample or custom JSON) together with the fields the template reads (the data contract), with kind, sample value and a mark for fields the template reads but the data lacks. A click inserts `{{$.Path}}` at the caret of the text field the user edited last (property grid, table editor, page settings, JSON editors), or creates a `Text` node when there is none (the `+` button always does); inside a `repeat` or a table the path goes through the loop variable (`{{item.Total}}`), `repeat.source` / `rows.source` get the bare path and `sortBy` the bare path with the loop variable. Typing `{{` in a text field suggests the data paths (loop variable fields first). New `DataPanelBuilder`, `DataBindingPaths` and `RepeatScope` in the editor; `DataFieldTreeNode` is a record with the new `Kind`, `SampleValue`, `Origin` and `Bindable`. - Template editor: more keyboard shortcuts and a command palette. `Ctrl+C` / `Ctrl+V` copy and paste a node, `Ctrl+D` duplicates the selected node right after itself (one undo step), `Alt+Up` / `Alt+Down` move it among its siblings (quick repeats of one node are one undo step), `Ctrl+S` saves the template, `F5` renders the preview (switching to the Preview tab first) and `Ctrl+K` opens the command palette. The palette searches, without diacritics, the commands of the editor (add a node of every type, insert a ready-made block, copy, duplicate, delete, move, undo, save, load, render, switch a tab...) and the nodes of the template by label, type or ID ("Go to"); it offers only what can be done in the current state. `Ctrl+S`, `F5` and `Ctrl+K` also work in text fields (`Ctrl+K` not in the JSON code editor, where it starts chords), the others only outside of them; the browser default of the claimed keys (save page, reload, bookmark) is suppressed. The shortcuts are one table, `EditorShortcuts.Bindings`, shared by C# and the JavaScript key handler, which now sends only the keys the table knows. `Delete` no longer reacts to `Ctrl`, `Shift` or `Alt` held with it. New `TemplateTreeService.ResolveDuplicateTarget`, `MoveWithinParent` and `GetMoveBlockReason`. - C# expressions `{{# … }}`: every different expression no longer holds 15-40 MB of native memory until the process ends (the 9 expressions of a typical invoice took ~450 MB). Roslyn scripting looked up the assemblies referenced by the references again in every compilation, which loaded a fresh native copy of their metadata (~38 MB per compilation); `CachingMetadataReferenceResolver` shares them between compilations, so a further expression costs a few hundred KB and ~25 ms (a template with 10 expressions: 368 → 118 MB of the whole process). The start of Roslyn (~60 MB, ~1 s) is paid once, with the first expression. Results and diagnostics of expressions are unchanged. - C# expressions: at most `RenderOptions.MaxCSharpExpressions` (default 2000, 0 or negative turns the limit off) different expressions are compiled in a process; a further new expression is evaluated as an empty value and the render reports the warning `NRP043` (`DiagnosticCodes.CSharpExpressionLimit`) with the JSON path, the expressions already compiled keep working. It protects a server that renders templates of other users against exhausting the memory. The option is not sent to ReportServer by `ReportServerRenderer`; the server uses its own (default) limit. New public constructors `BindingEvaluator(int)` and `BindingRuntime(bool, int)`. - Template format: the nodes `Text`, the spans of `Paragraph`, the columns of `Table` and the cells of a table (`rows.cells`, `rows.group.summary.cells`, `totalSummary.cells`) have the new optional property `format`, a default .NET format (`N2`, `C0`, `dd.MM.yyyy`) for the bindings `{{...}}` in the text that have no format of their own after the colon, so a format is written once per column or node instead of in every cell and in every summary (`{ "key": "price", "align": "Right", "format": "N2" }`). A format written in the binding wins over the `format` of the cell, which wins over the `format` of the column; literal text and the header of a column are never formatted, a text with several bindings formats each of them that has no format. An invalid format prints the value unformatted, as in a binding; the debug mode `RenderBindingsAsExpressions` still prints the binding as written. `PageNumbers.format` is unchanged (the style of the page number). Nothing changes for templates without `format`; a cell property `format` used to be the warning `NRP013` and is a string-checked property now (a value of another type is `NRP014`). The data contract takes the format of a node as the kind of the field (number, date) when the text has exactly one binding. New public `BindingRuntime.EvalStringWithFormat`, `TextTemplate.WithDefaultFormat` and `TextTemplateCache.GetOrParseWithFormat`; `IBindingEvaluator` is unchanged (a custom evaluator receives the default format in `BindingSegment.Format`). Template editor: the table editor has the new column **Formát** for the columns of the table and for the cells of the rows, of the group summary and of the total summary (a list of common formats is offered, a custom one is accepted; an empty cell format shows the format of the column as the placeholder); new `TableColumnModel.Format`, `TableCellModel.Format` and `TableEditorModel.GetCellFormat` / `SetCellFormat`. - MCP: `get_data_contract` now lists the item of an array of objects (`$.Items[*]`, kind `object`) in `fields[]` right after the array and before its properties; before, only the properties were listed (the item of an array of values already was). `generate_sample_data` reports `fieldCount` as the number of rows of `fields[]`, so a template with an array of objects has one more than before. - Template editor: the dialog **Moje motivy** can rename an own theme and change its description (the id, the palette, the styles and the visibility stay), export any listed theme to the file `{id}.theme.json` (format `netreporter-theme`) and import such a file (up to 1 MB). The import takes the id from the name like *Uložit jako motiv…*, so it never overwrites an existing theme (the same name gets `-2`); it keeps only what a theme may contain (unknown items and style properties a theme may not set, for example `paddingMm`, are dropped) and rejects, with the reason, a file that is not a theme, has no name, refers to a colour that is not in its palette or writes an item twice. A theme file with an item written twice no longer breaks the list of the themes in the `themes` folder (`ArgumentException` from `JsonObject`). Own themes are written with LF line endings on every system. New `ThemeCatalog.UpdateUserAsync`, `ThemeCatalog.ImportUserAsync` and `ThemeDefinition.ToJson`; the MCP server still offers only the bundled themes. Celý seznam změn: CHANGELOG.md v balíčku a https://dev.azure.com/advsoft/Knihovny/_git/NetReporter?path=/src/NetReporter/CHANGELOG.md

Version Downloads Last updated
3.9.2 4 10/28/2026
3.9.1 1 10/27/2026
3.9.0 8 10/20/2026
3.8.1 3 10/19/2026
3.8.0 3 10/19/2026
3.7.0 3 10/19/2026
3.6.0 4 10/17/2026
3.5.1 4 10/15/2026
3.5.0 4 10/15/2026
3.4.2 4 10/15/2026
3.4.0 4 10/12/2026
3.3.0 4 10/11/2026
3.2.2 4 10/10/2026
3.2.1 4 10/10/2026
3.2.0 4 10/10/2026
3.1.1 4 10/09/2026
3.1.0 4 10/09/2026
3.0.1 4 10/08/2026
3.0.0 4 10/08/2026
2.2.1 5 10/05/2026
2.2.0 4 10/05/2026
2.1.0 4 10/04/2026
2.0.1 4 10/04/2026
2.0.0 4 10/03/2026
1.7.0 4 10/03/2026