Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
113 commits
Select commit Hold shift + click to select a range
035c667
Filter panel: keep focus on the pressed filter
davd-gzl Sep 30, 2026
465523c
Toast: auto-dismiss again after a hovered toast is closed
davd-gzl Sep 30, 2026
4d0965d
Stat strip: stop remounting the counters on every store update
davd-gzl Sep 30, 2026
c433cc3
Guides: show the overview of the place on screen
davd-gzl Sep 30, 2026
0c800dc
Places: list every city you logged under Visited, Want and Favourites
davd-gzl Sep 30, 2026
e2b962b
Android: name the app Postcards in the launcher
davd-gzl Sep 30, 2026
22ac4e3
Dates: label months and weekdays in the zone they are built in
davd-gzl Sep 30, 2026
1c59f38
Android: sign every CI build with one key so updates install in place
davd-gzl Sep 30, 2026
6b8649a
Places: show the monument category as a filter chip and apply it at once
davd-gzl Sep 30, 2026
d72ffb7
Dates: show a month or year date at its own precision
davd-gzl Sep 30, 2026
c57e010
Schema: accept only real calendar dates
davd-gzl Sep 30, 2026
d22fe66
Places: offer on a world browse only the filters it applies
davd-gzl Sep 30, 2026
d7aab44
Trips: keep the day when a multi-stop trip is edited
davd-gzl Sep 30, 2026
91ff52a
Stats: count visited cities under the country scope
davd-gzl Sep 30, 2026
b86ebff
Boarding pass: date a scanned pass by the local year
davd-gzl Sep 30, 2026
bf4f50e
Photos: keep every photo when the visits table is rewritten
davd-gzl Sep 30, 2026
2ac40b5
Sync: keep the edits made while a sync is running
davd-gzl Sep 30, 2026
5bbcdec
Settings: stamp a saved map region with the local day
davd-gzl Sep 30, 2026
d87482a
Publish: date vague trip steps at their own precision in the reader
davd-gzl Sep 30, 2026
3f1894c
Stats: drill each city and monument tile into the places it counts
davd-gzl Sep 30, 2026
28337e5
Restore: keep restored records when the next sync runs
davd-gzl Sep 30, 2026
c476423
Sanitize: keep the zero-width joiners in user text
davd-gzl Sep 30, 2026
84083aa
Stats: open a record's own city, not a namesake
davd-gzl Sep 30, 2026
5b1d29a
Settings: link to the Android app on the web
davd-gzl Sep 30, 2026
dda82af
Undo: keep a trip or story undo after the next sync
davd-gzl Sep 30, 2026
1aa3a10
i18n: take the French singular for zero
davd-gzl Sep 30, 2026
54a8451
Storage: open in memory when IndexedDB cannot be read
davd-gzl Sep 30, 2026
87c6155
CSV: neutralize formulas in the CSV export, not in every stored string
davd-gzl Sep 30, 2026
cda189c
CSV: hold imported rows to the portable schema
davd-gzl Sep 30, 2026
a661a05
i18n: translate the stats notes, the shortcuts help and the data sources
davd-gzl Sep 30, 2026
87c930d
City page: save a note or caption left without a blur
davd-gzl Sep 30, 2026
df46d1f
Trips: count every stop of a multi-stop trip in airports and folders
davd-gzl Sep 30, 2026
df12db4
Search: let Escape close a suggestion list and nothing else
davd-gzl Sep 30, 2026
b790d25
Journal: stop keeping an untouched Edit as a draft
davd-gzl Sep 30, 2026
3521b0d
Maps: draw what changed before the map finished loading
davd-gzl Sep 30, 2026
1c8fa3e
Moved address: ask before restoring a handed-off file
davd-gzl Sep 30, 2026
61cf23f
Publish: keep month- and year-dated trips in a range that covers them
davd-gzl Sep 30, 2026
18b6c5d
Publish: include every stop of a multi-stop trip
davd-gzl Sep 30, 2026
7470620
Android: use the Postcards pin as the launcher icon
davd-gzl Sep 30, 2026
e2b332e
Trip composer: keep focus on a stop's arrow after moving it
davd-gzl Sep 30, 2026
41138e6
Publish: leave out photos from outside the chosen date range
davd-gzl Sep 30, 2026
42edca4
Sync: read a synced file over 1 MB
davd-gzl Sep 30, 2026
0158b51
Update banner: offer a dismissed new version again on the next poll
davd-gzl Sep 30, 2026
26acc5a
Shortcuts: stop a popup left on the hidden map from disabling them
davd-gzl Sep 30, 2026
b406dde
Moved address: clear the sync settings and places once they have moved
davd-gzl Sep 30, 2026
0f77756
Sync: wait for the stores to load before a sync
davd-gzl Sep 30, 2026
07d9042
Docs: name the sync token's scope and why it stays narrow
davd-gzl Sep 30, 2026
241a0ed
Docs: say what the CSP holds for the sync token and what it does not
davd-gzl Sep 30, 2026
047eac9
Sync: keep one visit per place across devices
davd-gzl Sep 30, 2026
bc7fb44
Publish: give non-Latin travel names their own folder
davd-gzl Sep 30, 2026
37f6475
Guides: link the Wikipedia fallback to Wikipedia
davd-gzl Sep 30, 2026
7847133
Export: list a multi-stop trip's stops and modes in the Markdown summary
davd-gzl Sep 30, 2026
3aa8047
Downloads: hand files to the share sheet in the native app
davd-gzl Sep 30, 2026
00b63bb
Sync: stop pushing an unchanged file on every run
davd-gzl Sep 30, 2026
8f93e12
Intro: keep focus where the user put it
davd-gzl Sep 30, 2026
94a56a2
Publish: keep locked travels unnamed and the repo's own pages untouched
davd-gzl Sep 30, 2026
e9837b6
Packs: give a pack the same place ids on every install
davd-gzl Sep 30, 2026
835a18a
Sync: order stamps in UTC and let a deletion win
davd-gzl Sep 30, 2026
94cd7c8
Import: keep one visit per visitId
davd-gzl Sep 30, 2026
9d4d804
Map: ask before the place card names a place to Wikipedia
davd-gzl Sep 30, 2026
564c418
Photos: keep an image stored twice under two captions
davd-gzl Sep 30, 2026
9923306
Storage: carry the pre-rename data over once
davd-gzl Sep 30, 2026
65da909
Guides: keep whole guides out of localStorage
davd-gzl Sep 30, 2026
198eeca
Storage: let an older tab go when a newer build opens
davd-gzl Sep 30, 2026
dbf83a8
Tests: expect imported text kept as typed in the hostile-import check
davd-gzl Sep 30, 2026
671170e
Tests: name the trip composer focus spec after what it checks
davd-gzl Sep 30, 2026
35fddf6
Merge branch 'fix-dates-publish' into audit-fixes
davd-gzl Sep 30, 2026
8920435
Merge branch 'fix-ui-state' into audit-fixes
davd-gzl Sep 30, 2026
b86b4b4
Merge branch 'fix-handoff-token' into audit-fixes
davd-gzl Sep 30, 2026
3e83e35
Merge branch 'apk-updates' into audit-fixes
davd-gzl Sep 30, 2026
2988429
Restore and move messages: count each kind in its own plural
davd-gzl Sep 30, 2026
0bd4388
Load failure: say so and offer a reload when app code fails to download
davd-gzl Sep 30, 2026
2ab0f90
E2E tooling: use Playwright's own Chromium, keep traces of a failed C…
davd-gzl Sep 30, 2026
c284261
E2E: wait for the map before acting, and stop racing each other under…
davd-gzl Sep 30, 2026
b9eb191
Full city list: record it only once it has downloaded
davd-gzl Sep 30, 2026
610229f
Offline: keep saved tiles and the city list, and save on the first visit
davd-gzl Sep 30, 2026
f39378e
E2E: seed places only through markVisited, which waits for the result…
davd-gzl Sep 30, 2026
220a202
Storage: stop two open tabs from writing over each other
davd-gzl Sep 30, 2026
2418cf1
Offline regions: stop a download when Offline mode turns on or Settin…
davd-gzl Sep 30, 2026
a95870b
E2E: a backup comes back whole, from the .json export and the .zip ar…
davd-gzl Sep 30, 2026
076dac9
Merge branch 'fix-platform-offline' into audit-fixes
davd-gzl Sep 30, 2026
1950b8c
Merge branch 'fix-sync-data-loss' into audit-fixes
davd-gzl Sep 30, 2026
3c1616b
Tests: keep two specs green once the branches meet
davd-gzl Sep 30, 2026
308d91d
i18n: drop 42 keys nothing reads
davd-gzl Sep 30, 2026
6b145c7
Styles: drop 21 class selectors no markup uses
davd-gzl Sep 30, 2026
be22d54
Store: drop helpers only their tests called
davd-gzl Sep 30, 2026
1eeb14a
Travel: drop trip and period helpers only their tests called
davd-gzl Sep 30, 2026
827a47c
Journal: drop storiesInFolder, which only its test called
davd-gzl Sep 30, 2026
36aa12e
Storage: make replaceAllPortable's stories argument required
davd-gzl Sep 30, 2026
87ed0c6
E2E: block service workers except where the installed app is under test
davd-gzl Sep 30, 2026
5c7b1c2
Places: resolve coordinates through the one coordsOf
davd-gzl Sep 30, 2026
927fa46
Photos: share one bytes-to-data-URL encoder
davd-gzl Sep 30, 2026
d80bf8f
Passport: draw the poster from the shared land geometry
davd-gzl Sep 30, 2026
ad4de55
Lists: page the country and journal lists with ListPager
davd-gzl Sep 30, 2026
1661db3
E2E: seed visited cities before the app opens where marking is not un…
davd-gzl Sep 30, 2026
7ea5577
Photos: store a percent-encoded photo's real bytes
davd-gzl Sep 30, 2026
413668a
Backup: save everything with a percent-encoded photo that is not UTF-8
davd-gzl Sep 30, 2026
75b49b6
Boot guard: a failed download of the app's entry shows the reload mes…
davd-gzl Sep 30, 2026
7ae8d86
AGENTS.md: name network-interface churn, not CPU load, as the e2e fla…
davd-gzl Sep 30, 2026
137fe53
E2E: the move test waits an app boot's budget for the screens that bo…
davd-gzl Sep 30, 2026
6838172
Import: refuse a photo whose base64 does not decode
davd-gzl Sep 30, 2026
43cd92e
Merge origin/main into audit-fixes
davd-gzl Sep 30, 2026
66f1254
Merge branch 'e2e-hardening' into audit-fixes
davd-gzl Sep 30, 2026
d5e23eb
Tests: expect French to count zero in the singular
davd-gzl Sep 30, 2026
d690cc1
Tests: let the service-worker spec run the worker the suite now blocks
davd-gzl Sep 30, 2026
78b75b6
Merge branch 'simplify-dead-code' into audit-fixes
davd-gzl Sep 30, 2026
7e2de17
Merge origin/main into audit-fixes
davd-gzl Sep 30, 2026
ebe053e
Tests: check the shortcuts help's postcard row is translated
davd-gzl Sep 30, 2026
3559d0a
Tests: check a moved trip stop keeps its date
davd-gzl Sep 30, 2026
d0bfe8f
E2E: seed visited cities in main's postcard and stop-date tests
davd-gzl Sep 30, 2026
7bfaa11
E2E: age the cache entries only once all four are recorded
davd-gzl Sep 30, 2026
0e6fc87
Merge origin/main into audit-fixes
davd-gzl Sep 30, 2026
71254c4
Tests: take a bidi mark, not a dash, as the tag that cleans away
davd-gzl Sep 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 63 additions & 18 deletions .github/workflows/android-apk.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# Build an installable Android debug APK and upload it as a downloadable
# artifact — so an APK can be obtained with NO local Android toolchain: push (or
# run this workflow manually), open the run, and download `postcards-debug-apk`.
# Build an installable Android APK and upload it as a downloadable artifact — so
# an APK can be obtained with NO local Android toolchain: push (or run this
# workflow manually), open the run, and download `postcards-apk`. On main it is
# also published at a permanent download URL (the release step below).
# With the signing secrets set (docs/NATIVE-BUILDS.md) the APK is a release build
# signed with one stable key, so each one installs as an update over the last;
# without them (forks) it is the debug build, signed with a throwaway key.
# Local headless build: `pnpm --filter postcards apk:debug` (see docs/NATIVE-BUILDS.md).
name: Android APK (debug)
name: Android APK

on:
push:
Expand Down Expand Up @@ -46,31 +50,72 @@ jobs:
- name: Sync the Android project
# Copies the fresh dist/ into the committed android/ project + updates plugins.
run: pnpm --filter postcards exec cap sync android
- name: Assemble the debug APK
- name: Assemble the APK
id: apk
working-directory: apps/postcards/android
env:
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
POSTCARDS_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
POSTCARDS_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
POSTCARDS_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
chmod +x ./gradlew
./gradlew --no-daemon assembleDebug
# The run number only grows, so every APK has a higher versionCode than
# the one it replaces.
version="$(node -p "require('../package.json').version")-${GITHUB_SHA::7}"
props=(-PversionCode="$GITHUB_RUN_NUMBER" -PversionName="$version")
if [ -n "$KEYSTORE_BASE64" ]; then
export POSTCARDS_KEYSTORE_FILE="$RUNNER_TEMP/postcards.jks"
printf '%s' "$KEYSTORE_BASE64" | base64 -d > "$POSTCARDS_KEYSTORE_FILE"
./gradlew --no-daemon assembleRelease "${props[@]}"
cp app/build/outputs/apk/release/app-release.apk "$RUNNER_TEMP/postcards.apk"
echo "signing=release" >> "$GITHUB_OUTPUT"
else
./gradlew --no-daemon assembleDebug "${props[@]}"
cp app/build/outputs/apk/debug/app-debug.apk "$RUNNER_TEMP/postcards.apk"
echo "signing=debug" >> "$GITHUB_OUTPUT"
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Upload the APK
uses: actions/upload-artifact@v4
with:
name: postcards-debug-apk
path: apps/postcards/android/app/build/outputs/apk/debug/app-debug.apk
name: postcards-apk
path: ${{ runner.temp }}/postcards.apk
if-no-files-found: error

# Also publish it to the Releases page so it's downloadable without opening
# the Actions run. One rolling prerelease ("debug-latest") holds the newest
# build: delete + recreate so its tag always points at the current commit.
# the Actions run. One rolling release ("android-latest") holds the newest
# build: delete + recreate so its tag always points at the current commit,
# while the download URL stays the same:
# https://github.com/<owner>/<repo>/releases/download/android-latest/postcards.apk
# main only — feature-branch pushes still get the run artifact above.
- name: Publish the APK to the rolling debug release
- name: Publish the APK to the rolling release
if: github.ref == 'refs/heads/main'
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.apk.outputs.version }}
SIGNING: ${{ steps.apk.outputs.signing }}
run: |
gh release delete debug-latest --yes --cleanup-tag 2>/dev/null || true
gh release create debug-latest \
"apps/postcards/android/app/build/outputs/apk/debug/app-debug.apk#postcards-debug.apk" \
--title "Latest debug APK" \
--notes "Automated debug build from ${{ github.sha }}. Debug/unsigned — not for production." \
--prerelease \
--target "${{ github.sha }}"
if [ "$SIGNING" = release ]; then
updates="Each new version installs over the previous one and keeps your places."
else
updates="This is a debug build signed with a one-off key: to install a newer one, export your data (Settings, Your data), uninstall this one, install the new APK and import the file."
fi
cat > "$RUNNER_TEMP/notes.md" <<EOF
Postcards for Android, version $VERSION, built from $GITHUB_SHA.

**Download:** [postcards.apk](https://github.com/$GITHUB_REPOSITORY/releases/download/android-latest/postcards.apk) — this link always points to the newest build.

**Install**
1. Open the link above on your Android phone (Android 5.1 or newer) and download the APK.
2. Open the downloaded file. If Android asks, allow your browser or file manager to *install unknown apps*, then go back and tap **Install**.
3. $updates

Your places stay on your phone: no account, no server, no tracking.
EOF
gh release delete android-latest --yes --cleanup-tag 2>/dev/null || true
gh release create android-latest "$RUNNER_TEMP/postcards.apk" \
--title "Postcards for Android" \
--notes-file "$RUNNER_TEMP/notes.md" \
--latest \
--target "$GITHUB_SHA"
7 changes: 7 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,10 @@ jobs:
run: pnpm --filter postcards exec playwright install --with-deps chromium
- name: E2E tests (incl. a11y gate)
run: pnpm --filter postcards test:e2e
- name: Upload e2e traces
if: failure()
uses: actions/upload-artifact@v4
with:
name: e2e-test-results
path: apps/postcards/test-results/
retention-days: 14
20 changes: 14 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,12 @@ observed working:
a key added to `en.ts` must be added to `fr.ts` and `ko.ts` (and removing one
means removing it from all three), or tsc fails.
3. `npx vitest run` — full unit suite green.
4. `npx playwright test` — e2e green. `smoke`/`photo`/`countryscope`/`import-csv`
can flake under full-suite CPU load; re-run the file in isolation to confirm,
CI retry absorbs it.
5. For any UI change, **screenshot and eyeball it** (Chromium is at
`/opt/pw-browsers/chromium-1194/chrome-linux/chrome`).
4. `npx playwright test` — e2e green. On a host whose network interfaces churn
(many docker containers, for one), Chromium aborts in-flight requests with
`net::ERR_NETWORK_CHANGED`, and a spec can find the app never booted (the
reload message or a blank page); re-run the file, and CI's retry absorbs it.
5. For any UI change, **screenshot and eyeball it** (Playwright's Chromium,
from `npx playwright install chromium` in `apps/postcards`).
6. Commit with a clear message. **Never** put the model identifier in commits,
PRs, or code — chat only.
7. Deploy: push HEAD to the feature branch **and** fast-forward the deployed
Expand Down Expand Up @@ -70,7 +71,14 @@ Cmd/Ctrl+Shift+R) before re-implementing.
user action. Optional egress (map tiles, guides, photos) is opt-in and off by
default; **Offline mode** is the master switch that forces zero egress.
- The GitHub sync token stays on-device: never in exports, published sites, or
logs. The `connect-src` CSP in `index.html` is the backstop.
logs. The `connect-src` CSP in `index.html` keeps a script in the page from
sending it to any host outside its list; it does not stop a request to
`api.github.com`, which the list allows, carrying the token to a repository
of the script's choosing. The token's safety also depends on the app being
the only app served from its origin, since every page of an origin shares its
localStorage: `offware-apps.github.io` currently also serves another app, and
a custom domain or another dedicated origin removes that dependency. Hence
the fine-grained token, one repository, Contents read and write only.
- Imports are validated + sanitized, never executed. One portable JSON file.
- WCAG 2.1 AA, keyboard-first; every interactive control carries a `title`
(plus `aria-label` when icon-only).
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

-> offware-apps.github.io/Postcards/

-> Android: [download postcards.apk](https://github.com/offware-apps/Postcards/releases/download/android-latest/postcards.apk) (newest build; open it on the phone and allow installing unknown apps)

<div align="center">

# Postcards
Expand Down
24 changes: 22 additions & 2 deletions apps/postcards/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,37 @@ android {
applicationId "coop.samourai.postcards"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
// CI passes -PversionCode (the run number, so each build installs as an
// update over the last) and -PversionName (app version + commit).
versionCode((project.findProperty("versionCode") ?: "1") as Integer)
versionName(project.findProperty("versionName") ?: "1.0")
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
// One stable key signs every release build, so Android accepts each new APK
// as an update and keeps the app's data. CI writes the keystore from
// repository secrets (docs/NATIVE-BUILDS.md); without them there is no
// release signing and CI builds the debug APK instead.
def keystoreFile = System.getenv("POSTCARDS_KEYSTORE_FILE")
signingConfigs {
if (keystoreFile) {
release {
storeFile file(keystoreFile)
storePassword System.getenv("POSTCARDS_KEYSTORE_PASSWORD")
keyAlias System.getenv("POSTCARDS_KEY_ALIAS")
keyPassword System.getenv("POSTCARDS_KEY_PASSWORD")
}
}
}
buildTypes {
release {
if (keystoreFile) {
signingConfig signingConfigs.release
}
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
<color name="ic_launcher_background">#4338CA</color>
</resources>
4 changes: 2 additions & 2 deletions apps/postcards/android/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">Place\'Been</string>
<string name="title_activity_main">Place\'Been</string>
<string name="app_name">Postcards</string>
<string name="title_activity_main">Postcards</string>
<string name="package_name">coop.samourai.postcards</string>
<string name="custom_url_scheme">coop.samourai.postcards</string>
</resources>
30 changes: 20 additions & 10 deletions apps/postcards/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,29 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="color-scheme" content="dark light" />
<!-- Content-Security-Policy — the ONE control the "token stays on device"
promise rests on. The GitHub sync token lives in this origin's
localStorage; the crucial defence is a strict `connect-src` (+ img-src,
object-src 'none', form-action 'none'): an injected or dependency-borne
script cannot POST the token to any host but the few the app legitimately
talks to (api.github.com + GitHub-raw for community data packs, OSM tiles,
Wikimedia — never an arbitrary host), nor smuggle it out via an <img>,
form or <object>. Delivered as
<!-- Content-Security-Policy. The GitHub sync token lives in this origin's
localStorage. A strict `connect-src` (+ img-src, object-src 'none',
form-action 'none') holds every request a script in this page makes to
the few hosts the app talks to (api.github.com + GitHub-raw for
community data packs, OSM tiles, Wikimedia — never an arbitrary host),
and none leaves via an <img>, form or <object>. What it does not hold:
api.github.com is on the list and serves every repository, so a script
running here can still send the token there, to a repository of its
choosing. Nor does it reach beyond this document: every page served
from the same origin shares its localStorage, so the token is only as
contained as the origin is dedicated to this app. offware-apps.github.io
currently also serves another app; a custom domain or another dedicated
origin removes that dependency, and a token scoped to the one sync
repository (Contents read and write only) bounds it meanwhile.
Delivered as
a meta because GitHub Pages sends no headers and the Capacitor WebView
never sees them for the bundled index.html — the only portable channel.
script-src keeps 'unsafe-inline' ONLY because the Publish PREVIEW renders
the generated reader in a `sandbox="allow-scripts"` srcdoc iframe, which
inherits this policy and is all inline script; that frame has an opaque
origin and cannot read this origin's token, and static hosting offers no
per-request nonce, so 'unsafe-inline' here does not widen the credential's
blast radius — the connect-src backstop still contains any exfiltration.
per-request nonce, so 'unsafe-inline' here does not widen what can
reach the token — that frame's requests keep to the same connect-src.
NOTE: `frame-ancestors`/`sandbox` are ignored in a meta CSP, so there is
no clickjacking protection on a header-less static host (a non-issue on
native). connect-src omits self-hosted GitHub Enterprise apiBase hosts;
Expand Down Expand Up @@ -61,6 +68,9 @@
<meta name="theme-color" content="#4338ca" />
<meta name="description" content="Remember the places you've been — private, offline, in a file you own." />
<title>Postcards</title>
<!-- Before the module entry, so a failed download of the app's own code shows
a reload message instead of a blank page (see the file). -->
<script src="%BASE_URL%boot-guard.js"></script>
<!-- Apply the saved colour-theme choice before first paint, so an explicit
Light/Dark selection never flashes the wrong palette while the app
bundle loads. First-party, inert: it only reads our own localStorage key
Expand Down
16 changes: 5 additions & 11 deletions apps/postcards/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { defineConfig, devices } from "@playwright/test";
import { existsSync } from "node:fs";

// Uses the preinstalled Chromium in this environment when present; elsewhere
// (CI) Playwright's own installed browser is used.
// Run with: pnpm --filter postcards test:e2e
const LOCAL_CHROMIUM = "/opt/pw-browsers/chromium-1194/chrome-linux/chrome";

export default defineConfig({
testDir: "./tests/e2e",
Expand All @@ -17,6 +13,10 @@ export default defineConfig({
use: {
baseURL: "http://localhost:4173",
trace: "on-first-retry",
// Every test starts a fresh context, so an allowed service worker would
// install and precache the whole app in each one. The specs that exercise
// the installed app opt back in with test.use({ serviceWorkers: "allow" }).
serviceWorkers: "block",
// Seed the "intro seen" flag so the first-run welcome modal never auto-opens
// over the app during tests (it would block the very first interaction). The
// real first-run intro is exercised by users, not the suite.
Expand All @@ -43,13 +43,7 @@ export default defineConfig({
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
// Use the environment's preinstalled Chromium instead of downloading.
...(existsSync(LOCAL_CHROMIUM)
? { launchOptions: { executablePath: LOCAL_CHROMIUM } }
: {}),
},
use: { ...devices["Desktop Chrome"] },
},
],
});
Loading
Loading