Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/apple.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ jobs:
# Signed ad hoc, as FTW.xcconfig sets for the simulator, so the App
# Group entitlement travels with the app and the widget. Device
# builds need a team, which the project does not have yet.
- name: Build the app and its widgets for the iOS Simulator
# The iPhone app embeds the Apple Watch app, so this builds the watch
# app and its complications for the watchOS Simulator too.
- name: Build the app, its widgets and the watch app for the simulators
working-directory: appleApp
run: >-
xcodebuild build -quiet -project FTW.xcodeproj -scheme FTW
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/ios-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,12 @@ jobs:
CURRENT_PROJECT_VERSION=${{ steps.version.outputs.build }}
FTW_SOURCE_REVISION=$GITHUB_SHA

# App Store Connect refuses a widget whose version differs from its
# app's, so check both before sending anything.
- name: Check the version in the app and the widget
# App Store Connect refuses an extension or a watch app whose version
# differs from its app's, so check them all before sending anything.
- name: Check the version in the app, its extensions and the watch app
run: |
app=$RUNNER_TEMP/FTW.xcarchive/Products/Applications/FTW.app
for plist in "$app/Info.plist" "$app"/PlugIns/*.appex/Info.plist; do
for plist in "$app/Info.plist" "$app"/PlugIns/*.appex/Info.plist "$app"/Watch/*.app/Info.plist "$app"/Watch/*.app/PlugIns/*.appex/Info.plist; do
got=$(plutil -extract CFBundleShortVersionString raw "$plist")/$(plutil -extract CFBundleVersion raw "$plist")
echo "$got ${plist#$app/} $(plutil -extract DTSDKName raw "$plist")"
[[ $got == ${{ steps.version.outputs.version }}/${{ steps.version.outputs.build }} ]] || { echo "::error::$plist has $got"; exit 1; }
Expand Down
33 changes: 31 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ in M1. FTW Cloud (an opt-in state cache, push and statistics, readable by
Sourceful rather than end-to-end encrypted) is designed separately; nothing
in M1 may depend on it.

**Apple Watch (`appleApp/FTWWatch*`).** W1, asked for by Fredrik on
2026-10-14: Now, the plan dial (the Crown steps it by quarter hour), the car
and the price, Charge now, Pause, Back to the plan and the mode, and
complications. watchOS 26 is the minimum. watchOS gives an ordinary app no
WebSocket (TN3135), so in W1 the watch reads and commands through the iPhone
(`WatchBridge`, WatchConnectivity); FTW Cloud never controls. The app is
built to stand alone (`WKRunsIndependentlyOfCompanionApp`): it opens on its
own cache, makes its own key and its own `cmdId`, and reads and sends
through two seams, `ReadPath` and `CommandPath`. `PhonePath` is the only
path now; FTW Cloud reads and the relay's exchange (ADR 0009) come later.
An unlocked watch on the wrist counts as unlocked for commands.

**Android (`androidApp/`, `shared/`).** Pair + Now, shipped on `main` as of
2026-08-22. Do not add Energy / History / Plan / EV, commands, escrow, LAN,
push, or store listing on Android until Pair + Now is solid there, including
Expand Down Expand Up @@ -117,6 +129,17 @@ native, and smooth enough to impress.
live is drawn in ink on the same curve. No dot travels along a line.
- **Every chart takes a finger.** The point under it is marked, and the
line under the chart says what was there. Two fingers give the span.
- **History sits small and grows** (Fredrik, 14 Oct 2026). A node's chart
is compact, so the view never scrolls for it; a tap opens it on the
whole screen, growing out of the small one. Chips add or take away
series, kept per viewer; the last one stays, and a hidden series never
changes what the others say.
- **One material for every power trace** (Fredrik, 14 Oct 2026). Live,
Today and 48 h, small and enlarged, draw through `TraceInk` (band,
body, line, glow only while live), `ChartAxes`, `LockOn` and
`SlidingSurface` (a slide of 0.25 s, a spring of 0.35 s), and the hero
follows the finger on each. Going from Live to Today changes the time,
never the material. A new chart starts from these.
- **Charts draw on the engine.** Geometry is a value built once per data
change (`FTWShared/Charts`, tested). A chart is one Canvas
(`ChartSurface`) that never sees the finger and a small `ScrubOverlay`
Expand Down Expand Up @@ -206,7 +229,9 @@ The rules below put these into practice.
`ThemeTests` holds the contrast (text 4.5:1 on ground and raised; energy
4.5:1 in light and 7:1 in dark) and fails on any colour written in code.
Increase Contrast turns ink3 into ink2 and hairlines into 2 px; Reduce
Transparency drops the glow.
Transparency drops the glow. watchOS compiles no dark appearance and a
watch is always dark, so each set also has a `watch` variant that equals
its dark one; `ThemeTests` holds them equal.
- The 0.3 names in `FTW/UI/Theme.swift` (`fg`, `surface`, `accent` and the
rest) point at these roles until each screen is rebuilt. New code reads
the roles.
Expand Down Expand Up @@ -271,7 +296,11 @@ On iPhone, one Swift package in `appleApp/FTWKit` holds three modules:
Review; FTWKit never does.

`appleApp/FTW` owns the camera, the passkey ceremony, the Keychain and every
pixel. `appleApp/FTWWidgets` owns the widgets. Keep logic out of views: if a
pixel. `appleApp/FTWWidgets` owns the widgets. `appleApp/FTWWatch` is the
watch app, `FTWWatchWidgets` its complications and `FTWWatchShared` what
both draw (the dial). The watch links `FTWShared` and `FTWCharts`, never
FTWKit. What the phone and the watch say to each other lives in
`FTWShared/Watch/`, and the phone's side of it in `FTWKit`'s `WatchShare`. Keep logic out of views: if a
sentence or a rule can be tested, it belongs in a package module with a
test. The App Group name is written once, in `appleApp/FTW.xcconfig`; the
entitlements and Info.plists read it from there.
Expand Down
10 changes: 7 additions & 3 deletions appleApp/FTW.xcconfig
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
// Settings the app and its widget extension must agree on, in one place.
// Both targets build on this file through the project.

// The App Group the app and widgets share. Written here once: both
// entitlements files and both Info.plists read it as $(FTW_APP_GROUP), and
// Swift reads it back from Info.plist (AppGroupFiles.infoKey).
// The App Group the app and widgets share. Written here once: every
// entitlements file and Info.plist reads it as $(FTW_APP_GROUP), and Swift
// reads it back from Info.plist (AppGroupFiles.infoKey). The Apple Watch
// app and its complications use the same name; an App Group stays on one
// device, so the watch holds its own copy, filled from the phone.
FTW_APP_GROUP = group.energy.ftw.app

// An extension's version must match its app's.
MARKETING_VERSION = 0.3.5
CURRENT_PROJECT_VERSION = 1

IPHONEOS_DEPLOYMENT_TARGET = 26.0
WATCHOS_DEPLOYMENT_TARGET = 26.0

// Sourceful's Apple team, for the app and the widget alike. Signing is
// automatic: Xcode, signed in to that team, makes the profiles. Nothing for
Expand All @@ -22,6 +25,7 @@ CODE_SIGN_STYLE = Automatic
// anyone without one. The entitlements, and with them the App Group, still
// travel with the app.
CODE_SIGN_IDENTITY[sdk=iphonesimulator*] = -
CODE_SIGN_IDENTITY[sdk=watchsimulator*] = -

// The commit Source & licenses links to, which the license asks the app to
// offer. CI passes the full commit (xcodebuild … FTW_SOURCE_REVISION=$GITHUB_SHA).
Expand Down
Loading
Loading