diff --git a/.env b/.env index 6f20f97..21fae53 100644 --- a/.env +++ b/.env @@ -110,6 +110,11 @@ API_KEY= # When false, the app keys page, the app-key commands and the app key login are not available. APP_KEYS_ENABLED=false +# Update check +# Davis does not phone home. Set this to true to have the Diagnostics page ask GitHub, at most +# once a day, whether a newer release exists, and link to its release notes. +UPDATE_CHECK_ENABLED=false + # Logging path # By default, it will log in the standard Symfony directory: var/log/prod.log (for production) # You can use /dev/null here if you want to discard logs entirely diff --git a/README.md b/README.md index e4c7ffb..3d2fc89 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Dependencies b. If you plan to run Davis on production ``` - composer install --no-dev + composer install --no-dev --optimize-autoloader ``` And set `APP_ENV=prod` in your `.env.local` file (see below) @@ -191,6 +191,14 @@ It works with every `AUTH_METHOD` and never contacts the backend: a wrong secret The secret is shown once at creation and stored hashed. Keep it on the server side, never in a browser, and revoke the key (dashboard, or `davis:app-key:revoke `) when the service goes away; an expiry date can be set when the access is meant to be temporary. This is the use case of [#183](https://github.com/tchapi/davis/issues/183). +**Update check** + +```shell +UPDATE_CHECK_ENABLED=false +``` + +Davis does not phone home. Set `UPDATE_CHECK_ENABLED=true` to have the [diagnostics page](#start-with-the-diagnostics-page) ask GitHub whether a newer release exists, and link to its release notes when there is one. The dashboard then counts it among the points that need your attention. GitHub (`api.github.com`) is asked at most once a day, anonymously, and only when you open the dashboard; nothing about your install is sent. If GitHub cannot be reached, the check says so and is tried again an hour later. + **d. The global flags to enable CalDAV, CardDAV and WebDAV**. You can also disable the option to have calendars public ```shell @@ -417,6 +425,105 @@ If you're migrating from Baïkal, then you will likely want to do the following > [!NOTE] > Some details / steps to resolve are also available in https://github.com/tchapi/davis/issues/226. +## Updating (without Docker) + +Davis is not distributed as a bundled archive: the release assets are the plain source tree, without `vendor/`. Updating therefore means fetching the new code and re-running the same steps as the installation. The whole procedure takes a minute; **the part that matters is reading the release notes**. + +> [!CAUTION] +> **Do not automate updates with cron.** Some releases need a human: v5.3.0 changed the `IMAP_AUTH_URL` format, v5.4.0 changed the dashboard URLs, and major versions (v3, v4, v5) each had their own migration notes. Every release has a *How to upgrade* section on [the releases page](https://github.com/tchapi/davis/releases) — read it first. + +### 0. Prerequisites + +- Run all commands below **as the user that owns the Davis directory** (ideally the same user PHP-FPM runs as). Running them as `root` leaves `var/cache` and `vendor/` unreadable or unwritable for the web server. +- If you keep your configuration in the web server (`SetEnv`, `fastcgi_param`) or in another folder (`ENV_DIR`) rather than in `.env.local`, export the same values for the shell session, otherwise the CLI falls back to the `dev` defaults of `.env`: + + ```shell + export APP_ENV=prod + export ENV_DIR=/var/lib/davis # only if you use it + # export DATABASE_DRIVER=... DATABASE_URL=... if they are not in an env file either + ``` + +- Check where you are: `git describe --tags` (git install). + +### 1. Back up the database + +Migrations can go wrong (even though they are generally reversible). Back up **before** touching anything: + +```shell +# MySQL / MariaDB +mysqldump -u davis -p davis > davis-$(date +%F).sql +# PostgreSQL +pg_dump -U davis davis > davis-$(date +%F).sql +# SQLite (safe while the server is running) +sqlite3 var/data.db ".backup davis-$(date +%F).db" +``` + +Also keep a copy of `.env.local` if you have one. + +### 2. Update the code + +**a. If you cloned the repository** + +```shell +cd /var/www/davis +git status # must be clean: local edits are not supported and may block the checkout +git fetch --tags +git checkout vX.Y.Z # always a release tag — never `main`, which is the unstable edge branch +``` + +To see the newest tag: `git tag --sort=-v:refname | head -n 1`. `.env.local`, `var/` and `vendor/` are untracked and survive the checkout. + +**b. If you installed from a release archive** + +Unpack the new archive **next to** the current install, not over it, then move your state into it: + +```shell +cd /var/www +tar xzf vX.Y.Z.tar.gz # creates davis-X.Y.Z/ +cp davis/.env.local davis-X.Y.Z/ # if you use one +mv davis/var davis-X.Y.Z/var # logs, cache, and the SQLite database if you use the default path +mv davis davis-old && mv davis-X.Y.Z davis +``` + +### 3. Install the dependencies + +```shell +composer install --no-dev --optimize-autoloader +``` + +This also clears the cache and installs the public assets (`cache:clear` and `assets:install` run automatically after install, see `composer.json`). Use plain `composer install` only for a development setup with `APP_ENV=dev`. + +### 4. Migrate the database + +```shell +bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration +``` + +`--allow-no-migration` makes the command succeed when the release contains no migration, so the same command works for every update. + +> [!NOTE] +> Coming from **before v3.0.0**? Follow the v3.0.0 release notes first: `doctrine:migrations:migrate` alone will not work. + +### 5. Reload PHP (OPcache) + +If `opcache.validate_timestamps=0` in your PHP configuration (a common production setting), the old code stays in memory until PHP-FPM is reloaded: + +```shell +sudo systemctl reload php8.4-fpm # adjust to your PHP version / service name +``` + +### 6. Check + +Open `/dashboard/diagnostics`. It reports pending migrations, the database engine in use and the state of each endpoint — if everything is green, you're done. The log, if any, is in `var/log/prod.log`. + +### Rolling back + +Code: `git checkout ` (or swap the directories back for an archive install), then `composer install --no-dev --optimize-autoloader` again. Database: restore the backup from step 1. + +### Being notified of new releases + +On GitHub, use *Watch → Custom → Releases*, or subscribe to the feed at `https://github.com/tchapi/davis/releases.atom`. Alternatively, set `UPDATE_CHECK_ENABLED=true` (see [Configuration](#configuration)) and the diagnostics page tells you when a newer release exists. Davis never checks on its own unless you turn that on, and never installs anything. + # 🌐 Access / Webserver A simple status page is available on the root `/` of the server. @@ -514,7 +621,7 @@ server { } location ~ ^/index\.php(/|$) { - fastcgi_pass unix:/var/run/php/php7.2-fpm.sock; # Change for your PHP version + fastcgi_pass unix:/var/run/php/php8.4-fpm.sock; # Change for your PHP version fastcgi_split_path_info ^(.+\.php)(/.*)$; include fastcgi_params; @@ -628,6 +735,9 @@ You can start the containers with : ### Updating from a previous version +> [!TIP] +> For installations without Docker, see [Updating (without Docker)](#updating-without-docker) + If you update the code, you need to make sure the database structure is in sync. **Before v3.0.0**, you need to force the update: @@ -636,7 +746,7 @@ If you update the code, you need to make sure the database structure is in sync. **For v3.0.0 and after**, you can just migrate again (_provided you correctly followed the migration notes in the v3.0.0 release_): - docker exec -it davis sh -c "APP_ENV=prod bin/console doctrine:migrations:migrate --no-interaction" + docker exec -it davis sh -c "APP_ENV=prod bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration" Then, head up to `http://:9000` to see the status display : @@ -695,7 +805,9 @@ The dashboard tells you how many points need your attention and links to **Diagn It groups the checks into Runtime, Database, Authentication, Scheduling and mail, and Endpoints, and each one that needs action says what to do about it and gives the command or query to run. Among other things it reports migrations that have not been run, the database engine actually in use, whether the log directory is writable, whether the selected authentication method has the PHP extension it needs, and how many accounts have no email address and therefore never send an invitation. -Everything on it is read-only and passive — no connection is opened, so it stays usable when something is down. Credentials are never shown: the mail transport is displayed as scheme and host only. Use `bin/console davis:mail:test you@example.org` to actually exercise the mailer. +Everything on it is read-only and passive — no connection is opened, so it stays usable when something is down. The only exception is the optional update check (`UPDATE_CHECK_ENABLED=true`), which asks GitHub at most once a day whether a newer release exists, and degrades to "unknown" when it cannot. + +Credentials are never shown: the mail transport is displayed as scheme and host only. Use `bin/console davis:mail:test you@example.org` to actually exercise the mailer. ### Logs diff --git a/config/services.yaml b/config/services.yaml index 4370a88..8ff9d28 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -20,6 +20,8 @@ parameters: default_webdav_public_dir_writable: "false" app_keys_enabled: "%env(bool:default:default_app_keys_enabled:APP_KEYS_ENABLED)%" default_app_keys_enabled: "false" + update_check_enabled: "%env(bool:default:default_update_check_enabled:UPDATE_CHECK_ENABLED)%" + default_update_check_enabled: "false" services: # default configuration for services in *this* file @@ -60,6 +62,10 @@ services: $appKeysEnabled: "%app_keys_enabled%" $adminAuthBypass: "%admin_auth_bypass%" + App\Services\UpdateChecker: + arguments: + $enabled: "%update_check_enabled%" + App\Command\MailTestCommand: arguments: $inviteAddress: "%env(INVITE_FROM_ADDRESS)%" diff --git a/docker/.env b/docker/.env index 5102c22..d0873e0 100644 --- a/docker/.env +++ b/docker/.env @@ -56,6 +56,9 @@ WEBDAV_HOMES_DIR= # App keys (see the README) APP_KEYS_ENABLED=false +# Update check: ask GitHub once a day whether a newer release exists (see the README) +UPDATE_CHECK_ENABLED=false + # Mail settings INVITE_FROM_ADDRESS=no-reply@example.org MAIL_HOST=smtp.myprovider.com diff --git a/src/Services/Diagnostics.php b/src/Services/Diagnostics.php index 5253fb7..cbc9ec5 100644 --- a/src/Services/Diagnostics.php +++ b/src/Services/Diagnostics.php @@ -12,6 +12,9 @@ * * Every check is passive: the page has to stay usable precisely when something is down, so it * opens no socket and runs no migration. `davis:mail:test` is the active counterpart for mail. + * + * The one exception is the update check, which is opt-in and asks GitHub at most once a day + * (see UpdateChecker); the page stays usable when GitHub is not reachable. */ final class Diagnostics { @@ -26,6 +29,7 @@ public function __construct( private Connection $connection, private DependencyFactory $migrations, private UrlGeneratorInterface $router, + private UpdateChecker $updateChecker, private string $environment, private bool $debug, private string $logFilePath, @@ -51,6 +55,7 @@ public function buckets(): array $buckets = [ 'diagnostics.bucket.runtime' => [ $this->versions(), + $this->update(), $this->environment(), $this->timezone(), $this->logFile(), @@ -118,6 +123,30 @@ private function versions(): array return $this->check(self::INFO, 'diagnostics.versions', sprintf('Davis %s · sabre/dav %s · PHP %s', \App\Version::VERSION, \Sabre\DAV\Version::VERSION, PHP_VERSION)); } + /** + * Releases are easy to miss on a self-hosted install, and the release notes are where the + * migration to run and any change of behaviour are announced. Off by default: see UpdateChecker. + */ + private function update(): array + { + if (!$this->updateChecker->isEnabled()) { + return $this->check(self::INFO, 'diagnostics.update', 'diagnostics.value.disabled', 'diagnostics.update.disabled', 'UPDATE_CHECK_ENABLED=true', UpdateChecker::RELEASES_URL); + } + + $latest = $this->updateChecker->latestRelease(); + if (null === $latest) { + return $this->check(self::INFO, 'diagnostics.update', 'diagnostics.value.unknown', 'diagnostics.update.unreachable', null, UpdateChecker::RELEASES_URL); + } + + $current = 'v'.UpdateChecker::normalize($this->updateChecker->currentVersion()); + + if ($this->updateChecker->isOutdated($latest['version'])) { + return $this->check(self::WARNING, 'diagnostics.update', sprintf('%s → v%s', $current, $latest['version']), 'diagnostics.update.available', null, $latest['url']); + } + + return $this->check(self::OK, 'diagnostics.update', $current, 'diagnostics.update.current', null, $latest['url']); + } + private function environment(): array { $value = $this->environment.($this->debug ? ' (debug)' : ''); diff --git a/src/Services/UpdateChecker.php b/src/Services/UpdateChecker.php new file mode 100644 index 0000000..354afc4 --- /dev/null +++ b/src/Services/UpdateChecker.php @@ -0,0 +1,117 @@ +enabled; + } + + public function currentVersion(): string + { + return $this->currentVersion; + } + + /** + * The latest published release, or null when it could not be looked up (offline, rate + * limited, unexpected payload). Both outcomes are cached, with different lifetimes. + * + * @return array{version: string, url: string}|null + */ + public function latestRelease(): ?array + { + $cached = $this->cache->get(self::CACHE_KEY, function (ItemInterface $item): array { + try { + $release = $this->fetchLatestRelease(); + } catch (\Throwable $e) { + $item->expiresAfter(self::TTL_AFTER_FAILURE); + + return ['error' => $e->getMessage()]; + } + + $item->expiresAfter(self::TTL); + + return $release; + }); + + if (!is_array($cached) || !isset($cached['version'], $cached['url'])) { + return null; + } + + return ['version' => (string) $cached['version'], 'url' => (string) $cached['url']]; + } + + /** + * Whether the running version is behind the given one. Tags carry a `v` prefix, the + * constant does not. + */ + public function isOutdated(string $latestVersion): bool + { + return version_compare(self::normalize($this->currentVersion), self::normalize($latestVersion), '<'); + } + + public static function normalize(string $version): string + { + return ltrim(trim($version), 'vV'); + } + + /** + * @return array{version: string, url: string} + */ + private function fetchLatestRelease(): array + { + $response = $this->httpClient->request('GET', self::LATEST_RELEASE_URL, [ + 'headers' => [ + 'Accept' => 'application/vnd.github+json', + 'User-Agent' => 'Davis/'.$this->currentVersion, + ], + 'timeout' => self::TIMEOUT, + 'max_duration' => 2.0 * self::TIMEOUT, + ]); + + $release = $response->toArray(); + $version = self::normalize((string) ($release['tag_name'] ?? '')); + + if (!preg_match('/^\d+(\.\d+)*/', $version)) { + throw new \UnexpectedValueException(sprintf('Unexpected tag name "%s"', $release['tag_name'] ?? '')); + } + + $url = (string) ($release['html_url'] ?? ''); + + return [ + 'version' => $version, + 'url' => str_starts_with($url, 'https://github.com/') ? $url : self::RELEASES_URL, + ]; + } +} diff --git a/templates/diagnostics.html.twig b/templates/diagnostics.html.twig index 8e303a6..795119b 100644 --- a/templates/diagnostics.html.twig +++ b/templates/diagnostics.html.twig @@ -39,7 +39,7 @@
{{ check.command }}
{% endif %} {% if check.link %} - {{ "diagnostics.go_there"|trans }} + {{ "diagnostics.go_there"|trans }} {% endif %} {% endfor %} diff --git a/tests/Functional/Service/DiagnosticsTest.php b/tests/Functional/Service/DiagnosticsTest.php index 1997855..01babb9 100644 --- a/tests/Functional/Service/DiagnosticsTest.php +++ b/tests/Functional/Service/DiagnosticsTest.php @@ -5,7 +5,12 @@ namespace App\Tests\Functional\Service; use App\Services\Diagnostics; +use App\Services\UpdateChecker; +use App\Version; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; /** @@ -26,6 +31,7 @@ private function diagnosticsWith(array $overrides = []): Diagnostics 'connection' => $container->get('doctrine')->getConnection(), 'migrations' => $container->get('doctrine.migrations.dependency_factory'), 'router' => $container->get(UrlGeneratorInterface::class), + 'updateChecker' => $this->updateChecker(null), 'environment' => 'prod', 'debug' => false, 'logFilePath' => sys_get_temp_dir().'/davis-diagnostics/prod.log', @@ -51,6 +57,18 @@ private function diagnosticsWith(array $overrides = []): Diagnostics return new Diagnostics(...$arguments); } + /** + * Off when null, otherwise on and GitHub answers with that tag (or a 503 for an empty tag). + */ + private function updateChecker(?string $latestTag): UpdateChecker + { + $response = '' === $latestTag + ? new MockResponse('', ['http_code' => 503]) + : new MockResponse(json_encode(['tag_name' => $latestTag, 'html_url' => 'https://github.com/tchapi/davis/releases/tag/'.$latestTag], JSON_THROW_ON_ERROR)); + + return new UpdateChecker(new MockHttpClient($response), new ArrayAdapter(), null !== $latestTag); + } + /** * @return array> keyed by check title */ @@ -240,4 +258,49 @@ public function testAnAuthBypassValueThatIsIgnoredIsReported(): void $this->assertSame($value, $check['value']); } } + + public function testTheUpdateCheckIsOffByDefaultAndSaysHowToTurnItOn(): void + { + $check = $this->checksOf($this->diagnosticsWith())['diagnostics.update']; + + $this->assertSame(Diagnostics::INFO, $check['severity']); + $this->assertSame('diagnostics.value.disabled', $check['value']); + $this->assertSame('UPDATE_CHECK_ENABLED=true', $check['command']); + $this->assertSame(UpdateChecker::RELEASES_URL, $check['link'], 'Releases are discoverable even with the check off'); + } + + public function testANewerReleaseIsSomethingToLookAt(): void + { + $diagnostics = $this->diagnosticsWith(['updateChecker' => $this->updateChecker('v99.0.0')]); + $check = $this->checksOf($diagnostics)['diagnostics.update']; + + $this->assertSame(Diagnostics::WARNING, $check['severity']); + $this->assertSame('v'.Version::VERSION.' → v99.0.0', $check['value']); + $this->assertSame('https://github.com/tchapi/davis/releases/tag/v99.0.0', $check['link'], 'The link goes to the release notes'); + $this->assertSame($this->diagnosticsWith()->attentionCount() + 1, $diagnostics->attentionCount()); + } + + public function testTheCurrentReleaseIsFine(): void + { + $diagnostics = $this->diagnosticsWith(['updateChecker' => $this->updateChecker('v'.Version::VERSION)]); + $check = $this->checksOf($diagnostics)['diagnostics.update']; + + $this->assertSame(Diagnostics::OK, $check['severity']); + $this->assertSame('v'.Version::VERSION, $check['value']); + $this->assertSame($this->diagnosticsWith()->attentionCount(), $diagnostics->attentionCount()); + } + + /** + * No network must not turn into a red line, nor take the page down. + */ + public function testGithubBeingUnreachableIsOnlyInformation(): void + { + $diagnostics = $this->diagnosticsWith(['updateChecker' => $this->updateChecker('')]); + $check = $this->checksOf($diagnostics)['diagnostics.update']; + + $this->assertSame(Diagnostics::INFO, $check['severity']); + $this->assertSame('diagnostics.value.unknown', $check['value']); + $this->assertSame(UpdateChecker::RELEASES_URL, $check['link']); + $this->assertSame($this->diagnosticsWith()->attentionCount(), $diagnostics->attentionCount()); + } } diff --git a/tests/Functional/Service/UpdateCheckerTest.php b/tests/Functional/Service/UpdateCheckerTest.php new file mode 100644 index 0000000..040df99 --- /dev/null +++ b/tests/Functional/Service/UpdateCheckerTest.php @@ -0,0 +1,89 @@ + $tag, 'html_url' => $url], JSON_THROW_ON_ERROR), [ + 'response_headers' => ['content-type' => 'application/json'], + ]); + } + + public function testTheLatestReleaseIsReadFromGithub(): void + { + $http = new MockHttpClient(self::release('v99.0.0')); + $checker = new UpdateChecker($http, new ArrayAdapter(), true, '5.6.3'); + + $this->assertSame(['version' => '99.0.0', 'url' => 'https://github.com/tchapi/davis/releases/tag/v99.0.0'], $checker->latestRelease()); + $this->assertTrue($checker->isOutdated('99.0.0')); + $this->assertFalse($checker->isOutdated('5.6.3')); + $this->assertFalse($checker->isOutdated('v5.6.3'), 'The tag prefix must not make the same version look newer'); + $this->assertFalse($checker->isOutdated('5.6.2')); + } + + public function testGithubIsAskedOnceThenTheAnswerIsCached(): void + { + $http = new MockHttpClient([self::release('v99.0.0'), self::release('v100.0.0')]); + $checker = new UpdateChecker($http, new ArrayAdapter(), true, '5.6.3'); + + $this->assertSame('99.0.0', $checker->latestRelease()['version'] ?? null); + $this->assertSame('99.0.0', $checker->latestRelease()['version'] ?? null); + $this->assertSame(1, $http->getRequestsCount()); + } + + public function testAFailedLookupIsNullAndNotRetriedOnEveryPageLoad(): void + { + $http = new MockHttpClient(new MockResponse('', ['http_code' => 503])); + $checker = new UpdateChecker($http, new ArrayAdapter(), true, '5.6.3'); + + $this->assertNull($checker->latestRelease()); + $this->assertNull($checker->latestRelease()); + $this->assertSame(1, $http->getRequestsCount()); + } + + public function testAPayloadThatIsNotAReleaseIsTreatedAsAFailure(): void + { + $checker = new UpdateChecker(new MockHttpClient(self::release('nightly')), new ArrayAdapter(), true, '5.6.3'); + + $this->assertNull($checker->latestRelease()); + } + + /** + * The link is shown to an administrator: it must stay on GitHub whatever the API returns. + */ + public function testTheLinkFallsBackToTheReleasesPage(): void + { + $checker = new UpdateChecker(new MockHttpClient(self::release('v99.0.0', 'https://example.org/phishing')), new ArrayAdapter(), true, '5.6.3'); + + $this->assertSame(UpdateChecker::RELEASES_URL, $checker->latestRelease()['url'] ?? null); + } + + public function testTheRequestIdentifiesItselfAndDoesNotWaitLong(): void + { + $http = new MockHttpClient(function (string $method, string $url, array $options): MockResponse { + $this->assertSame('GET', $method); + $this->assertSame('https://api.github.com/repos/tchapi/davis/releases/latest', $url); + $this->assertContains('User-Agent: Davis/5.6.3', $options['headers']); + $this->assertLessThanOrEqual(5, $options['timeout']); + + return self::release('v5.6.3'); + }); + + (new UpdateChecker($http, new ArrayAdapter(), true, '5.6.3'))->latestRelease(); + $this->assertSame(1, $http->getRequestsCount()); + } +} diff --git a/translations/messages+intl-icu.de.xlf b/translations/messages+intl-icu.de.xlf index 334e73d..fa3562b 100644 --- a/translations/messages+intl-icu.de.xlf +++ b/translations/messages+intl-icu.de.xlf @@ -989,6 +989,30 @@ diagnostics.accounts_without_email.help Ein Konto ohne E-Mail-Adresse versendet nie eine Einladung, denn anhand dieser Adresse gleicht Davis den Termin-Organisator ab. Die Adresse jedes Kontos muss diejenige sein, die sein Kalenderprogramm sendet. + + diagnostics.update + Neueste Version + + + diagnostics.update.disabled + Davis sendet keine Daten nach Hause. Setzen Sie UPDATE_CHECK_ENABLED=true, damit diese Seite höchstens einmal am Tag bei GitHub nachfragt, ob eine neuere Version vorliegt. + + + diagnostics.update.available + Eine neuere Version ist verfügbar. Lesen Sie vor dem Aktualisieren die Versionshinweise: Dort stehen die auszuführende Migration und alle Verhaltensänderungen. + + + diagnostics.update.current + Dies ist die neueste Version. GitHub wird höchstens einmal am Tag abgefragt. + + + diagnostics.update.unreachable + Die neueste Version konnte auf GitHub nicht ermittelt werden (kein Netzwerk oder API-Limit erreicht). In einer Stunde wird es erneut versucht. + + + diagnostics.value.unknown + Unbekannt + diff --git a/translations/messages+intl-icu.en.xlf b/translations/messages+intl-icu.en.xlf index 02f0ab1..043b291 100644 --- a/translations/messages+intl-icu.en.xlf +++ b/translations/messages+intl-icu.en.xlf @@ -989,6 +989,30 @@ diagnostics.accounts_without_email.help An account with no email address never sends an invitation, because the address is what Davis matches against the event organiser. Each account's address has to be the one its calendar client sends. + + diagnostics.update + Latest release + + + diagnostics.update.disabled + Davis does not phone home. Set UPDATE_CHECK_ENABLED=true to have this page ask GitHub, at most once a day, whether a newer release exists. + + + diagnostics.update.available + A newer release is available. Read its release notes before updating: they list the migration to run and anything that changed behaviour. + + + diagnostics.update.current + This is the latest release. GitHub is asked at most once a day. + + + diagnostics.update.unreachable + The latest release could not be looked up on GitHub (no network, or the API rate limit was hit). It is tried again in an hour. + + + diagnostics.value.unknown + Unknown + diff --git a/translations/messages+intl-icu.fr.xliff b/translations/messages+intl-icu.fr.xliff index 78681e3..c4169fb 100644 --- a/translations/messages+intl-icu.fr.xliff +++ b/translations/messages+intl-icu.fr.xliff @@ -989,6 +989,30 @@ diagnostics.accounts_without_email.help Un compte sans adresse e-mail n'envoie jamais d'invitation, car c'est cette adresse que Davis compare à l'organisateur de l'événement. L'adresse de chaque compte doit être celle qu'envoie son client de calendrier. + + diagnostics.update + Dernière version + + + diagnostics.update.disabled + Davis ne contacte aucun serveur de lui-même. Définissez UPDATE_CHECK_ENABLED=true pour que cette page demande à GitHub, au plus une fois par jour, si une version plus récente existe. + + + diagnostics.update.available + Une version plus récente est disponible. Lisez ses notes de version avant de mettre à jour : elles indiquent la migration à lancer et tout changement de comportement. + + + diagnostics.update.current + C'est la dernière version. GitHub est interrogé au plus une fois par jour. + + + diagnostics.update.unreachable + La dernière version n'a pas pu être consultée sur GitHub (pas de réseau, ou limite de l'API atteinte). Nouvel essai dans une heure. + + + diagnostics.value.unknown + Inconnue +