Alpha stage commit

This commit is contained in:
2026-07-02 21:04:05 +02:00
parent c03b183dfb
commit abed21be21
136 changed files with 15531 additions and 15 deletions

31
apps/mobile/README.md Normal file
View File

@@ -0,0 +1,31 @@
# Mobile App
Purpose: riding/offline companion.
MVP mobile scope can be a placeholder until the web planner and backend are stable.
Primary future screens:
- Trip list.
- Offline pack status.
- Riding mode: next water/food/sleep/repair/bailout.
- Map view.
- Report issue.
- Export/share route.
Offline-critical data:
- route geometry,
- stages,
- POIs,
- rules,
- exports,
- report queue.
## Implemented placeholder
The current package is not a full React Native shell. It provides compiled TypeScript contracts for the future mobile app:
- `OfflineReportQueue` for dated, location-bound reports captured without network access;
- `OfflinePackIndex` for downloaded route/stage/POI/rule/export manifests;
- storage abstraction that can later be backed by SQLite, AsyncStorage, or a native key-value store.

15
apps/mobile/package.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "@pikebacker/mobile",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "./dist/offlineStore.js",
"types": "./dist/offlineStore.d.ts",
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@pikebacker/shared": "0.1.0"
}
}

View File

@@ -0,0 +1,80 @@
import type { LocalReportCreate, OfflinePackManifest } from '@pikebacker/shared';
export type OfflineReportQueueItem = LocalReportCreate & {
offlineClientId: string;
queuedAt: string;
syncAttempts: number;
lastSyncError?: string;
};
export type OfflinePackRecord = {
manifest: OfflinePackManifest;
downloadedAt: string;
routeJsonPath?: string;
poiJsonPath?: string;
gpxPath?: string;
tilePackPath?: string;
};
export interface OfflineKeyValueStore {
getItem(key: string): Promise<string | null>;
setItem(key: string, value: string): Promise<void>;
}
const REPORT_QUEUE_KEY = 'pikebacker.mobile.offlineReportQueue';
const PACK_INDEX_KEY = 'pikebacker.mobile.offlinePackIndex';
export class OfflineReportQueue {
constructor(private readonly store: OfflineKeyValueStore) {}
async enqueue(report: LocalReportCreate, now = new Date().toISOString()): Promise<OfflineReportQueueItem> {
const item: OfflineReportQueueItem = {
...report,
offlineClientId: report.offlineClientId ?? crypto.randomUUID(),
queuedAt: now,
syncAttempts: 0
};
const queue = await this.list();
await this.store.setItem(REPORT_QUEUE_KEY, JSON.stringify([...queue, item]));
return item;
}
async list(): Promise<OfflineReportQueueItem[]> {
const raw = await this.store.getItem(REPORT_QUEUE_KEY);
return raw ? (JSON.parse(raw) as OfflineReportQueueItem[]) : [];
}
async recordSyncFailure(offlineClientId: string, error: string): Promise<void> {
const queue = await this.list();
const next = queue.map((item) =>
item.offlineClientId === offlineClientId
? { ...item, syncAttempts: item.syncAttempts + 1, lastSyncError: error }
: item
);
await this.store.setItem(REPORT_QUEUE_KEY, JSON.stringify(next));
}
async removeSynced(offlineClientId: string): Promise<void> {
const queue = await this.list();
await this.store.setItem(REPORT_QUEUE_KEY, JSON.stringify(queue.filter((item) => item.offlineClientId !== offlineClientId)));
}
}
export class OfflinePackIndex {
constructor(private readonly store: OfflineKeyValueStore) {}
async upsert(record: OfflinePackRecord): Promise<void> {
const records = await this.list();
const next = [...records.filter((item) => item.manifest.packId !== record.manifest.packId), record];
await this.store.setItem(PACK_INDEX_KEY, JSON.stringify(next));
}
async list(): Promise<OfflinePackRecord[]> {
const raw = await this.store.getItem(PACK_INDEX_KEY);
return raw ? (JSON.parse(raw) as OfflinePackRecord[]) : [];
}
async get(packId: string): Promise<OfflinePackRecord | undefined> {
return (await this.list()).find((record) => record.manifest.packId === packId);
}
}

11
apps/mobile/tsconfig.json Normal file
View File

@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src"]
}