pgkiln 0.87.0 → 0.89.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.
- package/db/migrations/156_pwa_prefetch.sql +13 -0
- package/docs/apex-feature-parity.md +2 -2
- package/docs/guide/03-builder.md +53 -25
- package/docs/guide/09-reference.md +1 -1
- package/docs/guide/17-mobile.md +35 -4
- package/examples/hr/hr_82_pwa_prefetch.sql +11 -0
- package/package.json +1 -1
- package/public/app.js +11 -0
- package/public/builder.css +57 -3
- package/public/builder.js +66 -0
- package/public/palette.js +8 -1
- package/public/sw.js +51 -1
- package/public/worksheet.css +39 -4
- package/public/worksheet.js +15 -1
- package/src/builder/designer.ts +2 -2
- package/src/builder/gallery.ts +5 -5
- package/src/builder/home.ts +71 -60
- package/src/builder/i18n/de.ts +28 -0
- package/src/builder/i18n/en.ts +28 -0
- package/src/builder/i18n/es.ts +28 -0
- package/src/builder/i18n/fr.ts +28 -0
- package/src/builder/i18n/it.ts +28 -0
- package/src/builder/i18n/ja.ts +28 -0
- package/src/builder/i18n/ko.ts +28 -0
- package/src/builder/i18n/nl.ts +28 -0
- package/src/builder/i18n/pt.ts +28 -0
- package/src/builder/i18n/zh-cn.ts +28 -0
- package/src/builder/i18n/zh-tw.ts +28 -0
- package/src/builder/pwa.ts +14 -3
- package/src/builder/routes.ts +21 -10
- package/src/builder/sql.ts +2 -2
- package/src/builder/ui.ts +47 -33
- package/src/builder/worksheet.ts +33 -24
- package/src/builder/workspace-home.ts +17 -0
- package/src/metadata.ts +3 -1
- package/src/runtime/pwa.ts +8 -3
- package/src/runtime/routes.ts +25 -6
- package/src/session.ts +2 -0
package/src/builder/worksheet.ts
CHANGED
|
@@ -561,6 +561,7 @@ async function worksheet(req: Req, reply: FastifyReply) {
|
|
|
561
561
|
|
|
562
562
|
const toolBtn = (m: RunMode, ic: string, label: string, key: string, hot = false) =>
|
|
563
563
|
html`<button class="btn btn-sm${hot ? ' btn-hot' : ''}" name="mode" value="${m}" title="${label} (${key})" data-ws-key="${key}">${icon(ic)}<span>${label}</span></button>`;
|
|
564
|
+
const runsAsText = `${tx('ws.runs_as', { who: String(runsAs) })}${searchSchema ? `; ${tx('ws.search_path_plain', { schema: searchSchema })}` : ''}`;
|
|
564
565
|
const main = html`${chartCss ? html`<style nonce="${req.cspNonce}">${raw(chartCss.replace(/</g, ''))}</style>` : ''}<div class="ws-page">
|
|
565
566
|
<h1 class="sr-only">${tx('ws.title')}</h1>
|
|
566
567
|
${workshopTabs('sql', s)}
|
|
@@ -615,20 +616,9 @@ async function worksheet(req: Req, reply: FastifyReply) {
|
|
|
615
616
|
${toolBtn('script', 'file', tx('ws.run_script'), 'F5')}
|
|
616
617
|
${toolBtn('explain', 'route', tx('ws.tab.explain'), 'F10')}
|
|
617
618
|
${toolBtn('autotrace', 'activity', tx('ws.tab.autotrace'), 'F6')}
|
|
618
|
-
<
|
|
619
|
+
<button type="button" class="btn btn-sm btn-icon" data-ws-suggest title="${tx('ws.suggest_title')}" hidden>${icon('list')}<span class="sr-only">${tx('ws.suggest')}</span></button>
|
|
620
|
+
<button type="button" class="btn btn-sm btn-icon" data-ws-check title="${tx('ws.check_title')}" hidden>${icon('check-circle')}<span class="sr-only">${tx('ws.check')}</span></button>
|
|
619
621
|
<span class="ws-spacer"></span>
|
|
620
|
-
<button type="button" class="btn btn-sm btn-icon" data-ws-format title="${tx('ws.format_title')}" hidden>${icon('sliders')}<span class="sr-only">${tx('ws.format')}</span></button>
|
|
621
|
-
<button type="button" class="btn btn-sm btn-icon" data-ws-case title="${tx('ws.case_title')}" hidden><span aria-hidden="true" class="ws-aa">Aa</span><span class="sr-only">${tx('ws.case')}</span></button>
|
|
622
|
-
<button type="button" class="btn btn-sm btn-icon" data-ws-find title="${tx('ws.find_title')}" hidden>${icon('search')}<span class="sr-only">${tx('ws.find')}</span></button>
|
|
623
|
-
<a class="btn btn-sm btn-icon" href="${BASE}/sql/load?schema=${encodeURIComponent(navSchema)}" title="${tx('ws.load_title')}">${icon('upload')}<span class="sr-only">${tx('ws.load')}</span></a>
|
|
624
|
-
<details class="menu ws-keys"><summary class="btn btn-sm btn-icon" title="${tx('ws.keys')}">?<span class="sr-only">${tx('ws.keys')}</span></summary>
|
|
625
|
-
<div class="menu-panel align-right"><div class="menu-section"><dl class="ws-keylist">
|
|
626
|
-
<dt>Ctrl+Enter</dt><dd>${tx('ws.run_statement')}</dd><dt>F5</dt><dd>${tx('ws.run_script')}</dd><dt>F10</dt><dd>${tx('ws.tab.explain')}</dd><dt>F6</dt><dd>${tx('ws.tab.autotrace')}</dd>
|
|
627
|
-
<dt>Ctrl+S</dt><dd>${tx('ws.key.save')}</dd><dt>Ctrl+F7</dt><dd>${tx('ws.format')}</dd><dt>Ctrl+B</dt><dd>${tx('ws.case')}</dd><dt>Ctrl+F / Ctrl+H</dt><dd>${tx('ws.key.find')}</dd>
|
|
628
|
-
<dt>Ctrl+G</dt><dd>${tx('ws.key.goto')}</dd><dt>Ctrl+/</dt><dd>${tx('ws.key.comment')}</dd><dt>Ctrl+Space</dt><dd>${tx('ws.key.suggest')}</dd><dt>F1</dt><dd>${tx('ws.key.help')}</dd><dt>Escape</dt><dd>${tx('ws.key.fullscreen')}</dd>
|
|
629
|
-
</dl>
|
|
630
|
-
<label class="ws-kwcase" hidden>${tx('ws.kwcase')} <select data-ws-kwcase><option value="lower">${tx('ws.lower')}</option><option value="upper">${tx('ws.upper')}</option></select></label>
|
|
631
|
-
<a href="https://github.com/pgkiln/pgkiln/blob/main/docs/guide/03-builder.md#sql-workshop" target="_blank" rel="noopener" data-ws-help>${tx('ws.help')}</a></div></div></details>
|
|
632
622
|
<span class="ws-txn" data-ws-txn data-state="${tState}" data-statements="${txn?.statements ?? 0}" data-started="${txn?.startedMs ?? ''}" data-last="${txn?.lastMs ?? ''}" data-idle="${txnIdleSeconds()}" data-note="${tNote ?? ''}" data-now="${Date.now()}" hidden>
|
|
633
623
|
<label class="ws-runas"><span>${tx('ws.commit_label')}</span><select name="txn" data-ws-txn-mode title="${tx('ws.txn.mode_title')}">
|
|
634
624
|
<option value="">${tx('ws.autocommit')}</option><option value="on"${txnOn ? raw(' selected') : ''}>${tx('ws.transaction')}</option></select></label>
|
|
@@ -636,17 +626,25 @@ async function worksheet(req: Req, reply: FastifyReply) {
|
|
|
636
626
|
<button class="btn btn-sm" name="mode" value="commit" data-ws-txn-commit title="${tx('ws.txn.commit_title')}"${txn ? '' : raw(' disabled')}>${icon('check')}<span>${tx('ws.commit_label')}</span></button>
|
|
637
627
|
<button class="btn btn-sm" name="mode" value="rollback" data-ws-txn-rollback title="${tx('ws.txn.rollback_title')}"${txn ? '' : raw(' disabled')}>${icon('undo')}<span>${tx('ws.rollback')}</span></button>
|
|
638
628
|
</span>
|
|
639
|
-
<label class="ws-runas"><span>${tx('ws.run_as')}</span>
|
|
640
|
-
<select name="as_app" title="${tx('ws.run_as_title')}">
|
|
641
|
-
<option value="">${db === owner ? tx('ws.owner_conn') : tx('ws.ws_login')}</option>
|
|
642
|
-
${apps.map((a) => html`<option value="${a.id}"${asApp?.id === a.id ? raw(' selected') : ''}>${a.name} (${a.db_role})</option>`)}
|
|
643
|
-
</select></label>
|
|
644
|
-
<button type="button" class="btn btn-sm btn-icon" data-ws-clear title="${tx('ws.clear_title')}" hidden>${icon('trash')}<span class="sr-only">${tx('ws.clear')}</span></button>
|
|
645
629
|
<button type="button" class="btn btn-sm btn-icon" data-ws-fullscreen title="${tx('ws.fullscreen_title')}" aria-pressed="false" hidden>${bicon('expand')}<span class="sr-only">${tx('ws.fullscreen')}</span></button>
|
|
630
|
+
<!-- (UX-1) the rest of the tools in one menu, so the toolbar is one row and the editor keeps its height -->
|
|
631
|
+
<details class="menu ws-more"><summary class="btn btn-sm btn-icon" title="${tx('ws.more')}"><span aria-hidden="true" class="ws-dots">⋯</span><span class="sr-only">${tx('ws.more')}</span></summary>
|
|
632
|
+
<div class="menu-panel align-right"><div class="menu-section ws-more-tools">
|
|
633
|
+
<label class="ws-check"><input type="checkbox" name="continue_on_error"${continueOnError ? raw(' checked') : ''}> ${tx('ws.continue')}</label>
|
|
634
|
+
<button type="button" class="ws-menu-item" data-ws-format title="${tx('ws.format_title')}" hidden>${icon('sliders')} ${tx('ws.format')} <kbd>Ctrl+F7</kbd></button>
|
|
635
|
+
<button type="button" class="ws-menu-item" data-ws-case title="${tx('ws.case_title')}" hidden><span aria-hidden="true" class="ws-aa">Aa</span> ${tx('ws.case')} <kbd>Ctrl+B</kbd></button>
|
|
636
|
+
<button type="button" class="ws-menu-item" data-ws-find title="${tx('ws.find_title')}" hidden>${icon('search')} ${tx('ws.find')} <kbd>Ctrl+F</kbd></button>
|
|
637
|
+
<a class="ws-menu-item" href="${BASE}/sql/load?schema=${encodeURIComponent(navSchema)}" title="${tx('ws.load_title')}">${icon('upload')} ${tx('ws.load')}</a>
|
|
638
|
+
<button type="button" class="ws-menu-item" data-ws-clear title="${tx('ws.clear_title')}" hidden>${icon('trash')} ${tx('ws.clear')}</button>
|
|
639
|
+
</div>
|
|
640
|
+
<div class="menu-section"><strong class="ws-more-head">${tx('ws.keys')}</strong><dl class="ws-keylist">
|
|
641
|
+
<dt>Ctrl+Enter</dt><dd>${tx('ws.run_statement')}</dd><dt>F5</dt><dd>${tx('ws.run_script')}</dd><dt>F10</dt><dd>${tx('ws.tab.explain')}</dd><dt>F6</dt><dd>${tx('ws.tab.autotrace')}</dd>
|
|
642
|
+
<dt>Ctrl+S</dt><dd>${tx('ws.key.save')}</dd><dt>Ctrl+F7</dt><dd>${tx('ws.format')}</dd><dt>Ctrl+B</dt><dd>${tx('ws.case')}</dd><dt>Ctrl+F / Ctrl+H</dt><dd>${tx('ws.key.find')}</dd>
|
|
643
|
+
<dt>Ctrl+G</dt><dd>${tx('ws.key.goto')}</dd><dt>Ctrl+/</dt><dd>${tx('ws.key.comment')}</dd><dt>Ctrl+Space</dt><dd>${tx('ws.key.suggest')}</dd><dt>Tab</dt><dd>${tx('ws.key.indent')}</dd><dt>F1</dt><dd>${tx('ws.key.help')}</dd><dt>Escape</dt><dd>${tx('ws.key.fullscreen')}</dd>
|
|
644
|
+
</dl>
|
|
645
|
+
<label class="ws-kwcase" hidden>${tx('ws.kwcase')} <select data-ws-kwcase><option value="lower">${tx('ws.lower')}</option><option value="upper">${tx('ws.upper')}</option></select></label>
|
|
646
|
+
<a href="https://github.com/pgkiln/pgkiln/blob/main/docs/guide/03-builder.md#sql-workshop" target="_blank" rel="noopener" data-ws-help>${tx('ws.help')}</a></div></div></details>
|
|
646
647
|
</div>
|
|
647
|
-
<p class="ws-runs-as muted small">${tx('ws.runs_as', { who: runsAs })}${searchSchema ? html`; ${tx.rich('ws.search_path', { schema: html`<code>${searchSchema}</code>`, change: html`<a href="${BASE}/sql">${tx('ws.change')}</a>` })}` : ''}. <span data-ws-commit-note>${txnOn
|
|
648
|
-
? tx('ws.commit.txn', { idle: idleLabel(tx) })
|
|
649
|
-
: tx('ws.commit.auto')}</span></p>
|
|
650
648
|
${fileError ? html`<p class="alert alert-error ws-file-error" role="alert">${fileError}</p>` : ''}
|
|
651
649
|
${txnError ? html`<p class="alert alert-error ws-txn-alert" role="alert">${txnError}</p>` : txnInfo ? html`<p class="alert alert-info ws-txn-alert" role="status">${txnInfo}</p>` : ''}
|
|
652
650
|
<div class="ws-wtabs" role="group" aria-label="${tx('ws.worksheets')}" hidden></div>
|
|
@@ -666,12 +664,23 @@ async function worksheet(req: Req, reply: FastifyReply) {
|
|
|
666
664
|
<textarea id="ws-sql" name="sql" aria-label="SQL" class="code sql-editor" rows="14" spellcheck="false" data-code="plpgsql">${text || (isPost ? '' : 'select current_user, version();')}</textarea>
|
|
667
665
|
</div>
|
|
668
666
|
<div class="ws-split ws-split-h" role="separator" aria-orientation="horizontal" aria-label="${tx('ws.resize_out')}" tabindex="0" data-ws-split="out" hidden></div>
|
|
669
|
-
<div class="ws-output" data-active="${active}">
|
|
667
|
+
<div class="ws-output" data-active="${active}"${!run && !req.query?.tab ? raw(' data-empty') : ''}>
|
|
670
668
|
<div class="ws-tabs" role="tablist" aria-label="${tx('ws.output')}">${TABS.map(([k]) => html`<button type="button" role="tab" id="ws-tab-${k}" aria-controls="ws-pane-${k}" aria-selected="${k === active ? 'true' : 'false'}" data-ws-tab="${k}">${tx(`ws.tab.${k}`)}</button>`)}</div>
|
|
671
669
|
${TABS.map(([k]) => html`<section class="ws-pane" id="ws-pane-${k}" role="tabpanel" aria-labelledby="ws-tab-${k}" data-pane="${k}"><h2 class="ws-pane-title">${tx(`ws.tab.${k}`)}</h2>${panes[k]}</section>`)}
|
|
672
670
|
</div>
|
|
673
671
|
</section>
|
|
674
|
-
<footer class="ws-status"
|
|
672
|
+
<footer class="ws-status">
|
|
673
|
+
<span class="ws-status-run" role="status" aria-live="polite">${status}</span>
|
|
674
|
+
<!-- (UX-1) who the statements run as: a chip in the status bar; the explanation is its description -->
|
|
675
|
+
<label class="ws-runas ws-runas-chip" title="${runsAsText}"><span>${tx('ws.run_as')}</span>
|
|
676
|
+
<select name="as_app" aria-describedby="ws-runs-as">
|
|
677
|
+
<option value="">${db === owner ? tx('ws.owner_conn') : tx('ws.ws_login')}</option>
|
|
678
|
+
${apps.map((a) => html`<option value="${a.id}"${asApp?.id === a.id ? raw(' selected') : ''}>${a.name} (${a.db_role})</option>`)}
|
|
679
|
+
</select></label>
|
|
680
|
+
<span class="sr-only" id="ws-runs-as">${tx('ws.runs_as', { who: runsAs })}${searchSchema ? html`; ${tx('ws.search_path_plain', { schema: searchSchema })}` : ''}. <span data-ws-commit-note>${txnOn
|
|
681
|
+
? tx('ws.commit.txn', { idle: idleLabel(tx) })
|
|
682
|
+
: tx('ws.commit.auto')}</span></span>
|
|
683
|
+
</footer>
|
|
675
684
|
</form>
|
|
676
685
|
${downloadable ? html`<form method="post" action="${BASE}/sql/unload" id="ws-dl" class="ws-dl-form">${csrf(s)}<input type="hidden" name="source" value="query"><input type="hidden" name="query" value="${downloadable}"><input type="hidden" name="header" value="1"><input type="hidden" name="bom" value="1"></form>` : ''}
|
|
677
686
|
</div>`;
|
|
@@ -121,6 +121,22 @@ async function systemMessage(s: Session, admin: boolean) {
|
|
|
121
121
|
return region(tx('wh.system'), html`${checks}${body}${admins}`, '', 'system-message');
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/** (UX-13) What an administrator must see first: lockout risks and a newer pgkiln, above the tools (details: System message). */
|
|
125
|
+
async function attention(s: Session, admin: boolean) {
|
|
126
|
+
if (!admin) return '';
|
|
127
|
+
const tx = tr(s);
|
|
128
|
+
const lockout = await attentionCount();
|
|
129
|
+
const check = await releaseCheckEnabled();
|
|
130
|
+
const r = check.on ? cachedRelease()?.release : null;
|
|
131
|
+
const newer = r && newerVersion(r.version, VERSION) ? r : null;
|
|
132
|
+
const items = [
|
|
133
|
+
lockout ? html`<li>${icon('alert')} ${tx('wh.lockout', { count: lockout })} <a href="${BASE}/instance#lockout-checks">${tx('wh.lockout_open')}</a></li>` : '',
|
|
134
|
+
newer ? html`<li>${icon('bell')} ${tx('wh.available', { version: newer.version })} <a href="#system-message">${tx('wh.release_notes')}</a></li>` : '',
|
|
135
|
+
].filter(Boolean);
|
|
136
|
+
return items.length ? html`<section class="alert alert-warning wh-attention" role="status" aria-labelledby="wh-attention-h">
|
|
137
|
+
<h2 id="wh-attention-h">${tx('wh.attention')}</h2><ul>${items}</ul></section>` : '';
|
|
138
|
+
}
|
|
139
|
+
|
|
124
140
|
/** (Instance settings page) the release check: on, off, or as the environment says. */
|
|
125
141
|
export async function releaseCheckRegion(s: Session) {
|
|
126
142
|
const check = await releaseCheckEnabled();
|
|
@@ -194,6 +210,7 @@ export async function workspaceHomeRoutes(app: FastifyInstance) {
|
|
|
194
210
|
const main = html`<div class="ab-home wh-home">
|
|
195
211
|
<div class="ab-main">
|
|
196
212
|
<h1 class="wh-title">${s.workspace ? tx('wh.title_ws', { name: s.workspace.name }) : tx('wh.title')}</h1>
|
|
213
|
+
${await attention(s, admin)}
|
|
197
214
|
<nav class="ab-tiles ab-tiles-wrap wh-tools" aria-label="${tx('wh.tools')}">
|
|
198
215
|
${card(`${BASE}/apps`, 'grid', tx('rail.app_builder'), tx('wh.card.apps'))}
|
|
199
216
|
${sql ? card(`${BASE}/sql`, 'database', tx('rail.sql_workshop'), tx('wh.card.sql')) : ''}
|
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,
|
package/src/runtime/pwa.ts
CHANGED
|
@@ -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 {
|
|
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')
|
package/src/runtime/routes.ts
CHANGED
|
@@ -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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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) });
|
package/src/session.ts
CHANGED
|
@@ -36,6 +36,8 @@ export interface Session {
|
|
|
36
36
|
/** Builder only: the developer's workspaces and the current one (src/builder/workspaces.ts). */
|
|
37
37
|
workspaces?: BuilderWorkspace[];
|
|
38
38
|
workspace?: BuilderWorkspace | null;
|
|
39
|
+
/** Builder only (UX-3): this request's path, so the rail marks the right section */
|
|
40
|
+
path?: string;
|
|
39
41
|
/** Builder only (sprint 99): an administrator of the installation (the Administration menu's Database users) */
|
|
40
42
|
instanceAdmin?: boolean;
|
|
41
43
|
/** Builder only (079): sha256 of the developer toolbar token (src/runtime/devbar.ts). */
|