A site, or one section of it, installs on a phone or tablet as a progressive web app, from a few settings.
A site can be installed from the browser's menu and opened from the home screen
in its own window, without an address bar. Lazysite generates the two documents
a browser needs for that - the web app manifest and the service worker -
from settings in lazysite.conf. Nobody writes either one, and nothing is put
under /lazysite-assets/.
The app can be the whole site (scope /) or one section (a scope under a
path, such as /portal/). A domain with its own settings can be its own app.
Set app_name and the site is an app. Leave it out and there is no manifest,
no worker, and nothing added to any page.
app_name: Example Portal
app_short_name: Portal
app_theme_colour: #0a5c36
app_background_colour: #ffffff
app_scope: /portal/
app_start_url: /portal/
app_icon_64: /icons/app-64.png
app_icon_192: /icons/app-192.png
app_icon_512: /icons/app-512.png
app_icon_maskable: /icons/app-maskable.png
app_offline_pages: /portal/, /portal/new
app_update: wait
These can also be set without shell access, on the manager's Config page under "Install as an app" (or by config-set). A value the app would be turned off for, or an icon the site does not serve, is refused when you save, naming the setting.
app_nameapp_short_nameapp_name.
app_scope/. The
default is /, the whole site. Pages inside the scope are part of the app;
a link outside it opens in the browser.
app_start_urlapp_theme_colour, app_background_colour#rgb or #rrggbb. The first colours the window frame, the second the
splash screen. A value that is not a hex colour is left out.
app_icon_64, app_icon_192, app_icon_512, app_icon_maskablelazysite check names it.
app_shellapp_offline_pages/portal/, /portal/new. See
"Pages that open offline". Optional; none by default.
app_updatenow (the default) or wait. See "How an update reaches an installed app".
Any other value turns the app off.
A scope or start page the engine cannot honour turns the app off instead of
being corrected: a scope of portal is not quietly read as /portal/.
lazysite check names the value.
On a domain declared with alias_hosts, each of these can be set per domain as
alias.<host>.app_name: and so on.
| Address | What it is |
|---|---|
<scope>manifest.webmanifest |
The manifest, served as application/manifest+json. |
<scope>lazysite-sw.js |
The service worker, served as text/javascript. |
/assets/lazysite-app.js |
A small shipped script that registers the worker. |
Both documents live inside the scope, which is why no Service-Worker-Allowed
header is sent. A page inside the scope gets a <link rel="manifest"> and the
registration script; a page outside it gets nothing. If a file really exists at
one of those addresses, the file wins.
Pages already rendered are cached by the engine: after turning the app on, invalidate the page cache so the pages carry the link.
The worker keeps the app shell - the files every page of the app needs - and
only the pages you list in app_offline_pages. Without that list it keeps no
page, and an installed app started with no signal shows the browser's offline
error instead of the app.
/assets/lazysite-chrome.css, lazysite-chrome.js),
the registration script, the icons, the stylesheets and scripts the start
page links (read from its rendered copy), and anything in app_shell.
The worker never handles the Odoo endpoint, the manager, the sign-in and
sign-out paths, the other lazysite-*.pl endpoints or the engine tree, and it
reads or stores no cookie, token or answer. It honours the server: a response
sent Cache-Control: no-store or private is never kept, and a copy kept
earlier of a file that now says so is deleted. There is no offline queue or
background sync.
The worker shows notices by web push for an account that turned them on for the
device on /account. A push carries nothing: the notification says "Something is
waiting for you" under the app's name, and tapping it opens /account, where the
notice is read from the server, signed in. The site's side - routing, the key, the
hourly bound - is in forms: notices to the app. The sign-in page, and
so the app's sign-in, offers a passkey (auth); the worker never answers
it or the auth wrapper, so a passkey sign-in always reaches the server.
List the pages the app must open with no signal in app_offline_pages. A page
qualifies when it is the same for every visitor; the worker keeps the page as
a signed-out visitor receives it, so a person's own data belongs in what the page
fetches as it runs, not in the page.
auth other than none (or the site's auth_default), auth_groups,
payment, addresses or addresses_deny, or an access key the engine
cannot read: the page is gated.odoo-identity: or db:...(mine) in the front matter: the page shows the
viewer.nocache: true, api: true or raw: true: rendered fresh, or data./cgi-bin/, any lazysite-*.pl, the
manager, the sign-in and sign-out paths or lazysite/.odoo: bindingodoo: binding in a page only when it takes no
visitor key and no Odoo sign-in, so the page is the same for everyone;
anything personal comes from the Odoo endpoint while the page runs, which the
worker never touches. A binding of the viewer's own, odoo-identity:, is the
one that makes a page per viewer, and it is refused.
Cache-Control: no-store or private, not a
redirect (a protected page sends an anonymous visitor to sign-in) and has no
Set-Cookie that a script can see. The page a signed-in person receives is
never the one kept. A page that later turns protected deletes its own copy at
the next visit. A browser hides Set-Cookie from a worker, so the guard that
matters is the request without credentials; a page that sets a cookie for
everyone is not a candidate and should not be listed.
A page may carry a form, or a template that asks the visitor's address; the
engine sends those no-store, private, and the worker then keeps nothing.
The worker's keeping fetch sends X-Lazysite-App: keep, and the visitor log
records it as the app keeping its copy, not as a visitor: it is counted apart as
app_hits in a day's figures, and the page is not counted twice. The person's own
visit does not send the header and is counted as ever. Someone who sends the header
by hand only hides their own request from the visitor count.
The worker's cache name is built from the installed release, a fingerprint of the shell files and the list of offline pages. A deploy that changes any of them changes the worker's text; the browser notices on its next check and installs the new worker, and the old cache is deleted.
app_update: now (the default)skipWaiting and clients.claim). Nothing
is done on the device. This is what the app did before the setting existed.
app_update: waitlazysite-app.js then fires a lazysite-app-update event on window when a
new version is ready; event.detail.apply() tells it to take over, and the
page reloads once. Without a listener the update is applied when every window
of the app has been closed and the app is opened again.
window.addEventListener('lazysite-app-update', function (e) {
if (confirm('A new version is ready. Reload?')) e.detail.apply();
});
A page that adds its listener late reads window.lazysiteApp.apply, which is set
while an update waits. Calling it posts skip-waiting to the new worker.
app-offline extension)A page the worker keeps opens with no signal; the offline kit makes it work with
none. It is an engine extension, app-offline, off until a sysop enables it on
the Extensions page, and it needs the Odoo extension: what it keeps and sends
goes through the Odoo browser helper and the page endpoint. Nothing is added to
a page until a layout asks for it.
[% app_offline_kit %] once, in <head> or before </body>. It is
the script tag while the extension is on and nothing otherwise, so a layout
can carry it on every site. No layout ships a script of its own: the kit is
the engine's, at /assets/app-offline-kit.js.
data-ls-app-banner (shown while the browser is offline, hidden while it is
online; write its text yourself), data-ls-app-show-out (shown while nobody
is signed in) and data-ls-app-show-in (shown while somebody is), and
data-ls-app-status (the kit writes "Installed as an app" or "Running in
the browser", the number of changes waiting, and "A new version is ready -
tap to update" when the worker holds one under app_update: wait; a tap
applies it). Each is toggled with hidden and marked data-ls-app-state,
so a stylesheet can style the states.
LazysiteOdoo.read, and the regions it fills) is kept; when the same
read fails for want of a network, the kept answer is given instead, marked
kept with the time it was kept, and a region is marked
data-ls-app-kept="<time>". A read never made is not invented.
LazysiteAppOffline.write(binding, params, opts) takes what
LazysiteOdoo.write takes. Online, the change is sent at once with an
Idempotency-Key (the page's opts.idempotencyKey, or one the kit makes) and
the answer is the endpoint's. With no signal, or when the send was cut off
before an answer came, the change is queued and the answer says
{kind: "queued", key}. A queued change is sent once the signal is back, in
order, with the same key - so a send the network cut off is sent again
without the server writing twice (see the Odoo guide, "A write sent twice").
A change that carries a file is never queued.
lazysite-app-offline event on window (detail.type is queued
or sent, with the key, the binding, and for sent the status and the
answer). It stays when the send was cut off, or when the endpoint says the
person must sign in again.
lazysite-app-update event.
?csrf=1 names the person by owner, a label of their login (the one the
call records use, never the login itself), and a kept read or a queued change
is stored under it, shown to and sent for no other login on the same browser.
A change made while nobody is signed in, or while the identity has lapsed
(odoo_identity_ttl), is refused as no-owner and kept nowhere.
odoo_site_signin: on, see /docs/odoo):
owner comes from the site session. On a site where the Odoo sign-in is not
the site sign-in, ?csrf=1 names no owner, so the kit keeps no reads, refuses
every change as no-owner, and its slots show the signed-out state. The store
is the browser's own, under the site's origin; it holds what the endpoint
answered a page and what a page asked to send, never a token, a cookie or a
password.
The worker needs a secure context: https, or localhost. A phone on
http://<lan-ip>:port never registers it and is never offered the install; that
is the browser's rule, not a setting. To try an app from a dev server, use https
or reach it through localhost (for example with a port forward).
lazysite-app.js writes a console warning, beginning lazysite-app:, when the
worker does not register: a 404 (the app is off, the scope is not the page's
directory, or the page cache predates turning the app on), a script error, or an
insecure origin. A site whose /assets/lazysite-app.js is missing, because its
assets/ predates the feature, cannot warn: the browser reports the missing
script in its network panel.
lazysite check reports the app's declaration: a bad scope or start page, a
colour that is not hex, an icon the site does not serve, and an app with no
icons at all. In a browser, the developer tools' Application panel shows the
manifest, the registered worker and the cache.