pgkiln 0.87.0 → 0.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ -- Sprint 168: the whole application offline from the first visit.
2
+ --
3
+ -- pwa_prefetch_hours with "Keep visited pages on the device" on: after a sign-in (or on the first page
4
+ -- opened on a device) the service worker fetches every page of the navigation menu and
5
+ -- navigation bar the user may open, and keeps them for offline use; again when the kept
6
+ -- copies are older than this many hours. Null: only the pages the user opens.
7
+ -- Prefetched pages run read-only on the server (routes.ts: X-Pgkiln-Prefetch).
8
+
9
+ alter table meta.app add column pwa_prefetch_hours integer
10
+ check (pwa_prefetch_hours between 1 and 168);
11
+
12
+ comment on column meta.app.pwa_prefetch_hours is
13
+ 'PWA: fetch every navigation page for offline use after sign-in, and again after this many hours (null: off)';
@@ -7,7 +7,7 @@ open item and open an issue or pull request; see [CONTRIBUTING.md](../CONTRIBUTI
7
7
  Legend: ✅ available · 🟡 partial (see notes) · ❌ not yet · ➖ not planned (a deliberate choice,
8
8
  or better served by the PostgreSQL ecosystem; see the notes and [extensions](guide/15-extensions.md))
9
9
 
10
- Last reviewed: 2026-11-06 against **APEX 26.2** (sprint 167: hidden items with Value Protected off, default values for an interactive grid's new rows, page items in any region type; sprint 164: every App Builder screen in the eleven builder languages (WS-7 leftover); sprint 165: navigation menu badges from SQL, the notification bell and live badges; sprint 162: the SQL Workshop tools and the region and report settings in the eleven builder languages (CD-8); sprint 161: component events of grids, calendars, maps and trees (CD-3); sprint 160: the Workflow Diagram region, an instance's steps with the reached, current and failed ones marked (CD-7); sprint 159: map layers Lines, Polygons and Extruded Polygons with fill and line styles per layer or per row (CD-6); sprint 158: the item types Image Upload, Text Field with autocomplete, Percent Graph and Display Map (CD-5); sprint 157: the process types Close Dialog, Clear Session State, Reset Pagination, User Preferences, Server Side Geocoding, Print Report, Human Task – Create / – Manage and Form – Initialization (CD-4); sprint 156: dynamic action events double click, key down / press, mouse enter / leave, select, resize, scroll, page unload, before refresh and facets change, greater / less and not-in-list conditions, and the actions Cancel Event, Set Style, Collapse / Expand Tree, Show AI Assistant, Trigger Geocoding and Open report dialog (CD-3); sprint 155: APEX's validation types, with an authorization scheme, Always execute and the error's display location (CD-1); sprint 154: one catalogue of server-side condition types for every component, shown in words in the Page Designer (CD-2); sprint 153: Database users in the eleven builder languages (WS-7 leftover); sprint 152: Workspace utilities, the dashboard, Mail queue, Network access, Instance settings and LDAP directories in the eleven builder languages (WS-7 leftover); sprint 151: vector providers with a model in the database, PostgresML (APEX's ONNX models, AP-1 leftover); sprint 150: remote databases, another PostgreSQL database's tables as foreign tables the applications query (REST Enabled SQL, AP-7 leftover); sprint 149: running workflow instances inside parallel branches migrate to a newer version (WS-6 leftover); sprint 148: the AI assistant streams its answers as the model writes them (WS-4 leftover); sprint 147: `pgkiln advise --offline` checks an export's files without a database (AP-10 leftover); sprint 146: the Advisor checks JSON sources' views against their data profile (AP-8 leftover); sprint 145: the texts that offered AI with "Claude or OpenAI" name all four providers: Claude, OpenAI, Gemini and OpenAI-compatible services (AP-11); sprint 144: `pgkiln advise` for CI with text, JSON and SARIF output (APEXlang static analysis / SQLcl CodeScan), security checks in the Advisor, and working copies and locks from the command line and MCP (the Working Copy APIs) (AP-10); sprint 143: the interactive report's 26.2 dialogs: *is one of* / *is not one of* filters with the column's values and counts, *is between* two values (dates with the date picker in the application's format), relative periods on date columns, and the column order by drag and drop or arrow buttons, followed by downloads and the PDF (AP-9); sprint 142: JSON sources with a data profile and updatable views whose arrays are rows, document views of a table with its children (APEX's duality views), JSON path columns in reports (AP-8); sprint 141: static workspace files (`#WORKSPACE_FILES#`), remote servers for REST data sources, catalogs and AI services (an environment variable per environment), and workspace-level web credentials (AP-7); sprint 140: the items Shuttle (ordered), Select One and Select Many, the regions Classic Report (report templates, pagination types), Breadcrumb and Help Text with a page help text, and a [mapping of every APEX item and region type](#item-and-region-types-one-by-one) (AP-4); sprint 139: OpenTelemetry, traces with a span per SQL statement, web call, AI request and tool call, logs, metrics and `/metrics` for Prometheus, browser spans relayed by pgkiln, per workspace off or its own collector (AP-6); sprint 138: an application's Utilities tab with a dashboard, database object dependencies, change history per component, Upgrade application, UI defaults (APEX's Attribute Dictionary), application and page checksums and an icon browser (AP-5); sprint 137: Create page as a grid of page types with previews, the page types interactive report, smart filters, search page, data loading, tree, timeline, media list, content row, comments, dashboard, wizard, unified task list and workflow console, a Feature tab with nine features that install once, and Create page as copy with shared components mapped or copied (AP-3); sprint 136: vector providers (an AI service with an embedding model or a SQL function), `meta.embed`, *Add vector column* in Table Properties with an HNSW index and a queue that keeps embeddings current, search types vector and hybrid (reciprocal rank fusion with full-text) and a Retrieve (RAG) tool for AI agents, all as the app's role through RLS, on pgvector (AP-1); sprint 135: full-text search configurations, APEX's Oracle Text search type on PostgreSQL full-text search with stemming, ranking, highlights, *Did you mean* and *Add full-text search* in Table Properties (AP-2); sprint 134: the builder also in Italiano, 中文 (简体), 中文 (繁體), 日本語 and 한국어, APEX's ten builder languages plus Nederlands (SEC-5); sprints 121, 123–125: the builder in Deutsch, English, Español, Français, Nederlands and Português, step by step: the frame, sign-in, Workspace home, App Builder, Page Designer menus, the SQL Workshop's home, SQL Worksheet, Object Browser and Table Properties, Shared Components, application settings, users, identity providers, developers and workspaces (WS-7); sprint 122: *Ask in your own words* sets faceted search and smart filter facets, smart filters read a typed phrase (WS-5), and Sample Maps, Sample Approvals, Sample Data Loading and Sample Files in the App Gallery (WS-8); sprint 120: the App Gallery's single-topic sample apps (charts, cards, calendar, interactive grids, dynamic actions, reporting), the UX Pattern Catalog and a Utility Apps tab with Lookup Lists, Release Notes and Announcements (WS-8); sprint 119: data security per table, a roles × columns matrix and masked values per application role in a masking view, the same identity for pages, AI tools, REST, AutoREST and MCP (WS-13); sprint 118: Administration → Mail queue with delivery per hour and per workspace, the slowest provider calls and failures with Retry and Purge, a workspace's own mail service, and workspace network access lists that narrow the installation's allow-list (WS-9); sprint 117: Pillar Light and Pillar Dark navigation appearances with a header strip, clearer navigation tree states, subtle colour utilities, RTL; sprint 116: the comparison moved from 26.1 to 26.2, with a section for the 26.2 workspace items WS-1 … WS-13 and OCI managed identities; the App Builder list with Updated by, working copies and the lock; `pgkiln import --only`, a file-level import; stale rows on facet menus and blueprints fixed; sprint 115: popup LOVs that load on scroll, Wait Popup dynamic actions and dynamic actions on menu entries; sprint 114: Workflow 26.2 with custom task outcomes, task events and notifications, Send E-Mail and draft steps, migrating running instances and the diagram as SVG / PNG; sprint 113: Team Development with issues, labels, milestones, templates, a board and a set-up wizard; sprint 112: AI interactive reports with chips, Undo, computed columns and a phrase parser that needs no AI service; sprint 111: AI Agents with tools as a shared component, a Navigate tool with checksummed URLs, Gemini and OpenAI-compatible services; sprint 110: Data Reporter as a workspace tool with datasets, reporting applications, editors and viewers, published reports with stable URLs and pins; sprint 109: the Workspace home above the App Builder and SQL Workshop, with a summary, top applications, users and developers, and an optional release check; sprint 108: the code editor's syntax colours in the builder's light and dark themes, read-only SQL highlighted, the Data Modeler in dark mode; sprint 107: a transaction mode in the SQL Worksheet with Commit and Rollback; DA-18: JavaScript (MLE) not planned, plv8 where installed; sprint 106: the change log of DDL change sets with Flyway, Liquibase and SQL downloads; sprint 104: RESTful Services modules, templates and handlers with PL/pgSQL and media handlers, privileges, OpenAPI and Try it; sprint 103: Schema Export / Import with pg_dump and pg_restore; sprint 102: the SQL Monitor with live sessions, Top SQL and a statement's plan; sprint 101: Scheduling of pg_cron jobs with a schedule builder, run now and run history; sprint 100: RESTful Services, tables and views published as REST endpoints with paging, filters, keys and OpenAPI; sprint 99: Administration → Database Users for PostgreSQL roles; sprint 98: the Data Modeler with relational diagrams, DDL and a data dictionary; sprint 97: charts of a query in the worksheet, saved charts and their dashboard; sprint 96: the SQL Workshop's Launchpad with pins and tool groups, the builder's search with Ctrl+K; sprint 95: Table Properties: create, edit or copy a table in a form with a DDL preview and Apply in one transaction; sprint 94: an object's menu with Open in a panel, Quick DDL, Add index / trigger / policy and Drop with dependents; sprint 93: worksheets saved as SQL Scripts with tabs and a Files list, Format, Convert case, find and replace, bind and substitution variables; sprint 92: the worksheet's Query Result with sort, value dialog, copy and downloads, plans as a tree with the slowest step and misestimates, SQL History with status, search and run again; sprint 91: the SQL Worksheet like Database Actions' SQL: navigator, Run Statement at the cursor and Run Script with a log per statement, Explain Plan, Autotrace, notices, history, Run as an application role; sprint 90: Comment Out in the page designer, gallery labels in title case; sprint 89: the Object Browser's schema, one row of tabs, blank NULLs, Modify column; sprint 88: home cards with colour bands and round icons, the hero on the page background, a page footer with the version and Back to top; sprint 87: the Create Application wizard reloads the page list for a schema, RLS badges, the alias from the name; sprint 86: the page designer by page number, HTML error pages in the builder; sprint 85: the Form Designer, beyond APEX; sprint 84: the date picker, facet menus; sprint 83: Component Settings and Component Groups; sprint 82: Report Queries; sprint 81: Application Computations and Shortcuts in Shared Components; sprint 80: the master grid's first row selected when the page opens and after a search; grid cells as text with their format mask until edited, with cell navigation by arrow keys, Enter, F2 and Escape; sprint 79: generated dashboards with sums, donuts and lines, leaving out charts that say nothing; applications open in their own light or dark mode until the user chooses, per application; a user menu with Appearance and Sign in when signed out; the developer toolbar's App and Page texts and debug state; the report search's column scope in the magnifier; sprint 78: generated faceted search reports with one column per foreign key, the faceted search's total row count, Show all and Yes/No values, one search field, friendly headings and number formats in generated grids and master-detail pages, friendly names in the Create Application wizard's page list; sprint 77: the Page Designer's layout and tree by display position, button positions, the tree's context menu with Duplicate and Copy to other page, Show Common / Show All, Go to group and the Template Options dialog; display positions, button positions and side-column page templates at runtime; sprint 76: the interactive report's menus and pagination, date items in the date format; sprint 75: the SQL Workshop's Object Browser and tabs; sprint 74: the builder's information architecture: Shared Components grid, Settings tabs, application home search and tasks; sprint 73: the Create Application wizard on one page with page types and features; sprint 72: the runtime chrome compared with APEX 26.2's: the developer toolbar, the Iris look, the navigation bar's Help menu and Install button; sprint 71: generated applications compared with APEX 26.2's: labels, foreign key names in reports, file columns, format masks, the Home page and the dashboard; sprint 70: treemap, sunburst and timeline charts; sprint 69: dragging tasks on a Gantt chart; sprint 68: the Data Reporter's welcome page, Find my workspace; sprint 67: REST source catalogs from OpenAPI documents, a workspace's export and import (two rows added); sprint 66: a blueprint from an existing application; sprint 65: facet charts; sprint 64: approval vacation rules and expiration policies; sprint 63: range, box plot and stock charts; sprint 62: workspace preferences (SQL Workshop and RESTful services per workspace), extension links, Manage backups across applications, SQL Workshop utilities with object reports, About Database and a database monitor; sprint 61: the builder's screens compared with APEX 26.1: the workspace sign-in page, the Administration menu with workspace message, environment banner and utilization, the SQL Workshop home, the App Gallery, application groups and cross application reports (four rows added); sprint 60: isolated workspaces with their own database logins, row-level security on pgkiln's tables and workspace database roles, workspace administrators; sprint 59: the Page Designer's help pane, Quick edit from the developer toolbar; sprint 58: page groups; sprint 57: region templates hero, title bar, buttons container, inline dialog and drawer, wizard with wizard progress, the carousel; sprint 56: Generate DDL and Schema Comparison in the SQL Workshop, `pgkiln ddl` and `pgkiln compare`; sprint 55: an editable login page with a Login region and the Minimal page template, Forgot password through the application's mail service; sprint 54: interactive grid views, the single row view, group by and chart; sprint 53: application settings, user preferences and items kept per user, dynamic translations, `meta.data_export` and data export downloads, display image and geocoded address items; sprint 52: every dynamic action event and action, Ajax callbacks, an error handling function, the session timeout warning; sprint 51: checked imports, application backups, availability and Run only, feedback, type-specific attribute help; sprint 50: lost update detection, warn on unsaved changes, the URL and search regions with search configurations, the developer toolbar with a session state viewer, and e-mail through SMTP with OAuth2, Microsoft Graph, Amazon SES and HTTP APIs; import checks, application availability, property help and e-mail compared again; 2026-10-06: pgkiln 0.31.0, in progress: static application files and the Execute JavaScript dynamic action; region, item, dynamic action and process plug-ins; conditional and dynamic theme styles; XML and Excel in `meta.parse_data`, zips in SQL, `meta.v_boolean`; file items in S3-compatible object storage; map layers by the visible area and as vector tiles; a graphical query builder; tenants for workflows and tasks; application files as YAML and region static IDs; Lucide icons and icon modifiers; PWA push notifications; 0.30.0: 136 icons and an icon picker; an axe-core accessibility audit; cropping pictures; session sharing between applications; twenty-two built-in languages; 0.29.0: workspaces; the Iris base style; drawers; built-in template components including the metric card; Theme Roller dark-mode colours, live preview and template options on items and report columns; report selection across pages; instance settings; fourteen built-in languages; 0.28.0: AI with Claude and OpenAI; 0.27.0: Data Reporter; sample data generator; create application from several sheets, pasted data or existing tables; 0.26.0: working copies with a three-way merge; theme, library and boilerplate applications with subscriptions; 0.25.0: Gantt, pyramid and polar charts; map layers, clustering and server-side spatial filters; REST data sources that write back from forms and grids and synchronise into tables, OAuth2 password and refresh-token grants; debug messages per request and an install/upgrade log; `meta.web_request()` and `meta.parse_data()`; Theme Roller style variants and template options; 0.24.0: automations with several actions, error handling per row and runs from SQL; invoke-API workflow steps; Unload Data in the Data Workshop; create page wizards for cards, calendar, chart, map, faceted search, form and master-detail; create an application from a file; 0.23.0: interactive grid aggregates, frozen/moved/resized columns, layouts and saved grid reports, master-detail, row actions and copy/paste; download processes, execution chains in the background, workflow processes, branches to a function's URL or another app, the Dialog Closed event; number format masks, automatic time zone, runtime messages in German, French and Spanish; SQL scripts, Quick SQL, a query builder, XML loading and data load definitions; custom authentication, lists, page and application locks with developer comments, supporting objects; 0.22.0: keyset paging, report PDFs and REST collections streamed from a cursor, database-account authentication; 0.21.0: HTTP-header authentication behind a reverse proxy; 0.20.0: a popup list of values with server-side search; 0.19.0: row-range pagination, maximum row counts and row limits, lazy loading and caching of regions, streamed CSV/Excel downloads; 0.18.0: calendar week/day/list views with drag and drop, bubble/gauge/funnel/radar charts and drill-down, smart filters, a region display selector and more facet types, computations, conditional branches, build options, menu buttons, REST data sources and web credentials, rich text/Markdown, rating, combobox, date range and QR code items; 0.17.0: an App Builder home, workspace dashboard and utilities like APEX's; 0.16.0: a Page Designer with drag-and-drop layout, a code editor with autocomplete, template components and plug-ins, parallel branches and versions of workflows, the `pgkiln` command line with one file per component; 0.15.0: drop and paste files, heat maps, filtering a report by the map area; 0.14.0: stacked, combo, scatter and pie charts, several files per upload item; 0.13.0: map and tree regions; 0.12.0: REST modules in the builder; 0.11.0: workflows and Progressive Web Apps; 0.10.0: LDAP, SAML, "Keep me signed in", document templates, JSON loading, approvals and the task list; pgkiln installs no application, HR is an example).
10
+ Last reviewed: 2026-11-06 against **APEX 26.2** (sprint 168: every page of the navigation offline from the first visit; sprint 167: hidden items with Value Protected off, default values for an interactive grid's new rows, page items in any region type; sprint 164: every App Builder screen in the eleven builder languages (WS-7 leftover); sprint 165: navigation menu badges from SQL, the notification bell and live badges; sprint 162: the SQL Workshop tools and the region and report settings in the eleven builder languages (CD-8); sprint 161: component events of grids, calendars, maps and trees (CD-3); sprint 160: the Workflow Diagram region, an instance's steps with the reached, current and failed ones marked (CD-7); sprint 159: map layers Lines, Polygons and Extruded Polygons with fill and line styles per layer or per row (CD-6); sprint 158: the item types Image Upload, Text Field with autocomplete, Percent Graph and Display Map (CD-5); sprint 157: the process types Close Dialog, Clear Session State, Reset Pagination, User Preferences, Server Side Geocoding, Print Report, Human Task – Create / – Manage and Form – Initialization (CD-4); sprint 156: dynamic action events double click, key down / press, mouse enter / leave, select, resize, scroll, page unload, before refresh and facets change, greater / less and not-in-list conditions, and the actions Cancel Event, Set Style, Collapse / Expand Tree, Show AI Assistant, Trigger Geocoding and Open report dialog (CD-3); sprint 155: APEX's validation types, with an authorization scheme, Always execute and the error's display location (CD-1); sprint 154: one catalogue of server-side condition types for every component, shown in words in the Page Designer (CD-2); sprint 153: Database users in the eleven builder languages (WS-7 leftover); sprint 152: Workspace utilities, the dashboard, Mail queue, Network access, Instance settings and LDAP directories in the eleven builder languages (WS-7 leftover); sprint 151: vector providers with a model in the database, PostgresML (APEX's ONNX models, AP-1 leftover); sprint 150: remote databases, another PostgreSQL database's tables as foreign tables the applications query (REST Enabled SQL, AP-7 leftover); sprint 149: running workflow instances inside parallel branches migrate to a newer version (WS-6 leftover); sprint 148: the AI assistant streams its answers as the model writes them (WS-4 leftover); sprint 147: `pgkiln advise --offline` checks an export's files without a database (AP-10 leftover); sprint 146: the Advisor checks JSON sources' views against their data profile (AP-8 leftover); sprint 145: the texts that offered AI with "Claude or OpenAI" name all four providers: Claude, OpenAI, Gemini and OpenAI-compatible services (AP-11); sprint 144: `pgkiln advise` for CI with text, JSON and SARIF output (APEXlang static analysis / SQLcl CodeScan), security checks in the Advisor, and working copies and locks from the command line and MCP (the Working Copy APIs) (AP-10); sprint 143: the interactive report's 26.2 dialogs: *is one of* / *is not one of* filters with the column's values and counts, *is between* two values (dates with the date picker in the application's format), relative periods on date columns, and the column order by drag and drop or arrow buttons, followed by downloads and the PDF (AP-9); sprint 142: JSON sources with a data profile and updatable views whose arrays are rows, document views of a table with its children (APEX's duality views), JSON path columns in reports (AP-8); sprint 141: static workspace files (`#WORKSPACE_FILES#`), remote servers for REST data sources, catalogs and AI services (an environment variable per environment), and workspace-level web credentials (AP-7); sprint 140: the items Shuttle (ordered), Select One and Select Many, the regions Classic Report (report templates, pagination types), Breadcrumb and Help Text with a page help text, and a [mapping of every APEX item and region type](#item-and-region-types-one-by-one) (AP-4); sprint 139: OpenTelemetry, traces with a span per SQL statement, web call, AI request and tool call, logs, metrics and `/metrics` for Prometheus, browser spans relayed by pgkiln, per workspace off or its own collector (AP-6); sprint 138: an application's Utilities tab with a dashboard, database object dependencies, change history per component, Upgrade application, UI defaults (APEX's Attribute Dictionary), application and page checksums and an icon browser (AP-5); sprint 137: Create page as a grid of page types with previews, the page types interactive report, smart filters, search page, data loading, tree, timeline, media list, content row, comments, dashboard, wizard, unified task list and workflow console, a Feature tab with nine features that install once, and Create page as copy with shared components mapped or copied (AP-3); sprint 136: vector providers (an AI service with an embedding model or a SQL function), `meta.embed`, *Add vector column* in Table Properties with an HNSW index and a queue that keeps embeddings current, search types vector and hybrid (reciprocal rank fusion with full-text) and a Retrieve (RAG) tool for AI agents, all as the app's role through RLS, on pgvector (AP-1); sprint 135: full-text search configurations, APEX's Oracle Text search type on PostgreSQL full-text search with stemming, ranking, highlights, *Did you mean* and *Add full-text search* in Table Properties (AP-2); sprint 134: the builder also in Italiano, 中文 (简体), 中文 (繁體), 日本語 and 한국어, APEX's ten builder languages plus Nederlands (SEC-5); sprints 121, 123–125: the builder in Deutsch, English, Español, Français, Nederlands and Português, step by step: the frame, sign-in, Workspace home, App Builder, Page Designer menus, the SQL Workshop's home, SQL Worksheet, Object Browser and Table Properties, Shared Components, application settings, users, identity providers, developers and workspaces (WS-7); sprint 122: *Ask in your own words* sets faceted search and smart filter facets, smart filters read a typed phrase (WS-5), and Sample Maps, Sample Approvals, Sample Data Loading and Sample Files in the App Gallery (WS-8); sprint 120: the App Gallery's single-topic sample apps (charts, cards, calendar, interactive grids, dynamic actions, reporting), the UX Pattern Catalog and a Utility Apps tab with Lookup Lists, Release Notes and Announcements (WS-8); sprint 119: data security per table, a roles × columns matrix and masked values per application role in a masking view, the same identity for pages, AI tools, REST, AutoREST and MCP (WS-13); sprint 118: Administration → Mail queue with delivery per hour and per workspace, the slowest provider calls and failures with Retry and Purge, a workspace's own mail service, and workspace network access lists that narrow the installation's allow-list (WS-9); sprint 117: Pillar Light and Pillar Dark navigation appearances with a header strip, clearer navigation tree states, subtle colour utilities, RTL; sprint 116: the comparison moved from 26.1 to 26.2, with a section for the 26.2 workspace items WS-1 … WS-13 and OCI managed identities; the App Builder list with Updated by, working copies and the lock; `pgkiln import --only`, a file-level import; stale rows on facet menus and blueprints fixed; sprint 115: popup LOVs that load on scroll, Wait Popup dynamic actions and dynamic actions on menu entries; sprint 114: Workflow 26.2 with custom task outcomes, task events and notifications, Send E-Mail and draft steps, migrating running instances and the diagram as SVG / PNG; sprint 113: Team Development with issues, labels, milestones, templates, a board and a set-up wizard; sprint 112: AI interactive reports with chips, Undo, computed columns and a phrase parser that needs no AI service; sprint 111: AI Agents with tools as a shared component, a Navigate tool with checksummed URLs, Gemini and OpenAI-compatible services; sprint 110: Data Reporter as a workspace tool with datasets, reporting applications, editors and viewers, published reports with stable URLs and pins; sprint 109: the Workspace home above the App Builder and SQL Workshop, with a summary, top applications, users and developers, and an optional release check; sprint 108: the code editor's syntax colours in the builder's light and dark themes, read-only SQL highlighted, the Data Modeler in dark mode; sprint 107: a transaction mode in the SQL Worksheet with Commit and Rollback; DA-18: JavaScript (MLE) not planned, plv8 where installed; sprint 106: the change log of DDL change sets with Flyway, Liquibase and SQL downloads; sprint 104: RESTful Services modules, templates and handlers with PL/pgSQL and media handlers, privileges, OpenAPI and Try it; sprint 103: Schema Export / Import with pg_dump and pg_restore; sprint 102: the SQL Monitor with live sessions, Top SQL and a statement's plan; sprint 101: Scheduling of pg_cron jobs with a schedule builder, run now and run history; sprint 100: RESTful Services, tables and views published as REST endpoints with paging, filters, keys and OpenAPI; sprint 99: Administration → Database Users for PostgreSQL roles; sprint 98: the Data Modeler with relational diagrams, DDL and a data dictionary; sprint 97: charts of a query in the worksheet, saved charts and their dashboard; sprint 96: the SQL Workshop's Launchpad with pins and tool groups, the builder's search with Ctrl+K; sprint 95: Table Properties: create, edit or copy a table in a form with a DDL preview and Apply in one transaction; sprint 94: an object's menu with Open in a panel, Quick DDL, Add index / trigger / policy and Drop with dependents; sprint 93: worksheets saved as SQL Scripts with tabs and a Files list, Format, Convert case, find and replace, bind and substitution variables; sprint 92: the worksheet's Query Result with sort, value dialog, copy and downloads, plans as a tree with the slowest step and misestimates, SQL History with status, search and run again; sprint 91: the SQL Worksheet like Database Actions' SQL: navigator, Run Statement at the cursor and Run Script with a log per statement, Explain Plan, Autotrace, notices, history, Run as an application role; sprint 90: Comment Out in the page designer, gallery labels in title case; sprint 89: the Object Browser's schema, one row of tabs, blank NULLs, Modify column; sprint 88: home cards with colour bands and round icons, the hero on the page background, a page footer with the version and Back to top; sprint 87: the Create Application wizard reloads the page list for a schema, RLS badges, the alias from the name; sprint 86: the page designer by page number, HTML error pages in the builder; sprint 85: the Form Designer, beyond APEX; sprint 84: the date picker, facet menus; sprint 83: Component Settings and Component Groups; sprint 82: Report Queries; sprint 81: Application Computations and Shortcuts in Shared Components; sprint 80: the master grid's first row selected when the page opens and after a search; grid cells as text with their format mask until edited, with cell navigation by arrow keys, Enter, F2 and Escape; sprint 79: generated dashboards with sums, donuts and lines, leaving out charts that say nothing; applications open in their own light or dark mode until the user chooses, per application; a user menu with Appearance and Sign in when signed out; the developer toolbar's App and Page texts and debug state; the report search's column scope in the magnifier; sprint 78: generated faceted search reports with one column per foreign key, the faceted search's total row count, Show all and Yes/No values, one search field, friendly headings and number formats in generated grids and master-detail pages, friendly names in the Create Application wizard's page list; sprint 77: the Page Designer's layout and tree by display position, button positions, the tree's context menu with Duplicate and Copy to other page, Show Common / Show All, Go to group and the Template Options dialog; display positions, button positions and side-column page templates at runtime; sprint 76: the interactive report's menus and pagination, date items in the date format; sprint 75: the SQL Workshop's Object Browser and tabs; sprint 74: the builder's information architecture: Shared Components grid, Settings tabs, application home search and tasks; sprint 73: the Create Application wizard on one page with page types and features; sprint 72: the runtime chrome compared with APEX 26.2's: the developer toolbar, the Iris look, the navigation bar's Help menu and Install button; sprint 71: generated applications compared with APEX 26.2's: labels, foreign key names in reports, file columns, format masks, the Home page and the dashboard; sprint 70: treemap, sunburst and timeline charts; sprint 69: dragging tasks on a Gantt chart; sprint 68: the Data Reporter's welcome page, Find my workspace; sprint 67: REST source catalogs from OpenAPI documents, a workspace's export and import (two rows added); sprint 66: a blueprint from an existing application; sprint 65: facet charts; sprint 64: approval vacation rules and expiration policies; sprint 63: range, box plot and stock charts; sprint 62: workspace preferences (SQL Workshop and RESTful services per workspace), extension links, Manage backups across applications, SQL Workshop utilities with object reports, About Database and a database monitor; sprint 61: the builder's screens compared with APEX 26.1: the workspace sign-in page, the Administration menu with workspace message, environment banner and utilization, the SQL Workshop home, the App Gallery, application groups and cross application reports (four rows added); sprint 60: isolated workspaces with their own database logins, row-level security on pgkiln's tables and workspace database roles, workspace administrators; sprint 59: the Page Designer's help pane, Quick edit from the developer toolbar; sprint 58: page groups; sprint 57: region templates hero, title bar, buttons container, inline dialog and drawer, wizard with wizard progress, the carousel; sprint 56: Generate DDL and Schema Comparison in the SQL Workshop, `pgkiln ddl` and `pgkiln compare`; sprint 55: an editable login page with a Login region and the Minimal page template, Forgot password through the application's mail service; sprint 54: interactive grid views, the single row view, group by and chart; sprint 53: application settings, user preferences and items kept per user, dynamic translations, `meta.data_export` and data export downloads, display image and geocoded address items; sprint 52: every dynamic action event and action, Ajax callbacks, an error handling function, the session timeout warning; sprint 51: checked imports, application backups, availability and Run only, feedback, type-specific attribute help; sprint 50: lost update detection, warn on unsaved changes, the URL and search regions with search configurations, the developer toolbar with a session state viewer, and e-mail through SMTP with OAuth2, Microsoft Graph, Amazon SES and HTTP APIs; import checks, application availability, property help and e-mail compared again; 2026-10-06: pgkiln 0.31.0, in progress: static application files and the Execute JavaScript dynamic action; region, item, dynamic action and process plug-ins; conditional and dynamic theme styles; XML and Excel in `meta.parse_data`, zips in SQL, `meta.v_boolean`; file items in S3-compatible object storage; map layers by the visible area and as vector tiles; a graphical query builder; tenants for workflows and tasks; application files as YAML and region static IDs; Lucide icons and icon modifiers; PWA push notifications; 0.30.0: 136 icons and an icon picker; an axe-core accessibility audit; cropping pictures; session sharing between applications; twenty-two built-in languages; 0.29.0: workspaces; the Iris base style; drawers; built-in template components including the metric card; Theme Roller dark-mode colours, live preview and template options on items and report columns; report selection across pages; instance settings; fourteen built-in languages; 0.28.0: AI with Claude and OpenAI; 0.27.0: Data Reporter; sample data generator; create application from several sheets, pasted data or existing tables; 0.26.0: working copies with a three-way merge; theme, library and boilerplate applications with subscriptions; 0.25.0: Gantt, pyramid and polar charts; map layers, clustering and server-side spatial filters; REST data sources that write back from forms and grids and synchronise into tables, OAuth2 password and refresh-token grants; debug messages per request and an install/upgrade log; `meta.web_request()` and `meta.parse_data()`; Theme Roller style variants and template options; 0.24.0: automations with several actions, error handling per row and runs from SQL; invoke-API workflow steps; Unload Data in the Data Workshop; create page wizards for cards, calendar, chart, map, faceted search, form and master-detail; create an application from a file; 0.23.0: interactive grid aggregates, frozen/moved/resized columns, layouts and saved grid reports, master-detail, row actions and copy/paste; download processes, execution chains in the background, workflow processes, branches to a function's URL or another app, the Dialog Closed event; number format masks, automatic time zone, runtime messages in German, French and Spanish; SQL scripts, Quick SQL, a query builder, XML loading and data load definitions; custom authentication, lists, page and application locks with developer comments, supporting objects; 0.22.0: keyset paging, report PDFs and REST collections streamed from a cursor, database-account authentication; 0.21.0: HTTP-header authentication behind a reverse proxy; 0.20.0: a popup list of values with server-side search; 0.19.0: row-range pagination, maximum row counts and row limits, lazy loading and caching of regions, streamed CSV/Excel downloads; 0.18.0: calendar week/day/list views with drag and drop, bubble/gauge/funnel/radar charts and drill-down, smart filters, a region display selector and more facet types, computations, conditional branches, build options, menu buttons, REST data sources and web credentials, rich text/Markdown, rating, combobox, date range and QR code items; 0.17.0: an App Builder home, workspace dashboard and utilities like APEX's; 0.16.0: a Page Designer with drag-and-drop layout, a code editor with autocomplete, template components and plug-ins, parallel branches and versions of workflows, the `pgkiln` command line with one file per component; 0.15.0: drop and paste files, heat maps, filtering a report by the map area; 0.14.0: stacked, combo, scatter and pie charts, several files per upload item; 0.13.0: map and tree regions; 0.12.0: REST modules in the builder; 0.11.0: workflows and Progressive Web Apps; 0.10.0: LDAP, SAML, "Keep me signed in", document templates, JSON loading, approvals and the task list; pgkiln installs no application, HR is an example).
11
11
 
12
12
  ## At a glance
13
13
 
@@ -256,7 +256,7 @@ review is a diff. Not counted in [At a glance](#at-a-glance): the rows above com
256
256
  | Accessibility | ✅ | Labels, keyboard, focus rings, reduced motion, table alternatives for charts, scrolling tables reachable from the keyboard; an automated WCAG 2.1 A/AA audit (axe-core) of every example page in light, dark and Iris and of the builder's main pages runs in CI with no violations allowed. Not a manual expert audit |
257
257
  | Drawers, top/bottom dialogs (26.1) | ✅ | Modal pages open as a centred dialog or as a drawer from the left, right, top or bottom edge, in three sizes, sliding in (without animation for reduced motion); on phones full screen ([chapter 4](guide/04-pages-and-regions.md#dialogs-and-drawers)). Not yet: a footer slot for inline drawers |
258
258
  | New "Iris" default style (26.1) | ✅ | Base style **Iris**, the default for new applications, light by default with the user's choice of dark: (sprint 72) like APEX 26's: a white body, a dark warm-grey header with an accent strip and a dark menu, 4 px corners, region headers with a rule, a dark primary button, a large page title; light and dark palettes checked for WCAG AA contrast. The earlier look stays as **Lavender** ([chapter 14](guide/14-globalization.md#base-styles-iris-lavender-and-standard)) |
259
- | Progressive Web App (push notifications: 23.1) | ✅ | Per app: installable (manifest, icon, standalone), service worker, offline pages (opt-in, wiped at sign-in/out), **forms sent offline queued on the device and sent later** (files included, once only, under the same user), location, camera and barcode items. (0.31) **Push notifications**: users turn them on per device (My account, or the dynamic action `push_subscribe`); `meta.send_push(user, title, body, page, items)` and a `send_push` process queue them, sent after the commit, encrypted for the device (RFC 8291) and signed with the application's own VAPID key (RFC 8292, stored encrypted, never exported); links signed for the recipient; devices end at sign-out, a new password, deactivation or removed access ([chapter 17](guide/17-mobile.md#push-notifications)). No images or action buttons in notifications |
259
+ | Progressive Web App (push notifications: 23.1) | ✅ | Per app: installable (manifest, icon, standalone), service worker, offline pages (opt-in, wiped at sign-in/out), **forms sent offline queued on the device and sent later** (files included, once only, under the same user), location, camera and barcode items. (0.31) **Push notifications**: users turn them on per device (My account, or the dynamic action `push_subscribe`); `meta.send_push(user, title, body, page, items)` and a `send_push` process queue them, sent after the commit, encrypted for the device (RFC 8291) and signed with the application's own VAPID key (RFC 8292, stored encrypted, never exported); links signed for the recipient; devices end at sign-out, a new password, deactivation or removed access ([chapter 17](guide/17-mobile.md#push-notifications)). No images or action buttons in notifications; (168) **every page of the navigation offline** from the first visit (fetched read-only in the background after sign-in, again every N hours), beyond APEX |
260
260
 
261
261
  ## Globalization
262
262
 
@@ -525,7 +525,7 @@ navigation entries and application processes have the same `build_option` column
525
525
  | Table | Contents | Readable by the runtime role |
526
526
  |---|---|---|
527
527
  | `session` | Sessions: `token_hash` (SHA-256 of the cookie), `app_id` (NULL = builder), `username`, `roles` (resolved at sign-in), `csrf_token`, `state` (jsonb session state), `created_at`, `last_seen` | yes |
528
- | `activity_log` | `at`, `app_id`, `page_no`, `username`, `event` (`page_view`, `login`, `login_failed`, `login_locked`, `login_unlocked`, `logout`, `error`, `forbidden`, `api_token`, `password_expired`, `password_changed`; builder: `lock_broken`, `supporting_objects`, `sample_data`, …), `ip`, `elapsed_ms`, `detail` | yes (insert/select) |
528
+ | `activity_log` | `at`, `app_id`, `page_no`, `username`, `event` (`page_view`, `page_prefetch` (168: a page fetched for offline use), `login`, `login_failed`, `login_locked`, `login_unlocked`, `logout`, `error`, `forbidden`, `api_token`, `password_expired`, `password_changed`; builder: `lock_broken`, `supporting_objects`, `sample_data`, …), `ip`, `elapsed_ms`, `detail` | yes (insert/select) |
529
529
  | `debug_view` | [Debug messages](06-processing.md#debug-messages): one row per recorded request: `app_id`, `page_no`, `username`, `session_id`, `method`, `path` (without the query string), `status`, `level`, `started_at`, `elapsed_ms`, `entries`. Written through `meta.debug_save()` (runtime role only), not exported | no |
530
530
  | `debug_message` | The entries of a recorded request: `view_id`, `seq`, `elapsed_ms` (since the start), `duration_ms` (timed steps), `level`, `component`, `message` | no |
531
531
  | `web_request_log` | [Web requests from SQL](#web-requests-from-sql): `app_id`, `status`, the request (`url` or `source` + `params`, `method`, `headers`, `body`, `credential` name, `timeout_s`), `requested_by`, times, the response (`status_code`, `response_url`, `response_headers`, `response_body`), `message`. Kept 24 hours, not exported | no (through `meta.web_response`) |
@@ -13,6 +13,7 @@ the home screen with its own icon, full screen, and working on when the network
13
13
  | Installable | Adds a web app manifest and a service worker to the application. Phones offer "Add to home screen" / "Install"; the app then opens full screen, without the browser bar. (0.42) Where the browser offers the installation (Chrome, Edge), the header shows an **Install app** button (APEX: the navigation bar's Install App entry) |
14
14
  | Name under the icon | The short name on the home screen (the application's name when empty) |
15
15
  | Keep visited pages on the device | Pages a user opened are shown again when there is no connection (with a notice that they may be out of date) |
16
+ | Make every page of the navigation available offline | (168) With *Keep visited pages* on: after signing in, the device also fetches every page of the navigation menu and navigation bar the user may open, so the whole app works offline from the first visit; fetched again after *Fetch them again after* hours (1–168, default 12). [Below](#every-page-offline) |
16
17
  | Keep forms sent without a connection | Forms submitted offline are kept on the device and sent when the connection is back |
17
18
  | Push notifications | Users may turn on notifications per device; application code sends them ([below](#push-notifications)) |
18
19
  | Icon | A square PNG of at least 512 × 512 pixels. Without one, the app gets a tile with its initial in the theme's accent colour |
@@ -35,9 +36,38 @@ Everything is per application, under its own address: `/a/<alias>/manifest.webma
35
36
  listing the pages that are on the device.
36
37
  - Kept pages contain the user's data. They stay on the device only while the user is signed in:
37
38
  signing in or out removes them, so the next person on a shared device doesn't see them.
38
- - Live data (reports, charts) is as fresh as the last visit. Actions that need the server (searching,
39
+ - Live data (reports, charts) is as fresh as the last visit (or the last fetch, below). Actions that need the server (searching,
39
40
  downloads, dynamic actions) wait for the connection.
40
41
 
42
+ ## Every page offline
43
+
44
+ (168) With *Make every page of the navigation available offline*, a user doesn't have to open every page
45
+ once before going out of range:
46
+
47
+ 1. On the first page after signing in (and on the first page opened on a device), the page hands the
48
+ links of its **navigation menu and navigation bar** to the service worker. Those are exactly the pages
49
+ the server showed this user: authorization schemes, conditions and page access already applied. Dialog
50
+ pages, other sites and the sign-in and sign-out pages are left out; at most 60 pages are fetched.
51
+ 2. The service worker fetches them **one at a time** in the background, with a short pause between
52
+ them, and keeps each page that comes back. It skips the whole thing when the browser asks to save
53
+ data (*Data Saver*), and stops at the first network error, to try again on the next page.
54
+ 3. When it is done it notes the time; after *Fetch them again after* hours (default 12) the next page
55
+ opened online starts a new round. Signing in or out empties the kept pages, so the next person
56
+ starts over.
57
+
58
+ The server shows a fetched page **read-only** (the request carries `X-Pgkiln-Prefetch: 1`):
59
+
60
+ - the page's transaction is read-only: an application process or a region query that writes fails
61
+ (the region shows its error in the kept copy) and nothing is changed;
62
+ - the session's state is not kept, and a message waiting for the next page stays there;
63
+ - a page with a process that runs when it opens (point *On load*) is not fetched at all: it is kept
64
+ only when the user opens it, as before. So are dialogs, documents and downloads;
65
+ - each fetch is logged as `page_prefetch`, apart from `page_view`, so activity reports count real visits.
66
+
67
+ Only the pages of the menus are fetched, with their first page of rows: a form for one record
68
+ (`?P3_ID=…`) or a report's other pages are kept only once the user opens them. The kept copies hold
69
+ the user's data, like visited pages: turn it on for apps used on devices that belong to one person.
70
+
41
71
  ## Forms sent offline
42
72
 
43
73
  With *Keep forms sent without a connection* on, a form submitted while the network is down (or
@@ -127,9 +157,10 @@ results, and a *Send test* button.
127
157
 
128
158
  ## Example
129
159
 
130
- The HR example application is a PWA: install it from the browser menu on a phone, open a few pages,
131
- switch on airplane mode, and file a leave request; it is sent when the connection is back. Its
132
- employee form records a work location and takes the photo with the camera (`examples/hr/hr_12_pwa.sql`).
160
+ The HR example application is a PWA: install it from the browser menu on a phone, sign in, wait a few
161
+ seconds, switch on airplane mode, and every page of the menu still opens (`examples/hr/hr_82_pwa_prefetch.sql`);
162
+ file a leave request, and it is sent when the connection is back. Its employee form records a work location
163
+ and takes the photo with the camera (`examples/hr/hr_12_pwa.sql`).
133
164
 
134
165
  ## Not included
135
166
 
@@ -0,0 +1,11 @@
1
+ -- =====================================================================
2
+ -- HR example, part 82: the whole app offline from the first visit
3
+ -- (docs/guide/17-mobile.md, sprint 168)
4
+ --
5
+ -- After signing in on a phone, every page of the navigation menu the
6
+ -- user may open is fetched in the background and kept on the device, so
7
+ -- the app works on the train even for pages never opened before; the
8
+ -- copies are fetched again after 12 hours. Pages that run processes when
9
+ -- they open (20, 35, 43) are kept only when the user opens them.
10
+ -- =====================================================================
11
+ update meta.app set pwa_prefetch_hours = 12 where alias = 'hr';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgkiln",
3
- "version": "0.87.0",
3
+ "version": "0.88.0",
4
4
  "description": "Open source low-code, SQL-driven application builder for PostgreSQL, inspired by Oracle APEX",
5
5
  "keywords": [
6
6
  "postgresql",
package/public/app.js CHANGED
@@ -2972,6 +2972,17 @@ const liveBadges = (() => {
2972
2972
  if (e.data && e.data.type === 'pgkiln:pushed') liveBadges.now();
2973
2973
  });
2974
2974
  if (new URLSearchParams(location.search).get('queued') === '1') note(t('pwa.queued'));
2975
+ // (168) every page of the navigation offline: this page and the menu's and navigation bar's pages of the app
2976
+ // (the server already left out the ones this user may not open; dialogs and other sites are not pages to keep)
2977
+ if (body.dataset.prefetch && navigator.onLine && !(navigator.connection && navigator.connection.saveData)) {
2978
+ const links = [...document.querySelectorAll('#t-nav a[href], .t-navbar a[href]')].filter((a) => !a.hasAttribute('data-dialog'));
2979
+ const urls = [location.href, ...links.map((a) => a.href)].filter((h) => {
2980
+ const u = new URL(h, location.href);
2981
+ return u.origin === location.origin && u.pathname.startsWith(`${body.dataset.base}/`) && !/\/(login|logout)$/.test(u.pathname);
2982
+ });
2983
+ // a page without the navigation (the sign-in page, a dialog) has nothing to hand over
2984
+ if (links.length) post({ type: 'pgkiln:prefetch', urls: [...new Set(urls)] });
2985
+ }
2975
2986
  }
2976
2987
 
2977
2988
  // (165) live badges: every data-live seconds the page asks for its badges (navigation menu and bar,
package/public/sw.js CHANGED
@@ -4,6 +4,8 @@
4
4
  // (offline, the cached copy of another version)
5
5
  // pages (navigations): network first; offline, the cached copy (only when the app keeps pages) or the offline page
6
6
  // signing in or out empties the page cache, so the next person on the device doesn't see them
7
+ // (168) with prefetchHours: the pages app.js hands over (the navigation's) are fetched one by one and kept, again
8
+ // when the copies are older than prefetchHours (a stamp in the page cache, so a sign-in starts over)
7
9
  // push notifications (migration 074): shown as they arrive, a click opens their page of the app;
8
10
  // signing out turns them off on this device
9
11
  /* global PGKILN */
@@ -77,10 +79,58 @@ self.addEventListener('message', (event) => {
77
79
  caches
78
80
  .open(PAGES)
79
81
  .then((c) => c.keys())
80
- .then((keys) => event.source.postMessage({ type: 'pgkiln:pages', pages: keys.map((k) => k.url) }));
82
+ .then((keys) => event.source.postMessage({ type: 'pgkiln:pages', pages: keys.map((k) => k.url).filter((u) => new URL(u).pathname !== STAMP) }));
81
83
  }
84
+ if (event.data && event.data.type === 'pgkiln:prefetch' && Array.isArray(event.data.urls)) event.waitUntil(prefetchPages(event.data.urls));
82
85
  });
83
86
 
87
+ // ------------------------------------------------------------------ every page offline (168, PGKILN.prefetchHours)
88
+ // The server answers a request with X-Pgkiln-Prefetch read-only, keeps no session state, and refuses (409) pages
89
+ // that run processes when they open; only 200 pages are kept. One page at a time, a short pause between them;
90
+ // the stamp is written when the list is done, so a list cut short by the network is tried again next time.
91
+ const STAMP = `${PGKILN.base}/__prefetched`;
92
+ const MAX_PREFETCH = 60;
93
+ let prefetching = false;
94
+ async function prefetchPages(urls) {
95
+ if (!PGKILN.offlinePages || !PGKILN.prefetchHours || prefetching) return;
96
+ prefetching = true;
97
+ try {
98
+ const cache = await caches.open(PAGES);
99
+ const stamp = await cache.match(STAMP);
100
+ if (stamp && Date.now() - Number(await stamp.text()) < PGKILN.prefetchHours * 3600 * 1000) return;
101
+ const seen = new Set();
102
+ let kept = 0;
103
+ for (const u of urls) {
104
+ if (seen.size >= MAX_PREFETCH) break;
105
+ let url;
106
+ try {
107
+ url = new URL(u, self.location.origin);
108
+ } catch {
109
+ continue;
110
+ }
111
+ url.hash = '';
112
+ if (url.origin !== self.location.origin || !url.pathname.startsWith(`${PGKILN.base}/`) || signInOrOut(url) || seen.has(url.href)) continue;
113
+ seen.add(url.href);
114
+ let res;
115
+ try {
116
+ res = await fetch(url.href, { credentials: 'same-origin', redirect: 'manual', headers: { 'X-Pgkiln-Prefetch': '1' } });
117
+ } catch {
118
+ return; // offline: again on the next page
119
+ }
120
+ if (res.status === 200 && (res.headers.get('content-type') || '').startsWith('text/html')) {
121
+ await cache.put(new Request(url.href), res);
122
+ kept++;
123
+ }
124
+ await new Promise((r) => setTimeout(r, 150));
125
+ }
126
+ if (!seen.size) return;
127
+ await cache.put(STAMP, new Response(String(Date.now())));
128
+ for (const c of await self.clients.matchAll({ type: 'window' })) c.postMessage({ type: 'pgkiln:prefetched', kept });
129
+ } finally {
130
+ prefetching = false;
131
+ }
132
+ }
133
+
84
134
  // ------------------------------------------------------------------ offline form queue (PGKILN.offlineSubmit)
85
135
  // A form posted while the network is down is kept on the device (IndexedDB, files included) and the
86
136
  // browser shows the page again with ?queued=1. It is sent later (on "online", when a page of the app
@@ -7510,4 +7510,10 @@ export const de: BuilderMessages = {
7510
7510
  'pjb.steps': 'Schritte',
7511
7511
  'pjb.message': 'Meldung',
7512
7512
  'pjb.no_runs': 'Noch keine Ausführungen.',
7513
+ // ------------------------------------------------------------ PWA: every page offline (sprint 168)
7514
+ 'pwa.prefetch': 'Alle Seiten der Navigation nach der Anmeldung offline verfügbar machen',
7515
+ 'pwa.prefetch_help': 'Erfordert „Besuchte Seiten behalten“. Nach einer Anmeldung und auf der ersten auf einem Gerät geöffneten Seite lädt das Gerät nacheinander alle Seiten des Navigationsmenüs und der Navigationsleiste, die der Benutzer öffnen darf. Der Server zeigt sie schreibgeschützt: Seiten, die beim Öffnen Prozesse ausführen, werden ausgelassen und erst gespeichert, wenn der Benutzer sie öffnet.',
7516
+ 'pwa.prefetch_hours': 'Erneut laden nach (Stunden)',
7517
+ 'pwa.prefetch_hours_help': '1 bis 168. Die Seiten werden nach dieser Zeit beim ersten Seitenaufruf erneut geladen, sofern eine Verbindung besteht.',
7518
+ 'pwa.prefetch_hours_range': 'Erneut laden nach: eine ganze Zahl von Stunden zwischen 1 und 168.',
7513
7519
  };
@@ -7514,4 +7514,10 @@ export const en = {
7514
7514
  'pjb.steps': 'Steps',
7515
7515
  'pjb.message': 'Message',
7516
7516
  'pjb.no_runs': 'No runs yet.',
7517
+ // ------------------------------------------------------------ PWA: every page offline (sprint 168)
7518
+ 'pwa.prefetch': 'Make every page of the navigation available offline after sign-in',
7519
+ 'pwa.prefetch_help': 'Needs Keep visited pages. After a sign-in, and on the first page opened on a device, the device fetches every page of the navigation menu and navigation bar the user may open, one at a time. The server shows them read-only: pages that run processes when they open are left out and kept only when the user opens them.',
7520
+ 'pwa.prefetch_hours': 'Fetch them again after (hours)',
7521
+ 'pwa.prefetch_hours_help': '1 to 168. The pages are fetched again on the first page opened after this time, while there is a connection.',
7522
+ 'pwa.prefetch_hours_range': 'Fetch them again after: a whole number of hours from 1 to 168.',
7517
7523
  } as const;
@@ -7510,4 +7510,10 @@ export const es: BuilderMessages = {
7510
7510
  'pjb.steps': 'Pasos',
7511
7511
  'pjb.message': 'Mensaje',
7512
7512
  'pjb.no_runs': 'Aún no hay ejecuciones.',
7513
+ // ------------------------------------------------------------ PWA: every page offline (sprint 168)
7514
+ 'pwa.prefetch': 'Hacer que todas las páginas de la navegación estén disponibles sin conexión después de iniciar sesión',
7515
+ 'pwa.prefetch_help': 'Requiere Guardar las páginas visitadas. Después de iniciar sesión, y en la primera página abierta en un dispositivo, el dispositivo obtiene una por una todas las páginas del menú y de la barra de navegación que el usuario puede abrir. El servidor las muestra en solo lectura: las páginas que ejecutan procesos al abrirse se omiten y solo se guardan cuando el usuario las abre.',
7516
+ 'pwa.prefetch_hours': 'Volver a obtenerlas después de (horas)',
7517
+ 'pwa.prefetch_hours_help': 'De 1 a 168. Las páginas se vuelven a obtener en la primera página abierta después de este tiempo, mientras haya conexión.',
7518
+ 'pwa.prefetch_hours_range': 'Volver a obtenerlas después de: un número entero de horas entre 1 y 168.',
7513
7519
  };
@@ -7510,4 +7510,10 @@ export const fr: BuilderMessages = {
7510
7510
  'pjb.steps': 'Étapes',
7511
7511
  'pjb.message': 'Message',
7512
7512
  'pjb.no_runs': 'Aucune exécution pour l’instant.',
7513
+ // ------------------------------------------------------------ PWA: every page offline (sprint 168)
7514
+ 'pwa.prefetch': 'Rendre toutes les pages de la navigation disponibles hors ligne après la connexion',
7515
+ 'pwa.prefetch_help': 'Nécessite Conserver les pages visitées. Après une connexion, et sur la première page ouverte sur un appareil, l’appareil récupère une à une toutes les pages du menu et de la barre de navigation que l’utilisateur peut ouvrir. Le serveur les affiche en lecture seule : les pages qui exécutent des processus à l’ouverture sont ignorées et conservées seulement quand l’utilisateur les ouvre.',
7516
+ 'pwa.prefetch_hours': 'Les récupérer à nouveau après (heures)',
7517
+ 'pwa.prefetch_hours_help': 'De 1 à 168. Les pages sont récupérées à nouveau à la première page ouverte après ce délai, s’il y a une connexion.',
7518
+ 'pwa.prefetch_hours_range': 'Les récupérer à nouveau après : un nombre entier d’heures entre 1 et 168.',
7513
7519
  };
@@ -7510,4 +7510,10 @@ export const it: BuilderMessages = {
7510
7510
  'pjb.steps': 'Passi',
7511
7511
  'pjb.message': 'Messaggio',
7512
7512
  'pjb.no_runs': 'Ancora nessuna esecuzione.',
7513
+ // ------------------------------------------------------------ PWA: every page offline (sprint 168)
7514
+ 'pwa.prefetch': 'Rendi disponibili offline tutte le pagine della navigazione dopo l’accesso',
7515
+ 'pwa.prefetch_help': 'Richiede Conserva le pagine visitate. Dopo l’accesso, e alla prima pagina aperta su un dispositivo, il dispositivo recupera una alla volta tutte le pagine del menu e della barra di navigazione che l’utente può aprire. Il server le mostra in sola lettura: le pagine che eseguono processi all’apertura vengono escluse e conservate solo quando l’utente le apre.',
7516
+ 'pwa.prefetch_hours': 'Recuperarle di nuovo dopo (ore)',
7517
+ 'pwa.prefetch_hours_help': 'Da 1 a 168. Le pagine vengono recuperate di nuovo alla prima pagina aperta dopo questo tempo, se c’è connessione.',
7518
+ 'pwa.prefetch_hours_range': 'Recuperarle di nuovo dopo: un numero intero di ore da 1 a 168.',
7513
7519
  };
@@ -7510,4 +7510,10 @@ export const ja: BuilderMessages = {
7510
7510
  'pjb.steps': 'ステップ',
7511
7511
  'pjb.message': 'メッセージ',
7512
7512
  'pjb.no_runs': 'まだ実行はありません。',
7513
+ // ------------------------------------------------------------ PWA: every page offline (sprint 168)
7514
+ 'pwa.prefetch': 'サインイン後、ナビゲーションのすべてのページをオフラインで使用できるようにする',
7515
+ 'pwa.prefetch_help': '「表示したページを保持」が必要です。サインイン後、およびデバイスで最初のページを開いたときに、ユーザーが開けるナビゲーション メニューとナビゲーション バーのすべてのページを 1 つずつ取得します。サーバーはそれらを読み取り専用で表示します。開くときにプロセスを実行するページは除外され、ユーザーが開いたときにのみ保持されます。',
7516
+ 'pwa.prefetch_hours': '再取得までの時間(時間)',
7517
+ 'pwa.prefetch_hours_help': '1~168。この時間が過ぎた後、接続があるときに最初にページを開くと再取得されます。',
7518
+ 'pwa.prefetch_hours_range': '再取得までの時間: 1~168 の整数の時間を指定してください。',
7513
7519
  };
@@ -7510,4 +7510,10 @@ export const ko: BuilderMessages = {
7510
7510
  'pjb.steps': '단계',
7511
7511
  'pjb.message': '메시지',
7512
7512
  'pjb.no_runs': '아직 실행이 없습니다.',
7513
+ // ------------------------------------------------------------ PWA: every page offline (sprint 168)
7514
+ 'pwa.prefetch': '로그인 후 탐색의 모든 페이지를 오프라인으로 사용할 수 있게 함',
7515
+ 'pwa.prefetch_help': '‘방문한 페이지 보관’이 필요합니다. 로그인 후와 기기에서 처음 페이지를 열 때, 기기는 사용자가 열 수 있는 탐색 메뉴와 탐색 모음의 모든 페이지를 하나씩 가져옵니다. 서버는 이를 읽기 전용으로 표시합니다. 열 때 프로세스를 실행하는 페이지는 제외되며 사용자가 열 때만 보관됩니다.',
7516
+ 'pwa.prefetch_hours': '다시 가져오기까지의 시간(시간)',
7517
+ 'pwa.prefetch_hours_help': '1~168. 이 시간이 지난 뒤 연결되어 있을 때 처음 페이지를 열면 다시 가져옵니다.',
7518
+ 'pwa.prefetch_hours_range': '다시 가져오기까지의 시간: 1~168 사이의 정수 시간입니다.',
7513
7519
  };
@@ -7510,4 +7510,10 @@ export const nl: BuilderMessages = {
7510
7510
  'pjb.steps': 'Stappen',
7511
7511
  'pjb.message': 'Melding',
7512
7512
  'pjb.no_runs': 'Nog geen uitvoeringen.',
7513
+ // ------------------------------------------------------------ PWA: every page offline (sprint 168)
7514
+ 'pwa.prefetch': 'Alle pagina\'s van de navigatie na het aanmelden offline beschikbaar maken',
7515
+ 'pwa.prefetch_help': 'Vereist Bezochte pagina\'s bewaren. Na het aanmelden, en op de eerste pagina die op een apparaat wordt geopend, haalt het apparaat één voor één alle pagina\'s van het navigatiemenu en de navigatiebalk op die de gebruiker mag openen. De server toont ze alleen-lezen: pagina\'s die bij het openen processen uitvoeren worden overgeslagen en pas bewaard als de gebruiker ze opent.',
7516
+ 'pwa.prefetch_hours': 'Opnieuw ophalen na (uren)',
7517
+ 'pwa.prefetch_hours_help': '1 tot 168. De pagina\'s worden opnieuw opgehaald bij de eerste pagina die na deze tijd wordt geopend, als er verbinding is.',
7518
+ 'pwa.prefetch_hours_range': 'Opnieuw ophalen na: een geheel aantal uren van 1 tot 168.',
7513
7519
  };
@@ -7510,4 +7510,10 @@ export const pt: BuilderMessages = {
7510
7510
  'pjb.steps': 'Etapas',
7511
7511
  'pjb.message': 'Mensagem',
7512
7512
  'pjb.no_runs': 'Nenhuma execução ainda.',
7513
+ // ------------------------------------------------------------ PWA: every page offline (sprint 168)
7514
+ 'pwa.prefetch': 'Disponibilizar todas as páginas da navegação offline após o login',
7515
+ 'pwa.prefetch_help': 'Requer Manter as páginas visitadas. Após o login, e na primeira página aberta em um dispositivo, o dispositivo busca uma a uma todas as páginas do menu e da barra de navegação que o usuário pode abrir. O servidor as exibe somente leitura: páginas que executam processos ao abrir são deixadas de fora e mantidas só quando o usuário as abre.',
7516
+ 'pwa.prefetch_hours': 'Buscá-las novamente após (horas)',
7517
+ 'pwa.prefetch_hours_help': 'De 1 a 168. As páginas são buscadas novamente na primeira página aberta após esse tempo, quando houver conexão.',
7518
+ 'pwa.prefetch_hours_range': 'Buscá-las novamente após: um número inteiro de horas de 1 a 168.',
7513
7519
  };
@@ -7510,4 +7510,10 @@ export const zhCN: BuilderMessages = {
7510
7510
  'pjb.steps': '步骤',
7511
7511
  'pjb.message': '消息',
7512
7512
  'pjb.no_runs': '尚无运行记录。',
7513
+ // ------------------------------------------------------------ PWA: every page offline (sprint 168)
7514
+ 'pwa.prefetch': '登录后使导航中的所有页面可离线使用',
7515
+ 'pwa.prefetch_help': '需要“将访问过的页面保留在设备上”。登录后,以及在设备上打开第一个页面时,设备会逐个获取用户可以打开的导航菜单和导航栏中的所有页面。服务器以只读方式显示它们:打开时运行进程的页面会被跳过,只有在用户打开时才会保留。',
7516
+ 'pwa.prefetch_hours': '多少小时后重新获取',
7517
+ 'pwa.prefetch_hours_help': '1 到 168。超过此时间后,在有网络连接时打开第一个页面时会重新获取这些页面。',
7518
+ 'pwa.prefetch_hours_range': '重新获取时间:1 到 168 之间的整数小时。',
7513
7519
  };
@@ -7510,4 +7510,10 @@ export const zhTW: BuilderMessages = {
7510
7510
  'pjb.steps': '步驟',
7511
7511
  'pjb.message': '訊息',
7512
7512
  'pjb.no_runs': '尚無執行記錄。',
7513
+ // ------------------------------------------------------------ PWA: every page offline (sprint 168)
7514
+ 'pwa.prefetch': '登入後使導覽中的所有頁面可離線使用',
7515
+ 'pwa.prefetch_help': '需要「將造訪過的頁面保留在裝置上」。登入後,以及在裝置上開啟第一個頁面時,裝置會逐一取得使用者可以開啟的導覽選單和導覽列中的所有頁面。伺服器以唯讀方式顯示它們:開啟時執行處理程序的頁面會被略過,只有在使用者開啟時才會保留。',
7516
+ 'pwa.prefetch_hours': '多少小時後重新取得',
7517
+ 'pwa.prefetch_hours_help': '1 到 168。超過此時間後,在有網路連線時開啟第一個頁面時會重新取得這些頁面。',
7518
+ 'pwa.prefetch_hours_range': '重新取得時間:1 到 168 之間的整數小時。',
7513
7519
  };
@@ -32,7 +32,7 @@ async function pushStats(appId: number) {
32
32
  ))!;
33
33
  }
34
34
 
35
- export async function pwaSection(a: { id: number; alias: string; pwa: boolean; pwa_short_name: string | null; pwa_offline_pages: boolean; pwa_offline_submit: boolean; pwa_push: boolean; has_icon: boolean }, s: Session) {
35
+ export async function pwaSection(a: { id: number; alias: string; pwa: boolean; pwa_short_name: string | null; pwa_offline_pages: boolean; pwa_prefetch_hours: number | null; pwa_offline_submit: boolean; pwa_push: boolean; has_icon: boolean }, s: Session) {
36
36
  const check = (name: string, label: string, on: boolean, help: string) =>
37
37
  html`<div class="field"><label class="check"><input type="checkbox" name="${name}" value="true"${on ? raw(' checked') : ''}> ${label}</label><small class="help">${help}</small></div>`;
38
38
  const stats = a.pwa && a.pwa_push ? await pushStats(a.id) : null;
@@ -42,6 +42,10 @@ export async function pwaSection(a: { id: number; alias: string; pwa: boolean; p
42
42
  ${input('pwa_short_name', tx('pwa.short_name'), a.pwa_short_name ?? '', { placeholder: tx('pwa.short_name_ph') })}
43
43
  </div>
44
44
  ${check('pwa_offline_pages', tx('pwa.offline_pages'), a.pwa_offline_pages, tx('pwa.offline_pages_help'))}
45
+ ${check('pwa_prefetch', tx('pwa.prefetch'), a.pwa_prefetch_hours !== null, tx('pwa.prefetch_help'))}
46
+ <div class="form-grid">
47
+ ${input('pwa_prefetch_hours', tx('pwa.prefetch_hours'), a.pwa_prefetch_hours ?? 12, { type: 'number', help: tx('pwa.prefetch_hours_help') })}
48
+ </div>
45
49
  ${check('pwa_offline_submit', tx('pwa.offline_submit'), a.pwa_offline_submit, tx('pwa.offline_submit_help'))}
46
50
  ${check('pwa_push', tx('pwa.push'), a.pwa_push, tx('pwa.push_help'))}
47
51
  <div class="field" data-wide><label class="label" for="f_pwa_icon">${tx('pwa.icon')}</label>
@@ -98,10 +102,17 @@ export async function pwaBuilderRoutes(app: FastifyInstance) {
98
102
  flash(s, tx('pwa.push_needs_key'), 'error');
99
103
  return back(reply, s, target);
100
104
  }
105
+ // (168) every navigation page offline: only with visited pages kept, every 1 to 168 hours
106
+ const hours = Number(b.pwa_prefetch_hours ?? 12);
107
+ if (b.pwa_prefetch === 'true' && (!Number.isInteger(hours) || hours < 1 || hours > 168)) {
108
+ flash(s, tx('pwa.prefetch_hours_range'), 'error');
109
+ return back(reply, s, target);
110
+ }
111
+ const prefetch = b.pwa_prefetch === 'true' && b.pwa_offline_pages === 'true' ? hours : null;
101
112
  await owner.query(
102
113
  `update meta.app set pwa = $2, pwa_short_name = $3, pwa_offline_pages = $4, pwa_offline_submit = $5,
103
- pwa_icon = case when $7 then $6 else pwa_icon end, pwa_push = $8, updated_at = now() where id = $1`,
104
- [id, b.pwa === 'true', (b.pwa_short_name ?? '').trim().slice(0, 30) || null, b.pwa_offline_pages === 'true', b.pwa_offline_submit === 'true', icon ?? null, icon !== undefined, push],
114
+ pwa_icon = case when $7 then $6 else pwa_icon end, pwa_push = $8, pwa_prefetch_hours = $9, updated_at = now() where id = $1`,
115
+ [id, b.pwa === 'true', (b.pwa_short_name ?? '').trim().slice(0, 30) || null, b.pwa_offline_pages === 'true', b.pwa_offline_submit === 'true', icon ?? null, icon !== undefined, push, prefetch],
105
116
  );
106
117
  if (push) await appPublicKey(Number(id));
107
118
  flash(s, push && b.pwa !== 'true' ? tx('pwa.saved_not_installable') : tx('pwa.saved'));
package/src/metadata.ts CHANGED
@@ -99,6 +99,8 @@ export interface App {
99
99
  pwa_short_name: string | null;
100
100
  pwa_has_icon: boolean;
101
101
  pwa_offline_pages: boolean;
102
+ /** (168) fetch every navigation page for offline use, again after this many hours; null: off */
103
+ pwa_prefetch_hours: number | null;
102
104
  pwa_offline_submit: boolean;
103
105
  /** (074) users may turn on push notifications (src/push.ts) */
104
106
  pwa_push: boolean;
@@ -448,7 +450,7 @@ const agg = (table: string, fk: string, parent: string, appId: string) =>
448
450
  // No caching on purpose: edits made in the builder show up on the next request.
449
451
  export async function loadApp(alias: string) {
450
452
  return runtime.one<App>(
451
- `select a.id, a.alias, a.name, a.version, a.home_page, a.authentication, a.access_control, a.sso_providers, a.local_login, a.remember_me_days, a.login_page, a.password_reset, a.session_group, a.mfa, a.mfa_roles, a.mfa_email, a.api_self_tokens, a.ldap_directories, a.header_name, a.header_auto_create, a.logout_url, a.db_auth_roles, a.db_auth_member_of, a.custom_auth_function, a.custom_auth_code, a.custom_auth_post_code, a.nav_list, a.navbar_list, a.pwa, a.pwa_short_name, a.pwa_icon is not null as pwa_has_icon, a.pwa_offline_pages, a.pwa_offline_submit, a.pwa_push, a.db_role, a.debug, a.debug_level, a.theme,
453
+ `select a.id, a.alias, a.name, a.version, a.home_page, a.authentication, a.access_control, a.sso_providers, a.local_login, a.remember_me_days, a.login_page, a.password_reset, a.session_group, a.mfa, a.mfa_roles, a.mfa_email, a.api_self_tokens, a.ldap_directories, a.header_name, a.header_auto_create, a.logout_url, a.db_auth_roles, a.db_auth_member_of, a.custom_auth_function, a.custom_auth_code, a.custom_auth_post_code, a.nav_list, a.navbar_list, a.pwa, a.pwa_short_name, a.pwa_icon is not null as pwa_has_icon, a.pwa_offline_pages, a.pwa_prefetch_hours, a.pwa_offline_submit, a.pwa_push, a.db_role, a.debug, a.debug_level, a.theme,
452
454
  a.availability, a.availability_message, a.availability_users, a.feedback, a.notifications, a.live_badges_s, a.error_function, a.session_warning_seconds, a.component_settings,
453
455
  a.language, a.languages, a.language_from, a.date_format, a.timestamp_format, a.time_zone, a.time_zone_auto, a.currency,
454
456
  coalesce((select jsonb_agg(jsonb_build_object('name', l.name, 'query', l.query, 'rest_source', l.rest_source)) from meta.lov l where l.app_id = a.id), '[]') as lovs,
@@ -38,9 +38,14 @@ export function pwaHead(a: Pick<App, 'alias' | 'pwa' | 'theme'>): Raw | '' {
38
38
  }
39
39
 
40
40
  /** <body> data attributes app.js uses: register the service worker, queue offline submissions. */
41
- export function pwaBody(a: Pick<App, 'alias' | 'pwa' | 'pwa_offline_submit'>, user: string): Record<string, string> {
41
+ export function pwaBody(a: Pick<App, 'alias' | 'pwa' | 'pwa_offline_submit' | 'pwa_offline_pages' | 'pwa_prefetch_hours'>, user: string): Record<string, string> {
42
42
  if (!a.pwa) return {};
43
- return { 'data-sw': `/a/${a.alias}/sw.js`, ...(a.pwa_offline_submit ? { 'data-offline-queue': '1', 'data-user': user } : {}) };
43
+ return {
44
+ 'data-sw': `/a/${a.alias}/sw.js`,
45
+ ...(a.pwa_offline_submit ? { 'data-offline-queue': '1', 'data-user': user } : {}),
46
+ // (168) app.js hands the navigation's pages to the service worker, which keeps them for offline use
47
+ ...(a.pwa_offline_pages && a.pwa_prefetch_hours ? { 'data-prefetch': String(a.pwa_prefetch_hours) } : {}),
48
+ };
44
49
  }
45
50
 
46
51
  // ------------------------------------------------------------------ icons (PNG, no image library)
@@ -175,7 +180,7 @@ export async function pwaRoutes(app: FastifyInstance) {
175
180
  const base = `/a/${a.alias}`;
176
181
  // the settings the worker needs, as a JSON literal (no user input in it)
177
182
  const version = staticVersion();
178
- const config = JSON.stringify({ base, offlinePages: a.pwa_offline_pages, offlineSubmit: a.pwa_offline_submit, version });
183
+ const config = JSON.stringify({ base, offlinePages: a.pwa_offline_pages, prefetchHours: a.pwa_offline_pages ? a.pwa_prefetch_hours : null, offlineSubmit: a.pwa_offline_submit, version });
179
184
  return reply
180
185
  .type('text/javascript; charset=utf-8')
181
186
  .header('cache-control', 'no-cache')
@@ -444,24 +444,37 @@ export async function runtimeRoutes(app: FastifyInstance) {
444
444
  const normalised = normaliseReportParams(ctx.params);
445
445
  if (normalised !== null) return reply.redirect(`${ctx.base}/${ctx.page.page_no}${normalised ? `?${normalised}` : ''}`);
446
446
 
447
+ // (168) the service worker fetching the page for offline use (pwa_prefetch_hours): the page runs read-only,
448
+ // its session state isn't kept and nothing waiting for the next page (a message) is taken; a page that runs
449
+ // processes when it opens is refused (kept only when the user opens it)
450
+ const prefetch = req.headers['x-pgkiln-prefetch'] === '1';
451
+ if (prefetch && (ctx.dialog || ctx.page.processes.some((p) => p.point === 'load')))
452
+ return reply.code(409).header('cache-control', 'no-store').send('Not kept for offline use: this page runs processes when it opens.');
453
+
447
454
  if (!applyUrlItems(ctx)) return forbidden(ctx, reply, ctx.locale.t('error.checksum'), `checksum error: ${req.url}`);
448
455
  // a master grid's selected row (signed link) into its item
449
456
  applyRowSelection(ctx);
450
- const flash = takeFlash(ctx.session);
451
- if (flash) ctx.messages.push(flash);
452
- const flashError = takeFlash(ctx.session, '__FLASH_ERROR');
453
- if (flashError) ctx.errors.page.push(flashError);
454
- const saved = takeFlash(ctx.session, '__GRID_SAVED');
455
- if (typeof saved === 'string') ctx.savedGrids = saved.split(',').map(Number).filter(Number.isInteger);
457
+ if (!prefetch) {
458
+ const flash = takeFlash(ctx.session);
459
+ if (flash) ctx.messages.push(flash);
460
+ const flashError = takeFlash(ctx.session, '__FLASH_ERROR');
461
+ if (flashError) ctx.errors.page.push(flashError);
462
+ const saved = takeFlash(ctx.session, '__GRID_SAVED');
463
+ if (typeof saved === 'string') ctx.savedGrids = saved.split(',').map(Number).filter(Number.isInteger);
464
+ }
456
465
  // Actions → Download CSV / Excel / PDF: r<region id>_csv=1, _xlsx=1 or _pdf=1
457
466
  const downloadKey = [...ctx.params.keys()].find((k) => /^r\d+_(csv|xlsx|pdf)$/.test(k));
458
467
  // a document template: ?doc=NAME (see documents.ts)
459
468
  const docName = ctx.params.get('doc');
460
469
 
470
+ if (prefetch && (docName || downloadKey)) return reply.code(409).header('cache-control', 'no-store').send('Not kept for offline use.');
471
+
461
472
  let result: { html?: string; streamed?: boolean; file?: Buffer; type?: string; name?: string; redirect?: string; download?: Download };
462
473
  try {
463
474
  result = await appTx(txContext(ctx), async (c) => {
464
475
  ctx.client = c;
476
+ // (168) a prefetch writes nothing: an application process or region that tries fails
477
+ if (prefetch) await c.query('set local transaction_read_only = on');
465
478
  await timed(ctx, 4, 'page', 'page authorization', () => checkPageAccess(ctx));
466
479
  await timed(ctx, 4, 'page', 'application processes (before page)', () => runAppProcesses(ctx, 'before_page'));
467
480
  // (086) items kept per user: their values from the user's preferences
@@ -516,6 +529,12 @@ export async function runtimeRoutes(app: FastifyInstance) {
516
529
  if (e instanceof Forbidden) return forbidden(ctx, reply, e.message, `page ${ctx.page.page_no}`);
517
530
  throw e;
518
531
  }
532
+ if (prefetch) {
533
+ // a redirect (a branch, a sign-in) or anything but the page itself is not kept on the device
534
+ logActivity({ appId: ctx.app.id, pageNo: ctx.page.page_no, username: ctx.user, event: 'page_prefetch', ip: ctx.ip, elapsedMs: Math.round(performance.now() - started) });
535
+ if (result.html === undefined) return reply.code(409).header('cache-control', 'no-store').send('Not kept for offline use.');
536
+ return reply.type('text/html').header('cache-control', 'private, no-store').send(result.html);
537
+ }
519
538
  await saveState(ctx.session);
520
539
  if (result.redirect) return reply.redirect(result.redirect, 303);
521
540
  logActivity({ appId: ctx.app.id, pageNo: ctx.page.page_no, username: ctx.user, event: 'page_view', ip: ctx.ip, elapsedMs: Math.round(performance.now() - started) });