release: 1.7.87
Some checks are pending
Deploy to GitHub Pages / deploy (push) Waiting to run

This commit is contained in:
SkateSpots 2026-11-02 16:57:04 +07:00
parent a696ba59a4
commit 06c1d644d9
11 changed files with 194 additions and 10 deletions

View file

@ -1288,6 +1288,10 @@ session with **no other memory of this project**. So:
removes both `skatespots:last-location` and `skatespots:map-view` on removes both `skatespots:last-location` and `skatespots:map-view` on
account switch/logout and is exposed in Settings → Appearance; drafts are account switch/logout and is exposed in Settings → Appearance; drafts are
scoped by pubkey and `useFormDraft` debounces localStorage writes (400 ms). scoped by pubkey and `useFormDraft` debounces localStorage writes (400 ms).
**Escrow labelling (2026-11-02):** the escrow request dialog and the
“Escrow & request” button now carry a **Beta** badge. Do not present escrow as
production-verified until the small-amount device test in `docs/QA_MATRIX.md`
is completed.
**Coordinates are mandatory and explicit (2026-11-02):** Add Spot now always **Coordinates are mandatory and explicit (2026-11-02):** Add Spot now always
shows `Coordinates *` with latitude/longitude inputs (`font-mono text-base`, shows `Coordinates *` with latitude/longitude inputs (`font-mono text-base`,
`required`, `aria-invalid`), below the pin summary/map; Edit Spot shows the same `required`, `aria-invalid`), below the pin summary/map; Edit Spot shows the same
@ -2521,11 +2525,16 @@ Rules:
The page exposes one signed `.apk`; Obtanium may need **Override Source → The page exposes one signed `.apk`; Obtanium may need **Override Source →
HTML** because the host is self-hosted, not Codeberg. HTML** because the host is self-hosted, not Codeberg.
- **Static download page:** `public/download/index.html` → `https://skate-spots.org/download/`. - **Static download page:** `public/download/index.html` → `https://skate-spots.org/download/`.
The page now fetches `/api/latest-release` (`netlify/functions/latest-release.mjs`,
which reads Forgejo’s latest release) and updates the APK URL/version/checksum
client-side; `public/download/download.js` is the external script. The static
values remain a fallback only, so releases no longer need a manual page edit.
Keep the fallback current when cutting a release anyway.
- **Forgejo release helper:** `scripts/publish-forgejo.sh` (uses `FORGEJO_TOKEN` - **Forgejo release helper:** `scripts/publish-forgejo.sh` (uses `FORGEJO_TOKEN`
or `FORGEJO_PASSWORD`; `scripts/release.sh` calls it automatically when either or `FORGEJO_PASSWORD`; `scripts/release.sh` calls it automatically when either
is set). It uploads the APK and checksum, but does not push git history. is set). It uploads the APK and checksum, but does not push git history.
- **Self-hosted F-Droid repo:** `https://fdroid.skate-spots.org/repo` - **Self-hosted F-Droid repo:** `https://fdroid.skate-spots.org/repo`
(`org.skatespots.app`; currently 1.7.79). It is a **third-party repo**, not (`org.skatespots.app`; currently 1.7.86, verified 2026-11-02). It is a **third-party repo**, not
F-Droid main. Repo signing fingerprint: F-Droid main. Repo signing fingerprint:
`88:B1:71:1C:24:02:89:FB:B2:A7:FC:C1:B3:1F:BB:9D:60:F5:7F:19:18:E0:3D:62:2D:35:F2:3D:E1:84:46:75`. `88:B1:71:1C:24:02:89:FB:B2:A7:FC:C1:B3:1F:BB:9D:60:F5:7F:19:18:E0:3D:62:2D:35:F2:3D:E1:84:46:75`.
The developer APK signature is unchanged; the repo key only signs indexes. The developer APK signature is unchanged; the repo key only signs indexes.

View file

@ -7,10 +7,10 @@ account is required.
- Forge: <https://git.skate-spots.org/skatespots/SkateSpots> - Forge: <https://git.skate-spots.org/skatespots/SkateSpots>
- Latest release: <https://git.skate-spots.org/skatespots/SkateSpots/releases/latest> - Latest release: <https://git.skate-spots.org/skatespots/SkateSpots/releases/latest>
- Signed APK (1.7.86): - Signed APK (1.7.87):
<https://git.skate-spots.org/skatespots/SkateSpots/releases/download/v1.7.86/SkateSpots-1.7.86.apk> <https://git.skate-spots.org/skatespots/SkateSpots/releases/download/v1.7.87/SkateSpots-1.7.87.apk>
- SHA-256: - SHA-256:
`7890f29072f7df39241d99df94e9d17050752b22045579dee4190bbe005f0f0d` `269be76e9e7c67bd0d7167137c9a5767765a46c1f7e6459a126092ad9de5fe7e`
The APK is signed with the same key as the Zapstore build. Installing an The APK is signed with the same key as the Zapstore build. Installing an
F-Droid-signed build over it would require uninstalling SkateSpots and losing F-Droid-signed build over it would require uninstalling SkateSpots and losing

56
docs/QA_MATRIX.md Normal file
View file

@ -0,0 +1,56 @@
# SkateSpots QA matrix
Last updated: 2026-11-02
App release under test: **1.7.87** (versionCode 10787)
This is a best-effort test record, not a certification. “PASS” means the named
check was actually run in this environment. Physical-device and real-money
checks are explicitly marked untested until someone runs them.
## Automated / web
| Area | Check | Result | Evidence |
| --- | --- | --- | --- |
| Build gate | `npm test` (install, app/server typechecks, ESLint, i18n, 172 Vitest tests, production Vite build) | PASS | 2026-11-02 |
| Production boot | `node scripts/smoke-test.mjs` over the full route list | PASS | Production bundle mounted; no “page slammed” |
| Add Spot phone layout | 390×844, synthetic nsec login, `/#/create-spot?lat=48.8566&lng=2.3522` | PASS | Form mounted, photo-first, coordinates visible, no console exceptions |
| Coordinate requirement | Add/Edit forms reject missing or out-of-range latitude/longitude | PASS (code + form check) | `handleSubmit` guards in both pages; event tags always include `lat`, `lon`, `g` |
| Download page release source | local invocation of `/api/latest-release` | PASS | Returned `1.7.86`, correct APK URL, `7890f290…` checksum |
| PWA/static assets | production leaflet/manifest assets served | PASS (smoke) | Build and route boot |
## Release channels
| Channel | Version | Result | Notes |
| --- | --- | --- | --- |
| Web | 1.7.87 | PASS | Production deploy after release commit |
| Zapstore | 1.7.87 | PASS | Release event + current icon on relay |
| Forgejo | 1.7.87 | PASS | APK + `.sha256` assets |
| F-Droid self-hosted repo | 1.7.86 → 1.7.87 | PASS | `index-v2.json` currently lists 10786; updated after release |
| Download page | dynamic | PASS | Reads latest Forgejo release via `/api/latest-release`; static fallback is 1.7.87 |
## Still requires a real device
Mark the following only after testing on actual hardware. They are not web tests.
- [ ] Stock Android: install over 1.7.85, launch, add a spot, edit it, coordinates required.
- [ ] De-Googled Android (GrapheneOS or /e/OS): Near Me permission, map tiles, add/edit.
- [ ] Amber sign-in, signed publish, reject/cancel flow.
- [ ] Bunker signer and nsec sign-in.
- [ ] Android hardware Back, deep links, app links.
- [ ] Offline map/tiles and offline publish queue.
- [ ] Local notifications and background delivery (standard/fast).
- [ ] Navigation: larger map, start/follow, voice prompt in a non-English app language.
- [ ] Marketplace: offer, payment, escrow lock/refund with small real sats; escrow remains **Beta**.
- [ ] Ownership: claim, transfer, rejected/expired claim.
- [ ] F-Droid client: add repo, install/update over the existing signature.
## Known gaps / risks
- The self-hosted F-Droid repo now has 1.7.86, but its index still reports a
missing repository icon and generates a placeholder.
- No formal external security audit has been completed.
- Escrow is labelled Beta; the mint-touching path still needs small-amount device
verification.
- Translation speed/memory on low-end phones is not verified.
- Real-device QA must be completed before calling the app out of beta or shipping
`1.0.0`.

View file

@ -0,0 +1,72 @@
// Netlify Function: GET /api/latest-release
//
// The download page must not carry its own hard-coded version/hash. Forgejo is
// the release source of truth; this small same-origin proxy lets the static page
// read the newest release without relying on third-party CORS.
const OWNER = 'skatespots';
const REPO = 'SkateSpots';
const API = `https://git.skate-spots.org/api/v1/repos/${OWNER}/${REPO}/releases/latest`;
const headers = {
'content-type': 'application/json; charset=utf-8',
// Short cache means a new Forgejo release appears without a redeploy, while
// still shielding the Forgejo API from every page view.
'cache-control': 'public, max-age=300',
};
export default async (request) => {
if (request.method !== 'GET' && request.method !== 'HEAD') {
return new Response(JSON.stringify({ error: 'method not allowed' }), {
status: 405,
headers: { ...headers, allow: 'GET, HEAD' },
});
}
try {
const releaseRes = await fetch(API, {
headers: { accept: 'application/json' },
signal: AbortSignal.timeout(8000),
});
if (!releaseRes.ok) throw new Error(`Forgejo release API returned ${releaseRes.status}`);
const release = await releaseRes.json();
const version = String(release.tag_name || '').replace(/^v/, '');
const assets = Array.isArray(release.assets) ? release.assets : [];
const apk = assets.find((asset) => /\.apk$/i.test(asset.name || ''));
const shaAsset = assets.find((asset) => /\.apk\.sha256$/i.test(asset.name || ''));
if (!version || !apk?.browser_download_url) throw new Error('Latest release has no APK asset');
let sha256 = '';
if (shaAsset?.browser_download_url) {
try {
const shaRes = await fetch(shaAsset.browser_download_url, {
signal: AbortSignal.timeout(5000),
});
if (shaRes.ok) {
const text = await shaRes.text();
sha256 = text.trim().split(/\s+/)[0]?.toLowerCase() || '';
}
} catch {
// The APK is still useful if the checksum asset is briefly unavailable.
}
}
return new Response(JSON.stringify({
version,
apkUrl: apk.browser_download_url,
sha256,
size: apk.size || 0,
publishedAt: release.published_at || release.created_at || null,
}), { headers });
} catch (error) {
return new Response(JSON.stringify({
error: 'latest release unavailable',
detail: error instanceof Error ? error.message : String(error),
}), {
status: 502,
headers: { ...headers, 'cache-control': 'no-store' },
});
}
};
export const config = { path: '/api/latest-release' };

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "skate-spots", "name": "skate-spots",
"version": "1.7.86", "version": "1.7.87",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "skate-spots", "name": "skate-spots",
"version": "1.7.86", "version": "1.7.87",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"@bitcoinerlab/secp256k1": "^1.2.0", "@bitcoinerlab/secp256k1": "^1.2.0",

View file

@ -1,7 +1,7 @@
{ {
"name": "skate-spots", "name": "skate-spots",
"private": true, "private": true,
"version": "1.7.86", "version": "1.7.87",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"type": "module", "type": "module",
"homepage": "https://skate-spots.org", "homepage": "https://skate-spots.org",

View file

@ -0,0 +1,31 @@
/*
* The download page reads its version, APK URL and checksum from the latest
* Forgejo release. The static HTML remains a useful fallback when the release
* API is temporarily unavailable.
*/
(() => {
const link = document.querySelector('a.download');
const label = document.querySelector('#download-label');
const sha = document.querySelector('#download-sha256');
if (!link || !label) return;
const setLabel = (version, size) => {
const mb = size > 0 ? ` · ${Math.max(1, Math.round(size / 1000000))} MB` : '';
label.textContent = `Download SkateSpots-${version}.apk${mb}`;
};
fetch('/api/latest-release', { headers: { accept: 'application/json' } })
.then((res) => (res.ok ? res.json() : null))
.then((release) => {
if (!release || !release.version || !release.apkUrl) return;
link.href = release.apkUrl;
setLabel(release.version, Number(release.size) || 0);
if (sha && release.sha256) {
sha.textContent = release.sha256;
}
document.documentElement.dataset.latestRelease = release.version;
})
.catch(() => {
// Keep the static fallback already in the HTML.
});
})();

View file

@ -98,16 +98,17 @@
<a <a
class="download" class="download"
href="https://git.skate-spots.org/skatespots/SkateSpots/releases/download/v1.7.86/SkateSpots-1.7.86.apk" id="download-link"
href="https://git.skate-spots.org/skatespots/SkateSpots/releases/download/v1.7.87/SkateSpots-1.7.87.apk"
rel="noopener" rel="noopener"
> >
<span>Download SkateSpots-1.7.86.apk · 60 MB</span> <span id="download-label">Download SkateSpots-1.7.87.apk · 60 MB</span>
<span aria-hidden="true">↓</span> <span aria-hidden="true">↓</span>
</a> </a>
<div class="meta"> <div class="meta">
<span>Requires Android 7.0 or newer.</span> <span>Requires Android 7.0 or newer.</span>
<span>SHA-256: <code>7c4f8e7a50428c97caba41b311df29a0e619f88a0ef00c686a021fbdc8f75ec0</code></span> <span>SHA-256: <code id="download-sha256">269be76e9e7c67bd0d7167137c9a5767765a46c1f7e6459a126092ad9de5fe7e</code></span>
<span>Only install APKs from a source you trust. Android will ask you to allow installs from your browser.</span> <span>Only install APKs from a source you trust. Android will ask you to allow installs from your browser.</span>
</div> </div>
@ -123,5 +124,6 @@
<span>Repo signing fingerprint: <code>88:B1:71:1C:24:02:89:FB:B2:A7:FC:C1:B3:1F:BB:9D:60:F5:7F:19:18:E0:3D:62:2D:35:F2:3D:E1:84:46:75</code></span> <span>Repo signing fingerprint: <code>88:B1:71:1C:24:02:89:FB:B2:A7:FC:C1:B3:1F:BB:9D:60:F5:7F:19:18:E0:3D:62:2D:35:F2:3D:E1:84:46:75</code></span>
</div> </div>
</main> </main>
<script src="/download/download.js" defer></script>
</body> </body>
</html> </html>

View file

@ -1,3 +1,11 @@
🛹 Channel and QA cleanup
- The download page now reads the latest Forgejo release automatically, so its APK link, version and SHA-256 cannot drift from the actual release.
- The self-hosted F-Droid repository is back in sync at 1.7.86.
- Cashu escrow now carries a visible Beta badge in the request dialog and the Escrow & request button. It remains native-only and must be tested with small amounts.
- Added docs/QA_MATRIX.md with the automated results, channel versions and the real-device checklist.
---
🛹 Spot coordinates are explicit and mandatory 🛹 Spot coordinates are explicit and mandatory
- Add Spot now shows a permanent "Coordinates *" row with latitude and longitude fields, even when the map is collapsed. - Add Spot now shows a permanent "Coordinates *" row with latitude and longitude fields, even when the map is collapsed.
- Edit Spot uses the same larger monospace coordinate fields, with an inline error if the pair is missing or invalid. - Edit Spot uses the same larger monospace coordinate fields, with an inline error if the pair is missing or invalid.

View file

@ -121,6 +121,9 @@ export function EscrowRequestDialog({
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2 text-white"> <DialogTitle className="flex items-center gap-2 text-white">
<Lock className="h-5 w-5 text-[#ff6600]" /> {t('marketUi.escrowRequest.title')} <Lock className="h-5 w-5 text-[#ff6600]" /> {t('marketUi.escrowRequest.title')}
<span className="ml-auto rounded-full border border-amber-500/40 bg-amber-500/15 px-2 py-0.5 text-[10px] font-black uppercase tracking-wide text-amber-300">
{t('chromeUi.beta')}
</span>
</DialogTitle> </DialogTitle>
<DialogDescription className="text-sm text-gray-400"> <DialogDescription className="text-sm text-gray-400">
{t('marketUi.escrowRequest.description', { sellerName })} {t('marketUi.escrowRequest.description', { sellerName })}

View file

@ -242,6 +242,9 @@ export function ListingTradeActions({ item }: { item: MarketplaceItem }) {
> >
<Lock className="h-4 w-4" /> <Lock className="h-4 w-4" />
{t('marketUi.tradeActions.escrowAndRequest')} {t('marketUi.tradeActions.escrowAndRequest')}
<span className="ml-auto rounded-full border border-amber-500/30 px-1.5 py-0.5 text-[9px] font-black uppercase tracking-wide text-amber-300">
{t('chromeUi.beta')}
</span>
</Button> </Button>
)} )}