build(todo): Todo deployable beside Learn's server, and released as an APK, on F-Droid and as a snap (ADR 0033, step 5)
deploy/compose.todo.yaml adds Todo's server at todo.halkyon.eu, a member of Learn's register, its database halkyon_todo in the instance's PostgreSQL, its image built as Train's. redeploy.sh and backup.sh go over every app server .env switches on (train, todo). Each compose file names its server to the register in a variable of its own (INSTANCE_MEMBER_TRAIN, INSTANCE_MEMBER_TODO), which halkyon_base adds to INSTANCE_MEMBERS, each address once, as a second compose file's default would have replaced Train's. apps/todo/release.dart configures the release tool for Todo's APK and snap: a key of its own (pinned once made; until then nothing is released), the F-Droid listing (Time, NonFreeNet), and the snap from apps/todo/snap/, which the tool builds in a clone of the repository at the tag with that folder in place of snap/, as snapcraft builds one recipe a folder and the root's is Learn's. What's new for 0.1.0 in English and German, now shown. Android's backup and transfer are off, as Train's (D53): the reminders scheduled keep their tasks' titles unsealed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
e673aa33aa
commit
e61d3abb4f
41 changed files with 963 additions and 107 deletions
|
|
@ -30,7 +30,9 @@ deploy/.env
|
||||||
apps/learn/halkyon_learn_flutter/
|
apps/learn/halkyon_learn_flutter/
|
||||||
apps/learn/halkyon_learn_client/
|
apps/learn/halkyon_learn_client/
|
||||||
apps/learn/tool/
|
apps/learn/tool/
|
||||||
apps/todo/
|
apps/todo/halkyon_todo_flutter/
|
||||||
|
apps/todo/halkyon_todo_client/
|
||||||
|
apps/todo/halkyon_todo_server/.serverpod/
|
||||||
apps/train/halkyon_train_flutter/
|
apps/train/halkyon_train_flutter/
|
||||||
apps/train/halkyon_train_client/
|
apps/train/halkyon_train_client/
|
||||||
apps/train/halkyon_train_server/.serverpod/
|
apps/train/halkyon_train_server/.serverpod/
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ Halkyon's apps and the packages they share, in one repository and one pub worksp
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
- **`apps/<app>/`**: each app's packages, as `serverpod create` lays them out (`halkyon_<app>_server`, `_client`, `_flutter`), plus what is the app's alone (Learn's curator, the scripts writing Learn's assets in `apps/learn/tool/`, run from `apps/learn`; the apps' icons are the root's `tool/app_icons.py`, ADR 0031). Learn is the app that runs; Todo is still Serverpod's scaffold, and Train a shell like it (ADR 0030); Meditate is to come.
|
- **`apps/<app>/`**: each app's packages, as `serverpod create` lays them out (`halkyon_<app>_server`, `_client`, `_flutter`), plus what is the app's alone (Learn's curator, the scripts writing Learn's assets in `apps/learn/tool/`, run from `apps/learn`; the apps' icons are the root's `tool/app_icons.py`, ADR 0031). Learn is the app that runs; Train and Todo run too (ADR 0030, ADR 0033), Todo not released yet; Meditate is to come.
|
||||||
- **`packages/`**: what the apps share, each with its README and licence, which the apps take by path (`path: ../../../packages/<package>`), so a change to a shared package and to the apps using it is one commit:
|
- **`packages/`**: what the apps share, each with its README and licence, which the apps take by path (`path: ../../../packages/<package>`), so a change to a shared package and to the apps using it is one commit:
|
||||||
- `halkyon_ui`: Halkyon's look, the theme around cobalt and copper, Noto Sans bundled, the window-size classes and the widgets every screen is built of (the floating bar and rail, `FoldingTabs`' tab row folding into the title), with texts of its own (English, German).
|
- `halkyon_ui`: Halkyon's look, the theme around cobalt and copper, Noto Sans bundled, the window-size classes and the widgets every screen is built of (the floating bar and rail, `FoldingTabs`' tab row folding into the title), with texts of its own (English, German).
|
||||||
- `halkyon_base_server`, `halkyon_base_client`: the Serverpod module every Halkyon server includes (ADR 0014): signing in (a password with TOTP and the resets of ADR 0017, SimpleLogin on `halkyon_oidc`, email codes proving an address; no anonymous accounts since ADR 0020, D25, nor GitHub since D36), an account's ways in and its deletion with a hook for each app's own data, the pruning, the instance's basics, people's handles and public profiles, who may sign up (`SIGN_UP`: open, approval, closed) with the accounts waiting for approval and the instance's operators who approve them (Halkyon Learn's ADR 0021, step 1), the operator's assistants forwarded on halkyon_llm with their grants, each person's budget, the efforts they allow and counted figures, and the instance's privacy page made from its settings (step 2), and the instance's register (ADR 0016): one account per person across an operator's services, held by one service (Learn's server on learn.halkyon.eu) and asked by the others, its members (`ACCOUNT_REGISTER_URL`), all sharing a secret (`INSTANCE_SECRET`); its README says how.
|
- `halkyon_base_server`, `halkyon_base_client`: the Serverpod module every Halkyon server includes (ADR 0014): signing in (a password with TOTP and the resets of ADR 0017, SimpleLogin on `halkyon_oidc`, email codes proving an address; no anonymous accounts since ADR 0020, D25, nor GitHub since D36), an account's ways in and its deletion with a hook for each app's own data, the pruning, the instance's basics, people's handles and public profiles, who may sign up (`SIGN_UP`: open, approval, closed) with the accounts waiting for approval and the instance's operators who approve them (Halkyon Learn's ADR 0021, step 1), the operator's assistants forwarded on halkyon_llm with their grants, each person's budget, the efforts they allow and counted figures, and the instance's privacy page made from its settings (step 2), and the instance's register (ADR 0016): one account per person across an operator's services, held by one service (Learn's server on learn.halkyon.eu) and asked by the others, its members (`ACCOUNT_REGISTER_URL`), all sharing a secret (`INSTANCE_SECRET`); its README says how.
|
||||||
|
|
@ -15,7 +15,7 @@ Halkyon's apps and the packages they share, in one repository and one pub worksp
|
||||||
- `halkyon_keys` (pure Dart on libsodium, ADR 0017): an account's keys on the device, the password's Argon2id and SRP's x, the key pairs and their wrapping, the recovery phrase, data keys, content sealed and signed, row IDs keyed by the account (`keyedRowId`). SRP-6a's two halves are the module's (`halkyon_base_server`'s `base.dart`, `halkyon_base_client`'s `srp.dart`), as are TOTP and the recovery codes. `sodium`'s build hook compiles libsodium, so every machine that builds or tests needs a C compiler and `make`.
|
- `halkyon_keys` (pure Dart on libsodium, ADR 0017): an account's keys on the device, the password's Argon2id and SRP's x, the key pairs and their wrapping, the recovery phrase, data keys, content sealed and signed, row IDs keyed by the account (`keyedRowId`). SRP-6a's two halves are the module's (`halkyon_base_server`'s `base.dart`, `halkyon_base_client`'s `srp.dart`), as are TOTP and the recovery codes. `sodium`'s build hook compiles libsodium, so every machine that builds or tests needs a C compiler and `make`.
|
||||||
- `halkyon_llm` (pure Dart, Halkyon Learn's ADR 0021, D15 and D46): language models' APIs streamed over plain HTTP, no SDKs: Anthropic's Messages API, OpenAI's chat completions (each known host's fields, D45) and its Responses API, behind one interface (`LlmClient`), with `FakeLlmClient` for tests; tested on replies written from the providers' documentation and on those `tool/record_llm.dart` records with a key (D36). The module forwards the operator's assistants with it, and the apps call a person's own provider. BSD-3-Clause as `halkyon_oidc`, `publish_to: none`.
|
- `halkyon_llm` (pure Dart, Halkyon Learn's ADR 0021, D15 and D46): language models' APIs streamed over plain HTTP, no SDKs: Anthropic's Messages API, OpenAI's chat completions (each known host's fields, D45) and its Responses API, behind one interface (`LlmClient`), with `FakeLlmClient` for tests; tested on replies written from the providers' documentation and on those `tool/record_llm.dart` records with a key (D36). The module forwards the operator's assistants with it, and the apps call a person's own provider. BSD-3-Clause as `halkyon_oidc`, `publish_to: none`.
|
||||||
- `halkyon_oidc_server`, `halkyon_oidc_client` (ADR 0023): signing in with OpenID Connect, with a SimpleLogin preset, as a Serverpod module that halkyon_base builds on; `halkyon_oidc_flutter`, the device's half: the provider's page opened, the way back caught (the app's scheme, or the loopback on Linux and Windows, `LoopbackWebAuth`) and its state checked. Nothing of Halkyon's is in it, so that others can use it with Serverpod: under BSD-3-Clause, Serverpod's own licence, where the other packages are AGPL; `publish_to: none` until it is published to pub.dev, which is asked again first.
|
- `halkyon_oidc_server`, `halkyon_oidc_client` (ADR 0023): signing in with OpenID Connect, with a SimpleLogin preset, as a Serverpod module that halkyon_base builds on; `halkyon_oidc_flutter`, the device's half: the provider's page opened, the way back caught (the app's scheme, or the loopback on Linux and Windows, `LoopbackWebAuth`) and its state checked. Nothing of Halkyon's is in it, so that others can use it with Serverpod: under BSD-3-Clause, Serverpod's own licence, where the other packages are AGPL; `publish_to: none` until it is published to pub.dev, which is asked again first.
|
||||||
- **`deploy/`**: the instance deploy at the root, learn.halkyon.eu's today (Learn's `AGENTS.md`, *CI and deploys*), where other services' compose files are to join Learn's. **`snap/`**: Learn's snap, built from the root, which its build needs whole. **`docs/adr/`**: the decision records of every app and package, one numbering. **`tool/`**: the repository's tools (below).
|
- **`deploy/`**: the instance deploy at the root, learn.halkyon.eu's today (Learn's `AGENTS.md`, *CI and deploys*), where other services' compose files join Learn's (Train's `compose.train.yaml`, Todo's `compose.todo.yaml`, each switched on in `.env`). **`snap/`**: Learn's snap, built from the root, which its build needs whole; Todo's is `apps/todo/snap/`, which the release tool builds in a clone of the repository with it in place of `snap/` (Todo's `AGENTS.md`, *Releases*). **`docs/adr/`**: the decision records of every app and package, one numbering. **`tool/`**: the repository's tools (below).
|
||||||
- **One workspace, one lock**: the root `pubspec.yaml` lists every package, `pubspec.lock` holds one version of each dependency for all of them, and its `dependency_overrides` hold for every app (the offline sync fork, Learn's `AGENTS.md`; `cli_util`, below). **Serverpod's version moves as one**: every package pins Serverpod's packages to the same version (4.0.4), the module's, Todo's and Train's too, together with the CLI version CI generates code with (`SERVERPOD_VERSION` in `.forgejo/workflows/ci.yaml`) and the sync packages Learn's server depends on.
|
- **One workspace, one lock**: the root `pubspec.yaml` lists every package, `pubspec.lock` holds one version of each dependency for all of them, and its `dependency_overrides` hold for every app (the offline sync fork, Learn's `AGENTS.md`; `cli_util`, below). **Serverpod's version moves as one**: every package pins Serverpod's packages to the same version (4.0.4), the module's, Todo's and Train's too, together with the CLI version CI generates code with (`SERVERPOD_VERSION` in `.forgejo/workflows/ci.yaml`) and the sync packages Learn's server depends on.
|
||||||
|
|
||||||
## How work is done here
|
## How work is done here
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -18,7 +18,6 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
|
||||||
applicationId = "app.halkyon.todo"
|
applicationId = "app.halkyon.todo"
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
|
|
@ -32,13 +31,46 @@ android {
|
||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The app's own key signs release builds when tool/release.dart hands it over
|
||||||
|
// through the environment, as Learn's and Train's do: the keystore's path and the
|
||||||
|
// key's alias, and the password asked for at each release, never written down. Other
|
||||||
|
// release builds are signed with the debug key, so `flutter run --release` works
|
||||||
|
// anywhere; the release tool releases no APK signed otherwise.
|
||||||
|
val releaseKeystore = System.getenv("HALKYON_ANDROID_KEYSTORE")
|
||||||
|
signingConfigs {
|
||||||
|
if (releaseKeystore != null) {
|
||||||
|
create("release") {
|
||||||
|
storeFile = file(releaseKeystore)
|
||||||
|
keyAlias = System.getenv("HALKYON_ANDROID_KEY_ALIAS")
|
||||||
|
// A PKCS #12 keystore has one password, for the store and its key.
|
||||||
|
storePassword = System.getenv("HALKYON_ANDROID_KEYSTORE_PASSWORD")
|
||||||
|
keyPassword = System.getenv("HALKYON_ANDROID_KEYSTORE_PASSWORD")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// TODO: Add your own signing config for the release build.
|
signingConfig =
|
||||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
signingConfigs.findByName("release") ?: signingConfigs.getByName("debug")
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The native libraries go into the APK compressed, as Learn's: one APK serves
|
||||||
|
// every phone, holding them for three architectures, and Android unpacks the
|
||||||
|
// phone's own as it installs the app.
|
||||||
|
packaging {
|
||||||
|
jniLibs {
|
||||||
|
useLegacyPackaging = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leaves out the list of dependencies Gradle would otherwise add to the APK,
|
||||||
|
// encrypted for Google Play alone; F-Droid's and others' scanners flag it.
|
||||||
|
dependenciesInfo {
|
||||||
|
includeInApk = false
|
||||||
|
includeInBundle = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- The app talks to its instance: release builds need this too (Flutter's
|
<!-- The app talks to its instance: release builds need this too (Flutter's
|
||||||
template grants it to debug and profile builds only). -->
|
template grants it to debug and profile builds only). -->
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
|
@ -8,10 +9,19 @@
|
||||||
them off; without, Android may bring a reminder a few minutes late). -->
|
them off; without, Android may bring a reminder a few minutes late). -->
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
||||||
|
<!-- No backup and no transfer to another phone (ADR 0033, step 5, as
|
||||||
|
Train's ADR 0030, D53): the reminders scheduled, with their tasks'
|
||||||
|
titles, are kept unsealed. Through Android 11 allowBackup and
|
||||||
|
fullBackupContent stop both, from 12 on the extraction rules stop the
|
||||||
|
transfer. -->
|
||||||
<application
|
<application
|
||||||
android:label="Halkyon Todo"
|
android:label="Halkyon Todo"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:allowBackup="false"
|
||||||
|
android:fullBackupContent="false"
|
||||||
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
tools:targetApi="s">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Nothing of Todo's goes to a backup or to another phone, from Android 12
|
||||||
|
on (ADR 0033, step 5, as Train's ADR 0030, D53): allowBackup="false"
|
||||||
|
stops the backup, and only these rules stop the transfer. What Todo keeps
|
||||||
|
unsealed (the reminders it has scheduled, with their tasks' titles) stays
|
||||||
|
on this phone; what is sealed comes back from the instance on the next. -->
|
||||||
|
<data-extraction-rules>
|
||||||
|
<cloud-backup>
|
||||||
|
<exclude domain="root" path="."/>
|
||||||
|
<exclude domain="file" path="."/>
|
||||||
|
<exclude domain="database" path="."/>
|
||||||
|
<exclude domain="sharedpref" path="."/>
|
||||||
|
<exclude domain="external" path="."/>
|
||||||
|
<exclude domain="device_root" path="."/>
|
||||||
|
<exclude domain="device_file" path="."/>
|
||||||
|
<exclude domain="device_database" path="."/>
|
||||||
|
<exclude domain="device_sharedpref" path="."/>
|
||||||
|
</cloud-backup>
|
||||||
|
<device-transfer>
|
||||||
|
<exclude domain="root" path="."/>
|
||||||
|
<exclude domain="file" path="."/>
|
||||||
|
<exclude domain="database" path="."/>
|
||||||
|
<exclude domain="sharedpref" path="."/>
|
||||||
|
<exclude domain="external" path="."/>
|
||||||
|
<exclude domain="device_root" path="."/>
|
||||||
|
<exclude domain="device_file" path="."/>
|
||||||
|
<exclude domain="device_database" path="."/>
|
||||||
|
<exclude domain="device_sharedpref" path="."/>
|
||||||
|
</device-transfer>
|
||||||
|
</data-extraction-rules>
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
- Die erste Version von Halkyon Todo: Aufgaben im Eingang und in Projekten mit Abschnitten, Heute und Demnächst, wiederkehrende Aufgaben und Erinnerungen, jedes Projekt als Liste, Board oder Kalender, alles versiegelt.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
- The first release of Halkyon Todo: tasks in an Inbox and in projects with sections, Today and Upcoming, repeating tasks and reminders, each project as a list, a board or a calendar, everything sealed.
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:halkyon_app/halkyon_app.dart';
|
import 'package:halkyon_app/halkyon_app.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/app/licences.dart';
|
import 'package:halkyon_todo_flutter/src/app/licences.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/app/todo_app.dart';
|
import 'package:halkyon_todo_flutter/src/app/todo_app.dart';
|
||||||
|
|
@ -28,12 +29,23 @@ Future<void> main() async {
|
||||||
);
|
);
|
||||||
await instances.load(usedBefore: device.storage.usedBefore());
|
await instances.load(usedBefore: device.storage.usedBefore());
|
||||||
runApp(
|
runApp(
|
||||||
TodoApp(
|
// What's new in each release (Train's ADR 0030, D61), from
|
||||||
choice: instances,
|
// assets/whats_new/, shown once after an update.
|
||||||
// Only an instance of Halkyon Todo: one of another Halkyon app's
|
WhatsNewScope(
|
||||||
// answers too, but holds nothing of this app's.
|
whatsNew: WhatsNew(
|
||||||
checker: ServerpodInstanceChecker(app: 'todo'),
|
AssetWhatsNewSource(rootBundle),
|
||||||
open: (instance) => TodoDependencies.create(instance, device),
|
FileWhatsNewStore(File(p.join(device.support, 'whats_new.json'))),
|
||||||
|
version: device.appVersion,
|
||||||
|
),
|
||||||
|
// An instance chosen before: the app ran on this device already.
|
||||||
|
usedBefore: instances.current != null,
|
||||||
|
child: TodoApp(
|
||||||
|
choice: instances,
|
||||||
|
// Only an instance of Halkyon Todo: one of another Halkyon app's
|
||||||
|
// answers too, but holds nothing of this app's.
|
||||||
|
checker: ServerpodInstanceChecker(app: 'todo'),
|
||||||
|
open: (instance) => TodoDependencies.create(instance, device),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,10 @@ flutter:
|
||||||
# The instance the build names, unless --dart-define=SERVER_URL=… does
|
# The instance the build names, unless --dart-define=SERVER_URL=… does
|
||||||
# (serverpod_flutter's getServerUrl).
|
# (serverpod_flutter's getServerUrl).
|
||||||
- assets/config.json
|
- assets/config.json
|
||||||
|
# What each release changed, one file a release and language
|
||||||
|
# (<version>.<language>.txt), drafted by tool/release.dart and edited
|
||||||
|
# (Train's ADR 0030, D61): halkyon_app's What's new shows them.
|
||||||
|
- assets/whats_new/
|
||||||
# The app's icon, Lucide's list-todo (ADR 0031), and Lucide's licence for the
|
# The app's icon, Lucide's list-todo (ADR 0031), and Lucide's licence for the
|
||||||
# licence list (the repository's tool/app_icons.py writes both).
|
# licence list (the repository's tool/app_icons.py writes both).
|
||||||
- assets/icon/halkyon_todo_icon.png
|
- assets/icon/halkyon_todo_icon.png
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
/// Nothing of Todo's leaves the phone by Android's backup or a transfer to
|
||||||
|
/// another phone (ADR 0033, step 5, as Train's ADR 0030, D53): the reminders
|
||||||
|
/// scheduled are kept unsealed, with their tasks' titles, and what is sealed
|
||||||
|
/// comes back from the instance anyway. Read as text, the files are small and
|
||||||
|
/// Android's own.
|
||||||
|
void main() {
|
||||||
|
const folder = 'android/app/src/main';
|
||||||
|
|
||||||
|
test('backup is off, through Android 11 as from 12 on', () {
|
||||||
|
final manifest = File('$folder/AndroidManifest.xml').readAsStringSync();
|
||||||
|
final application = RegExp(
|
||||||
|
'<application([^>]*)>',
|
||||||
|
).firstMatch(manifest)![1]!;
|
||||||
|
|
||||||
|
expect(application, contains('android:allowBackup="false"'));
|
||||||
|
expect(application, contains('android:fullBackupContent="false"'));
|
||||||
|
expect(
|
||||||
|
application,
|
||||||
|
contains('android:dataExtractionRules="@xml/data_extraction_rules"'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("from Android 12 on, neither a backup nor a transfer takes any of the app's "
|
||||||
|
'folders', () {
|
||||||
|
// allowBackup="false" leaves a transfer to another phone on from Android
|
||||||
|
// 12 on: the rules have to exclude it too, folder by folder.
|
||||||
|
final rules = File(
|
||||||
|
'$folder/res/xml/data_extraction_rules.xml',
|
||||||
|
).readAsStringSync();
|
||||||
|
for (final kind in ['cloud-backup', 'device-transfer']) {
|
||||||
|
final section = RegExp(
|
||||||
|
'<$kind>(.*?)</$kind>',
|
||||||
|
dotAll: true,
|
||||||
|
).firstMatch(rules)?[1];
|
||||||
|
expect(section, isNotNull, reason: '$kind is missing');
|
||||||
|
expect(section, isNot(contains('<include')), reason: kind);
|
||||||
|
final excluded = {
|
||||||
|
for (final exclude in RegExp(
|
||||||
|
r'<exclude domain="(\w+)" path="\."/>',
|
||||||
|
).allMatches(section!))
|
||||||
|
exclude[1],
|
||||||
|
};
|
||||||
|
expect(excluded, {
|
||||||
|
'root',
|
||||||
|
'file',
|
||||||
|
'database',
|
||||||
|
'sharedpref',
|
||||||
|
'external',
|
||||||
|
'device_root',
|
||||||
|
'device_file',
|
||||||
|
'device_database',
|
||||||
|
'device_sharedpref',
|
||||||
|
}, reason: kind);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
60
apps/todo/halkyon_todo_server/Dockerfile
Normal file
60
apps/todo/halkyon_todo_server/Dockerfile
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
# Todo's server image (ADR 0033, step 5). Built from the repository root (the build
|
||||||
|
# context), because the workspace's root pubspec and lockfile live there:
|
||||||
|
#
|
||||||
|
# docker build -f apps/todo/halkyon_todo_server/Dockerfile .
|
||||||
|
#
|
||||||
|
# Train's server image (apps/train/halkyon_train_server/Dockerfile) for Todo, as
|
||||||
|
# Learn's: the same Dart, the same Ubuntu runtime and start command; no git, no
|
||||||
|
# repositories folder, no data of its own.
|
||||||
|
|
||||||
|
# ── Build stage: compile the server to a native executable ────────────────────────
|
||||||
|
FROM dart:3.13.4 AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY pubspec.yaml pubspec.lock ./
|
||||||
|
COPY tool/workspace_only.sh tool/
|
||||||
|
COPY apps/todo/halkyon_todo_server apps/todo/halkyon_todo_server
|
||||||
|
COPY packages/halkyon_base_server packages/halkyon_base_server
|
||||||
|
COPY packages/halkyon_base_client packages/halkyon_base_client
|
||||||
|
COPY packages/halkyon_core packages/halkyon_core
|
||||||
|
COPY packages/halkyon_llm packages/halkyon_llm
|
||||||
|
COPY packages/halkyon_oidc_server packages/halkyon_oidc_server
|
||||||
|
COPY packages/halkyon_oidc_client packages/halkyon_oidc_client
|
||||||
|
|
||||||
|
# The workspace root with only the server and the modules it builds on, as Learn's
|
||||||
|
# image has it (the reasons there).
|
||||||
|
RUN sh tool/workspace_only.sh apps/todo/halkyon_todo_server packages/halkyon_base_server packages/halkyon_base_client packages/halkyon_core packages/halkyon_llm packages/halkyon_oidc_server packages/halkyon_oidc_client
|
||||||
|
RUN dart pub get
|
||||||
|
WORKDIR /app/apps/todo/halkyon_todo_server
|
||||||
|
RUN dart build cli --target bin/main.dart --output build
|
||||||
|
RUN mv build/bundle/bin/main build/bundle/bin/server
|
||||||
|
RUN mkdir -p config migrations
|
||||||
|
|
||||||
|
# ── Runtime stage ─────────────────────────────────────────────────────────────────
|
||||||
|
# Ubuntu 24.04 as Learn's; CA certificates for outgoing TLS (the operator's
|
||||||
|
# assistants, which the module forwards, ADR 0021).
|
||||||
|
FROM ubuntu:24.04
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV runmode=production
|
||||||
|
ENV serverid=default
|
||||||
|
ENV logging=normal
|
||||||
|
ENV role=monolith
|
||||||
|
|
||||||
|
COPY --from=build /app/apps/todo/halkyon_todo_server/build/bundle/ ./
|
||||||
|
COPY --from=build /app/apps/todo/halkyon_todo_server/config/ config/
|
||||||
|
COPY --from=build /app/apps/todo/halkyon_todo_server/migrations/ migrations/
|
||||||
|
COPY --from=build /app/apps/todo/halkyon_todo_server/lib/src/generated/protocol.yaml lib/src/generated/protocol.yaml
|
||||||
|
|
||||||
|
# No root rights needed: it listens on 8080 and 8082 and writes nothing.
|
||||||
|
RUN useradd --system --no-create-home --shell /usr/sbin/nologin app
|
||||||
|
USER app
|
||||||
|
|
||||||
|
# The API server, and the web server the register tells of accounts gone (ADR
|
||||||
|
# 0016), reached on the instance's own network alone.
|
||||||
|
EXPOSE 8080 8082
|
||||||
|
|
||||||
|
ENTRYPOINT ["/bin/sh", "-c", "exec ./bin/server --mode=$runmode --server-id=$serverid --logging=$logging --role=$role \"$@\"", "--"]
|
||||||
|
CMD []
|
||||||
|
|
@ -1,27 +1,20 @@
|
||||||
# This is the configuration file for your production environment.
|
# Todo's server on learn.halkyon.eu's server, at todo.halkyon.eu (ADR 0033, step 5),
|
||||||
# Typically, you will want to route the traffic through a load balancer
|
# behind the instance's Caddy (deploy/compose.todo.yaml): its API and its web server
|
||||||
# which adds SSL security through https. If you use Serverpod's standard
|
# on the one domain, as Learn's and Train's; its database in the instance's PostgreSQL.
|
||||||
# Terraform scripts to deploy your server, all you need to change in
|
|
||||||
# this file is the examplepod.com domain name.
|
|
||||||
|
|
||||||
# Configuration for the main API server.
|
# Configuration for the main API server.
|
||||||
apiServer:
|
apiServer:
|
||||||
port: 8080
|
port: 8080
|
||||||
publicHost: api.examplepod.com
|
publicHost: todo.halkyon.eu
|
||||||
publicPort: 443
|
publicPort: 443
|
||||||
publicScheme: https
|
publicScheme: https
|
||||||
|
|
||||||
# Configuration for the Insights server.
|
# No insightsServer, as Learn's: without the block Serverpod doesn't start Insights.
|
||||||
insightsServer:
|
|
||||||
port: 8081
|
|
||||||
publicHost: insights.examplepod.com
|
|
||||||
publicPort: 443
|
|
||||||
publicScheme: https
|
|
||||||
|
|
||||||
# Configuration for the web server.
|
# Configuration for the web server.
|
||||||
webServer:
|
webServer:
|
||||||
port: 8082
|
port: 8082
|
||||||
publicHost: app.examplepod.com
|
publicHost: todo.halkyon.eu
|
||||||
publicPort: 443
|
publicPort: 443
|
||||||
publicScheme: https
|
publicScheme: https
|
||||||
|
|
||||||
|
|
@ -30,11 +23,12 @@ webServer:
|
||||||
# If you are connecting on a public IP (e.g. on AWS or Google Cloud Run), you
|
# If you are connecting on a public IP (e.g. on AWS or Google Cloud Run), you
|
||||||
# connect on the public IP of the database e.g. database.examplepod.com.
|
# connect on the public IP of the database e.g. database.examplepod.com.
|
||||||
database:
|
database:
|
||||||
host: database.private-production.examplepod.com
|
host: postgres
|
||||||
port: 5432
|
port: 5432
|
||||||
name: serverpod
|
name: halkyon_todo
|
||||||
user: postgres
|
user: halkyon_todo
|
||||||
requireSsl: true
|
# On the instance's own network, never leaving the server.
|
||||||
|
requireSsl: false
|
||||||
#isUnixSocket: true # defaults to false
|
#isUnixSocket: true # defaults to false
|
||||||
#maxConnectionCount: 10 # Defaults to 10, a negative or null value removes the limit
|
#maxConnectionCount: 10 # Defaults to 10, a negative or null value removes the limit
|
||||||
|
|
||||||
|
|
|
||||||
44
apps/todo/release.dart
Normal file
44
apps/todo/release.dart
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
// How Halkyon Todo is released (`dart run tool/release.dart todo`): the
|
||||||
|
// release tool's configuration of it, registered in tool/src/release/apps.dart.
|
||||||
|
import '../../tool/src/release/app_release.dart';
|
||||||
|
|
||||||
|
/// Halkyon Todo's releases (ADR 0033, step 5): the APK and the snap, both
|
||||||
|
/// connecting to todo.halkyon.eu, published on code.halkyon.app, the APK in
|
||||||
|
/// the project's F-Droid repository beside Learn and Train.
|
||||||
|
const todoRelease = AppRelease(
|
||||||
|
id: 'todo',
|
||||||
|
name: 'Halkyon Todo',
|
||||||
|
applicationId: 'app.halkyon.todo',
|
||||||
|
directory: 'apps/todo',
|
||||||
|
flutterPackage: 'apps/todo/halkyon_todo_flutter',
|
||||||
|
artifactName: 'halkyon-todo',
|
||||||
|
serverUrl: 'https://todo.halkyon.eu/',
|
||||||
|
forgejo: 'https://code.halkyon.app',
|
||||||
|
repository: 'project-halkyon/halkyon',
|
||||||
|
// A PKCS #12 keystore of its own outside the repository (also in the
|
||||||
|
// maintainer's password manager), as Learn's and Train's: it is the app's
|
||||||
|
// identity, and lost, installed apps can't be updated; one key for several
|
||||||
|
// apps would let either's build pass for the other's update.
|
||||||
|
android: AndroidSigning(
|
||||||
|
certificateFile: 'tool/release/todo-android-certificate.sha256',
|
||||||
|
keystoreVariable: 'HALKYON_TODO_ANDROID_KEYSTORE',
|
||||||
|
defaultKeystore: '.local/share/halkyon-todo/android-release.p12',
|
||||||
|
aliasVariable: 'HALKYON_TODO_ANDROID_KEY_ALIAS',
|
||||||
|
defaultAlias: 'halkyon-todo',
|
||||||
|
),
|
||||||
|
fdroid: FdroidListing(
|
||||||
|
script: 'tool/fdroid_repo.sh',
|
||||||
|
listing: 'tool/release/fdroid',
|
||||||
|
indexCertificateFile: 'tool/release/fdroid-index-certificate.sha256',
|
||||||
|
repositoryUrl: 'https://halkyon.app/fdroid/repo',
|
||||||
|
),
|
||||||
|
// Built in a clone of its own (`SnapRelease.projectPath`), as the root's
|
||||||
|
// snap/ is Learn's. To the Store's edge channel, as Learn's, once the
|
||||||
|
// name is registered (`snapcraft register halkyon-todo`).
|
||||||
|
snap: SnapRelease(
|
||||||
|
name: 'halkyon-todo',
|
||||||
|
part: 'halkyon-todo',
|
||||||
|
directory: 'apps/todo/snap',
|
||||||
|
storeChannel: 'edge',
|
||||||
|
),
|
||||||
|
);
|
||||||
9
apps/todo/snap/gui/halkyon-todo.desktop
Normal file
9
apps/todo/snap/gui/halkyon-todo.desktop
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Halkyon Todo
|
||||||
|
Comment=Tasks and projects, sealed and synced between your devices
|
||||||
|
Exec=halkyon-todo
|
||||||
|
Icon=${SNAP}/meta/gui/halkyon-todo.png
|
||||||
|
Terminal=false
|
||||||
|
Categories=Office;ProjectManagement;
|
||||||
|
StartupWMClass=app.halkyon.todo
|
||||||
BIN
apps/todo/snap/gui/halkyon-todo.png
Normal file
BIN
apps/todo/snap/gui/halkyon-todo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
145
apps/todo/snap/snapcraft.yaml
Normal file
145
apps/todo/snap/snapcraft.yaml
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
# Halkyon Todo's Linux app as a Snap (ADR 0033, step 5), built in LXD by the release
|
||||||
|
# tool (`dart run tool/release.dart todo`): snapcraft builds one recipe a folder, from
|
||||||
|
# its snap/snapcraft.yaml, and the root's snap/ is Learn's, so the tool builds this one
|
||||||
|
# in a clone of the repository at the release's tag with this folder in place of the
|
||||||
|
# clone's snap/ (`SnapRelease.projectPath`), but for snap/local/, which the recipes
|
||||||
|
# share. By hand, the same: in such a clone, `snapcraft pack`.
|
||||||
|
#
|
||||||
|
# It builds what is committed (the source is the clone's HEAD, the tag), never a
|
||||||
|
# working tree with its build caches, and always for todo.halkyon.eu. Flutter is
|
||||||
|
# pinned like the local and CI one (FLUTTER_VERSION in .forgejo/workflows/ci.yaml),
|
||||||
|
# spelled out as in Learn's recipe, which says why.
|
||||||
|
name: halkyon-todo
|
||||||
|
title: Halkyon Todo
|
||||||
|
base: core24
|
||||||
|
adopt-info: halkyon-todo # the version, from the app's pubspec.yaml
|
||||||
|
summary: Tasks and projects, sealed and synced between your devices
|
||||||
|
description: |
|
||||||
|
Halkyon Todo keeps your tasks: an Inbox for what comes up, projects with
|
||||||
|
sections, sub-tasks and labels, Today with what is overdue, and Upcoming as
|
||||||
|
an agenda that opens into the month. Tasks repeat (every day, every other
|
||||||
|
Tuesday, the first Monday of each month), have reminders, deadlines and
|
||||||
|
priorities, and are typed the quick way: "Call Anna tomorrow 9am #Home p1".
|
||||||
|
|
||||||
|
Each project shows as a list, a board or a calendar, grouped, sorted and
|
||||||
|
filtered as you set it, the same on every device. Tasks are dragged between
|
||||||
|
sections, days and hours.
|
||||||
|
|
||||||
|
Everything is sealed on your device with your account's keys before it
|
||||||
|
reaches the instance, which can't read it. You sign in with SimpleLogin, by
|
||||||
|
Proton, or with an email address, a password and an authenticator app. It
|
||||||
|
connects to todo.halkyon.eu, the project's instance, or to any other:
|
||||||
|
everything an instance needs is in the repository, to run your own. The app
|
||||||
|
is AGPL-3.0 and the server SSPL-1.0, so all of its code is public.
|
||||||
|
|
||||||
|
Questions and reports: support@halkyon.app
|
||||||
|
license: AGPL-3.0-or-later
|
||||||
|
source-code: https://code.halkyon.app/project-halkyon/halkyon
|
||||||
|
# The project's, not an instance's: the app works with any instance.
|
||||||
|
website: https://halkyon.app/todo
|
||||||
|
# No contact or issues, as Learn's (its recipe says why): the address is in the
|
||||||
|
# description.
|
||||||
|
# The app's icon with rounded corners, made by the repository's tool/app_icons.py; also
|
||||||
|
# the desktop file's.
|
||||||
|
icon: snap/gui/halkyon-todo.png
|
||||||
|
grade: stable
|
||||||
|
confinement: strict
|
||||||
|
platforms:
|
||||||
|
amd64:
|
||||||
|
|
||||||
|
apps:
|
||||||
|
halkyon-todo:
|
||||||
|
command: bin/halkyon_todo_flutter
|
||||||
|
# How the session is kept: see snap/local/secret-backend (Learn's, kept in the
|
||||||
|
# clone).
|
||||||
|
command-chain: [bin/secret-backend]
|
||||||
|
# GTK, WebKitGTK (the sign-in's page) and libsecret come from the GNOME runtime,
|
||||||
|
# the reminders' notifications through the desktop's own; files and the browser
|
||||||
|
# go through the desktop's portals.
|
||||||
|
extensions: [gnome]
|
||||||
|
plugs:
|
||||||
|
- network
|
||||||
|
# SimpleLogin's sign-in comes back on a loopback address the app listens on.
|
||||||
|
- network-bind
|
||||||
|
# Never connected by itself; whoever connects it by hand gets the keyring.
|
||||||
|
- password-manager-service
|
||||||
|
# As Learn's: the sync's connection ended at once when the network goes;
|
||||||
|
# not connected until the Store grants it.
|
||||||
|
- network-manager-observe
|
||||||
|
|
||||||
|
parts:
|
||||||
|
flutter:
|
||||||
|
plugin: nil
|
||||||
|
source: https://github.com/flutter/flutter.git
|
||||||
|
source-tag: '3.47.8'
|
||||||
|
source-depth: 1
|
||||||
|
build-packages:
|
||||||
|
- clang
|
||||||
|
- cmake
|
||||||
|
- curl
|
||||||
|
- git
|
||||||
|
- libgtk-3-dev
|
||||||
|
- lld
|
||||||
|
- llvm-18
|
||||||
|
- ninja-build
|
||||||
|
- pkg-config
|
||||||
|
- unzip
|
||||||
|
- xz-utils
|
||||||
|
- zip
|
||||||
|
override-build: |
|
||||||
|
mkdir -p "$CRAFT_PART_INSTALL/usr/libexec" "$CRAFT_PART_INSTALL/usr/bin"
|
||||||
|
cp -r "$CRAFT_PART_SRC" "$CRAFT_PART_INSTALL/usr/libexec/flutter"
|
||||||
|
ln -s ../libexec/flutter/bin/flutter "$CRAFT_PART_INSTALL/usr/bin/flutter"
|
||||||
|
ln -s ../libexec/flutter/bin/dart "$CRAFT_PART_INSTALL/usr/bin/dart"
|
||||||
|
"$CRAFT_PART_INSTALL/usr/bin/flutter" --disable-analytics
|
||||||
|
"$CRAFT_PART_INSTALL/usr/bin/flutter" precache --linux
|
||||||
|
# Needed to build, not in the Snap.
|
||||||
|
override-prime: ''
|
||||||
|
|
||||||
|
halkyon-todo:
|
||||||
|
after: [flutter]
|
||||||
|
plugin: nil
|
||||||
|
source: .
|
||||||
|
source-type: git
|
||||||
|
# sodium's build hook compiles libsodium with its configure script and make
|
||||||
|
# (ADR 0017), with the Flutter part's clang.
|
||||||
|
build-packages:
|
||||||
|
- make
|
||||||
|
override-build: |
|
||||||
|
set -eu
|
||||||
|
export PATH="$CRAFT_STAGE/usr/bin:$PATH"
|
||||||
|
cd apps/todo/halkyon_todo_flutter
|
||||||
|
craftctl set version="$(sed -n 's/^version: *\([^+]*\).*/\1/p' pubspec.yaml)"
|
||||||
|
flutter pub get --enforce-lockfile
|
||||||
|
flutter build linux --release \
|
||||||
|
--dart-define=SERVER_URL=https://todo.halkyon.eu/
|
||||||
|
mkdir -p "$CRAFT_PART_INSTALL/bin"
|
||||||
|
cp -r build/linux/x64/release/bundle/. "$CRAFT_PART_INSTALL/bin/"
|
||||||
|
|
||||||
|
secret-backend:
|
||||||
|
plugin: dump
|
||||||
|
source: snap/local
|
||||||
|
organize:
|
||||||
|
secret-backend: bin/secret-backend
|
||||||
|
|
||||||
|
# As Learn's: every file the GNOME runtime or the base has at the same path
|
||||||
|
# goes, so that nothing staged hides the runtime's own (Learn's recipe says
|
||||||
|
# what that broke there). Todo stages no package today; the part keeps a
|
||||||
|
# later one from doing so unnoticed.
|
||||||
|
cleanup:
|
||||||
|
after: [halkyon-todo, secret-backend]
|
||||||
|
plugin: nil
|
||||||
|
build-snaps: [core24, gnome-46-2404]
|
||||||
|
override-prime: |
|
||||||
|
set -eu
|
||||||
|
for snap in core24 gnome-46-2404; do
|
||||||
|
cd "/snap/$snap/current/usr"
|
||||||
|
find . \( -type f -o -type l \) -print0 |
|
||||||
|
while IFS= read -r -d '' path; do
|
||||||
|
case "$path" in */blas/* | */lapack/*) continue ;; esac
|
||||||
|
target="$CRAFT_PRIME/usr/$path"
|
||||||
|
if [ -L "$target" ] || [ -f "$target" ]; then
|
||||||
|
rm -f "$target"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
@ -28,7 +28,8 @@ DOMAIN=http://localhost
|
||||||
# data too, and told again hourly until each answers. Each of them sets, in its own
|
# data too, and told again hourly until each answers. Each of them sets, in its own
|
||||||
# environment, the same INSTANCE_SECRET and ACCOUNT_REGISTER_URL, this server's web
|
# environment, the same INSTANCE_SECRET and ACCOUNT_REGISTER_URL, this server's web
|
||||||
# server as it reaches it (`http://server:8082`). Learn never takes ACCOUNT_REGISTER_URL:
|
# server as it reaches it (`http://server:8082`). Learn never takes ACCOUNT_REGISTER_URL:
|
||||||
# it doesn't start with one, as it holds the register.
|
# it doesn't start with one, as it holds the register. Train's and Todo's compose files
|
||||||
|
# name their own servers (INSTANCE_MEMBER_TRAIN, INSTANCE_MEMBER_TODO): this is for others.
|
||||||
#INSTANCE_MEMBERS=
|
#INSTANCE_MEMBERS=
|
||||||
|
|
||||||
# The instance's assistants, which an operator grants to people (Halkyon Learn's
|
# The instance's assistants, which an operator grants to people (Halkyon Learn's
|
||||||
|
|
@ -38,7 +39,8 @@ DOMAIN=http://localhost
|
||||||
# Sidecars switched on beside the instance (sidecars/README.md), each by its compose file
|
# Sidecars switched on beside the instance (sidecars/README.md), each by its compose file
|
||||||
# after the instance's own: compose.jellyfin.yaml, a Jellyfin people watch and listen to
|
# after the instance's own: compose.jellyfin.yaml, a Jellyfin people watch and listen to
|
||||||
# learn from; compose.languagetool.yaml, spelling and grammar checked; compose.train.yaml,
|
# learn from; compose.languagetool.yaml, spelling and grammar checked; compose.train.yaml,
|
||||||
# Halkyon Train's server with its BRouter. Unset: the instance alone.
|
# Halkyon Train's server with its BRouter; compose.todo.yaml, Halkyon Todo's server.
|
||||||
|
# Unset: the instance alone.
|
||||||
#COMPOSE_FILE=compose.yaml:compose.jellyfin.yaml
|
#COMPOSE_FILE=compose.yaml:compose.jellyfin.yaml
|
||||||
|
|
||||||
# With the Jellyfin sidecar: the domain it answers at, which the instance then suggests
|
# With the Jellyfin sidecar: the domain it answers at, which the instance then suggests
|
||||||
|
|
@ -50,8 +52,7 @@ DOMAIN=http://localhost
|
||||||
# pepper (never change it once people sign in), INSTANCE_SECRET above set, and the
|
# pepper (never change it once people sign in), INSTANCE_SECRET above set, and the
|
||||||
# region of BRouter's segments, `west,south,east,north` in whole degrees, multiples of
|
# region of BRouter's segments, `west,south,east,north` in whole degrees, multiples of
|
||||||
# 5 (empty: Europe's, -25,35,40,70, about 3.3 GB). Generate each secret:
|
# 5 (empty: Europe's, -25,35,40,70, about 3.3 GB). Generate each secret:
|
||||||
# openssl rand -hex 32. Its server is told of accounts gone without INSTANCE_MEMBERS;
|
# openssl rand -hex 32. Its compose file names its server to Learn's register.
|
||||||
# setting it, name Train's too (`http://train-server:8082/`).
|
|
||||||
#TRAIN_DOMAIN=train.example.org
|
#TRAIN_DOMAIN=train.example.org
|
||||||
#TRAIN_POSTGRES_PASSWORD=
|
#TRAIN_POSTGRES_PASSWORD=
|
||||||
#TRAIN_SESSION_KEY_HASH_PEPPER=
|
#TRAIN_SESSION_KEY_HASH_PEPPER=
|
||||||
|
|
@ -84,6 +85,14 @@ DOMAIN=http://localhost
|
||||||
#PHOTON_HEAP=
|
#PHOTON_HEAP=
|
||||||
#PHOTON_MEMORY=
|
#PHOTON_MEMORY=
|
||||||
|
|
||||||
|
# With Halkyon Todo beside the instance (compose.todo.yaml, sidecars/README.md, Todo):
|
||||||
|
# the domain its apps reach it at, its database's password and its own session pepper
|
||||||
|
# (never change it once people sign in), INSTANCE_SECRET above set. Generate each
|
||||||
|
# secret: openssl rand -hex 32. Its compose file names its server to Learn's register.
|
||||||
|
#TODO_DOMAIN=todo.example.org
|
||||||
|
#TODO_POSTGRES_PASSWORD=
|
||||||
|
#TODO_SESSION_KEY_HASH_PEPPER=
|
||||||
|
|
||||||
# Postgres password, shared by the postgres and server services.
|
# Postgres password, shared by the postgres and server services.
|
||||||
# Generate: openssl rand -base64 32
|
# Generate: openssl rand -base64 32
|
||||||
POSTGRES_PASSWORD=
|
POSTGRES_PASSWORD=
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@
|
||||||
# how it was set up and how to restore.
|
# how it was set up and how to restore.
|
||||||
#
|
#
|
||||||
# Everything people keep is in the database, their files too (Serverpod's database
|
# Everything people keep is in the database, their files too (Serverpod's database
|
||||||
# cloud storage), and in Train's where .env switches Train on (compose.train.yaml,
|
# cloud storage), and in Train's and Todo's where .env switches each on (compose.train.yaml,
|
||||||
# Halkyon Train's ADR 0030, D46), but for the releases of published studysets, which are git
|
# Halkyon Train's ADR 0030, D46; compose.todo.yaml, Halkyon Todo's ADR 0033), but for the
|
||||||
|
# releases of published studysets, which are git
|
||||||
# repositories in the server's volume: a dump and a bundle of each repository cover
|
# repositories in the server's volume: a dump and a bundle of each repository cover
|
||||||
# the instance; the secrets in deploy/.env are kept in Proton Pass instead. restic stores the dump encrypted in a Hetzner Storage Box
|
# the instance; the secrets in deploy/.env are kept in Proton Pass instead. restic stores the dump encrypted in a Hetzner Storage Box
|
||||||
# and keeps 24 hourly, 7 daily, 4 weekly and 3 monthly snapshots (decided 2026-10-03).
|
# and keeps 24 hourly, 7 daily, 4 weekly and 3 monthly snapshots (decided 2026-10-03).
|
||||||
|
|
@ -36,11 +37,15 @@ cd "$(dirname "$0")"
|
||||||
# the last hour and stores it only once; restic compresses what it stores.
|
# the last hour and stores it only once; restic compresses what it stores.
|
||||||
mkdir -p -m 700 "$HOME/backups"
|
mkdir -p -m 700 "$HOME/backups"
|
||||||
dump="$HOME/backups/latest.dump"
|
dump="$HOME/backups/latest.dump"
|
||||||
train_dump="$HOME/backups/latest-train.dump"
|
# The apps' servers .env switches on beside Learn's, each with its database
|
||||||
train=
|
# (halkyon_<app>) dumped to latest-<app>.dump.
|
||||||
if docker compose config --services | grep -qx train-server; then
|
services=$(docker compose config --services)
|
||||||
train=yes
|
apps=
|
||||||
fi
|
for app in train todo; do
|
||||||
|
if printf '%s\n' "$services" | grep -qx "$app-server"; then
|
||||||
|
apps="$apps $app"
|
||||||
|
fi
|
||||||
|
done
|
||||||
(
|
(
|
||||||
umask 077
|
umask 077
|
||||||
# Never during a deploy (redeploy.sh holds this lock): its migrations would wait
|
# Never during a deploy (redeploy.sh holds this lock): its migrations would wait
|
||||||
|
|
@ -48,14 +53,17 @@ fi
|
||||||
exec 9>"$HOME/.redeploy.lock"
|
exec 9>"$HOME/.redeploy.lock"
|
||||||
flock 9
|
flock 9
|
||||||
docker compose exec -T postgres pg_dump -U halkyon_learn -Fc -Z 0 halkyon_learn >"$dump.partial"
|
docker compose exec -T postgres pg_dump -U halkyon_learn -Fc -Z 0 halkyon_learn >"$dump.partial"
|
||||||
if [ -n "$train" ]; then
|
for app in $apps; do
|
||||||
docker compose exec -T postgres pg_dump -U halkyon_learn -Fc -Z 0 halkyon_train >"$train_dump.partial"
|
docker compose exec -T postgres pg_dump -U halkyon_learn -Fc -Z 0 "halkyon_$app" \
|
||||||
fi
|
>"$HOME/backups/latest-$app.dump.partial"
|
||||||
|
done
|
||||||
)
|
)
|
||||||
mv "$dump.partial" "$dump"
|
mv "$dump.partial" "$dump"
|
||||||
if [ -n "$train" ]; then
|
set -- "$dump"
|
||||||
mv "$train_dump.partial" "$train_dump"
|
for app in $apps; do
|
||||||
fi
|
mv "$HOME/backups/latest-$app.dump.partial" "$HOME/backups/latest-$app.dump"
|
||||||
|
set -- "$@" "$HOME/backups/latest-$app.dump"
|
||||||
|
done
|
||||||
|
|
||||||
# Each repository as a git bundle, after the dump: a release the dump lacks is never
|
# Each repository as a git bundle, after the dump: a release the dump lacks is never
|
||||||
# numbered again (the server numbers past the highest tag), while one the bundles
|
# numbered again (the server numbers past the highest tag), while one the bundles
|
||||||
|
|
@ -79,13 +87,9 @@ releases="$HOME/backups/repositories.tar"
|
||||||
)
|
)
|
||||||
mv "$releases.partial" "$releases"
|
mv "$releases.partial" "$releases"
|
||||||
|
|
||||||
if [ -n "$train" ]; then
|
restic backup --quiet "$@" "$releases"
|
||||||
restic backup --quiet "$dump" "$train_dump" "$releases"
|
|
||||||
else
|
|
||||||
restic backup --quiet "$dump" "$releases"
|
|
||||||
fi
|
|
||||||
# By host alone: restic groups by the paths backed up too, so a path added (Train's
|
# By host alone: restic groups by the paths backed up too, so a path added (Train's
|
||||||
# dump) would leave the snapshots before it a group of their own, never forgotten, and
|
# dump, Todo's) would leave the snapshots before it a group of their own, never forgotten, and
|
||||||
# a deleted account in them for good.
|
# a deleted account in them for good.
|
||||||
restic forget --quiet --group-by host --keep-hourly 24 --keep-daily 7 --keep-weekly 4 --keep-monthly 3
|
restic forget --quiet --group-by host --keep-hourly 24 --keep-daily 7 --keep-weekly 4 --keep-monthly 3
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -211,6 +211,11 @@ docker compose exec -T postgres \
|
||||||
whose role is the cluster's and so survives the restore (on a new server, make it first:
|
whose role is the cluster's and so survives the restore (on a new server, make it first:
|
||||||
`sidecars/README.md`, *Train*, step 2).
|
`sidecars/README.md`, *Train*, step 2).
|
||||||
|
|
||||||
|
Todo's database, where `.env` switches Todo on (`compose.todo.yaml`, Halkyon Todo's
|
||||||
|
ADR 0033), is `latest-todo.dump` in the same snapshots, restored as Train's, with
|
||||||
|
`todo` for `train` (its role made first on a new server: `sidecars/README.md`, *Todo*,
|
||||||
|
step 1).
|
||||||
|
|
||||||
To check a backup without touching the instance, restore it into a database of its own
|
To check a backup without touching the instance, restore it into a database of its own
|
||||||
next to the real one, look, and drop it:
|
next to the real one, look, and drop it:
|
||||||
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue