- Template editor (ToDo #185): *Uložit jako…* (the *Soubor* menu, the command palette, a click on the template name in the top bar) saves the template under another name as a copy; the original stays unchanged and the new one stays in the editor. A template that is not in the store yet (a gallery sample, the empty template, an uploaded file, after *Reset*) is named at the first *Uložit* or *Publikovat*, so two new templates no longer overwrite each other silently under `nova_sablona`; an existing template of that name is overwritten only after a confirmation.
- ReportServer UI (ToDo #186) fix: a template without a published version (stored only by the editor, version `#0`) now shows the badge *nepublikováno* in the list and a warning in its detail (the JSON is empty because the API returns the published version), and an error of the API (for example saving an empty JSON) is shown in the detail panel instead of silently closing the form.
- Styles (ToDo #182): the new style properties `selfAlign` (`Left`, `Center`, `Right`) and `selfAlignVertical` (`Top`, `Center`, `Bottom`) align the whole node in its parent, while `align` and `alignVertical` align the content inside it. In the PDF the alignment is the outermost layer (before margin, background and border), so a box with a background and a border shrinks to its content and moves as a whole, margin included; the HTML output uses `width:fit-content` and automatic margins (a link and an image become `display:block`). The vertical variant shows only where the parent gives the node more room than it needs (a `Row` with a taller neighbour, a fixed height). `Table` and its cells ignore both properties, so a value in `styles.base` does not break the columns. `Middle` and `End` are accepted as synonyms like for `align`. New members `StyleNode.SelfAlign`, `StyleNode.SelfAlignVertical`, `ResolvedStyle.SelfAlign` and `ResolvedStyle.SelfAlignVertical`; the template schema has the enum `SelfAlign`.
- Template editor (ToDo #182): the new ribbon tab *Formát* aligns the selected node with one click: horizontally (left, center, right, justify) and vertically (top, center, bottom). The buttons are icon-only with a tooltip, the button of the value that applies to the node is on (the value comes from the node's style, its `styleRef` and `styles`, and `styles.base`). The buttons *Obsah* and *Celý uzel* choose what is aligned: the content inside the node (`align`, `alignVertical`: the text of a text node, the content of a box that keeps its full width) or the whole node in its parent (`selfAlign`, `selfAlignVertical`). Nodes without content (image, barcode, 2D code, payment QR, checkbox, emoji, icon, chart) always align the whole node; the tab is off for a table, a spacer, a page break, a line, an absolute node, a section and the template root. The value is written into the style of the node as one undo step (quick repeated clicks on the same node merge), a click on the button that is on removes it, *Zrušit* removes both axes of the chosen target (a named style applies again). Like the insertion of nodes it works for a node selected in the tree, by the cursor in the JSON or in the preview.
- Styles (ToDo #182) fix: `align: Justify` justified nothing, the PDF and the HTML output treated it as `Left`. Text (`Text`, `Paragraph`, a link, a table cell) is now justified in both outputs (`text-align: justify` in HTML); a container or an image still takes `Justify` as `Left`.
- Template editor (ToDo #181): dockable panels like in Visual Studio. The properties of the selected node (JSON of the node, property grid, style) are a panel on the left that is reachable from every tab (they were only next to the preview); *Struktura* (the tree), *Data* (the field tree of the preview data and the data contract), *Chyby* (the diagnostics with quick fixes) and *Asistent* are separate panels, by default on the right. Every panel can be moved to the other side (the arrows in its header), a side is either pinned or auto-hidden (the pin in the header: the panel shrinks to a vertical strip of tabs at the edge, a click slides it over the center, a click outside, the Esc key or a click into the preview hides it), the panels of one side are tabs in the strip, and the width is dragged by the inner edge. The tab of *Chyby* takes the color of the most severe finding (red error, orange warning, blue information) with the number of findings. The layout is remembered per user in the browser (`localStorage`). The content of a panel is in the DOM once and only CSS classes change, so the tree, Monaco and the conversation of the assistant are never re-created when a panel is pinned, unpinned or moved. Ctrl+I and the *Asistent* button open the assistant as a panel. The preview tab no longer has the splitter with the properties; selecting a node no longer switches the tab (only the tabs *Tabulka* and *Graf* follow the node type). Because the properties now sit next to the JSON tab, *Uložit uzel* first writes the unparsed text of the JSON tab (ToDo #58).
- Template editor (ToDo #181): the ribbon (Syncfusion `SfRibbon`, layout Simplified) replaces the buttons of the app bar and the toolbar above the tree: *Soubor* (load, save, publish, versions, assets, bundle, clear, reset), *Domů* (save, publish, undo, redo, copy, paste, duplicate, delete, assistant), *Vložit* (all node types and the blocks) and *Zobrazení* (the panels, the default layout, the command palette). Inserting nodes is global, not tied to the tree: it is active whenever a node is selected (in the tree, by the cursor in the JSON, in the preview). A status bar at the bottom shows the errors and warnings (a click opens *Chyby*), the saved and published state and the path to the selected node (a click on an ancestor selects it). A right click on a tree node opens a context menu (add a child, clipboard, duplicate, move, delete, save as a block, edit with the assistant). The notices (error, assistant is running, degraded template, information, the offered draft) float over the tabs and no longer push the content down.
- Template editor: the new function *Vyčistit šablonu* (the button on the *Domů* tab, the *Soubor* menu, the command palette) asks for a confirmation and then reduces the template to the base of the page: the version, `meta.name`, `meta.culture`, `page.size`, `orientation`, the custom size and the margins stay; the content, the styles, the palette, the assets, the watermark, the letterhead and the document sections go, and the template gets a base style and an empty root `Stack`. One step of Undo brings the template back.
- ReportServer UI (ToDo #184): the pages *Render* and *Templates* use the whole screen like the editor. *Render* is a workspace: on the left the template (stored or ad-hoc) and the data in the JSON editor (Monaco) with the output options folded below, on the right the result (the PDF is shown in the panel, an image fits the panel; tabs for the binding diagnostics and the ISDOC), a draggable divider (remembered) and a status bar with the state, format, pages, cache hit, size and time. *Templates* is master-detail: a dense filterable table on the left, the metadata, the JSON editor and the preview of the selected template on the right (a second click confirms a delete). The JSON editors are Monaco with highlighting and syntax checks (the same fallback to a `textarea` without internet). The result is handed to the browser as a stream (`DotNetStreamReference`, no base64 in the DOM).
- New project `NetReporter.UiShared` (a Razor class library for both web apps): the JSON editor component `JsonEditor` with its script, its types (`JsonEditorSchema`, `JsonEditorMarker`, `JsonTextRange`...) and the divider script `nrSplit.js`. The editor moved there from `NetReporter.TemplateEditor`; the design files `nr-ui.css` and `nr-theme.js` stay in two copies (the account pages of `NetReporter.Identity` link them from the host's `wwwroot`) and `DesignAssetsSyncTests` still guards them. The Monaco surface fills its root element absolutely instead of by the percentage of height (the percentage did not resolve in a chain of flex and tab elements and Monaco stayed a few pixels high).
- **Behavior change** ReportServer (ToDo #171): a bitmap (PNG, JPEG, WebP) whose longer side exceeds `ReportServer:Content:Assets:MaxImageEdgePixels` (`ReportServer__Content__Assets__MaxImageEdgePixels`, default 3508 = the longer side of A4 at 300 DPI, `0` turns it off) is stored shrunk when it is uploaded or its content is replaced (`POST /content/assets`, `PUT /content/assets/{id}/content`): the aspect ratio and the format stay, JPEG and WebP use quality 90, PNG stays lossless, transparency stays. The PDF and image renders resample an image to the page resolution anyway, but they decode the whole bitmap on every render: a 6000 × 4000 JPEG took about 265 ms and 780 MB of peak memory (PNG about 600 ms and 1.5 GB) against 20 ms and 210 MB for the shrunk 1200 × 800. The shrinking drops the metadata (EXIF including GPS) and writes the EXIF rotation into the pixels (the render honours the orientation, so the image would lie on its side without it). The asset `sha256`, `length` and `ETag` belong to the stored (shrunk) content, the original is not kept. SVG, GIF and BMP stay byte for byte. Bundle import and the moving of data URIs into assets shrink the image too, before the hash of the content is computed, so the same picture is still reused as one asset and `ExtractedAssetResponse.Length` is the length of the stored bytes. At most two images are processed at a time. New dependency `SkiaSharp` in the API (the editor already has it).
- **Behavior change** ReportServer (ToDo #179): metadata are removed from every PNG, JPEG and WebP that is stored as an asset (upload, replacing the content, bundle import, data URIs moved into assets), because anybody can download a public asset and a photo carries the place where it was taken. EXIF including GPS, XMP, IPTC, comments, the text chunks of PNG and `tIME` are cut out losslessly (`ImageMetadataStripper`, the pixels stay byte for byte; the ICC profile, the Adobe marker, transparency, gamma and APNG animation stay). An image whose EXIF orientation is not "as is" is re-encoded with the rotation written into the pixels (the render honours the orientation, so the image would lie on its side without the tag). `ReportServer:Content:Assets:StripImageMetadata` (`ReportServer__Content__Assets__StripImageMetadata`, default `true`) turns it off. The dialog *Assety* of the template editor tells the user when the server stored something other than what was uploaded and shows the size before and after.
- Template editor (ToDo #168): fewer and overlapping calls to the ReportServer API. At start the blocks, the themes and the sample data list load while the draft, the analysis and the preview data are prepared (they were loaded one after another). The gallery lists the sample templates, the sample data names and the user templates at the same time. Saving a template whose identifier the store already knows (it was loaded, listed or saved before) is one call instead of two, publishing and the version list too (a stale identifier falls back to the lookup by name). Saving a template under a name that is not loaded asks `GET content/templates/lookup` instead of listing every template. A gallery thumbnail resolves its card with one small call (`lookup` or `GET {id}`) instead of the full list of templates once per card. The content of the shipped sample data is kept in the editor process (keyed by the revision, 10 minutes), so thumbnails and opening a sample no longer fetch it again. `ITemplateStore` has the new members `GetInfoAsync`, `GetSharedInfoAsync` and `ExistsAsync` (default implementations over `ListAsync`).
- Kubernetes manifests (ToDo #169): `deploy/k8s/03-configmap.yaml` sets `ASPNETCORE_ENVIRONMENT: Production` (it was `Development`; the editor and the MCP server already set `Production` in their Deployments, so the value matters for the API and the UI): no developer exception page that returns the stack trace to the client, the UI gets the error handler and HSTS, `/openapi/v1.json` of the API is gone. The new `Serilog__MinimumLevel__Override__Microsoft.EntityFrameworkCore.Database.Command: Warning` stops the API and the UI from logging every SQL command (the environment never controlled that: Serilog has no minimum level configured, so it was `Information` everywhere). An existing deployment keeps its own copy of the configmap: add both keys there and restart the API and the UI.
- Editor metrics: the histogram `netreporter_editor_api_call_duration_seconds{operation,status}` times every call of the editor to the ReportServer API (the operation is the method and the path without identifiers, the status is `2xx` to `5xx` or `error`), the base for measuring what an action in the editor costs; each call is also logged at Debug. The Grafana dashboard `netreporter-business` has two new panels (calls per minute and p95 by operation).
- Template editor (ToDo #180) fix: a color added to the palette on the tab *Stránka* did not get into the template. The cards *Color paleta* and *Assety* changed only the form model; `meta.palette` and `meta.assets` were written by the button *Uložit parametry* in another part of the tab, so until then the color was unknown to the validation, the color suggestions and the draft, and the next analysis (for example saving a style) reloaded the palette from the JSON and the color vanished. Now every change is written to the template at once as one undo step: adding a color (with a proposed name `barva1`, `barva2`…), changing it, renaming it (on leaving the field) and removing it; an asset is written once its row has a name. Only `meta.palette` or `meta.assets` is written, so the half-typed page parameters of the other cards are neither saved nor lost. Renaming a color rewrites the references to it in styles, nodes and charts; removing a color the template uses asks first; an empty or an already used name is refused and the field returns to the old name.
- Kubernetes manifests (ToDo #170): the API and the editor containers in `deploy/k8s/05-workloads.yaml` get the memory limit `768Mi` and the request `256Mi` (were `512Mi` and `128Mi`; the MCP server already had these values), because a loaded FontAwesome Pro 7 pack takes about 53 MB in every process that loads it (about 106 MB with Pro 6 and 7). CPU stays as it was (installing a pack costs about 3 s of CPU). An existing deployment keeps its own copy of the manifest: raise the limits there too.
- New: lightweight clients of ReportServer for **Node.js/TypeScript** (`clients/node`, `@netreporter/client`, no runtime dependencies, built-in `fetch`) and **Python** (`clients/python`, `netreporter-client`, one dependency `httpx`, sync and async API). They match `NETReporter.Client`: ad-hoc render in every format with all options (PDF/A, attachments, encryption, watermark, resolution; the options are checked locally before anything is sent), render by pages, binding diagnostics, the four template tools (validate, validate data, data contract with a local JSON Schema, sample data) and the error mapping including `Retry-After`; they authenticate with an API token (`nrp_…`). Source only for now, not published to npm or PyPI. The shared contract of the three clients lives in `clients/contract` (golden files) and the new test `ClientContractFixturesTests` checks it against `NETReporter.Client` and `NetReporter.ReportServer.Contracts`. Docs: `doc/netreporter-knihovna/user/pouziti-klienta-node.md` and `pouziti-klienta-python.md`.
- ReportServer (ToDo #189, phase F1 of `.work/plan-billing.md`): a usage ledger, the new table `billing.UsageEvents` (migration `AddUsageEvents`), the base for usage overviews and billing. Every render through the API (success, failure and cache hit, written in the same save as the job's metric, with the job's id), every operation of the AI assistant (written next to `AssistantUsage` with the same id) and every render the MCP server draws itself (`render_report`, see below) is one row that is only ever added: it survives the deletion of the job, the API token and the user. It stores facts (channel API token / signed-in user / MCP / editor, the token, format, pages, bytes, cache hit, outcome, tokens and cost of the AI) and the number of documents (a successful render outside the cache is one document per started 20 pages, a failure and a cache hit are 0); no price is computed yet. The migration copies the history (render jobs with a metric and the `AssistantUsage` records) into the ledger.
- **Behavior change** ReportServer: `RenderCount` and `LastRenderUtc` of the API tokens (`GET /auth/tokens`, `GET /admin/api-tokens`) and the render values of `/metrics/me`, `/metrics/users/{id}` and `/metrics/summary` are computed from the usage ledger instead of the render jobs, so they survive the deletion of jobs and users and include the renders the MCP server drew itself with the caller's token. The sums and the months are computed by the database; before, every render metric since the start was loaded into memory on each call.
- ReportServer: `POST /me/usage/renders` (policy User, JWT and API token) records a render the MCP server drew itself, idempotent by `id` (`{"created": false}` on a repeat); an id of another user is `409 USAGE_ID_CONFLICT`, an invalid record `400 USAGE_INVALID_REQUEST`. New contract types `RenderUsageRecordRequest` and `UsageRecordResponse`.
- MCP server: `render_report` (rendered by the MCP server itself) is recorded in the usage of the caller through `POST /me/usage/renders` when the caller is authenticated with a user API token (`nrp_…`), so the render counts for that user and token like a render through ReportServer. Static tokens, the service account and stdio record nothing. A failed record is only logged and never fails the render.
- Template editor: the purchase price of the shared model, `EDITOR_AI_LOCAL_INPUT_PRICE_PER_MTOK`, `EDITOR_AI_LOCAL_OUTPUT_PRICE_PER_MTOK` and `EDITOR_AI_LOCAL_CACHE_READ_PRICE_PER_MTOK` (USD per million tokens; `Assistant:Local:InputPricePerMTok`…), gives the runs of the shared model an estimated cost, which goes into the usage (`EstimatedCostUsd`) and the run summary of the panel; without it only the tokens are recorded, as before. The cost warning (`EDITOR_AI_COST_WARNING_USD`) applies only to models paid with the user's own key.
- New role **Owner** (ToDo #189, Identity migration `AddOwnerRole`): the business owner of the deployment, separate from the technical `Admin`. An admin does not get it, cannot grant it and cannot change an owner's account (roles, password, deactivation, external logins, deletion): `NetReporterUserAdministration` returns the new error `OwnerOnly`, the API `403`, and the user management pages show the role disabled for non-owners. Only an owner grants the role; the first owner comes from the new setting `REPORTSERVER_OWNER_EMAILS` (`ReportServer:OwnerEmails`, comma-separated e-mails of existing accounts that get the role at startup; the role is only added, an invalid e-mail stops the start). An owner cannot remove the role from their own account.
- ReportServer: the owner section of the API (`/owner/*`, policy `InteractiveOwner`: role Owner signed in with JWT, never an API token, `Admin` alone is `403`): `GET /owner/usage/overview` (totals and a gap-free series by day or month with documents per channel: API token, web UI, MCP), `GET /owner/usage/consumers` (users by documents with the breakdown per API token, including revoked and deleted tokens and the work without a token), `GET /owner/usage/ai` (AI by model and user, split into the cost the operator pays for the shared model and the cost users pay with their own key) and `GET /owner/revenue-estimate` (an indicative monthly revenue under the draft price list of the billing plan: every user with usage gets the cheapest tariff; not an invoice). New contract types `OwnerUsageOverviewResponse`, `OwnerConsumersResponse`, `OwnerAiUsageResponse`, `OwnerRevenueEstimateResponse` and their rows; the draft price list is `DraftPriceList` in `NetReporter.ReportServer.Core`.
- ReportServer UI: a separate owner section at `/owner` with its own layout and menu (Usage overview, Consumers, AI costs, Revenue estimate), reached by a briefcase icon next to the user name, visible only to owners. The overview has stat tiles, a stacked column chart of documents per channel, a column chart of the operator's AI cost and a table with the same numbers; the period is 7, 30 or 90 days by day or 12 months by month. The chart is plain HTML and CSS (`ColumnChart`) with series colors for light and dark mode and a tooltip on hover and keyboard focus.
Celý seznam změn: CHANGELOG.md v balíčku a https://dev.azure.com/advsoft/Knihovny/_git/NetReporter?path=/src/NetReporter/CHANGELOG.md