Miler rider app: surface system, visible design language, backend lifecycle

Design system
- MilerSurface ladder (canvas → working → raised → floating) with MilerPanel
  as layer 1; canvas moved to #DEE3EA so white separates at 1.290:1.
- Visible vocabulary applied across Home, Deliveries, Activity, Account and
  the sheets: hero heads (tabular numeral + small caption, clamped at 1.3x),
  canvas wells for anything that opens, small filled tags for shelf labels,
  demoted placeholders. Recorded in DESIGN_SYSTEM.md §6.
- One icon family: 222 Material glyphs migrated to Lucide; none left outside
  lib/xpress.
- Colour semantics corrected: amber only for what is genuinely owed, brand red
  reserved for the live stop, disabled primaries go neutral rather than pale.

Data and lifecycle
- lib/data/lifecycle.dart reads mutations for what they prove; route_order.dart
  makes admin sequence the single ordering authority; service_day.dart, and
  stop_area.dart rewritten against live Coimbatore addresses (digit-token
  stripping, city stoplist, street suffixes, stammer collapse).
- countLabel states the load once, in bags.

Testing
- 1440 tests passing; golden shot harnesses for Home, Deliveries, Activity,
  sheets and verify, with test/failures/ now gitignored (diff debris).
- New pins: home_gutter_test, stop_area_test, plus updated structural bounds.

Note: this commit also carries pre-existing working-tree deletions that were
present before this work (API_SPEC.md, README.md, demo test fixtures).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 05:40:35 +05:30
parent c8350563d9
commit d7348e253f
387 changed files with 80693 additions and 12272 deletions

View File

@@ -0,0 +1,165 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/data/proof_store.dart';
import 'package:miler/data/service_profile.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// PROOF OF DELIVERY
///
/// A photo taken at a door is evidence, and evidence that disappears is worse
/// than none — it makes the rider believe he has cover he does not have.
/// These pin the two properties that make it real: it outlives the OS cache
/// `ImagePicker` hands back, and it belongs to the session that took it.
/// ─────────────────────────────────────────────────────────────────────────
void main() {
late Directory docs;
setUp(() async {
ServiceProfile.setActive(ServiceProfile.milkMan);
SharedPreferences.setMockInitialValues({'userid': 38});
// path_provider has no host under the test binding.
docs = await Directory.systemTemp.createTemp('proof_docs');
TestWidgetsFlutterBinding.ensureInitialized();
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('plugins.flutter.io/path_provider'),
(call) async => docs.path,
);
});
tearDown(() async {
ServiceProfile.setActive(ServiceProfile.parcel);
if (await docs.exists()) await docs.delete(recursive: true);
});
/// A real 1×1 PNG. The store tests never decode it, but the widget test
/// does: `Image.file` on 64 bytes of nonsense never completes its frame, so
/// `pumpAndSettle` waits for a decode that will never land and the test
/// hangs for the full ten minutes rather than failing.
const List<int> onePixelPng = [
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, //
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4,
0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41,
0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00,
0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00,
0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE,
0x42, 0x60, 0x82,
];
/// Stands in for what the camera hands back: a file in a volatile place.
Future<File> cameraShot(String name) async {
final cache = await Directory.systemTemp.createTemp('picker_cache');
final f = File('${cache.path}/$name.jpg');
await f.writeAsBytes(onePixelPng);
return f;
}
group('the store', () {
test('the photo is copied out of the camera cache', () async {
final shot = await cameraShot('a');
final saved = await ProofStore.save('ORDER-1', shot.path);
expect(saved, isNotNull);
expect(
saved,
isNot(shot.path),
reason:
'ImagePicker returns an OS cache path the system reclaims — '
'a record pointing at it loses its evidence',
);
expect(File(saved!).existsSync(), isTrue);
// The original can vanish and the proof survives, which is the point.
await shot.parent.delete(recursive: true);
expect(File(saved).existsSync(), isTrue);
expect(await ProofStore.pathFor('ORDER-1'), saved);
});
test('a retake replaces rather than accumulates', () async {
final first = await ProofStore.save(
'ORDER-1',
(await cameraShot('a')).path,
);
final second = await ProofStore.save(
'ORDER-1',
(await cameraShot('b')).path,
);
expect(second, first, reason: 'one stop, one proof');
expect(await ProofStore.pathFor('ORDER-1'), second);
});
test('a proof whose file has gone is not proof', () async {
final saved = await ProofStore.save(
'ORDER-1',
(await cameraShot('a')).path,
);
await File(saved!).delete();
expect(
await ProofStore.pathFor('ORDER-1'),
isNull,
reason: 'returning a dead path would draw a broken image as evidence',
);
});
test('nothing is invented from nothing', () async {
expect(await ProofStore.save('', 'whatever'), isNull);
expect(await ProofStore.save('ORDER-1', ''), isNull);
expect(await ProofStore.save('ORDER-1', '/no/such/file.jpg'), isNull);
expect(await ProofStore.pathFor('ORDER-NONE'), isNull);
});
test('a device path is never offered as a remote url', () async {
await ProofStore.save('ORDER-1', (await cameraShot('a')).path);
expect(
await ProofStore.remoteUrlFor('ORDER-1'),
'',
reason:
'the Miler API has no upload route; a filesystem path written '
"into the hub's photourl would look like evidence and resolve to "
'nothing',
);
});
});
group('ownership', () {
test("another rider cannot open this rider's doorstep photos", () async {
await ProofStore.save('ORDER-1', (await cameraShot('a')).path);
expect(await ProofStore.pathFor('ORDER-1'), isNotNull);
SharedPreferences.setMockInitialValues({'userid': 99});
expect(
await ProofStore.pathFor('ORDER-1'),
isNull,
reason: 'proof is scoped like every other record — see WorkScope',
);
});
test('the other line cannot either', () async {
await ProofStore.save('ORDER-1', (await cameraShot('a')).path);
ServiceProfile.setActive(ServiceProfile.parcel);
expect(await ProofStore.pathFor('ORDER-1'), isNull);
});
test('logout deletes the photos, not just the index', () async {
final saved = await ProofStore.save(
'ORDER-1',
(await cameraShot('a')).path,
);
await ProofStore.clearScope();
expect(await ProofStore.pathFor('ORDER-1'), isNull);
expect(
File(saved!).existsSync(),
isFalse,
reason: "a doorstep photo must not outlive the session that took it",
);
});
});
}