This commit is contained in:
parent
a696ba59a4
commit
06c1d644d9
11 changed files with 194 additions and 10 deletions
11
AGENTS.md
11
AGENTS.md
|
|
@ -1288,6 +1288,10 @@ session with **no other memory of this project**. So:
|
|||
removes both `skatespots:last-location` and `skatespots:map-view` on
|
||||
account switch/logout and is exposed in Settings → Appearance; drafts are
|
||||
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
|
||||
shows `Coordinates *` with latitude/longitude inputs (`font-mono text-base`,
|
||||
`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 →
|
||||
HTML** because the host is self-hosted, not Codeberg.
|
||||
- **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`
|
||||
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.
|
||||
- **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:
|
||||
`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.
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ account is required.
|
|||
|
||||
- Forge: <https://git.skate-spots.org/skatespots/SkateSpots>
|
||||
- Latest release: <https://git.skate-spots.org/skatespots/SkateSpots/releases/latest>
|
||||
- Signed APK (1.7.86):
|
||||
<https://git.skate-spots.org/skatespots/SkateSpots/releases/download/v1.7.86/SkateSpots-1.7.86.apk>
|
||||
- Signed APK (1.7.87):
|
||||
<https://git.skate-spots.org/skatespots/SkateSpots/releases/download/v1.7.87/SkateSpots-1.7.87.apk>
|
||||
- SHA-256:
|
||||
`7890f29072f7df39241d99df94e9d17050752b22045579dee4190bbe005f0f0d`
|
||||
`269be76e9e7c67bd0d7167137c9a5767765a46c1f7e6459a126092ad9de5fe7e`
|
||||
|
||||
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
|
||||
|
|
|
|||
56
docs/QA_MATRIX.md
Normal file
56
docs/QA_MATRIX.md
Normal 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`.
|
||||
72
netlify/functions/latest-release.mjs
Normal file
72
netlify/functions/latest-release.mjs
Normal 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
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "skate-spots",
|
||||
"version": "1.7.86",
|
||||
"version": "1.7.87",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "skate-spots",
|
||||
"version": "1.7.86",
|
||||
"version": "1.7.87",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@bitcoinerlab/secp256k1": "^1.2.0",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "skate-spots",
|
||||
"private": true,
|
||||
"version": "1.7.86",
|
||||
"version": "1.7.87",
|
||||
"license": "AGPL-3.0-only",
|
||||
"type": "module",
|
||||
"homepage": "https://skate-spots.org",
|
||||
|
|
|
|||
31
public/download/download.js
Normal file
31
public/download/download.js
Normal 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.
|
||||
});
|
||||
})();
|
||||
|
|
@ -98,16 +98,17 @@
|
|||
|
||||
<a
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
</a>
|
||||
|
||||
<div class="meta">
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</main>
|
||||
<script src="/download/download.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
- 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.
|
||||
|
|
|
|||
|
|
@ -121,6 +121,9 @@ export function EscrowRequestDialog({
|
|||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-white">
|
||||
<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>
|
||||
<DialogDescription className="text-sm text-gray-400">
|
||||
{t('marketUi.escrowRequest.description', { sellerName })}
|
||||
|
|
|
|||
|
|
@ -242,6 +242,9 @@ export function ListingTradeActions({ item }: { item: MarketplaceItem }) {
|
|||
>
|
||||
<Lock className="h-4 w-4" />
|
||||
{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>
|
||||
)}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue