diff --git a/hledger-lib/locale/de.po b/hledger-lib/locale/de.po index a9edaae81a6..cd4a4e2ef68 100644 --- a/hledger-lib/locale/de.po +++ b/hledger-lib/locale/de.po @@ -742,6 +742,9 @@ msgstr "Zurück" msgid "Saved journal {file}" msgstr "Journal {file} gespeichert" +msgid "hledger-web is no longer running. It exits after fifteen minutes with no browser window showing it, or when stopped. To continue, start it again and reload this page." +msgstr "hledger-web läuft nicht mehr. Es beendet sich nach fünfzehn Minuten ohne ein Browserfenster, das es anzeigt, oder wenn es angehalten wird. Zum Weiterarbeiten starten Sie es neu und laden diese Seite erneut." + msgctxt "column heading" msgid "Commodity" msgstr "Einheit" diff --git a/hledger-web/Hledger/Web/Main.hs b/hledger-web/Hledger/Web/Main.hs index 8a9d8e534c7..0a418689af1 100644 --- a/hledger-web/Hledger/Web/Main.hs +++ b/hledger-web/Hledger/Web/Main.hs @@ -159,12 +159,12 @@ web opts0 j = do let warpsettings = setHost (fromString h) (setPort p defaultSettings) if server_mode_ opts == ServeBrowse then do - putStrLn "This server will exit after 2m with no browser windows open (or press ctrl-c)" + putStrLn "This server will exit after 15m with no browser windows open (or press ctrl-c)" putStrLn "Opening web browser..." hFlush stdout -- returns normally only after the idle exit (ctrl-c or a server failure raises instead) serveAndBrowse warpsettings (snd <$> mtcpsock) u app - putStrLn "No browser windows were open for 2m, exiting. (Use --serve to serve without this timeout.)" + putStrLn "No browser windows were open for 15m, exiting. (Use --serve to serve without this timeout.)" else do putStrLn "Press ctrl-c to quit" @@ -195,8 +195,8 @@ web opts0 j = do (Nothing, Nothing) -> Network.Wai.Handler.Warp.runSettings warpsettings app -- | Browse mode: serve the app, open the default web browser on it once it --- is listening, and return when no browser window has shown it for two --- minutes. A page says it is open by pinging /_ping while it is (see +-- is listening, and return when no browser window has shown it for +-- browseIdleSeconds. A page says it is open by pinging /_ping while it is (see -- browsePingInit in static/hledger.js). The pings are answered here, before -- they reach the app, and the time of the latest one is kept. -- With --port 0 the listening socket is already bound (to the port the OS @@ -236,10 +236,13 @@ waitForIdle lastping = do (max lp (woke + browsePingSeconds + 5 - browseIdleSeconds), ()) waitForIdle lastping --- | How long browse mode keeps serving after the last ping. The pages ping --- every browsePingSeconds, so a few pings can go missing before this runs out. +-- | How long browse mode keeps serving after the last ping. The pages ask to +-- ping every browsePingSeconds, but browsers slow down the timers of pages in +-- background tabs, and Safari may let one run only every five minutes or so. +-- This must outlast that, or the server would exit under a page that is +-- still open. browseIdleSeconds :: Double -browseIdleSeconds = 120 +browseIdleSeconds = 15 * 60 -- | How often an open page pings, as set in hledger.js. browsePingSeconds :: Double diff --git a/hledger-web/Hledger/Web/WebOptions.hs b/hledger-web/Hledger/Web/WebOptions.hs index 6ff53095be1..5a184b106c2 100644 --- a/hledger-web/Hledger/Web/WebOptions.hs +++ b/hledger-web/Hledger/Web/WebOptions.hs @@ -61,7 +61,7 @@ webflags = [ flagNone ["serve-browse"] (setboolopt "serve-browse") - (serveprefix ++ "serve the web UI and JSON API, and open a browser, and exit if inactive for 2m (default)") + (serveprefix ++ "serve the web UI and JSON API, and open a browser, and exit if inactive for 15m (default)") , flagNone ["serve"] (setboolopt "serve") diff --git a/hledger-web/hledger-web.m4.md b/hledger-web/hledger-web.m4.md index 7d8bd3c1780..b4517816bd1 100644 --- a/hledger-web/hledger-web.m4.md +++ b/hledger-web/hledger-web.m4.md @@ -53,7 +53,7 @@ hledger-web can be run in three modes: - `--serve-browse` mode (the default): the app serves the web UI and JSON API, and opens your default web browser to show the app if possible, - and exits automatically once no browser window has shown it for two minutes. + and exits automatically once no browser window has shown it for fifteen minutes. - `--serve`: the app just serves the web UI and JSON API. diff --git a/hledger-web/static/hledger.js b/hledger-web/static/hledger.js index cad59d2537b..6b5ede02d86 100644 --- a/hledger-web/static/hledger.js +++ b/hledger-web/static/hledger.js @@ -404,20 +404,37 @@ function registerChartSelect(ev, ranges) { // BROWSE MODE // In the default --serve-browse mode the server exits once no browser -// window has shown it for two minutes (serveAndBrowse in Main.hs). It -// knows a window is open because the page pings it: on load, then every -// 30 seconds. Pages are marked for this by defaultLayout in browse mode -// only; the server answers /_ping in that mode only. The ping goes to the -// page's own origin, whatever address the browser reached us at (the -// policy allows requests to our origin only), under the base url's path, -// in case a proxy in front of us expects one. +// window has shown it for fifteen minutes (serveAndBrowse in Main.hs). It +// knows a window is open because the page pings it: on load, every 30 +// seconds, and whenever the page is shown again. Browsers run the timers of +// background tabs less often, so the pings from a hidden page can be minutes +// apart; the ping on showing makes up for that as soon as the page is seen. +// Pages are marked for this by defaultLayout in browse mode only; the server +// answers /_ping in that mode only. The ping goes to the page's own origin, +// whatever address the browser reached us at (the policy allows requests to +// our origin only), under the base url's path, in case a proxy in front of +// us expects one. +// +// A ping that can't reach the server means it has stopped, so the page shows +// the #server-stopped notice, and hides it again if a later ping gets through +// (eg after the server was restarted on the same address). function browsePingInit() { if (!document.body.hasAttribute('data-browse-mode')) { return; } var base = new URL(document.hledgerWebBaseurl, document.baseURI); var url = base.pathname.replace(/\/$/, '') + '/_ping'; + var notice = document.getElementById('server-stopped'); var ping = function() { - fetch(url, { cache: 'no-store' }).catch(function() {}); + fetch(url, { cache: 'no-store' }).then( + function() { if (notice) { notice.hidden = true; } }, + function() { if (notice) { notice.hidden = false; } }); }; ping(); setInterval(ping, 30000); + document.addEventListener('visibilitychange', function() { + if (document.visibilityState === 'visible') { ping(); } + }); + // A page restored from the back/forward cache didn't run meanwhile. + window.addEventListener('pageshow', function(e) { + if (e.persisted) { ping(); } + }); } diff --git a/hledger-web/templates/default-layout.hamlet b/hledger-web/templates/default-layout.hamlet index 01f1e93f5cb..936b696aad3 100644 --- a/hledger-web/templates/default-layout.hamlet +++ b/hledger-web/templates/default-layout.hamlet @@ -42,6 +42,10 @@ $if elem ViewPermission perms })(); + $# Shown by hledger.js while the page can't reach the server. + $if browsemode +