Skip to content

Commit 1a6ef28

Browse files
i99devclaude
andcommitted
Initial commit: iLINK
A standalone Flutter dashboard for BYD head units (Leopard 8 / DiLink 5.1). Vehicle controls, automations and speech recognition run on the device — no account, no subscription, no hosted API, no MQTT broker, no license check. Every outbound connection sits behind a switch that ships off, and there is no analytics SDK or telemetry. The bundled radio directory (5,166 stations across 212 countries) browses and searches with no network at all; only playing a live stream needs the internet, and only after the driver enables it. Contributors need nothing that is not in this repository: no private submodule, no secrets, no backend access. See docs/ORIENTATION.md to get running on an ordinary Android emulator in about twenty minutes. Notes carried forward from development, since this history starts fresh: - Gradle is capped below 9.6 in dependabot.yml. Gradle 9.6.0 removed the internal API `org.gradle.api.problems.internal.InternalProblems` that every AGP 8.x relies on, and AGP is held at 8.x until AGP 9 is Flutter-ready. CI never runs Gradle, so a 9.6+ bump passes every check and still breaks the build. - Releases must be tagged `v<versionName>` with no component prefix. GitHubReleaseSource enforces `tag == 'v$name'` on the device, so a component-prefixed tag publishes cleanly and is then silently refused by every installed app. - `radioPlayerProvider` must not be autoDispose. RadioController takes it with `ref.read`, which creates no subscription, so autoDispose tore it down and left the controller holding a player whose `ref` was unmounted — every station tap became a silent no-op. - CI pins Flutter 3.47.2. Older toolchains cannot analyze this code. Host checks: analyzer clean, 1,462 tests passing, debug APK builds, no broken documentation links. Physical head-unit acceptance (fresh install → speech → vehicle execution) has not been completed; docs/offline-first/final-verification.md records what is and is not proven. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AjhsQbfGjJw2V8j2WmRXpN
0 parents  commit 1a6ef28

1,195 files changed

Lines changed: 194360 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Release signing for iLINK. Never commit the real .env — see .gitignore.
2+
#
3+
# iLINK ships under its own applicationId (com.i99dev.ilink) with its own
4+
# signing key. Generate a keystore once, store it outside this repository, and
5+
# back it up: losing it means no future build can update an installed iLINK.
6+
#
7+
# Point this at that keystore with an absolute path outside the repo.
8+
DASH_KEYSTORE_PATH=
9+
DASH_KEYSTORE_PASSWORD=
10+
DASH_KEY_PASSWORD=
11+
DASH_KEY_ALIAS=dash
12+
13+
# SHA-256 of the certificate in that keystore. scripts/prepare-signing.sh
14+
# refuses to build unless the keystore matches, which catches a wrong or
15+
# swapped key before it produces artifacts no installed app can accept.
16+
# Read it with:
17+
# keytool -list -v -keystore <path> -alias <alias> | grep 'SHA256:'
18+
DASH_EXPECTED_SIGNER_SHA=

.gitattributes

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# 3D model assets — binary glTF. Treat as binary so git-diff doesn't
2+
# attempt textual diffs on multi-MB embedded mesh + texture data.
3+
# We deliberately do NOT use Git LFS today: dev_only assets are
4+
# gitignored, and shipped per-model .glb files are <= 25 MB.
5+
# Re-evaluate LFS if the shipped catalog grows past ~10 cars.
6+
*.glb binary
7+
*.gltf binary
8+
*.bin binary
9+
*.ktx2 binary
10+
11+
# Source 3D files (.blend, .fbx, .obj) are kept out of the runtime
12+
# repo entirely (artist's deliverable is the .glb), but mark them
13+
# binary in case a stray source file is committed to docs/ or
14+
# similar.
15+
*.blend binary
16+
*.fbx binary
17+
*.obj binary
18+
19+
# Public offline bundles and standard wrapper must survive Windows checkouts.
20+
*.dex binary
21+
*.zip binary
22+
*.jar binary
23+
*.sh text eol=lf
24+
android/gradlew text eol=lf
25+
android/gradlew.bat text eol=crlf

.githooks/install.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env bash
2+
# Run once after cloning to point git at the in-repo hooks dir.
3+
# Idempotent — safe to re-run.
4+
set -euo pipefail
5+
cd "$(git rev-parse --show-toplevel)"
6+
git config core.hooksPath .githooks
7+
chmod +x .githooks/pre-commit
8+
echo "✓ hooks installed (core.hooksPath=.githooks)"
9+
echo " pre-commit will block commits on dart format / flutter analyze /"
10+
echo " audit_hex failures."

.githooks/pre-commit

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/usr/bin/env bash
2+
# ilink (Flutter) pre-commit gate. Mirrors .github/workflows/ci.yml so
3+
# CI-failing changes bounce locally before push.
4+
#
5+
# Policy:
6+
# - dart format → whole repo (matches CI's --set-exit-if-changed).
7+
# - flutter analyze → whole repo (matches CI; fails on info+ by default).
8+
# - audit_hex → ``tool/audit_hex.dart`` — feature-ID hex must
9+
# live in the encrypted dispatch table only.
10+
# - flutter test → full suite (matches CI). Slowest gate (~10s)
11+
# but the only one that catches behaviour
12+
# regressions; cheap enough to run on every
13+
# commit that touches Dart.
14+
#
15+
# Skip with ``git commit --no-verify`` only in genuine emergencies —
16+
# CI still gates. Or set ``SKIP_PRECOMMIT=1`` for a one-off bypass.
17+
set -uo pipefail
18+
19+
cd "$(git rev-parse --show-toplevel)"
20+
21+
if [ "${SKIP_PRECOMMIT:-}" = "1" ]; then
22+
echo "[pre-commit] SKIP_PRECOMMIT=1 — skipping all checks"
23+
exit 0
24+
fi
25+
26+
# Collect staged Dart sources so we can short-circuit when no Dart
27+
# code is touched (e.g. a docs- or workflow-only commit).
28+
STAGED_DART=()
29+
while IFS= read -r f; do
30+
[ -n "$f" ] && STAGED_DART+=("$f")
31+
done < <(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.dart$' || true)
32+
33+
if [ ${#STAGED_DART[@]} -eq 0 ]; then
34+
echo "[pre-commit] no .dart files staged — skipping checks"
35+
exit 0
36+
fi
37+
38+
run_step() {
39+
name="$1"; shift
40+
echo
41+
echo "[pre-commit] ── $name ──"
42+
if ! "$@"; then
43+
echo
44+
echo "[pre-commit] ✗ $name failed — commit blocked."
45+
echo "[pre-commit] Fix the issue above and re-stage, or"
46+
echo "[pre-commit] set SKIP_PRECOMMIT=1 to bypass (CI still gates)."
47+
exit 1
48+
fi
49+
}
50+
51+
# `dart format` walks `build/` recursively if you pass `.`, which on
52+
# Windows trips a PathNotFoundException when Gradle leaves dangling
53+
# symlinks under `build/flutter_inappwebview_android/.transforms/…`.
54+
# Scoping to the staged set is also faster (~50ms vs ~1.5s on cold
55+
# cache) and cross-platform.
56+
run_step "dart format (check, staged Dart files)" \
57+
dart format --set-exit-if-changed --output=none "${STAGED_DART[@]}"
58+
run_step "flutter analyze (whole repo)" flutter analyze
59+
run_step "audit stray feature-ID hex" dart run tool/audit_hex.dart
60+
run_step "flutter test (full suite)" flutter test
61+
62+
echo
63+
echo "[pre-commit] ✓ all checks passed"
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: Bug Report
2+
description: Report a bug in Dash
3+
labels: ["bug", "triage"]
4+
body:
5+
- type: markdown
6+
attributes:
7+
value: |
8+
Thanks for taking the time to file a bug. Please fill in the fields below.
9+
- type: input
10+
id: version
11+
attributes:
12+
label: Dash version
13+
description: Check Settings → Diagnostics.
14+
placeholder: "1.2.3"
15+
validations:
16+
required: true
17+
- type: input
18+
id: car
19+
attributes:
20+
label: Car model + head-unit firmware
21+
placeholder: "Leopard 8 / DiLink 5.1"
22+
validations:
23+
required: true
24+
- type: textarea
25+
id: steps
26+
attributes:
27+
label: Steps to reproduce
28+
placeholder: |
29+
1. Open the app
30+
2. Tap "..."
31+
3. See error
32+
validations:
33+
required: true
34+
- type: textarea
35+
id: expected
36+
attributes:
37+
label: Expected behaviour
38+
validations:
39+
required: true
40+
- type: textarea
41+
id: actual
42+
attributes:
43+
label: Actual behaviour
44+
validations:
45+
required: true
46+
- type: textarea
47+
id: logs
48+
attributes:
49+
label: Logs
50+
description: Paste relevant logcat output or screenshots.
51+
render: shell

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
blank_issues_enabled: false
2+
contact_links:
3+
- name: Release notes
4+
url: https://github.com/i99dev/ilink/releases
5+
about: Looking for what's new? See the Releases page.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: Feature Request
2+
description: Suggest a new capability for Dash
3+
labels: ["enhancement", "triage"]
4+
body:
5+
- type: textarea
6+
id: problem
7+
attributes:
8+
label: What problem does this solve?
9+
description: Describe the situation where the current app falls short.
10+
validations:
11+
required: true
12+
- type: textarea
13+
id: proposal
14+
attributes:
15+
label: Proposed behaviour
16+
validations:
17+
required: true
18+
- type: textarea
19+
id: alternatives
20+
attributes:
21+
label: Alternatives you've considered

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
## Problem and resulting behavior
2+
3+
Explain the concrete problem and what this change does. Link the relevant issue, if any.
4+
5+
## Validation
6+
7+
List checks run and their results. Explain any failures or checks you could not run.
8+
9+
- [ ] Formatting, analyzer and Flutter tests
10+
- [ ] Hex audit and production configuration validator
11+
- [ ] Applicable Android build/native tests, or explain why not applicable
12+
13+
For UI changes, include screenshots. For head-unit changes, identify the tested model/firmware, steps and results; explicitly distinguish emulator/mock checks from physical hardware testing.
14+
15+
## Compatibility and review notes
16+
17+
Describe changes to local data, permission scopes, network access, bundled assets or vehicle behavior. Note migration needs and remaining limitations. Remove private information from evidence; do not attach signing keys or credentials.
18+
19+
- [ ] PR title follows the convention in [Contributing](../CONTRIBUTING.md)

.github/SECRETS.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Release signing secrets
2+
3+
iLINK has no backend, payment, analytics or account credentials. The only
4+
secrets it needs are for signing the release APK. Never commit a keystore or a
5+
password.
6+
7+
## Creating the signing key (once)
8+
9+
iLINK ships under its own applicationId, `com.i99dev.ilink`, so it has its own
10+
signing identity. Generate the keystore once, outside this repository:
11+
12+
```sh
13+
keytool -genkeypair -v -keystore ilink-release.jks -alias dash \
14+
-keyalg RSA -keysize 4096 -validity 10000
15+
```
16+
17+
**Back it up before you use it.** Android will not accept an update signed by a
18+
different key, so losing this keystore means no future build can ever update an
19+
installed iLINK — the only remedy is a new applicationId and every user
20+
reinstalling from scratch. Keep at least one encrypted copy off the build
21+
machine.
22+
23+
Read the certificate fingerprint you will pin against:
24+
25+
```sh
26+
keytool -list -v -keystore ilink-release.jks -alias dash | grep 'SHA256:'
27+
```
28+
29+
## Repository secrets
30+
31+
Release Actions read these from the `prod` environment:
32+
33+
| Secret | Purpose |
34+
| --- | --- |
35+
| `DASH_RELEASE_KEYSTORE_B64` | Base64 of the release keystore, restored only under `RUNNER_TEMP` and deleted at the end of the job. |
36+
| `DASH_KEYSTORE_PASSWORD` | Keystore password. |
37+
| `DASH_KEY_PASSWORD` | Private-key password. |
38+
| `DASH_KEY_ALIAS` | Key alias (`dash` by default). |
39+
| `DASH_EXPECTED_SIGNER_SHA` | Certificate SHA-256 the preflight pins against. |
40+
41+
Base64 the keystore with `base64 -w0 ilink-release.jks` (macOS: `base64 -i`).
42+
43+
Secrets live in the `prod` environment, so they are never exposed to pull-request
44+
workflows — including PRs from forks. Do not move them to repository-wide
45+
secrets.
46+
47+
## How the preflight uses them
48+
49+
`scripts/prepare-signing.sh` reads the keystore's certificate and refuses to
50+
build unless it matches `DASH_EXPECTED_SIGNER_SHA`. That catches a wrong or
51+
swapped keystore before it produces artifacts no installed app can accept. The
52+
fingerprint is configuration rather than a literal in the repository, so
53+
rotating the key before the first public release is a secret change, not a code
54+
change. The script never generates a keystore. Signing keeps v1, v2 and v3
55+
enabled.
56+
57+
For local release builds, set the same variables (or use the gitignored `.env`,
58+
see [`.env.example`](../.env.example)) and run `scripts/prepare-signing.sh`
59+
before building.
60+
61+
Once iLINK has a public release, treat the signing identity as permanent and do
62+
not rotate it as part of any secret cleanup.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Build Flutter APK
2+
description: Build a standalone APK using local configuration and the existing release signer.
3+
inputs:
4+
dart-define-file:
5+
description: Local build configuration.
6+
required: false
7+
default: config/prod.json
8+
flutter-build-mode:
9+
description: apk or appbundle.
10+
required: false
11+
default: apk
12+
outputs:
13+
apk-path:
14+
description: Built release package.
15+
value: ${{ steps.build.outputs.apk-path }}
16+
runs:
17+
using: composite
18+
steps:
19+
- name: Build
20+
id: build
21+
shell: bash
22+
env:
23+
BUILD_MODE: ${{ inputs.flutter-build-mode }}
24+
DEFINE_FILE: ${{ inputs.dart-define-file }}
25+
run: |
26+
test -f "$DEFINE_FILE"
27+
test "$BUILD_MODE" = apk || test "$BUILD_MODE" = appbundle
28+
flutter pub get
29+
flutter build "$BUILD_MODE" --release --dart-define-from-file="$DEFINE_FILE" --dart-define=GITHUB_RELEASE_REPOSITORY="${{ github.repository }}"
30+
if [ "$BUILD_MODE" = appbundle ]; then
31+
echo "apk-path=build/app/outputs/bundle/release/app-release.aab" >> "$GITHUB_OUTPUT"
32+
else
33+
echo "apk-path=build/app/outputs/flutter-apk/app-release.apk" >> "$GITHUB_OUTPUT"
34+
fi

0 commit comments

Comments
 (0)