@trillboards/edge-sdk 0.2.135 → 0.2.136
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deploy/docker/README.md +0 -11
- package/deploy/macos/README.md +7 -1
- package/deploy/macos/TrillboardsSetupApp.m +391 -14
- package/deploy/macos/pair.command +6 -1
- package/deploy/windows/trillboards-pair.cmd +7 -1
- package/deploy/windows/trillboards-setup.ps1 +349 -16
- package/dist/EdgeAgent.js +1 -1
- package/dist/InstallerOnboardingClient.d.ts +102 -0
- package/dist/InstallerOnboardingClient.d.ts.map +1 -0
- package/dist/InstallerOnboardingClient.js +219 -0
- package/dist/InstallerOnboardingClient.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +37 -0
- package/dist/cli.js.map +1 -1
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.js +1 -1
- package/dist/installerOnboardingMode.d.ts +19 -0
- package/dist/installerOnboardingMode.d.ts.map +1 -0
- package/dist/installerOnboardingMode.js +349 -0
- package/dist/installerOnboardingMode.js.map +1 -0
- package/dist/setupProgress.d.ts +25 -1
- package/dist/setupProgress.d.ts.map +1 -1
- package/dist/setupProgress.js +43 -0
- package/dist/setupProgress.js.map +1 -1
- package/package.json +1 -1
package/deploy/docker/README.md
CHANGED
|
@@ -84,17 +84,6 @@ docker pull ghcr.io/trillboards/edge-sdk:openvino
|
|
|
84
84
|
docker pull ghcr.io/trillboards/edge-sdk:auto-sense # BYO-CCTV connector
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
**AWS ECR (private, for production fleet):**
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
aws ecr get-login-password --region us-east-1 | \
|
|
91
|
-
docker login --username AWS --password-stdin 975050306809.dkr.ecr.us-east-1.amazonaws.com
|
|
92
|
-
|
|
93
|
-
docker pull 975050306809.dkr.ecr.us-east-1.amazonaws.com/trillboards-edge-sdk:latest
|
|
94
|
-
docker pull 975050306809.dkr.ecr.us-east-1.amazonaws.com/trillboards-edge-sdk:cuda
|
|
95
|
-
docker pull 975050306809.dkr.ecr.us-east-1.amazonaws.com/trillboards-edge-sdk:openvino
|
|
96
|
-
```
|
|
97
|
-
|
|
98
87
|
## Quick Start
|
|
99
88
|
|
|
100
89
|
```bash
|
package/deploy/macos/README.md
CHANGED
|
@@ -46,7 +46,13 @@ delivered **only by pairing** — it is never baked into the `.pkg`.
|
|
|
46
46
|
CurrentUserHomeDirectory`.)
|
|
47
47
|
3. The installer installs and verifies the connector and included local AI
|
|
48
48
|
model. Only then does **Sense Anything Connector** open automatically. Select
|
|
49
|
-
**Start setup**.
|
|
49
|
+
**Start setup**. Follow **Welcome → Account → Account setup → Plan & Payment**.
|
|
50
|
+
Sign in with Google or email, choose the account if you manage more than one,
|
|
51
|
+
accept the current agreements, then choose a plan
|
|
52
|
+
and finish secure Stripe checkout. Cancelling checkout preserves account setup;
|
|
53
|
+
use **Back to plans** in the Connector to retry. Existing subscribed accounts
|
|
54
|
+
resume at recorder setup. The Connector checks subscription access automatically
|
|
55
|
+
before opening the signed-in Sense Anything camera wizard,
|
|
50
56
|
finds recorders on the local network, and validates each channel before any
|
|
51
57
|
camera screens are created. Black, disconnected, and unreadable inputs are
|
|
52
58
|
ignored. After the working cameras are saved, the already-installed local AI
|
|
@@ -48,8 +48,33 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
48
48
|
@property(nonatomic, strong) NSButton *closeButton;
|
|
49
49
|
@property(nonatomic, strong) NSButton *updateButton;
|
|
50
50
|
@property(nonatomic, strong) NSTextField *updateStatus;
|
|
51
|
+
@property(nonatomic, strong) NSTextField *privacyLabel;
|
|
51
52
|
@property(nonatomic, strong) NSArray<NSTextField *> *stepLabels;
|
|
53
|
+
@property(nonatomic, strong) NSArray<NSView *> *onboardingPanels;
|
|
54
|
+
@property(nonatomic, strong) NSView *accountPanel;
|
|
55
|
+
@property(nonatomic, strong) NSView *termsPanel;
|
|
56
|
+
@property(nonatomic, strong) NSView *chooseAccountPanel;
|
|
57
|
+
@property(nonatomic, strong) NSPopUpButton *accountPicker;
|
|
58
|
+
@property(nonatomic, strong) NSButton *chooseAccountSubmit;
|
|
59
|
+
@property(nonatomic, strong) NSView *planPanel;
|
|
60
|
+
@property(nonatomic, strong) NSTextField *nameInput;
|
|
61
|
+
@property(nonatomic, strong) NSTextField *emailInput;
|
|
62
|
+
@property(nonatomic, strong) NSSecureTextField *passwordInput;
|
|
63
|
+
@property(nonatomic, strong) NSTextField *nameLabel;
|
|
64
|
+
@property(nonatomic, strong) NSTextField *passwordLabel;
|
|
65
|
+
@property(nonatomic, strong) NSButton *accountSubmit;
|
|
66
|
+
@property(nonatomic, strong) NSButton *authModeButton;
|
|
67
|
+
@property(nonatomic, strong) NSButton *termsCheck;
|
|
68
|
+
@property(nonatomic, strong) NSTextField *termsVersion;
|
|
69
|
+
@property(nonatomic, strong) NSButton *termsSubmit;
|
|
70
|
+
@property(nonatomic, strong) NSButton *monthlyRadio;
|
|
71
|
+
@property(nonatomic, strong) NSButton *annualRadio;
|
|
72
|
+
@property(nonatomic, strong) NSButton *standardRadio;
|
|
73
|
+
@property(nonatomic, strong) NSButton *multiRadio;
|
|
74
|
+
@property(nonatomic, strong) NSTextField *standardPlan;
|
|
75
|
+
@property(nonatomic, strong) NSTextField *multiPlan;
|
|
52
76
|
@property(nonatomic, strong) NSTimer *timer;
|
|
77
|
+
@property(nonatomic, strong) NSFileHandle *setupInput;
|
|
53
78
|
@property(nonatomic, assign) pid_t pairProcessIdentifier;
|
|
54
79
|
@property(nonatomic, strong) dispatch_source_t pairProcessSource;
|
|
55
80
|
@property(nonatomic, copy) NSString *lastStage;
|
|
@@ -57,6 +82,7 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
57
82
|
@property(nonatomic, copy) NSString *requiredUpdateURL;
|
|
58
83
|
@property(nonatomic, assign) BOOL setupStarted;
|
|
59
84
|
@property(nonatomic, assign) BOOL terminating;
|
|
85
|
+
@property(nonatomic, assign) BOOL signingIn;
|
|
60
86
|
@end
|
|
61
87
|
|
|
62
88
|
@implementation TrillboardsSetupDelegate
|
|
@@ -74,6 +100,24 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
74
100
|
return field;
|
|
75
101
|
}
|
|
76
102
|
|
|
103
|
+
- (NSTextField *)inputWithFrame:(NSRect)frame placeholder:(NSString *)placeholder secure:(BOOL)secure {
|
|
104
|
+
NSTextField *field = secure
|
|
105
|
+
? [[NSSecureTextField alloc] initWithFrame:frame]
|
|
106
|
+
: [[NSTextField alloc] initWithFrame:frame];
|
|
107
|
+
field.placeholderString = placeholder;
|
|
108
|
+
field.font = [NSFont systemFontOfSize:14 weight:NSFontWeightRegular];
|
|
109
|
+
return field;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
- (NSButton *)panelButton:(NSString *)title frame:(NSRect)frame action:(SEL)action {
|
|
113
|
+
NSButton *button = [[NSButton alloc] initWithFrame:frame];
|
|
114
|
+
button.title = title;
|
|
115
|
+
button.bezelStyle = NSBezelStyleRounded;
|
|
116
|
+
button.target = self;
|
|
117
|
+
button.action = action;
|
|
118
|
+
return button;
|
|
119
|
+
}
|
|
120
|
+
|
|
77
121
|
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
|
|
78
122
|
(void)notification;
|
|
79
123
|
self.lastStage = @"welcome";
|
|
@@ -118,6 +162,10 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
118
162
|
- (void)stopPairProcess {
|
|
119
163
|
pid_t pid = self.pairProcessIdentifier;
|
|
120
164
|
self.pairProcessIdentifier = 0;
|
|
165
|
+
if (self.setupInput) {
|
|
166
|
+
[self.setupInput closeFile];
|
|
167
|
+
self.setupInput = nil;
|
|
168
|
+
}
|
|
121
169
|
if (self.pairProcessSource) {
|
|
122
170
|
dispatch_source_cancel(self.pairProcessSource);
|
|
123
171
|
self.pairProcessSource = nil;
|
|
@@ -150,12 +198,22 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
150
198
|
return NO;
|
|
151
199
|
}
|
|
152
200
|
|
|
201
|
+
int inputPipe[2];
|
|
202
|
+
if (pipe(inputPipe) != 0) {
|
|
203
|
+
close(logFd);
|
|
204
|
+
if (error) *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil];
|
|
205
|
+
return NO;
|
|
206
|
+
}
|
|
207
|
+
|
|
153
208
|
posix_spawn_file_actions_t fileActions;
|
|
154
209
|
posix_spawnattr_t attributes;
|
|
155
210
|
posix_spawn_file_actions_init(&fileActions);
|
|
156
211
|
posix_spawn_file_actions_adddup2(&fileActions, logFd, STDOUT_FILENO);
|
|
157
212
|
posix_spawn_file_actions_adddup2(&fileActions, logFd, STDERR_FILENO);
|
|
213
|
+
posix_spawn_file_actions_adddup2(&fileActions, inputPipe[0], STDIN_FILENO);
|
|
158
214
|
posix_spawn_file_actions_addclose(&fileActions, logFd);
|
|
215
|
+
posix_spawn_file_actions_addclose(&fileActions, inputPipe[0]);
|
|
216
|
+
posix_spawn_file_actions_addclose(&fileActions, inputPipe[1]);
|
|
159
217
|
posix_spawnattr_init(&attributes);
|
|
160
218
|
posix_spawnattr_setflags(&attributes, POSIX_SPAWN_SETPGROUP);
|
|
161
219
|
posix_spawnattr_setpgroup(&attributes, 0);
|
|
@@ -179,11 +237,14 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
179
237
|
posix_spawnattr_destroy(&attributes);
|
|
180
238
|
posix_spawn_file_actions_destroy(&fileActions);
|
|
181
239
|
close(logFd);
|
|
240
|
+
close(inputPipe[0]);
|
|
182
241
|
if (spawnError != 0) {
|
|
242
|
+
close(inputPipe[1]);
|
|
183
243
|
if (error) *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:spawnError userInfo:nil];
|
|
184
244
|
return NO;
|
|
185
245
|
}
|
|
186
246
|
|
|
247
|
+
self.setupInput = [[NSFileHandle alloc] initWithFileDescriptor:inputPipe[1] closeOnDealloc:YES];
|
|
187
248
|
self.pairProcessIdentifier = pid;
|
|
188
249
|
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC,
|
|
189
250
|
(uintptr_t)pid,
|
|
@@ -203,6 +264,10 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
203
264
|
int status = 0;
|
|
204
265
|
pid_t waited = waitpid(pid, &status, 0);
|
|
205
266
|
self.pairProcessIdentifier = 0;
|
|
267
|
+
if (self.setupInput) {
|
|
268
|
+
[self.setupInput closeFile];
|
|
269
|
+
self.setupInput = nil;
|
|
270
|
+
}
|
|
206
271
|
if (self.pairProcessSource) {
|
|
207
272
|
dispatch_source_cancel(self.pairProcessSource);
|
|
208
273
|
self.pairProcessSource = nil;
|
|
@@ -241,7 +306,7 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
241
306
|
NSTextField *brand = [self label:@"Sense Anything" size:16 weight:NSFontWeightSemibold frame:NSMakeRect(24, 480, 145, 28)];
|
|
242
307
|
[sidebar addSubview:brand];
|
|
243
308
|
|
|
244
|
-
NSArray<NSString *> *names = @[@"Welcome", @"Account", @"Recorder", @"Cameras", @"Local AI", @"Ready"];
|
|
309
|
+
NSArray<NSString *> *names = @[@"Welcome", @"Account", @"Account setup", @"Plan & Payment", @"Recorder", @"Cameras", @"Local AI", @"Ready"];
|
|
245
310
|
NSMutableArray<NSTextField *> *labels = [NSMutableArray array];
|
|
246
311
|
for (NSUInteger index = 0; index < names.count; index++) {
|
|
247
312
|
NSTextField *step = [self label:[NSString stringWithFormat:@"%lu %@", index + 1, names[index]]
|
|
@@ -300,12 +365,12 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
300
365
|
self.updateStatus.textColor = NSColor.secondaryLabelColor;
|
|
301
366
|
[root addSubview:self.updateStatus];
|
|
302
367
|
|
|
303
|
-
|
|
368
|
+
self.privacyLabel = [self label:@"Camera credentials stay on this computer. Cloud inference is off by default."
|
|
304
369
|
size:12
|
|
305
370
|
weight:NSFontWeightRegular
|
|
306
371
|
frame:NSMakeRect(236, 34, 300, 42)];
|
|
307
|
-
|
|
308
|
-
[root addSubview:
|
|
372
|
+
self.privacyLabel.textColor = NSColor.secondaryLabelColor;
|
|
373
|
+
[root addSubview:self.privacyLabel];
|
|
309
374
|
|
|
310
375
|
self.actionButton = [[NSButton alloc] initWithFrame:NSMakeRect(562, 36, 139, 40)];
|
|
311
376
|
self.actionButton.title = @"Start setup";
|
|
@@ -321,9 +386,229 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
321
386
|
self.closeButton.action = @selector(closePressed);
|
|
322
387
|
self.closeButton.hidden = YES;
|
|
323
388
|
[root addSubview:self.closeButton];
|
|
389
|
+
|
|
390
|
+
// Native onboarding panels. Provider-owned Google and Stripe UI opens in
|
|
391
|
+
// the system browser; every product-owned choice remains in this window.
|
|
392
|
+
self.accountPanel = [[NSView alloc] initWithFrame:NSMakeRect(236, 78, 465, 220)];
|
|
393
|
+
NSButton *google = [self panelButton:@"Continue with Google"
|
|
394
|
+
frame:NSMakeRect(0, 178, 465, 34)
|
|
395
|
+
action:@selector(googlePressed)];
|
|
396
|
+
[self.accountPanel addSubview:google];
|
|
397
|
+
NSTextField *orEmail = [self label:@"or use email" size:11 weight:NSFontWeightRegular frame:NSMakeRect(0, 153, 465, 18)];
|
|
398
|
+
orEmail.alignment = NSTextAlignmentCenter;
|
|
399
|
+
orEmail.textColor = NSColor.secondaryLabelColor;
|
|
400
|
+
[self.accountPanel addSubview:orEmail];
|
|
401
|
+
self.nameLabel = [self label:@"Full name" size:11 weight:NSFontWeightRegular frame:NSMakeRect(0, 132, 220, 17)];
|
|
402
|
+
[self.accountPanel addSubview:self.nameLabel];
|
|
403
|
+
self.nameInput = [self inputWithFrame:NSMakeRect(0, 103, 220, 28) placeholder:@"Maya Chen" secure:NO];
|
|
404
|
+
[self.accountPanel addSubview:self.nameInput];
|
|
405
|
+
NSTextField *emailLabel = [self label:@"Work email" size:11 weight:NSFontWeightRegular frame:NSMakeRect(235, 132, 230, 17)];
|
|
406
|
+
[self.accountPanel addSubview:emailLabel];
|
|
407
|
+
self.emailInput = [self inputWithFrame:NSMakeRect(235, 103, 230, 28) placeholder:@"maya@yourshop.com" secure:NO];
|
|
408
|
+
[self.accountPanel addSubview:self.emailInput];
|
|
409
|
+
self.passwordLabel = [self label:@"Create password" size:11 weight:NSFontWeightRegular frame:NSMakeRect(0, 78, 220, 17)];
|
|
410
|
+
[self.accountPanel addSubview:self.passwordLabel];
|
|
411
|
+
self.passwordInput = (NSSecureTextField *)[self inputWithFrame:NSMakeRect(0, 49, 220, 28) placeholder:@"At least 8 characters" secure:YES];
|
|
412
|
+
[self.accountPanel addSubview:self.passwordInput];
|
|
413
|
+
self.authModeButton = [self panelButton:@"Already have an account? Sign in"
|
|
414
|
+
frame:NSMakeRect(230, 49, 235, 28)
|
|
415
|
+
action:@selector(authModePressed)];
|
|
416
|
+
[self.accountPanel addSubview:self.authModeButton];
|
|
417
|
+
self.accountSubmit = [self panelButton:@"Create account"
|
|
418
|
+
frame:NSMakeRect(285, 5, 180, 34)
|
|
419
|
+
action:@selector(accountSubmitPressed)];
|
|
420
|
+
[self.accountPanel addSubview:self.accountSubmit];
|
|
421
|
+
[root addSubview:self.accountPanel];
|
|
422
|
+
|
|
423
|
+
self.termsPanel = [[NSView alloc] initWithFrame:NSMakeRect(236, 78, 465, 220)];
|
|
424
|
+
NSTextField *termsSummary = [self label:@"Review and accept the current Sense Anything terms, privacy notice, earner agreement, and data processing addendum for this account."
|
|
425
|
+
size:13 weight:NSFontWeightRegular frame:NSMakeRect(0, 150, 465, 58)];
|
|
426
|
+
termsSummary.textColor = NSColor.secondaryLabelColor;
|
|
427
|
+
[self.termsPanel addSubview:termsSummary];
|
|
428
|
+
self.termsCheck = [NSButton checkboxWithTitle:@"I reviewed and accept the current agreements shown for this account."
|
|
429
|
+
target:self
|
|
430
|
+
action:@selector(termsCheckPressed)];
|
|
431
|
+
self.termsCheck.frame = NSMakeRect(0, 103, 465, 38);
|
|
432
|
+
[self.termsPanel addSubview:self.termsCheck];
|
|
433
|
+
self.termsVersion = [self label:@"" size:11 weight:NSFontWeightRegular frame:NSMakeRect(24, 66, 430, 34)];
|
|
434
|
+
self.termsVersion.textColor = NSColor.secondaryLabelColor;
|
|
435
|
+
[self.termsPanel addSubview:self.termsVersion];
|
|
436
|
+
NSButton *reviewTerms = [self panelButton:@"Review terms"
|
|
437
|
+
frame:NSMakeRect(0, 39, 145, 24)
|
|
438
|
+
action:@selector(reviewTermsPressed)];
|
|
439
|
+
[self.termsPanel addSubview:reviewTerms];
|
|
440
|
+
NSButton *reviewPrivacy = [self panelButton:@"Review privacy"
|
|
441
|
+
frame:NSMakeRect(150, 39, 145, 24)
|
|
442
|
+
action:@selector(reviewPrivacyPressed)];
|
|
443
|
+
[self.termsPanel addSubview:reviewPrivacy];
|
|
444
|
+
NSButton *reviewDpa = [self panelButton:@"Review DPA"
|
|
445
|
+
frame:NSMakeRect(300, 39, 145, 24)
|
|
446
|
+
action:@selector(reviewDpaPressed)];
|
|
447
|
+
[self.termsPanel addSubview:reviewDpa];
|
|
448
|
+
self.termsSubmit = [self panelButton:@"Accept & continue"
|
|
449
|
+
frame:NSMakeRect(285, 5, 180, 34)
|
|
450
|
+
action:@selector(termsSubmitPressed)];
|
|
451
|
+
self.termsSubmit.enabled = NO;
|
|
452
|
+
[self.termsPanel addSubview:self.termsSubmit];
|
|
453
|
+
[root addSubview:self.termsPanel];
|
|
454
|
+
|
|
455
|
+
self.planPanel = [[NSView alloc] initWithFrame:NSMakeRect(236, 78, 465, 220)];
|
|
456
|
+
self.monthlyRadio = [NSButton radioButtonWithTitle:@"Monthly" target:self action:@selector(intervalPressed:)];
|
|
457
|
+
self.monthlyRadio.frame = NSMakeRect(0, 181, 100, 24);
|
|
458
|
+
self.monthlyRadio.state = NSControlStateValueOn;
|
|
459
|
+
[self.planPanel addSubview:self.monthlyRadio];
|
|
460
|
+
self.annualRadio = [NSButton radioButtonWithTitle:@"Annual" target:self action:@selector(intervalPressed:)];
|
|
461
|
+
self.annualRadio.frame = NSMakeRect(105, 181, 100, 24);
|
|
462
|
+
[self.planPanel addSubview:self.annualRadio];
|
|
463
|
+
self.standardRadio = [NSButton radioButtonWithTitle:@"" target:self action:@selector(planPressed:)];
|
|
464
|
+
self.standardRadio.frame = NSMakeRect(0, 120, 22, 24);
|
|
465
|
+
self.standardRadio.state = NSControlStateValueOn;
|
|
466
|
+
[self.planPanel addSubview:self.standardRadio];
|
|
467
|
+
self.standardPlan = [self label:@"Sense Anything" size:14 weight:NSFontWeightSemibold frame:NSMakeRect(28, 105, 195, 58)];
|
|
468
|
+
[self.planPanel addSubview:self.standardPlan];
|
|
469
|
+
self.multiRadio = [NSButton radioButtonWithTitle:@"" target:self action:@selector(planPressed:)];
|
|
470
|
+
self.multiRadio.frame = NSMakeRect(238, 120, 22, 24);
|
|
471
|
+
[self.planPanel addSubview:self.multiRadio];
|
|
472
|
+
self.multiPlan = [self label:@"Multi-site" size:14 weight:NSFontWeightSemibold frame:NSMakeRect(266, 105, 199, 58)];
|
|
473
|
+
[self.planPanel addSubview:self.multiPlan];
|
|
474
|
+
NSTextField *planNote = [self label:@"Stripe handles checkout securely in your browser. This installer resumes automatically when your plan is active."
|
|
475
|
+
size:11 weight:NSFontWeightRegular frame:NSMakeRect(0, 52, 465, 42)];
|
|
476
|
+
planNote.textColor = NSColor.secondaryLabelColor;
|
|
477
|
+
[self.planPanel addSubview:planNote];
|
|
478
|
+
NSButton *planSubmit = [self panelButton:@"Continue to checkout"
|
|
479
|
+
frame:NSMakeRect(265, 5, 200, 34)
|
|
480
|
+
action:@selector(planSubmitPressed)];
|
|
481
|
+
[self.planPanel addSubview:planSubmit];
|
|
482
|
+
[root addSubview:self.planPanel];
|
|
483
|
+
|
|
484
|
+
self.chooseAccountPanel = [[NSView alloc] initWithFrame:NSMakeRect(236, 78, 465, 220)];
|
|
485
|
+
self.accountPicker = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(0, 135, 465, 32) pullsDown:NO];
|
|
486
|
+
[self.chooseAccountPanel addSubview:self.accountPicker];
|
|
487
|
+
self.chooseAccountSubmit = [self panelButton:@"Continue with this account" frame:NSMakeRect(215, 0, 250, 34) action:@selector(chooseAccountPressed)];
|
|
488
|
+
[self.chooseAccountPanel addSubview:self.chooseAccountSubmit];
|
|
489
|
+
[root addSubview:self.chooseAccountPanel];
|
|
490
|
+
|
|
491
|
+
self.onboardingPanels = @[self.accountPanel, self.termsPanel, self.planPanel, self.chooseAccountPanel];
|
|
492
|
+
[self showOnboardingPanel:nil];
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
- (void)showOnboardingPanel:(NSView *)panel {
|
|
496
|
+
for (NSView *candidate in self.onboardingPanels ?: @[]) candidate.hidden = candidate != panel;
|
|
497
|
+
BOOL showStatus = panel == nil;
|
|
498
|
+
self.statusValue.hidden = !showStatus;
|
|
499
|
+
self.metric.hidden = !showStatus;
|
|
500
|
+
self.progress.hidden = !showStatus;
|
|
501
|
+
self.updateButton.hidden = !showStatus;
|
|
502
|
+
self.updateStatus.hidden = !showStatus;
|
|
503
|
+
self.privacyLabel.hidden = !showStatus;
|
|
504
|
+
self.actionButton.hidden = !showStatus;
|
|
505
|
+
self.closeButton.hidden = YES;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
- (void)chooseAccountPressed {
|
|
509
|
+
NSString *owner = self.accountPicker.selectedItem.representedObject;
|
|
510
|
+
if (!owner.length) return;
|
|
511
|
+
self.chooseAccountSubmit.enabled = NO;
|
|
512
|
+
[self sendOnboardingCommand:@{ @"action": @"select_account", @"ownerUserId": owner }];
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
- (void)sendOnboardingCommand:(NSDictionary *)command {
|
|
516
|
+
if (!self.setupInput || ![NSJSONSerialization isValidJSONObject:command]) return;
|
|
517
|
+
NSData *json = [NSJSONSerialization dataWithJSONObject:command options:0 error:nil];
|
|
518
|
+
if (!json) return;
|
|
519
|
+
NSMutableData *line = [json mutableCopy];
|
|
520
|
+
[line appendData:[@"\n" dataUsingEncoding:NSUTF8StringEncoding]];
|
|
521
|
+
@try {
|
|
522
|
+
[self.setupInput writeData:line];
|
|
523
|
+
} @catch (__unused NSException *exception) {
|
|
524
|
+
[self showLocalError:@"Could not continue setup"
|
|
525
|
+
message:@"Close and reopen Sense Anything Connector, then try again."];
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
- (void)googlePressed {
|
|
530
|
+
[self sendOnboardingCommand:@{ @"action": @"google_start" }];
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
- (void)authModePressed {
|
|
534
|
+
self.signingIn = !self.signingIn;
|
|
535
|
+
self.nameLabel.hidden = self.signingIn;
|
|
536
|
+
self.nameInput.hidden = self.signingIn;
|
|
537
|
+
self.passwordLabel.stringValue = self.signingIn ? @"Password" : @"Create password";
|
|
538
|
+
self.passwordInput.placeholderString = self.signingIn ? @"Your password" : @"At least 8 characters";
|
|
539
|
+
self.accountSubmit.title = self.signingIn ? @"Sign in & continue" : @"Create account";
|
|
540
|
+
self.authModeButton.title = self.signingIn ? @"New here? Create account" : @"Already have an account? Sign in";
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
- (void)showInputMessage:(NSString *)message {
|
|
544
|
+
NSAlert *alert = [[NSAlert alloc] init];
|
|
545
|
+
alert.messageText = @"Sense Anything";
|
|
546
|
+
alert.informativeText = message;
|
|
547
|
+
[alert addButtonWithTitle:@"OK"];
|
|
548
|
+
[alert runModal];
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
- (void)accountSubmitPressed {
|
|
552
|
+
NSString *email = [self.emailInput.stringValue stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
|
|
553
|
+
NSString *password = self.passwordInput.stringValue;
|
|
554
|
+
NSString *name = [self.nameInput.stringValue stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
|
|
555
|
+
if (email.length == 0 || password.length == 0) {
|
|
556
|
+
[self showInputMessage:@"Enter your email and password to continue."];
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (!self.signingIn && (name.length == 0 || password.length < 8)) {
|
|
560
|
+
[self showInputMessage:@"Enter your name and a password with at least 8 characters."];
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
NSDictionary *command = self.signingIn
|
|
564
|
+
? @{ @"action": @"email_sign_in", @"email": email, @"password": password }
|
|
565
|
+
: @{ @"action": @"email_sign_up", @"name": name, @"email": email, @"password": password };
|
|
566
|
+
[self sendOnboardingCommand:command];
|
|
567
|
+
self.passwordInput.stringValue = @"";
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
- (void)termsCheckPressed {
|
|
571
|
+
self.termsSubmit.enabled = self.termsCheck.state == NSControlStateValueOn;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
- (void)reviewTermsPressed {
|
|
575
|
+
[self openURL:@"https://trillboards.com/legal/terms"];
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
- (void)reviewPrivacyPressed {
|
|
579
|
+
[self openURL:@"https://trillboards.com/legal/privacy-policy"];
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
- (void)reviewDpaPressed {
|
|
583
|
+
[self openURL:@"https://trillboards.com/legal/data-processing"];
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
- (void)termsSubmitPressed {
|
|
587
|
+
if (self.termsCheck.state != NSControlStateValueOn) return;
|
|
588
|
+
self.termsSubmit.enabled = NO;
|
|
589
|
+
[self sendOnboardingCommand:@{ @"action": @"accept_terms" }];
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
- (void)planSubmitPressed {
|
|
593
|
+
NSButton *selected = self.multiRadio.state == NSControlStateValueOn ? self.multiRadio : self.standardRadio;
|
|
594
|
+
NSString *tier = selected.identifier;
|
|
595
|
+
if (tier.length == 0) return;
|
|
596
|
+
NSString *interval = self.annualRadio.state == NSControlStateValueOn ? @"annual" : @"monthly";
|
|
597
|
+
[self sendOnboardingCommand:@{ @"action": @"select_plan", @"tier": tier, @"interval": interval }];
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
- (void)intervalPressed:(NSButton *)sender {
|
|
601
|
+
self.monthlyRadio.state = sender == self.monthlyRadio ? NSControlStateValueOn : NSControlStateValueOff;
|
|
602
|
+
self.annualRadio.state = sender == self.annualRadio ? NSControlStateValueOn : NSControlStateValueOff;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
- (void)planPressed:(NSButton *)sender {
|
|
606
|
+
self.standardRadio.state = sender == self.standardRadio ? NSControlStateValueOn : NSControlStateValueOff;
|
|
607
|
+
self.multiRadio.state = sender == self.multiRadio ? NSControlStateValueOn : NSControlStateValueOff;
|
|
324
608
|
}
|
|
325
609
|
|
|
326
610
|
- (void)showWelcome {
|
|
611
|
+
[self showOnboardingPanel:nil];
|
|
327
612
|
self.lastStage = @"welcome";
|
|
328
613
|
self.headline.stringValue = @"Set up this computer for CCTV";
|
|
329
614
|
self.detail.stringValue = @"Connect your Sense Anything account, find your recorder, and verify every camera before screens are created.";
|
|
@@ -338,6 +623,7 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
338
623
|
}
|
|
339
624
|
|
|
340
625
|
- (void)showLocalError:(NSString *)title message:(NSString *)message {
|
|
626
|
+
[self showOnboardingPanel:nil];
|
|
341
627
|
self.lastStage = @"error";
|
|
342
628
|
self.headline.stringValue = title;
|
|
343
629
|
self.detail.stringValue = message;
|
|
@@ -450,7 +736,10 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
450
736
|
[self openURL:self.requiredUpdateURL];
|
|
451
737
|
return;
|
|
452
738
|
}
|
|
453
|
-
if ([self.lastStage isEqualToString:@"
|
|
739
|
+
if ([self.lastStage isEqualToString:@"onboarding_google"] ||
|
|
740
|
+
[self.lastStage isEqualToString:@"onboarding_checkout"]) {
|
|
741
|
+
[self sendOnboardingCommand:@{ @"action": @"refresh" }];
|
|
742
|
+
} else if ([self.lastStage isEqualToString:@"waiting_for_account"] ||
|
|
454
743
|
[self.lastStage isEqualToString:@"account_attention"]) {
|
|
455
744
|
[self openURL:self.portalURL];
|
|
456
745
|
} else if ([self.lastStage isEqualToString:@"ready"]) {
|
|
@@ -510,6 +799,7 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
510
799
|
- (void)renderState:(NSDictionary *)state {
|
|
511
800
|
NSString *stage = [state[@"stage"] isKindOfClass:NSString.class] ? state[@"stage"] : nil;
|
|
512
801
|
if (!stage) return;
|
|
802
|
+
NSString *previousStage = self.lastStage;
|
|
513
803
|
self.lastStage = stage;
|
|
514
804
|
if ([state[@"portalUrl"] isKindOfClass:NSString.class]) self.portalURL = state[@"portalUrl"];
|
|
515
805
|
if ([state[@"title"] isKindOfClass:NSString.class]) self.headline.stringValue = state[@"title"];
|
|
@@ -519,11 +809,85 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
519
809
|
NSNumber *validated = state[@"validatedFeeds"];
|
|
520
810
|
NSNumber *devices = state[@"devicesFound"];
|
|
521
811
|
NSNumber *rejected = state[@"rejectedFeeds"];
|
|
812
|
+
if ([stage isEqualToString:@"onboarding_account"]) {
|
|
813
|
+
[self showOnboardingPanel:self.accountPanel];
|
|
814
|
+
[self updateSteps:1];
|
|
815
|
+
self.accountSubmit.enabled = YES;
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
if ([stage isEqualToString:@"onboarding_choose_account"]) {
|
|
819
|
+
[self showOnboardingPanel:self.chooseAccountPanel];
|
|
820
|
+
[self updateSteps:2];
|
|
821
|
+
[self.accountPicker removeAllItems];
|
|
822
|
+
NSArray *accounts = [state[@"accounts"] isKindOfClass:NSArray.class] ? state[@"accounts"] : @[];
|
|
823
|
+
for (NSDictionary *account in accounts) {
|
|
824
|
+
NSString *label = [NSString stringWithFormat:@"%@ (%@)", account[@"label"] ?: @"Account", account[@"role"] ?: @""];
|
|
825
|
+
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:label action:nil keyEquivalent:@""];
|
|
826
|
+
item.representedObject = account[@"ownerUserId"];
|
|
827
|
+
[self.accountPicker.menu addItem:item];
|
|
828
|
+
}
|
|
829
|
+
if (accounts.count) [self.accountPicker selectItemAtIndex:0];
|
|
830
|
+
self.chooseAccountSubmit.enabled = accounts.count > 0;
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
if ([stage isEqualToString:@"onboarding_terms"]) {
|
|
834
|
+
[self showOnboardingPanel:self.termsPanel];
|
|
835
|
+
[self updateSteps:2];
|
|
836
|
+
if (![previousStage isEqualToString:@"onboarding_terms"]) {
|
|
837
|
+
self.termsCheck.state = NSControlStateValueOff;
|
|
838
|
+
self.termsSubmit.enabled = NO;
|
|
839
|
+
}
|
|
840
|
+
self.termsSubmit.enabled = self.termsCheck.state == NSControlStateValueOn;
|
|
841
|
+
NSArray *agreements = [state[@"requiredAgreements"] isKindOfClass:NSArray.class] ? state[@"requiredAgreements"] : @[];
|
|
842
|
+
NSMutableArray<NSString *> *versions = [NSMutableArray array];
|
|
843
|
+
for (NSDictionary *agreement in agreements) {
|
|
844
|
+
if (![agreement isKindOfClass:NSDictionary.class]) continue;
|
|
845
|
+
NSString *type = [agreement[@"type"] isKindOfClass:NSString.class] ? agreement[@"type"] : @"";
|
|
846
|
+
NSString *label = [agreement[@"label"] isKindOfClass:NSString.class] ? agreement[@"label"] : [type stringByReplacingOccurrencesOfString:@"_" withString:@" "];
|
|
847
|
+
NSString *version = [agreement[@"version"] isKindOfClass:NSString.class] ? agreement[@"version"] : @"";
|
|
848
|
+
if (type.length && version.length) [versions addObject:[NSString stringWithFormat:@"%@ v%@", label, version]];
|
|
849
|
+
}
|
|
850
|
+
self.termsVersion.stringValue = [versions componentsJoinedByString:@" • "];
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
if ([stage isEqualToString:@"onboarding_plan"]) {
|
|
854
|
+
[self showOnboardingPanel:self.planPanel];
|
|
855
|
+
[self updateSteps:3];
|
|
856
|
+
NSArray *plans = [state[@"plans"] isKindOfClass:NSArray.class] ? state[@"plans"] : @[];
|
|
857
|
+
if (plans.count > 0) {
|
|
858
|
+
NSDictionary *first = plans[0];
|
|
859
|
+
self.standardRadio.identifier = [first[@"tier"] isKindOfClass:NSString.class] ? first[@"tier"] : @"";
|
|
860
|
+
self.standardPlan.stringValue = [NSString stringWithFormat:@"%@\n%@/month • %@/year",
|
|
861
|
+
first[@"label"] ?: @"Sense Anything", first[@"monthly"] ?: @"—", first[@"annual"] ?: @"—"];
|
|
862
|
+
self.standardRadio.hidden = NO;
|
|
863
|
+
self.standardPlan.hidden = NO;
|
|
864
|
+
}
|
|
865
|
+
if (plans.count > 1) {
|
|
866
|
+
NSDictionary *second = plans[1];
|
|
867
|
+
self.multiRadio.identifier = [second[@"tier"] isKindOfClass:NSString.class] ? second[@"tier"] : @"";
|
|
868
|
+
self.multiPlan.stringValue = [NSString stringWithFormat:@"%@\n%@/month • %@/year",
|
|
869
|
+
second[@"label"] ?: @"Multi-site", second[@"monthly"] ?: @"—", second[@"annual"] ?: @"—"];
|
|
870
|
+
self.multiRadio.hidden = NO;
|
|
871
|
+
self.multiPlan.hidden = NO;
|
|
872
|
+
} else {
|
|
873
|
+
self.multiRadio.hidden = YES;
|
|
874
|
+
self.multiPlan.hidden = YES;
|
|
875
|
+
}
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
[self showOnboardingPanel:nil];
|
|
522
880
|
if (([stage isEqualToString:@"waiting_for_account"] ||
|
|
523
881
|
[stage isEqualToString:@"account_attention"]) &&
|
|
524
882
|
[state[@"displayCode"] isKindOfClass:NSString.class]) {
|
|
525
883
|
self.statusValue.font = [NSFont monospacedSystemFontOfSize:25 weight:NSFontWeightSemibold];
|
|
526
884
|
self.statusValue.stringValue = state[@"displayCode"];
|
|
885
|
+
} else if ([stage isEqualToString:@"onboarding_google"]) {
|
|
886
|
+
self.statusValue.stringValue = @"Waiting for Google";
|
|
887
|
+
} else if ([stage isEqualToString:@"onboarding_checkout"]) {
|
|
888
|
+
self.statusValue.stringValue = @"Waiting for Stripe";
|
|
889
|
+
} else if ([stage isEqualToString:@"onboarding_install"]) {
|
|
890
|
+
self.statusValue.stringValue = @"Onboarding complete";
|
|
527
891
|
} else if (cameraCount.integerValue > 0) {
|
|
528
892
|
self.statusValue.font = [NSFont systemFontOfSize:25 weight:NSFontWeightSemibold];
|
|
529
893
|
self.statusValue.stringValue = [NSString stringWithFormat:@"%ld camera%@", cameraCount.integerValue, cameraCount.integerValue == 1 ? @"" : @"s"];
|
|
@@ -566,7 +930,16 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
566
930
|
self.progress.doubleValue = [self progressForStage:stage];
|
|
567
931
|
[self updateSteps:[self stepForStage:stage]];
|
|
568
932
|
self.closeButton.hidden = ![stage isEqualToString:@"ready"];
|
|
569
|
-
if ([stage isEqualToString:@"
|
|
933
|
+
if ([stage isEqualToString:@"onboarding_google"]) {
|
|
934
|
+
self.actionButton.title = @"Use another sign-in";
|
|
935
|
+
self.actionButton.enabled = YES;
|
|
936
|
+
} else if ([stage isEqualToString:@"onboarding_checkout"]) {
|
|
937
|
+
self.actionButton.title = @"Back to plans";
|
|
938
|
+
self.actionButton.enabled = YES;
|
|
939
|
+
} else if ([stage isEqualToString:@"onboarding_install"]) {
|
|
940
|
+
self.actionButton.title = @"Preparing cameras…";
|
|
941
|
+
self.actionButton.enabled = NO;
|
|
942
|
+
} else if ([stage isEqualToString:@"waiting_for_account"]) {
|
|
570
943
|
self.actionButton.title = @"Open Sense Anything";
|
|
571
944
|
self.actionButton.enabled = YES;
|
|
572
945
|
} else if ([stage isEqualToString:@"account_attention"]) {
|
|
@@ -592,25 +965,29 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
592
965
|
}
|
|
593
966
|
|
|
594
967
|
- (NSInteger)stepForStage:(NSString *)stage {
|
|
595
|
-
if ([@[@"
|
|
596
|
-
if ([@[@"
|
|
597
|
-
if ([@[@"
|
|
598
|
-
if ([@[@"
|
|
599
|
-
if ([
|
|
968
|
+
if ([@[@"onboarding_account", @"onboarding_google"] containsObject:stage]) return 1;
|
|
969
|
+
if ([@[@"onboarding_terms", @"onboarding_choose_account"] containsObject:stage]) return 2;
|
|
970
|
+
if ([@[@"onboarding_plan", @"onboarding_checkout"] containsObject:stage]) return 3;
|
|
971
|
+
if ([@[@"onboarding_install", @"starting", @"waiting_for_account", @"account_attention", @"account_connected", @"discovering", @"devices_found"] containsObject:stage]) return 4;
|
|
972
|
+
if ([@[@"validating", @"cameras_created"] containsObject:stage]) return 5;
|
|
973
|
+
if ([@[@"local_ai", @"local_ai_error", @"starting_service"] containsObject:stage]) return 6;
|
|
974
|
+
if ([stage isEqualToString:@"ready"]) return 7;
|
|
600
975
|
return 0;
|
|
601
976
|
}
|
|
602
977
|
|
|
603
978
|
- (double)progressForStage:(NSString *)stage {
|
|
604
979
|
NSDictionary<NSString *, NSNumber *> *values = @{
|
|
605
|
-
@"
|
|
606
|
-
@"
|
|
980
|
+
@"onboarding_account": @10, @"onboarding_google": @12, @"onboarding_terms": @25,
|
|
981
|
+
@"onboarding_plan": @42, @"onboarding_checkout": @50, @"onboarding_install": @65,
|
|
982
|
+
@"waiting_for_account": @65, @"account_attention": @65, @"account_connected": @65, @"discovering": @68,
|
|
983
|
+
@"devices_found": @70, @"validating": @75, @"cameras_created": @80,
|
|
607
984
|
@"local_ai": @88, @"starting_service": @94, @"ready": @100
|
|
608
985
|
};
|
|
609
986
|
return values[stage] ? values[stage].doubleValue : 8;
|
|
610
987
|
}
|
|
611
988
|
|
|
612
989
|
- (void)updateSteps:(NSInteger)active {
|
|
613
|
-
NSArray<NSString *> *names = @[@"Welcome", @"Account", @"Recorder", @"Cameras", @"Local AI", @"Ready"];
|
|
990
|
+
NSArray<NSString *> *names = @[@"Welcome", @"Account", @"Account setup", @"Plan & Payment", @"Recorder", @"Cameras", @"Local AI", @"Ready"];
|
|
614
991
|
for (NSInteger index = 0; index < (NSInteger)self.stepLabels.count; index++) {
|
|
615
992
|
NSTextField *field = self.stepLabels[(NSUInteger)index];
|
|
616
993
|
NSString *prefix = index < active ? @"OK" : [NSString stringWithFormat:@"%ld", index + 1];
|
|
@@ -363,7 +363,12 @@ if [ "${SETUP_MODE:-0}" = "1" ]; then
|
|
|
363
363
|
plog "setup: starting validate-first setup mode"
|
|
364
364
|
set +e
|
|
365
365
|
if is_ui; then
|
|
366
|
-
|
|
366
|
+
# The native app owns account, legal and plan selection. This long-lived
|
|
367
|
+
# process receives commands on inherited stdin and exits only after the API
|
|
368
|
+
# says legal + entitlement are current, so cameras cannot open early.
|
|
369
|
+
# The same process keeps the authenticated session in memory through camera
|
|
370
|
+
# setup and mints a one-use signed-in browser handoff at first launch.
|
|
371
|
+
"$LAUNCHER" installer-onboarding --config "$CONFIG" --status-file "$UI_STATUS_FILE"
|
|
367
372
|
else
|
|
368
373
|
"$LAUNCHER" setup --config "$CONFIG"
|
|
369
374
|
fi
|
|
@@ -148,7 +148,13 @@ REM ── Validate-first setup: the CLI displays the code, keeps this window's
|
|
|
148
148
|
REM staged progress, and exits 0 once the portal finalizes cameras and the
|
|
149
149
|
REM config is written — then the SAME honest service stages below run.
|
|
150
150
|
if defined UI_STATUS_FILE (
|
|
151
|
-
|
|
151
|
+
REM Account, current legal versions and paid entitlement are completed in
|
|
152
|
+
REM the native installer BEFORE camera setup is allowed to open the portal.
|
|
153
|
+
REM Commands arrive on inherited stdin; passwords never touch argv or disk.
|
|
154
|
+
REM The onboarding process keeps its authenticated session in memory, starts
|
|
155
|
+
REM camera setup itself, and wraps the first portal launch in a one-use login
|
|
156
|
+
REM handoff. Email users therefore do not sign in a second time in the browser.
|
|
157
|
+
call "%~dp0trillboards-edge.cmd" installer-onboarding --config "%CFG%" --status-file "%UI_STATUS_FILE%"
|
|
152
158
|
) else (
|
|
153
159
|
call "%~dp0trillboards-edge.cmd" setup --config "%CFG%"
|
|
154
160
|
)
|