Author SHA1 Message Date
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 2e8b07c8f3 fix(calibration): observe WizardViewModel directly in CalibrationView
macOS CI / build-and-test (push) Successful in 11m42s
macOS CI / package (push) Successful in 6m15s
The Milestone 6 calibration UI test could not find `btnCreateLayout` because
`CalibrationView` only bound `CalibrationViewModel`. Its `canGenerate`
computed property reads `wizard.basename` and `wizard.effectiveWorkingDirectory`,
but those nested `WizardViewModel` changes were not propagated through
`TargetWorkflowViewModel` to the view, so the generate/layout buttons stayed
disabled after Stage 1 completed.

Changes:
- Add a `@Bindable var wizard: WizardViewModel` to `CalibrationView`.
- Pass `workflow.wizard` from `WizardStageContent` into `CalibrationView`.
- Replace `.disabled(!model.canGenerate)` with direct reads of
  `wizard.basename`, `wizard.effectiveWorkingDirectory`, and `model.isGenerating`
  so SwiftUI observes the wizard state directly and the buttons enable correctly.

Fixes the CI failure in
`Milestone6CalibrationUITests.testCalibrationDashboardOpensAndCanGenerate`.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-10 14:38:30 +01:00
gronod 03d6558708 ci(macos): export system CA bundle for artifact upload
macOS CI / build-and-test (push) Failing after 11m21s
macOS CI / package (push) Skipped
Export macOS system certificates to NODE_EXTRA_CA_CERTS before uploading the DMG artifact so the actions/upload-artifact step trusts the Gitea server's TLS chain.
2026-09-10 12:34:59 +01:00
gronod e0012dc344 Merge pull request 'Apply v1 ICCery app icon, About icon, and DMG scene' (#78) from feat/77-apply-v1-icon into develop
macOS CI / build-and-test (push) Successful in 10m1s
macOS CI / package (push) Failing after 4m42s
Reviewed-on: #78
2026-09-10 11:45:29 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> e424eefa5a feat: apply v1 ICCery app icon and DMG scene
macOS CI / package (pull_request) Canceled after 0s
macOS CI / build-and-test (pull_request) Canceled after 3m50s
Port the original cone icon into the macOS asset catalog, About dialog, and DMG artwork so v2 retains the established ICCery identity.

Issue #77

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-10 11:39:46 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 63ec0bde63 fix(calibration): strong task capture and bind WizardViewModel for stage transitions (#32)
The Milestone 6 calibration UI test was flaky/consistent in failing because
`generateTarget` / `computeCurves` used `Task { @MainActor [weak self] in`
with an early `guard let self else { return }`. On a loaded host the task
could be dropped before it resumed, leaving the UI stuck on the calibration
dashboard with no error shown.

Changes:
- Capture `self` strongly in `CalibrationViewModel` async tasks so they run
  to completion and update `wizard.go(to:)` / `restoreCalibration()`.
- Split `RootView.stageContent` into `WizardStageContent` with its own
  `@Bindable var model: WizardViewModel` so `model.stage` mutations are
  observed reliably, rather than relying on observation through the parent
  `TargetWorkflowViewModel`.

Fixes the CI failure in
`Milestone6CalibrationUITests.testCalibrationDashboardOpensAndCanGenerate`.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-10 11:06:04 +01:00
gronod 6309f9fd12 chore: ignore docs/megaplans/ directory
macOS CI / package (push) Canceled after 0s
macOS CI / build-and-test (push) Canceled after 30s
2026-09-10 09:51:28 +01:00
gronod 43d35515e1 Merge remote-tracking branch 'origin/develop' into develop
macOS CI / build-and-test (push) Failing after 17m16s
macOS CI / package (push) Skipped
2026-09-10 09:27:44 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> a110eb81e6 fix(packaging): skip notarization entirely for ad-hoc builds
Notarization requires a Developer ID signature. When CODESIGN_IDENTITY
is unset/"-" or any NOTARIZE_* secret is missing, exit the script
without printing a misleading "skipping" line — the DMG is already
usable.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-10 09:24:39 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> d40f0052d2 ci(macos): restore workflow, fix test signing and packaging (#32)
macOS CI / build-and-test (push) Successful in 10m54s
macOS CI / package (push) Failing after 5m2s
- Keep CI active on develop (push + PR) and tags v*.
- Generate the project with `xcodegen generate --spec project.yml`.
- build-for-testing / test-without-building share one derivedDataPath,
  destination, universal archs, and ad-hoc signing flags. The XCTest
  runner is never re-signed with --options runtime.
- Package job runs package-release.sh after tests pass and uploads the
  ICCery-*.dmg the script actually writes.
- README documents the env vars the pipeline reads.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-10 09:01:43 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 80408dd682 fix(packaging): use fixed derived-data path, correct sign/verify flow
package-release.sh previously parsed -showBuildSettings for the app path,
which could disagree with the actual build. Now use a single
derivedDataPath and locate Release/ICCery.app directly. Add Developer ID
signing with --timestamp and a post-sign verify, while leaving sidecars
ad-hoc. Re-verify sidecars after app signing and re-ad-hoc any that were
stripped. Install dmgbuild in a venv and use DMG_APP env.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-10 09:01:43 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 2ffcabbd52 fix(packaging): stage the built .app into the DMG (#32)
dmgbuild never set `files`, so the volume mounted empty. Export DMG_APP
from package-release.sh and require it in dmgbuild-settings.py, copying
the bundle by its source path. Keep the Applications symlink and the
optional background fallback.

Verified by mounting a test image and seeing ICCery.app + Applications.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-10 09:01:43 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 85f0de631c fix(packaging): make sidecar sign/verify recursive (#32, #165)
The Argyll tree inside the bundle is nested (macos-universal/, mocks/),
so a top-level-only scan left real binaries unverified. Scan recursively
under Contents/Resources/Argyll, hard-fail on any unsigned Mach-O or a
missing/empty sidecar dir, and sign fetched tools recursively too.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-10 09:01:43 +01:00
22 changed files with 138 additions and 21 deletions
+13
View File
@@ -67,8 +67,21 @@ jobs:
NOTARIZE_PASSWORD: ${{ secrets.NOTARIZE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- name: Prepare Node CA bundle
run: |
NODE_CA_FILE="/tmp/macos-ca-bundle.pem"
security find-certificate -a -p \
/System/Library/Keychains/SystemRootCertificates.keychain \
/Library/Keychains/System.keychain \
> "$NODE_CA_FILE" 2>/dev/null || true
if [ ! -s "$NODE_CA_FILE" ] && [ -f /etc/ssl/cert.pem ]; then
cp /etc/ssl/cert.pem "$NODE_CA_FILE"
fi
- name: Upload DMG artifact
uses: actions/upload-artifact@v3
env:
NODE_EXTRA_CA_CERTS: /tmp/macos-ca-bundle.pem
with:
name: iccery-dmg
path: ICCery-*.dmg
+1
View File
@@ -27,3 +27,4 @@ ICCery.xcodeproj/
Release/
notarization/
build/
docs/megaplans/
@@ -1,51 +1,61 @@
{
"images" : [
{
"filename" : "icon_16x16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "icon_16x16@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "icon_32x32.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "icon_32x32@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "icon_128x128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "icon_128x128@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "icon_256x256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "icon_256x256@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "icon_512x512.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "icon_512x512@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+4 -1
View File
@@ -1,3 +1,4 @@
import AppKit
import SwiftUI
import ICCeryCore
@@ -9,10 +10,12 @@ struct AboutView: View {
var body: some View {
VStack(spacing: 20) {
Image("ICCery-logo")
if let icon = NSImage(named: NSImage.applicationIconName) {
Image(nsImage: icon)
.resizable()
.scaledToFit()
.frame(height: 64)
}
Text("ICCery")
.font(.title)
+7 -2
View File
@@ -4,6 +4,7 @@ import ICCeryCore
/// Stage 0 calibration dashboard (issue #29, docs/07).
struct CalibrationView: View {
@Bindable var model: CalibrationViewModel
@Bindable var wizard: WizardViewModel
var body: some View {
VStack(alignment: .leading, spacing: 0) {
@@ -51,11 +52,15 @@ struct CalibrationView: View {
HStack(spacing: 12) {
Button("Generate Target") { model.generateTarget() }
.accessibilityIdentifier("btnCalGenerate")
.disabled(!model.canGenerate)
.disabled(wizard.basename.isEmpty
|| wizard.effectiveWorkingDirectory == nil
|| model.isGenerating)
Button("Create Layout & Print") { model.createLayout() }
.accessibilityIdentifier("btnCalLayout")
.disabled(!model.canGenerate)
.disabled(wizard.basename.isEmpty
|| wizard.effectiveWorkingDirectory == nil
|| model.isGenerating)
Button("Measure") { model.measureChart() }
.accessibilityIdentifier("btnCalMeasure")
+2 -4
View File
@@ -91,8 +91,7 @@ final class CalibrationViewModel {
workingDirectory: cwd
)
Task { @MainActor [weak self] in
guard let self else { return }
Task { @MainActor in
defer { self.isGenerating = false }
do {
@@ -160,8 +159,7 @@ final class CalibrationViewModel {
channelLimits: []
)
Task { @MainActor [weak self] in
guard let self else { return }
Task { @MainActor in
defer { self.isComputing = false }
do {
+12 -4
View File
@@ -29,7 +29,7 @@ struct RootView: View {
if let notice = model.notice {
NoticeBanner(notice: notice, onClose: model.dismissNotice)
}
stageContent
WizardStageContent(model: model, workflow: workflow)
}
}
.frame(minWidth: 1100, minHeight: 700)
@@ -61,8 +61,16 @@ struct RootView: View {
}
}
@ViewBuilder
private var stageContent: some View {
}
/// Content for the active wizard stage. Isolated into its own view so that
/// `WizardViewModel` is tracked via `@Bindable` instead of the parent's
/// `TargetWorkflowViewModel`, which does not observe nested `wizard` mutations.
private struct WizardStageContent: View {
@Bindable var model: WizardViewModel
var workflow: TargetWorkflowViewModel
var body: some View {
switch model.stage {
case .generate:
Stage1View(workflow: workflow)
@@ -75,7 +83,7 @@ struct RootView: View {
case .verifyInstall:
Stage5View(model: workflow.profile)
case .calibrate:
CalibrationView(model: workflow.calibration)
CalibrationView(model: workflow.calibration, wizard: workflow.wizard)
@unknown default:
StagePlaceholderView(stage: model.stage)
}
+78
View File
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 660 400" width="660" height="400">
<defs>
<!-- Logo Clip Path -->
<clipPath id="coneClip">
<path d="M 50 82 L 110 82 L 80 142 Z" />
</clipPath>
<!-- Logo Text Gradient -->
<linearGradient id="textGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#00AEEF" />
<stop offset="100%" stop-color="#0066CC" />
</linearGradient>
<!-- Arrow Drop Shadow Filter -->
<filter id="arrowShadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-color="#000000" flood-opacity="0.35"/>
</filter>
</defs>
<!-- Solid Background (matches app --bg-color: #1e1e1e) -->
<rect width="660" height="400" fill="#1e1e1e" />
<!-- Top-Left Branding Logo & Text (standard header height) -->
<g transform="translate(24, 18) scale(0.44)">
<!-- Drop Shadow -->
<ellipse cx="80" cy="145" rx="25" ry="5" fill="#1E293B" opacity="0.1" />
<!-- Ice Cream Cone (Waffle) -->
<path d="M 50 82 L 110 82 L 80 142 Z" fill="#FAD7A1" stroke="#E59866" stroke-width="2.5" stroke-linejoin="round"/>
<g clip-path="url(#coneClip)" stroke="#E59866" stroke-width="2">
<line x1="40" y1="80" x2="120" y2="160" />
<line x1="55" y1="80" x2="135" y2="160" />
<line x1="70" y1="80" x2="150" y2="160" />
<line x1="85" y1="80" x2="165" y2="160" />
<line x1="120" y1="80" x2="40" y2="160" />
<line x1="105" y1="80" x2="25" y2="160" />
<line x1="90" y1="80" x2="10" y2="160" />
<line x1="75" y1="80" x2="-5" y2="160" />
</g>
<!-- CMYK Scoops (C, M, Y) -->
<circle cx="63" cy="72" r="22" fill="#00BCEB" stroke="#ffffff" stroke-width="2.5"/>
<circle cx="97" cy="72" r="22" fill="#EC008C" stroke="#ffffff" stroke-width="2.5"/>
<circle cx="80" cy="48" r="22" fill="#FFED00" stroke="#ffffff" stroke-width="2.5"/>
<!-- Black (Key) Cherry -->
<path d="M 80 23 Q 92 12 96 16" fill="none" stroke="#1E293B" stroke-width="2" stroke-linecap="round"/>
<circle cx="80" cy="24" r="7" fill="#1E293B" stroke="#ffffff" stroke-width="1.5"/>
<!-- Highlights on scoops for 3D effect -->
<circle cx="58" cy="67" r="4" fill="#ffffff" opacity="0.35"/>
<circle cx="92" cy="67" r="4" fill="#ffffff" opacity="0.35"/>
<circle cx="75" cy="43" r="4" fill="#ffffff" opacity="0.45"/>
<circle cx="78" cy="22" r="1.5" fill="#ffffff" opacity="0.6"/>
<!-- Text: ICCery -->
<text font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="58" font-weight="800" x="140" y="105" fill="#ffffff" letter-spacing="-1">ICC<tspan fill="url(#textGrad)">ery</tspan></text>
</g>
<!-- Center White Arrow (Pointing right between App Icon and Applications folder) -->
<g filter="url(#arrowShadow)">
<path d="M 285 214
L 342 214
L 342 201
Q 342 198 345 199.5
L 380 217.5
Q 383 219 383 220
Q 383 221 380 222.5
L 345 240.5
Q 342 242 342 239
L 342 226
L 285 226
Q 282 226 282 223
L 282 217
Q 282 214 285 214 Z"
fill="#ffffff" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

+3 -3
View File
@@ -32,7 +32,7 @@ if background and not os.path.exists(background):
icon = None
# Window size is enough for the app icon and the Applications alias.
window_rect = ((100, 100), (640, 480))
window_rect = ((100, 100), (660, 400))
# Use icon view without extra chrome.
default_view = 'icon-view'
@@ -45,8 +45,8 @@ sidebar_width = 180
# Position the .app on the left and the Applications alias on the right.
icon_locations = {
'ICCery.app': (140, 240),
'Applications': (500, 240),
'ICCery.app': (180, 220),
'Applications': (480, 220),
}
# Symlink to /Applications for drag-and-drop install.
+5 -4
View File
@@ -132,8 +132,11 @@ dmgbuild -s scripts/dmgbuild-settings.py "$VOLUME_NAME" "$DMG"
echo "DMG: $PWD/$DMG"
# Optional notarization/stapling when credentials are present.
if [ -n "${NOTARIZE_APPLE_ID:-}" ] && \
# Notarization requires a Developer ID signature. If the app was ad-hoc
# signed or any notarization secret is missing, skip silently — the DMG is
# still usable for local/testing installs.
if [ -n "${CODESIGN_IDENTITY:-}" ] && [ "$CODESIGN_IDENTITY" != "-" ] && \
[ -n "${NOTARIZE_APPLE_ID:-}" ] && \
[ -n "${NOTARIZE_PASSWORD:-}" ] && \
[ -n "${APPLE_TEAM_ID:-}" ]; then
echo "==> Submitting $DMG for notarization"
@@ -144,6 +147,4 @@ if [ -n "${NOTARIZE_APPLE_ID:-}" ] && \
--wait
xcrun stapler staple "$DMG"
echo "==> Stapled $DMG"
else
echo "==> Notarization credentials not set; skipping"
fi