@trillboards/edge-sdk 0.2.138 → 0.2.140
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/macos/TrillboardsSetupApp.m +131 -3
- package/deploy/macos/build-macos-pkg.sh +3 -0
- package/deploy/setup-guidance.json +112 -0
- package/deploy/windows/build-windows.ps1 +1 -0
- package/deploy/windows/trillboards-setup.ps1 +101 -2
- package/dist/EdgeAgent.js +1 -1
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.js +1 -1
- package/package.json +1 -1
|
@@ -40,6 +40,14 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
40
40
|
|
|
41
41
|
@interface TrillboardsSetupDelegate : NSObject <NSApplicationDelegate, NSWindowDelegate, WKNavigationDelegate, WKUIDelegate>
|
|
42
42
|
@property(nonatomic, strong) NSWindow *window;
|
|
43
|
+
@property(nonatomic, strong) NSDictionary *guidanceCatalog;
|
|
44
|
+
@property(nonatomic, strong) NSView *guidancePanel;
|
|
45
|
+
@property(nonatomic, strong) NSTextField *guidanceTitle;
|
|
46
|
+
@property(nonatomic, strong) NSTextField *guidanceBody;
|
|
47
|
+
@property(nonatomic, strong) NSButton *guidanceToggle;
|
|
48
|
+
@property(nonatomic, strong) NSButton *guidanceRestore;
|
|
49
|
+
@property(nonatomic, strong) NSMutableSet<NSString *> *dismissedGuidance;
|
|
50
|
+
|
|
43
51
|
@property(nonatomic, strong) NSFileHandle *setupOutput;
|
|
44
52
|
@property(nonatomic, strong) NSMutableData *setupOutputBuffer;
|
|
45
53
|
@property(nonatomic, strong) NSWindow *cameraWindow;
|
|
@@ -327,14 +335,14 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
327
335
|
|
|
328
336
|
- (void)buildWindow {
|
|
329
337
|
self.window = [[NSWindow alloc]
|
|
330
|
-
initWithContentRect:NSMakeRect(0, 0, 760,
|
|
338
|
+
initWithContentRect:NSMakeRect(0, 0, 760, 700)
|
|
331
339
|
styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable
|
|
332
340
|
backing:NSBackingStoreBuffered
|
|
333
341
|
defer:NO];
|
|
334
342
|
self.window.title = @"Sense Anything Connector";
|
|
335
343
|
self.window.delegate = self;
|
|
336
|
-
self.window.minSize = NSMakeSize(760,
|
|
337
|
-
self.window.maxSize = NSMakeSize(760,
|
|
344
|
+
self.window.minSize = NSMakeSize(760, 700);
|
|
345
|
+
self.window.maxSize = NSMakeSize(760, 700);
|
|
338
346
|
[self.window center];
|
|
339
347
|
|
|
340
348
|
NSView *root = self.window.contentView;
|
|
@@ -536,10 +544,85 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
536
544
|
[self.chooseAccountPanel addSubview:self.chooseAccountSubmit];
|
|
537
545
|
[root addSubview:self.chooseAccountPanel];
|
|
538
546
|
|
|
547
|
+
// Reserve a separate help region; never cover native forms or recovery actions.
|
|
548
|
+
for (NSView *view in root.subviews) {
|
|
549
|
+
NSRect frame = view.frame;
|
|
550
|
+
frame.origin.y += 160;
|
|
551
|
+
view.frame = frame;
|
|
552
|
+
}
|
|
553
|
+
NSView *guideSidebar = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 190, 160)];
|
|
554
|
+
guideSidebar.wantsLayer = YES;
|
|
555
|
+
guideSidebar.layer.backgroundColor = NSColor.controlBackgroundColor.CGColor;
|
|
556
|
+
[root addSubview:guideSidebar];
|
|
557
|
+
NSString *guidePath = [NSBundle.mainBundle pathForResource:@"setup-guidance" ofType:@"json"];
|
|
558
|
+
NSData *guideData = guidePath ? [NSData dataWithContentsOfFile:guidePath] : nil;
|
|
559
|
+
id catalog = guideData ? [NSJSONSerialization JSONObjectWithData:guideData options:0 error:nil] : nil;
|
|
560
|
+
self.guidanceCatalog = [catalog isKindOfClass:NSDictionary.class] ? catalog : @{};
|
|
561
|
+
self.dismissedGuidance = [NSMutableSet set];
|
|
562
|
+
self.guidanceToggle = [NSButton checkboxWithTitle:@"Guiding tips" target:self action:@selector(toggleGuidance)];
|
|
563
|
+
self.guidanceToggle.frame = NSMakeRect(24, 118, 150, 24);
|
|
564
|
+
self.guidanceToggle.state = [NSUserDefaults.standardUserDefaults boolForKey:@"setupGuidingTipsDisabled"] ? NSControlStateValueOff : NSControlStateValueOn;
|
|
565
|
+
[root addSubview:self.guidanceToggle];
|
|
566
|
+
self.guidanceRestore = [self panelButton:@"Show this tip" frame:NSMakeRect(20, 80, 150, 28) action:@selector(restoreGuidance)];
|
|
567
|
+
[root addSubview:self.guidanceRestore];
|
|
568
|
+
self.guidancePanel = [[NSView alloc] initWithFrame:NSMakeRect(236, 12, 465, 145)];
|
|
569
|
+
self.guidancePanel.wantsLayer = YES;
|
|
570
|
+
self.guidancePanel.layer.backgroundColor = [NSColor colorWithCalibratedRed:1 green:0.96 blue:0.91 alpha:1].CGColor;
|
|
571
|
+
self.guidancePanel.layer.borderColor = [NSColor colorWithCalibratedRed:0.93 green:0.65 blue:0.40 alpha:1].CGColor;
|
|
572
|
+
self.guidancePanel.layer.borderWidth = 1;
|
|
573
|
+
self.guidancePanel.layer.cornerRadius = 10;
|
|
574
|
+
self.guidanceTitle = [self label:@"" size:13 weight:NSFontWeightSemibold frame:NSMakeRect(14, 94, 405, 40)];
|
|
575
|
+
self.guidanceBody = [self label:@"" size:12 weight:NSFontWeightRegular frame:NSMakeRect(14, 35, 435, 60)];
|
|
576
|
+
self.guidanceTitle.textColor = [NSColor colorWithCalibratedRed:0.28 green:0.20 blue:0.14 alpha:1];
|
|
577
|
+
self.guidanceBody.textColor = [NSColor colorWithCalibratedRed:0.38 green:0.29 blue:0.23 alpha:1];
|
|
578
|
+
[self.guidancePanel addSubview:self.guidanceTitle];
|
|
579
|
+
[self.guidancePanel addSubview:self.guidanceBody];
|
|
580
|
+
NSButton *dismiss = [self panelButton:@"×" frame:NSMakeRect(424, 110, 30, 26) action:@selector(dismissGuidance)];
|
|
581
|
+
dismiss.accessibilityLabel = @"Dismiss this tip";
|
|
582
|
+
[self.guidancePanel addSubview:dismiss];
|
|
583
|
+
[self.guidancePanel addSubview:[self panelButton:@"More help" frame:NSMakeRect(8, 5, 110, 26) action:@selector(showGuidanceDetail)]];
|
|
584
|
+
[root addSubview:self.guidancePanel];
|
|
585
|
+
|
|
539
586
|
self.onboardingPanels = @[self.accountPanel, self.termsPanel, self.planPanel, self.chooseAccountPanel];
|
|
540
587
|
[self showOnboardingPanel:nil];
|
|
541
588
|
}
|
|
542
589
|
|
|
590
|
+
- (void)renderGuidance {
|
|
591
|
+
NSDictionary *guide = self.guidanceCatalog[self.lastStage];
|
|
592
|
+
BOOL enabled = self.guidanceToggle.state == NSControlStateValueOn;
|
|
593
|
+
BOOL dismissed = [self.dismissedGuidance containsObject:self.lastStage];
|
|
594
|
+
self.guidancePanel.hidden = !enabled || dismissed || ![guide isKindOfClass:NSDictionary.class];
|
|
595
|
+
self.guidanceRestore.hidden = !enabled || !dismissed;
|
|
596
|
+
self.guidanceTitle.stringValue = [guide[@"title"] isKindOfClass:NSString.class] ? guide[@"title"] : @"";
|
|
597
|
+
self.guidanceBody.stringValue = [guide[@"body"] isKindOfClass:NSString.class] ? guide[@"body"] : @"";
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
- (void)toggleGuidance {
|
|
601
|
+
[NSUserDefaults.standardUserDefaults setBool:self.guidanceToggle.state != NSControlStateValueOn forKey:@"setupGuidingTipsDisabled"];
|
|
602
|
+
[self.dismissedGuidance removeAllObjects];
|
|
603
|
+
[self renderGuidance];
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
- (void)dismissGuidance {
|
|
607
|
+
[self.dismissedGuidance addObject:self.lastStage];
|
|
608
|
+
[self renderGuidance];
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
- (void)restoreGuidance {
|
|
612
|
+
[self.dismissedGuidance removeObject:self.lastStage];
|
|
613
|
+
[self renderGuidance];
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
- (void)showGuidanceDetail {
|
|
617
|
+
NSDictionary *guide = self.guidanceCatalog[self.lastStage];
|
|
618
|
+
if (![guide[@"detail"] isKindOfClass:NSString.class]) return;
|
|
619
|
+
NSAlert *alert = [[NSAlert alloc] init];
|
|
620
|
+
alert.messageText = self.guidanceTitle.stringValue;
|
|
621
|
+
alert.informativeText = guide[@"detail"];
|
|
622
|
+
[alert addButtonWithTitle:@"Got it"];
|
|
623
|
+
[alert beginSheetModalForWindow:self.window completionHandler:nil];
|
|
624
|
+
}
|
|
625
|
+
|
|
543
626
|
- (void)showOnboardingPanel:(NSView *)panel {
|
|
544
627
|
for (NSView *candidate in self.onboardingPanels ?: @[]) candidate.hidden = candidate != panel;
|
|
545
628
|
BOOL showStatus = panel == nil;
|
|
@@ -663,6 +746,7 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
663
746
|
- (void)showWelcome {
|
|
664
747
|
[self showOnboardingPanel:nil];
|
|
665
748
|
self.lastStage = @"welcome";
|
|
749
|
+
[self renderGuidance];
|
|
666
750
|
self.headline.stringValue = @"Set up this computer for CCTV";
|
|
667
751
|
self.detail.stringValue = @"Connect your Sense Anything account, find your recorder, and verify every camera before screens are created.";
|
|
668
752
|
self.statusValue.stringValue = @"About 5 minutes";
|
|
@@ -678,6 +762,7 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
678
762
|
- (void)showLocalError:(NSString *)title message:(NSString *)message {
|
|
679
763
|
[self showOnboardingPanel:nil];
|
|
680
764
|
self.lastStage = @"error";
|
|
765
|
+
[self renderGuidance];
|
|
681
766
|
self.headline.stringValue = title;
|
|
682
767
|
self.detail.stringValue = message;
|
|
683
768
|
self.statusValue.stringValue = @"Setup stopped";
|
|
@@ -763,6 +848,7 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
763
848
|
}
|
|
764
849
|
self.requiredUpdateURL = downloadURL;
|
|
765
850
|
self.lastStage = @"update_required";
|
|
851
|
+
[self renderGuidance];
|
|
766
852
|
self.headline.stringValue = @"Update required before setup can continue";
|
|
767
853
|
self.detail.stringValue = @"Install the latest Sense Anything Connector, then reopen it to finish camera setup.";
|
|
768
854
|
self.statusValue.stringValue = [NSString stringWithFormat:@"Version %@", availableText];
|
|
@@ -835,6 +921,7 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
835
921
|
if (![files fileExistsAtPath:logPath]) [files createFileAtPath:logPath contents:nil attributes:nil];
|
|
836
922
|
|
|
837
923
|
self.lastStage = @"starting";
|
|
924
|
+
[self renderGuidance];
|
|
838
925
|
self.setupStarted = YES;
|
|
839
926
|
self.headline.stringValue = @"Starting camera setup";
|
|
840
927
|
self.detail.stringValue = @"Connecting securely to Sense Anything.";
|
|
@@ -868,6 +955,7 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
868
955
|
if (!stage) return;
|
|
869
956
|
NSString *previousStage = self.lastStage;
|
|
870
957
|
self.lastStage = stage;
|
|
958
|
+
[self renderGuidance];
|
|
871
959
|
if ([state[@"portalUrl"] isKindOfClass:NSString.class]) self.portalURL = state[@"portalUrl"];
|
|
872
960
|
if ([state[@"title"] isKindOfClass:NSString.class]) self.headline.stringValue = state[@"title"];
|
|
873
961
|
if ([state[@"message"] isKindOfClass:NSString.class]) self.detail.stringValue = state[@"message"];
|
|
@@ -1168,6 +1256,43 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
1168
1256
|
|
|
1169
1257
|
@end
|
|
1170
1258
|
|
|
1259
|
+
static int runGuidanceSmokeTest(void) {
|
|
1260
|
+
[NSApplication sharedApplication];
|
|
1261
|
+
TrillboardsSetupDelegate *delegate = [[TrillboardsSetupDelegate alloc] init];
|
|
1262
|
+
[delegate buildWindow];
|
|
1263
|
+
if (delegate.guidanceCatalog.count == 0) return 1;
|
|
1264
|
+
delegate.guidanceToggle.state = NSControlStateValueOn;
|
|
1265
|
+
for (NSString *stage in delegate.guidanceCatalog) {
|
|
1266
|
+
delegate.lastStage = stage;
|
|
1267
|
+
[delegate renderGuidance];
|
|
1268
|
+
if (delegate.guidancePanel.hidden || !delegate.guidanceTitle.stringValue.length) return 2;
|
|
1269
|
+
for (NSTextField *field in @[delegate.guidanceTitle, delegate.guidanceBody]) {
|
|
1270
|
+
NSRect textBounds = [field.attributedStringValue boundingRectWithSize:NSMakeSize(field.frame.size.width - 4, CGFLOAT_MAX)
|
|
1271
|
+
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading];
|
|
1272
|
+
if (textBounds.size.height > field.frame.size.height) {
|
|
1273
|
+
fprintf(stderr, "Guidance text clipped at %s\n", stage.UTF8String);
|
|
1274
|
+
return 3;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
[delegate dismissGuidance];
|
|
1278
|
+
if (!delegate.guidancePanel.hidden || delegate.guidanceRestore.hidden) return 4;
|
|
1279
|
+
[delegate restoreGuidance];
|
|
1280
|
+
if (delegate.guidancePanel.hidden) return 5;
|
|
1281
|
+
}
|
|
1282
|
+
delegate.lastStage = @"onboarding_terms";
|
|
1283
|
+
delegate.termsCheck.state = NSControlStateValueOff;
|
|
1284
|
+
delegate.termsSubmit.enabled = NO;
|
|
1285
|
+
delegate.guidanceToggle.state = NSControlStateValueOff;
|
|
1286
|
+
[delegate renderGuidance];
|
|
1287
|
+
if (!delegate.guidancePanel.hidden || delegate.termsSubmit.enabled) return 6;
|
|
1288
|
+
if (NSIntersectsRect(delegate.guidancePanel.frame, delegate.accountPanel.frame) ||
|
|
1289
|
+
NSIntersectsRect(delegate.guidancePanel.frame, delegate.actionButton.frame)) return 7;
|
|
1290
|
+
fprintf(stdout, "Native guidance stages, dismissal, text bounds and consent isolation passed.\n");
|
|
1291
|
+
fflush(stdout);
|
|
1292
|
+
[delegate.window close];
|
|
1293
|
+
return 0;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1171
1296
|
static int runCancellationSmokeTest(void) {
|
|
1172
1297
|
NSString *pidPath = [NSTemporaryDirectory()
|
|
1173
1298
|
stringByAppendingPathComponent:[NSString stringWithFormat:@"trillboards-setup-child-%@.pid", NSUUID.UUID.UUIDString]];
|
|
@@ -1288,6 +1413,9 @@ int main(int argc, const char *argv[]) {
|
|
|
1288
1413
|
if (argc == 3 && [[NSString stringWithUTF8String:argv[1]] isEqualToString:@"--smoke-test-ui-pipe"]) {
|
|
1289
1414
|
return runNativePipeSmokeTest([NSString stringWithUTF8String:argv[2]]);
|
|
1290
1415
|
}
|
|
1416
|
+
if (argc == 2 && [[NSString stringWithUTF8String:argv[1]] isEqualToString:@"--smoke-test-guidance"]) {
|
|
1417
|
+
return runGuidanceSmokeTest();
|
|
1418
|
+
}
|
|
1291
1419
|
if (argc == 2 && [[NSString stringWithUTF8String:argv[1]] isEqualToString:@"--smoke-test-cancel"]) {
|
|
1292
1420
|
return runCancellationSmokeTest();
|
|
1293
1421
|
}
|
|
@@ -490,6 +490,7 @@ SETUP_APP_BIN="$SETUP_APP_MACOS/Sense Anything Connector"
|
|
|
490
490
|
mkdir -p "$SETUP_APP_MACOS" "$SETUP_APP_RESOURCES"
|
|
491
491
|
cp "$PKG_ROOT/deploy/branding/SenseAnythingConnector.icns" \
|
|
492
492
|
"$SETUP_APP_RESOURCES/SenseAnythingConnector.icns"
|
|
493
|
+
cp "$PKG_ROOT/deploy/setup-guidance.json" "$SETUP_APP_RESOURCES/setup-guidance.json"
|
|
493
494
|
log "Building native Sense Anything Connector.app…"
|
|
494
495
|
if [ "$UNIVERSAL" -eq 1 ]; then
|
|
495
496
|
clang -fobjc-arc -Wall -Wextra -Werror -O2 -mmacosx-version-min=12.0 -arch arm64 -arch x86_64 -framework Cocoa -framework WebKit \
|
|
@@ -536,6 +537,8 @@ cat > "$SETUP_APP/Contents/Info.plist" <<PLIST
|
|
|
536
537
|
</plist>
|
|
537
538
|
PLIST
|
|
538
539
|
plutil -lint "$SETUP_APP/Contents/Info.plist" >/dev/null || die "native setup app Info.plist is invalid"
|
|
540
|
+
"$SETUP_APP_BIN" --smoke-test-guidance \
|
|
541
|
+
|| die "native setup guidance smoke test failed"
|
|
539
542
|
file "$SETUP_APP_BIN" | grep -q 'Mach-O' || die "native setup app executable is not Mach-O"
|
|
540
543
|
log "Native setup app staged: $SETUP_APP"
|
|
541
544
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
{
|
|
2
|
+
"welcome": {
|
|
3
|
+
"title": "Start on the computer that will stay here.",
|
|
4
|
+
"body": "Keep this computer powered and connected to the camera network. Have your recorder login ready.",
|
|
5
|
+
"detail": "A recorder is the box your cameras connect to, often called an NVR or DVR. Internet access alone does not mean this computer can reach it."
|
|
6
|
+
},
|
|
7
|
+
"starting": {
|
|
8
|
+
"title": "Start on the computer that will stay here.",
|
|
9
|
+
"body": "Keep this computer powered and connected to the camera network. Have your recorder login ready.",
|
|
10
|
+
"detail": "A recorder is the box your cameras connect to, often called an NVR or DVR. Internet access alone does not mean this computer can reach it."
|
|
11
|
+
},
|
|
12
|
+
"onboarding_account": {
|
|
13
|
+
"title": "Choose email or Google.",
|
|
14
|
+
"body": "Use your Sense Anything email and password, or continue with Google. New here? Create an account with either option.",
|
|
15
|
+
"detail": "Your Sense Anything login connects your account. Later, camera setup asks for the separate login for your recorder or camera."
|
|
16
|
+
},
|
|
17
|
+
"onboarding_google": {
|
|
18
|
+
"title": "Finish signing in in the setup browser tab.",
|
|
19
|
+
"body": "Keep the setup tab open, complete Google sign-in, then return to this window. Your setup stays here.",
|
|
20
|
+
"detail": "Choose the intended Google account. If the handoff does not complete, use the recovery action in this window instead of reinstalling."
|
|
21
|
+
},
|
|
22
|
+
"onboarding_choose_account": {
|
|
23
|
+
"title": "Check which account will own the cameras.",
|
|
24
|
+
"body": "Choose the account for this location before continuing. Use an account you are authorized to manage.",
|
|
25
|
+
"detail": "If you manage several businesses, check the account name. Follow any account-access notice above before proceeding."
|
|
26
|
+
},
|
|
27
|
+
"account_attention": {
|
|
28
|
+
"title": "Check which account will own the cameras.",
|
|
29
|
+
"body": "Choose the account for this location before continuing. Use an account you are authorized to manage.",
|
|
30
|
+
"detail": "If you manage several businesses, check the account name. Follow any account-access notice above before proceeding."
|
|
31
|
+
},
|
|
32
|
+
"onboarding_terms": {
|
|
33
|
+
"title": "Review the agreements before continuing.",
|
|
34
|
+
"body": "Open the agreement links and review the current terms. Accept only when you are authorized to do so.",
|
|
35
|
+
"detail": "Guiding tips are optional. Turning them off never accepts an agreement or changes a required account step."
|
|
36
|
+
},
|
|
37
|
+
"onboarding_plan": {
|
|
38
|
+
"title": "Choose the plan for this account.",
|
|
39
|
+
"body": "Check the plan and billing interval before continuing to secure Stripe checkout.",
|
|
40
|
+
"detail": "Existing subscription access is checked automatically. If payment is needed, complete checkout in the setup browser and return here."
|
|
41
|
+
},
|
|
42
|
+
"onboarding_checkout": {
|
|
43
|
+
"title": "Complete checkout in your browser.",
|
|
44
|
+
"body": "Keep the setup browser tab open. The Connector continues after subscription access is confirmed.",
|
|
45
|
+
"detail": "If you cancel checkout, account setup is preserved. Use Back to plans to retry. Avoid a second purchase while payment is still being confirmed."
|
|
46
|
+
},
|
|
47
|
+
"onboarding_install": {
|
|
48
|
+
"title": "Continue in camera setup.",
|
|
49
|
+
"body": "Keep the Connector running while you connect the recorder and check its channels in the camera setup window.",
|
|
50
|
+
"detail": "If camera setup asks for a code, use the current code shown in this Connector. An old code belongs to an earlier session."
|
|
51
|
+
},
|
|
52
|
+
"waiting_for_account": {
|
|
53
|
+
"title": "Continue in camera setup.",
|
|
54
|
+
"body": "Keep the Connector running while you connect the recorder and check its channels in the camera setup window.",
|
|
55
|
+
"detail": "If camera setup asks for a code, use the current code shown in this Connector. An old code belongs to an earlier session."
|
|
56
|
+
},
|
|
57
|
+
"account_connected": {
|
|
58
|
+
"title": "Continue in camera setup.",
|
|
59
|
+
"body": "Keep the Connector running while you connect the recorder and check its channels in the camera setup window.",
|
|
60
|
+
"detail": "If camera setup asks for a code, use the current code shown in this Connector. An old code belongs to an earlier session."
|
|
61
|
+
},
|
|
62
|
+
"discovering": {
|
|
63
|
+
"title": "Keep the recorder within reach.",
|
|
64
|
+
"body": "The Connector searches the local network. Select your recorder in camera setup and enter that device\u2019s login.",
|
|
65
|
+
"detail": "If nothing appears, check power and network connections or enter the recorder address manually. Guest Wi-Fi and isolated camera ports can prevent discovery."
|
|
66
|
+
},
|
|
67
|
+
"devices_found": {
|
|
68
|
+
"title": "Keep the recorder within reach.",
|
|
69
|
+
"body": "The Connector searches the local network. Select your recorder in camera setup and enter that device\u2019s login.",
|
|
70
|
+
"detail": "If nothing appears, check power and network connections or enter the recorder address manually. Guest Wi-Fi and isolated camera ports can prevent discovery."
|
|
71
|
+
},
|
|
72
|
+
"validating": {
|
|
73
|
+
"title": "Check each camera before adding it.",
|
|
74
|
+
"body": "Keep setup open while every channel is checked. Only validated feeds can be selected and added.",
|
|
75
|
+
"detail": "Empty, black or unreadable channels are left out. Check a missing camera\u2019s power, cable, permissions and stream settings, then retry in camera setup."
|
|
76
|
+
},
|
|
77
|
+
"cameras_created": {
|
|
78
|
+
"title": "Keep this computer awake while we finish.",
|
|
79
|
+
"body": "Camera pairing is saved. Let the local model and sensing service checks finish before closing setup.",
|
|
80
|
+
"detail": "The local model runs on this computer. If local AI needs attention, retry that step; you do not need to repeat camera pairing."
|
|
81
|
+
},
|
|
82
|
+
"local_ai": {
|
|
83
|
+
"title": "Keep this computer awake while we finish.",
|
|
84
|
+
"body": "Camera pairing is saved. Let the local model and sensing service checks finish before closing setup.",
|
|
85
|
+
"detail": "The local model runs on this computer. If local AI needs attention, retry that step; you do not need to repeat camera pairing."
|
|
86
|
+
},
|
|
87
|
+
"starting_service": {
|
|
88
|
+
"title": "Keep this computer awake while we finish.",
|
|
89
|
+
"body": "Camera pairing is saved. Let the local model and sensing service checks finish before closing setup.",
|
|
90
|
+
"detail": "The local model runs on this computer. If local AI needs attention, retry that step; you do not need to repeat camera pairing."
|
|
91
|
+
},
|
|
92
|
+
"local_ai_error": {
|
|
93
|
+
"title": "Retry local AI without pairing again.",
|
|
94
|
+
"body": "Your camera pairing is saved. Use Retry local AI to prepare the local model and service again.",
|
|
95
|
+
"detail": "Keep the computer powered and check its available disk space. Follow the error above. If the retry fails, contact support with the setup error, without camera credentials."
|
|
96
|
+
},
|
|
97
|
+
"ready": {
|
|
98
|
+
"title": "Leave this computer powered and connected.",
|
|
99
|
+
"body": "Sense Anything starts automatically when this computer logs in. Use View cameras to check sensing status.",
|
|
100
|
+
"detail": "Sleep, power loss or a disconnected camera network can interrupt sensing. Reopen Sense Anything Connector to resume or repair setup."
|
|
101
|
+
},
|
|
102
|
+
"error": {
|
|
103
|
+
"title": "Follow the setup notice above.",
|
|
104
|
+
"body": "Use the recovery action shown in this window. Keep your account and recorder credentials private.",
|
|
105
|
+
"detail": "If the problem continues, contact support with the visible error. Do not share passwords or camera stream links in screenshots."
|
|
106
|
+
},
|
|
107
|
+
"update_required": {
|
|
108
|
+
"title": "Follow the setup notice above.",
|
|
109
|
+
"body": "Use the recovery action shown in this window. Keep your account and recorder credentials private.",
|
|
110
|
+
"detail": "If the problem continues, contact support with the visible error. Do not share passwords or camera stream links in screenshots."
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -193,6 +193,7 @@ Log "Bundled ffmpeg OK"
|
|
|
193
193
|
# PATH=bin first so the connector's default ffmpeg='ffmpeg' resolves to OURS.
|
|
194
194
|
Copy-Item -Force "$Here\trillboards-edge.cmd" "$Payload\bin\trillboards-edge.cmd"
|
|
195
195
|
Copy-Item -Force "$Here\trillboards-pair.cmd" "$Payload\bin\trillboards-pair.cmd"
|
|
196
|
+
Copy-Item -Force "$Here\..\setup-guidance.json" "$Payload\bin\setup-guidance.json"
|
|
196
197
|
Copy-Item -Force "$Here\trillboards-setup.ps1" "$Payload\bin\trillboards-setup.ps1"
|
|
197
198
|
Copy-Item -Force "$Here\trillboards-setup.vbs" "$Payload\bin\trillboards-setup.vbs"
|
|
198
199
|
Copy-Item -Force "$Here\trillboards-edge-task.vbs" "$Payload\bin\trillboards-edge-task.vbs"
|
|
@@ -120,8 +120,8 @@ function Format-ByteCount($Value) {
|
|
|
120
120
|
$Form = New-Object System.Windows.Forms.Form
|
|
121
121
|
$Form.Text = 'Sense Anything Connector'
|
|
122
122
|
$Form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
|
|
123
|
-
$Form.ClientSize = New-Object System.Drawing.Size(760,
|
|
124
|
-
$Form.MinimumSize = New-Object System.Drawing.Size(
|
|
123
|
+
$Form.ClientSize = New-Object System.Drawing.Size(760, 700)
|
|
124
|
+
$Form.MinimumSize = New-Object System.Drawing.Size(760, 700)
|
|
125
125
|
$Form.BackColor = [System.Drawing.Color]::White
|
|
126
126
|
$Form.MaximizeBox = $false
|
|
127
127
|
|
|
@@ -224,6 +224,85 @@ $CloseAction.Size = New-Object System.Drawing.Size(100, 42)
|
|
|
224
224
|
$CloseAction.Visible = $false
|
|
225
225
|
$Form.Controls.Add($CloseAction)
|
|
226
226
|
|
|
227
|
+
# Guidance preferences contain only a boolean; setup never depends on storage.
|
|
228
|
+
$GuidancePreferencePath = Join-Path $StateDir 'setup-guiding-tips.json'
|
|
229
|
+
$script:GuidanceCatalog = $null
|
|
230
|
+
$script:DismissedGuidance = @{}
|
|
231
|
+
$guidePath = Join-Path $BinDir 'setup-guidance.json'
|
|
232
|
+
if (-not (Test-Path $guidePath)) { $guidePath = Join-Path $BinDir '..\setup-guidance.json' }
|
|
233
|
+
try { $script:GuidanceCatalog = Get-Content -Raw $guidePath | ConvertFrom-Json } catch { }
|
|
234
|
+
$guidanceDisabled = $false
|
|
235
|
+
try { $guidanceDisabled = (Get-Content -Raw $GuidancePreferencePath | ConvertFrom-Json).disabled -eq $true } catch { }
|
|
236
|
+
$GuidanceToggle = New-Object System.Windows.Forms.CheckBox
|
|
237
|
+
$GuidanceToggle.Text = 'Guiding tips'
|
|
238
|
+
$GuidanceToggle.Location = New-Object System.Drawing.Point(24, 510)
|
|
239
|
+
$GuidanceToggle.Size = New-Object System.Drawing.Size(145, 28)
|
|
240
|
+
$GuidanceToggle.Checked = -not $guidanceDisabled
|
|
241
|
+
$Sidebar.Controls.Add($GuidanceToggle)
|
|
242
|
+
$GuidanceRestore = New-Object System.Windows.Forms.Button
|
|
243
|
+
$GuidanceRestore.Text = 'Show this tip'
|
|
244
|
+
$GuidanceRestore.Location = New-Object System.Drawing.Point(24, 548)
|
|
245
|
+
$GuidanceRestore.Size = New-Object System.Drawing.Size(140, 30)
|
|
246
|
+
$Sidebar.Controls.Add($GuidanceRestore)
|
|
247
|
+
$GuidancePanel = New-Object System.Windows.Forms.Panel
|
|
248
|
+
$GuidancePanel.Location = New-Object System.Drawing.Point(236, 530)
|
|
249
|
+
$GuidancePanel.Size = New-Object System.Drawing.Size(465, 150)
|
|
250
|
+
$GuidancePanel.BackColor = [System.Drawing.Color]::FromArgb(255, 245, 232)
|
|
251
|
+
$GuidancePanel.BorderStyle = [System.Windows.Forms.BorderStyle]::FixedSingle
|
|
252
|
+
$Form.Controls.Add($GuidancePanel)
|
|
253
|
+
$GuidanceTitle = New-Label -Text '' -Size 10 -Style ([System.Drawing.FontStyle]::Bold)
|
|
254
|
+
$GuidanceTitle.Location = New-Object System.Drawing.Point(12, 9)
|
|
255
|
+
$GuidanceTitle.Size = New-Object System.Drawing.Size(402, 38)
|
|
256
|
+
$GuidanceTitle.ForeColor = [System.Drawing.Color]::FromArgb(71, 51, 36)
|
|
257
|
+
$GuidancePanel.Controls.Add($GuidanceTitle)
|
|
258
|
+
$GuidanceBody = New-Label -Text '' -Size 9
|
|
259
|
+
$GuidanceBody.Location = New-Object System.Drawing.Point(12, 48)
|
|
260
|
+
$GuidanceBody.Size = New-Object System.Drawing.Size(435, 62)
|
|
261
|
+
$GuidanceBody.ForeColor = [System.Drawing.Color]::FromArgb(97, 74, 59)
|
|
262
|
+
$GuidancePanel.Controls.Add($GuidanceBody)
|
|
263
|
+
$GuidanceDismiss = New-Object System.Windows.Forms.Button
|
|
264
|
+
$GuidanceDismiss.Text = 'x'
|
|
265
|
+
$GuidanceDismiss.AccessibleName = 'Dismiss this tip'
|
|
266
|
+
$GuidanceDismiss.Location = New-Object System.Drawing.Point(423, 8)
|
|
267
|
+
$GuidanceDismiss.Size = New-Object System.Drawing.Size(28, 26)
|
|
268
|
+
$GuidancePanel.Controls.Add($GuidanceDismiss)
|
|
269
|
+
$GuidanceMore = New-Object System.Windows.Forms.Button
|
|
270
|
+
$GuidanceMore.Text = 'More help'
|
|
271
|
+
$GuidanceMore.Location = New-Object System.Drawing.Point(12, 115)
|
|
272
|
+
$GuidanceMore.Size = New-Object System.Drawing.Size(100, 26)
|
|
273
|
+
$GuidancePanel.Controls.Add($GuidanceMore)
|
|
274
|
+
|
|
275
|
+
function Get-CurrentGuidance {
|
|
276
|
+
if ($null -eq $script:GuidanceCatalog) { return $null }
|
|
277
|
+
$entry = $script:GuidanceCatalog.PSObject.Properties[$script:LastStage]
|
|
278
|
+
if ($null -eq $entry) { return $null }
|
|
279
|
+
return $entry.Value
|
|
280
|
+
}
|
|
281
|
+
function Update-Guidance {
|
|
282
|
+
$guide = Get-CurrentGuidance
|
|
283
|
+
$dismissed = $script:DismissedGuidance.ContainsKey($script:LastStage)
|
|
284
|
+
$GuidancePanel.Visible = $GuidanceToggle.Checked -and -not $dismissed -and $null -ne $guide
|
|
285
|
+
$GuidanceRestore.Visible = $GuidanceToggle.Checked -and $dismissed
|
|
286
|
+
if ($null -ne $guide) {
|
|
287
|
+
$GuidanceTitle.Text = [string]$guide.title
|
|
288
|
+
$GuidanceBody.Text = [string]$guide.body
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
$GuidanceToggle.Add_CheckedChanged({
|
|
292
|
+
$script:DismissedGuidance.Clear()
|
|
293
|
+
Update-Guidance
|
|
294
|
+
try {
|
|
295
|
+
[void][System.IO.Directory]::CreateDirectory($StateDir)
|
|
296
|
+
@{ disabled = -not $GuidanceToggle.Checked } | ConvertTo-Json | Set-Content -Path $GuidancePreferencePath -Encoding UTF8
|
|
297
|
+
} catch { }
|
|
298
|
+
})
|
|
299
|
+
$GuidanceDismiss.Add_Click({ $script:DismissedGuidance[$script:LastStage] = $true; Update-Guidance })
|
|
300
|
+
$GuidanceRestore.Add_Click({ $script:DismissedGuidance.Remove($script:LastStage); Update-Guidance })
|
|
301
|
+
$GuidanceMore.Add_Click({
|
|
302
|
+
$guide = Get-CurrentGuidance
|
|
303
|
+
if ($null -ne $guide) { [void][System.Windows.Forms.MessageBox]::Show($Form, [string]$guide.detail, [string]$guide.title) }
|
|
304
|
+
})
|
|
305
|
+
|
|
227
306
|
# Native onboarding controls
|
|
228
307
|
# These panels are real WinForms controls rather than a webview. Google and
|
|
229
308
|
# Stripe alone open the system browser for provider-owned secure UI, then the
|
|
@@ -467,6 +546,7 @@ $PlanSubmit.Add_Click({
|
|
|
467
546
|
function Show-Welcome {
|
|
468
547
|
Set-OnboardingPanel $null
|
|
469
548
|
$script:LastStage = 'welcome'
|
|
549
|
+
Update-Guidance
|
|
470
550
|
$Headline.Text = 'Set up this computer for CCTV'
|
|
471
551
|
$Detail.Text = 'Connect your Sense Anything account, find your recorder, and verify every camera before screens are created.'
|
|
472
552
|
$StatusValue.Text = 'About 5 minutes'
|
|
@@ -481,6 +561,7 @@ function Show-Welcome {
|
|
|
481
561
|
function Show-LocalError([string]$Title, [string]$Message) {
|
|
482
562
|
Set-OnboardingPanel $null
|
|
483
563
|
$script:LastStage = 'error'
|
|
564
|
+
Update-Guidance
|
|
484
565
|
$Headline.Text = $Title
|
|
485
566
|
$Detail.Text = $Message
|
|
486
567
|
$StatusValue.Text = 'Setup stopped'
|
|
@@ -565,6 +646,7 @@ function Complete-UpdateCheck {
|
|
|
565
646
|
}
|
|
566
647
|
$script:RequiredUpdateUrl = $downloadUrl
|
|
567
648
|
$script:LastStage = 'update_required'
|
|
649
|
+
Update-Guidance
|
|
568
650
|
$Headline.Text = 'Update required before setup can continue'
|
|
569
651
|
$Detail.Text = 'Install the latest Sense Anything Connector, then reopen it to finish camera setup.'
|
|
570
652
|
$StatusValue.Text = "Version $availableText"
|
|
@@ -623,6 +705,7 @@ function Start-Setup {
|
|
|
623
705
|
$Progress.Value = 8
|
|
624
706
|
$Action.Enabled = $false
|
|
625
707
|
$script:LastStage = 'starting'
|
|
708
|
+
Update-Guidance
|
|
626
709
|
$script:SetupStarted = $true
|
|
627
710
|
|
|
628
711
|
$startInfo = New-Object System.Diagnostics.ProcessStartInfo
|
|
@@ -653,6 +736,7 @@ function Start-LocalAiSetup {
|
|
|
653
736
|
$Action.Text = 'Working...'
|
|
654
737
|
$Action.Enabled = $false
|
|
655
738
|
$script:LastStage = 'local_ai'
|
|
739
|
+
Update-Guidance
|
|
656
740
|
|
|
657
741
|
$startInfo = New-Object System.Diagnostics.ProcessStartInfo
|
|
658
742
|
$startInfo.FileName = 'powershell.exe'
|
|
@@ -721,6 +805,7 @@ function Render-State($State) {
|
|
|
721
805
|
if (-not $stage) { return }
|
|
722
806
|
$previousStage = $script:LastStage
|
|
723
807
|
$script:LastStage = $stage
|
|
808
|
+
Update-Guidance
|
|
724
809
|
$url = Get-StateValue $State 'portalUrl'
|
|
725
810
|
if ($url) { $script:PortalUrl = [string]$url }
|
|
726
811
|
$Headline.Text = [string](Get-StateValue $State 'title')
|
|
@@ -932,6 +1017,20 @@ $Timer.Add_Tick({
|
|
|
932
1017
|
})
|
|
933
1018
|
Show-Welcome
|
|
934
1019
|
if ($SmokeTest) {
|
|
1020
|
+
if ($null -eq $script:GuidanceCatalog) { throw 'Setup guidance catalog is missing.' }
|
|
1021
|
+
foreach ($entry in $script:GuidanceCatalog.PSObject.Properties) {
|
|
1022
|
+
$script:LastStage = $entry.Name
|
|
1023
|
+
Update-Guidance
|
|
1024
|
+
foreach ($label in @($GuidanceTitle, $GuidanceBody)) {
|
|
1025
|
+
$available = New-Object System.Drawing.Size(($label.Width - 6), 1000)
|
|
1026
|
+
$measured = [System.Windows.Forms.TextRenderer]::MeasureText($label.Text, $label.Font, $available, [System.Windows.Forms.TextFormatFlags]::WordBreak)
|
|
1027
|
+
if ($measured.Height -gt $label.Height) { throw "Setup guidance text clipped: $($entry.Name)" }
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
if ($GuidancePanel.Bounds.IntersectsWith($Action.Bounds) -or $GuidancePanel.Bounds.IntersectsWith($AccountPanel.Bounds)) {
|
|
1031
|
+
throw 'Guidance overlaps setup controls.'
|
|
1032
|
+
}
|
|
1033
|
+
Show-Welcome
|
|
935
1034
|
if ($StepLabels.Count -ne 8 -or $Form.Controls.Count -lt 8 -or $Action.Text -ne 'Start setup') {
|
|
936
1035
|
throw 'Windows setup UI did not construct its expected controls.'
|
|
937
1036
|
}
|
package/dist/EdgeAgent.js
CHANGED
|
@@ -44,7 +44,7 @@ const edge_ads_1 = require("@trillboards/edge-ads");
|
|
|
44
44
|
const CommandRouter_1 = require("./CommandRouter");
|
|
45
45
|
const config_1 = require("./config");
|
|
46
46
|
const dataDir_1 = require("./dataDir");
|
|
47
|
-
const SDK_VERSION = '0.2.
|
|
47
|
+
const SDK_VERSION = '0.2.140';
|
|
48
48
|
class EdgeAgent extends events_1.EventEmitter {
|
|
49
49
|
constructor(config, deps) {
|
|
50
50
|
super();
|
package/dist/diagnose.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export interface DiagnosticReport {
|
|
|
31
31
|
skipped: number;
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
-
declare const SDK_VERSION = "0.2.
|
|
34
|
+
declare const SDK_VERSION = "0.2.140";
|
|
35
35
|
declare function checkNodeVersion(): CheckResult;
|
|
36
36
|
declare function checkPlatform(): CheckResult;
|
|
37
37
|
declare function checkMemory(): CheckResult;
|
package/dist/diagnose.js
CHANGED
|
@@ -63,7 +63,7 @@ const localNetworkAccess_1 = require("./localNetworkAccess");
|
|
|
63
63
|
// ---------------------------------------------------------------------------
|
|
64
64
|
// SDK version — mirrors package.json; kept in sync by CI
|
|
65
65
|
// ---------------------------------------------------------------------------
|
|
66
|
-
const SDK_VERSION = '0.2.
|
|
66
|
+
const SDK_VERSION = '0.2.140';
|
|
67
67
|
exports._SDK_VERSION = SDK_VERSION;
|
|
68
68
|
// ---------------------------------------------------------------------------
|
|
69
69
|
// ANSI helpers (no external deps)
|
package/package.json
CHANGED