Coverage for integrations / agent_engine / native_onboarding.py: 0.0%

256 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-12 04:49 +0000

1#!/usr/bin/env python3 

2""" 

3HART OS Native Onboarding — "Light Your HART" 

4 

5GTK4/libadwaita native application for the first-boot onboarding ceremony. 

6Runs before any web server. Pure native UI. Zero JavaScript. 

7 

8Architecture: 

9 - Imports hart_onboarding.py directly (no HTTP dependency) 

10 - GTK4/libadwaita for native GNOME experience 

11 - Full-screen dark ceremony with phase transitions 

12 - Auto-advances with timed pauses (like the PA is speaking) 

13 

14Launch: 

15 python native_onboarding.py # Normal launch 

16 python native_onboarding.py --user-id 1 # Specify user 

17 python native_onboarding.py --check # Just check if onboarded 

18 

19Requires: PyGObject, GTK4, libadwaita (all standard on GNOME/NixOS) 

20""" 

21 

22import os 

23import sys 

24 

25# Ensure HART OS root is in path 

26_HART_DIR = os.environ.get( 

27 'HART_INSTALL_DIR', 

28 os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) 

29) 

30if _HART_DIR not in sys.path: 

31 sys.path.insert(0, _HART_DIR) 

32 

33import gi 

34gi.require_version('Gtk', '4.0') 

35gi.require_version('Adw', '1') 

36from gi.repository import Gtk, Adw, GLib, Gdk # noqa: E402 

37 

38from hart_onboarding import ( # noqa: E402 

39 HARTOnboardingSession, CONVERSATION_SCRIPT, 

40 PASSION_OPTIONS, ESCAPE_OPTIONS, 

41 ACKNOWLEDGMENTS_PASSION, ACKNOWLEDGMENT_ESCAPE, 

42 has_hart_name, get_hart_profile, 

43 ELEMENTS, SPIRITS, 

44) 

45 

46# ═══════════════════════════════════════════════════════════════════════ 

47# CSS — Dark ceremony aesthetic 

48# ═══════════════════════════════════════════════════════════════════════ 

49 

50_CSS = """ 

51window { 

52 background: #080808; 

53} 

54 

55.ceremony-bg { 

56 background: #080808; 

57} 

58 

59/* ── Language selection ── */ 

60 

61.lang-grid { 

62 margin: 48px; 

63} 

64 

65.lang-whisper { 

66 color: rgba(255, 255, 255, 0.3); 

67 font-size: 15px; 

68 font-weight: 400; 

69 letter-spacing: 3px; 

70} 

71 

72.lang-btn { 

73 background: rgba(255, 255, 255, 0.03); 

74 border: 1px solid rgba(255, 255, 255, 0.06); 

75 border-radius: 14px; 

76 padding: 18px 28px; 

77 min-width: 170px; 

78 min-height: 52px; 

79 transition: all 250ms ease; 

80} 

81 

82.lang-btn:hover { 

83 background: rgba(255, 255, 255, 0.08); 

84 border-color: rgba(255, 255, 255, 0.12); 

85} 

86 

87.lang-btn:active { 

88 background: rgba(255, 255, 255, 0.12); 

89} 

90 

91.lang-native { 

92 color: #d8d8d8; 

93 font-size: 19px; 

94 font-weight: 400; 

95} 

96 

97.lang-english { 

98 color: rgba(255, 255, 255, 0.35); 

99 font-size: 13px; 

100 font-weight: 300; 

101 margin-top: 4px; 

102} 

103 

104/* ── PA text (the voice) ── */ 

105 

106.pa-text { 

107 color: #e0e0e0; 

108 font-size: 28px; 

109 font-weight: 300; 

110 line-height: 1.7; 

111} 

112 

113.pa-text-warm { 

114 color: #f0e8e0; 

115 font-size: 24px; 

116 font-weight: 300; 

117 line-height: 1.5; 

118} 

119 

120/* ── Questions ── */ 

121 

122.question-text { 

123 color: #ffffff; 

124 font-size: 26px; 

125 font-weight: 400; 

126 margin-bottom: 40px; 

127} 

128 

129.option-card { 

130 background: rgba(255, 255, 255, 0.04); 

131 border: 1px solid rgba(255, 255, 255, 0.06); 

132 border-radius: 16px; 

133 padding: 22px 32px; 

134 min-width: 220px; 

135 min-height: 52px; 

136 transition: all 250ms ease; 

137} 

138 

139.option-card:hover { 

140 background: rgba(255, 255, 255, 0.09); 

141 border-color: rgba(255, 255, 255, 0.14); 

142} 

143 

144.option-card:active { 

145 background: rgba(255, 255, 255, 0.14); 

146} 

147 

148.option-label { 

149 color: #c8c8c8; 

150 font-size: 18px; 

151 font-weight: 400; 

152} 

153 

154/* ── Acknowledgment ── */ 

155 

156.ack-text { 

157 color: rgba(255, 255, 255, 0.7); 

158 font-size: 26px; 

159 font-weight: 300; 

160 font-style: italic; 

161} 

162 

163/* ── Pre-reveal ── */ 

164 

165.pre-reveal { 

166 color: rgba(255, 255, 255, 0.6); 

167 font-size: 30px; 

168 font-weight: 300; 

169 font-style: italic; 

170} 

171 

172/* ── The Name ── */ 

173 

174.name-reveal { 

175 color: #ffffff; 

176 font-size: 72px; 

177 font-weight: 600; 

178 letter-spacing: 8px; 

179} 

180 

181.hart-tag { 

182 color: rgba(255, 255, 255, 0.4); 

183 font-size: 18px; 

184 font-weight: 300; 

185 letter-spacing: 1px; 

186 margin-top: 12px; 

187} 

188 

189.emoji-display { 

190 font-size: 40px; 

191 margin-top: 20px; 

192} 

193 

194.reveal-intro { 

195 color: rgba(255, 255, 255, 0.5); 

196 font-size: 24px; 

197 font-weight: 300; 

198 margin-bottom: 32px; 

199} 

200 

201/* ── Post-reveal ── */ 

202 

203.post-text { 

204 color: rgba(255, 255, 255, 0.5); 

205 font-size: 22px; 

206 font-weight: 300; 

207 line-height: 1.7; 

208 margin-top: 48px; 

209} 

210 

211/* ── Buttons ── */ 

212 

213.action-btn { 

214 background: rgba(255, 255, 255, 0.06); 

215 color: #d0d0d0; 

216 border: 1px solid rgba(255, 255, 255, 0.08); 

217 border-radius: 28px; 

218 padding: 12px 36px; 

219 font-size: 16px; 

220 font-weight: 400; 

221 min-width: 140px; 

222 transition: all 200ms ease; 

223} 

224 

225.action-btn:hover { 

226 background: rgba(255, 255, 255, 0.12); 

227} 

228 

229.action-btn-primary { 

230 background: rgba(255, 255, 255, 0.10); 

231 color: #ffffff; 

232 font-weight: 500; 

233} 

234 

235.action-btn-primary:hover { 

236 background: rgba(255, 255, 255, 0.18); 

237} 

238 

239/* ── Sealed identity card ── */ 

240 

241.sealed-name { 

242 color: #ffffff; 

243 font-size: 48px; 

244 font-weight: 600; 

245 letter-spacing: 4px; 

246} 

247 

248.sealed-tag { 

249 color: rgba(255, 255, 255, 0.4); 

250 font-size: 16px; 

251 letter-spacing: 1px; 

252 margin-top: 8px; 

253} 

254 

255.sealed-message { 

256 color: rgba(255, 255, 255, 0.4); 

257 font-size: 17px; 

258 font-weight: 300; 

259 margin-top: 40px; 

260} 

261""" 

262 

263# Language display names (native + English) 

264_LANG_DISPLAY = { 

265 'en': ('English', ''), 

266 'ta': ('\u0ba4\u0bae\u0bbf\u0bb4\u0bcd', 'Tamil'), 

267 'hi': ('\u0939\u093f\u0928\u094d\u0926\u0940', 'Hindi'), 

268 'es': ('Espa\u00f1ol', 'Spanish'), 

269 'fr': ('Fran\u00e7ais', 'French'), 

270 'de': ('Deutsch', 'German'), 

271 'ja': ('\u65e5\u672c\u8a9e', 'Japanese'), 

272 'ko': ('\ud55c\uad6d\uc5b4', 'Korean'), 

273 'zh': ('\u4e2d\u6587', 'Chinese'), 

274 'pt': ('Portugu\u00eas', 'Portuguese'), 

275 'ar': ('\u0627\u0644\u0639\u0631\u0628\u064a\u0629', 'Arabic'), 

276 'ru': ('\u0420\u0443\u0441\u0441\u043a\u0438\u0439', 'Russian'), 

277} 

278 

279 

280class HARTOnboardingWindow(Adw.ApplicationWindow): 

281 """Full-screen onboarding ceremony window.""" 

282 

283 def __init__(self, user_id='1', **kwargs): 

284 super().__init__(**kwargs) 

285 self.user_id = user_id 

286 self.session = HARTOnboardingSession(user_id) 

287 self._pending_timeout = None 

288 

289 self.set_default_size(1200, 800) 

290 self.fullscreen() 

291 

292 # Load CSS 

293 provider = Gtk.CssProvider() 

294 provider.load_from_string(_CSS) 

295 Gtk.StyleContext.add_provider_for_display( 

296 Gdk.Display.get_default(), 

297 provider, 

298 Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, 

299 ) 

300 

301 # Main stack for phase transitions 

302 self.stack = Gtk.Stack( 

303 transition_type=Gtk.StackTransitionType.CROSSFADE, 

304 transition_duration=800, 

305 ) 

306 self.set_content(self.stack) 

307 

308 # Build first phase 

309 self._build_language_page() 

310 

311 # ── Phase builders ────────────────────────────────────────── 

312 

313 def _center_box(self, **kwargs): 

314 """Create a centered vertical box (common layout).""" 

315 outer = Gtk.Box( 

316 orientation=Gtk.Orientation.VERTICAL, 

317 halign=Gtk.Align.CENTER, 

318 valign=Gtk.Align.CENTER, 

319 spacing=0, 

320 ) 

321 outer.add_css_class('ceremony-bg') 

322 return outer 

323 

324 def _build_language_page(self): 

325 """Phase 1: Language selection grid.""" 

326 page = self._center_box() 

327 

328 # Whisper title 

329 title = Gtk.Label(label='CHOOSE YOUR LANGUAGE') 

330 title.add_css_class('lang-whisper') 

331 page.append(title) 

332 

333 spacer = Gtk.Box() 

334 spacer.set_size_request(-1, 40) 

335 page.append(spacer) 

336 

337 # Language grid (4 columns) 

338 grid = Gtk.FlowBox( 

339 max_children_per_line=4, 

340 min_children_per_line=2, 

341 row_spacing=16, 

342 column_spacing=16, 

343 selection_mode=Gtk.SelectionMode.NONE, 

344 halign=Gtk.Align.CENTER, 

345 homogeneous=True, 

346 ) 

347 grid.add_css_class('lang-grid') 

348 

349 for lang_code, (native_name, english_name) in _LANG_DISPLAY.items(): 

350 btn = Gtk.Button() 

351 btn.add_css_class('lang-btn') 

352 

353 box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2) 

354 native_lbl = Gtk.Label(label=native_name) 

355 native_lbl.add_css_class('lang-native') 

356 box.append(native_lbl) 

357 

358 if english_name: 

359 eng_lbl = Gtk.Label(label=english_name) 

360 eng_lbl.add_css_class('lang-english') 

361 box.append(eng_lbl) 

362 

363 btn.set_child(box) 

364 btn.connect('clicked', self._on_language_selected, lang_code) 

365 grid.append(btn) 

366 

367 page.append(grid) 

368 self.stack.add_named(page, 'language') 

369 self.stack.set_visible_child_name('language') 

370 

371 def _build_text_page(self, name, text, css_class='pa-text', 

372 auto_advance_ms=0, next_builder=None): 

373 """Build a page that shows PA text, optionally auto-advances.""" 

374 page = self._center_box() 

375 

376 # Clamp width for readability 

377 clamp = Adw.Clamp(maximum_size=700, tightening_threshold=500) 

378 label = Gtk.Label( 

379 label=text, 

380 wrap=True, 

381 wrap_mode=2, # WORD_CHAR 

382 justify=Gtk.Justification.CENTER, 

383 halign=Gtk.Align.CENTER, 

384 ) 

385 label.add_css_class(css_class) 

386 clamp.set_child(label) 

387 page.append(clamp) 

388 

389 self.stack.add_named(page, name) 

390 self.stack.set_visible_child_name(name) 

391 

392 if auto_advance_ms and next_builder: 

393 self._schedule(auto_advance_ms, next_builder) 

394 

395 def _build_question_page(self, name, question_text, options, callback): 

396 """Build a question page with selectable option cards.""" 

397 page = self._center_box() 

398 

399 # Question 

400 clamp = Adw.Clamp(maximum_size=700, tightening_threshold=500) 

401 q_label = Gtk.Label( 

402 label=question_text, 

403 wrap=True, 

404 wrap_mode=2, 

405 justify=Gtk.Justification.CENTER, 

406 ) 

407 q_label.add_css_class('question-text') 

408 clamp.set_child(q_label) 

409 page.append(clamp) 

410 

411 spacer = Gtk.Box() 

412 spacer.set_size_request(-1, 20) 

413 page.append(spacer) 

414 

415 # Options grid (2 columns) 

416 grid = Gtk.FlowBox( 

417 max_children_per_line=2, 

418 min_children_per_line=1, 

419 row_spacing=14, 

420 column_spacing=14, 

421 selection_mode=Gtk.SelectionMode.NONE, 

422 halign=Gtk.Align.CENTER, 

423 homogeneous=True, 

424 ) 

425 

426 for opt in options: 

427 btn = Gtk.Button() 

428 btn.add_css_class('option-card') 

429 

430 lbl = Gtk.Label(label=opt['label']) 

431 lbl.add_css_class('option-label') 

432 btn.set_child(lbl) 

433 

434 btn.connect('clicked', callback, opt['key']) 

435 grid.append(btn) 

436 

437 page.append(grid) 

438 self.stack.add_named(page, name) 

439 self.stack.set_visible_child_name(name) 

440 

441 def _build_reveal_page(self, result): 

442 """Build the name reveal page.""" 

443 page = self._center_box() 

444 page.set_spacing(0) 

445 

446 # "I'm going to call you..." 

447 intro_text = self._line('reveal_intro') 

448 intro = Gtk.Label(label=intro_text) 

449 intro.add_css_class('reveal-intro') 

450 page.append(intro) 

451 

452 spacer1 = Gtk.Box() 

453 spacer1.set_size_request(-1, 32) 

454 page.append(spacer1) 

455 

456 # THE NAME (large, dramatic) 

457 name_label = Gtk.Label(label=result['name']) 

458 name_label.add_css_class('name-reveal') 

459 page.append(name_label) 

460 

461 # HART tag: @element.spirit.name 

462 tag_label = Gtk.Label(label=result.get('hart_tag', '')) 

463 tag_label.add_css_class('hart-tag') 

464 page.append(tag_label) 

465 

466 # Emoji combo 

467 emoji_label = Gtk.Label(label=result.get('emoji_combo', '')) 

468 emoji_label.add_css_class('emoji-display') 

469 page.append(emoji_label) 

470 

471 spacer2 = Gtk.Box() 

472 spacer2.set_size_request(-1, 48) 

473 page.append(spacer2) 

474 

475 # Buttons 

476 btn_box = Gtk.Box( 

477 orientation=Gtk.Orientation.HORIZONTAL, 

478 spacing=16, 

479 halign=Gtk.Align.CENTER, 

480 ) 

481 

482 # "That's me" button 

483 accept_btn = Gtk.Button(label="That's me") 

484 accept_btn.add_css_class('action-btn') 

485 accept_btn.add_css_class('action-btn-primary') 

486 accept_btn.connect('clicked', self._on_accept_name) 

487 btn_box.append(accept_btn) 

488 

489 # "Try another" (only if first attempt) 

490 if result.get('can_try_another', True): 

491 retry_btn = Gtk.Button(label='Try another') 

492 retry_btn.add_css_class('action-btn') 

493 retry_btn.connect('clicked', self._on_try_another) 

494 btn_box.append(retry_btn) 

495 

496 page.append(btn_box) 

497 

498 self.stack.add_named(page, 'reveal') 

499 self.stack.set_visible_child_name('reveal') 

500 

501 def _build_sealed_page(self, result): 

502 """Build the final sealed identity page.""" 

503 page = self._center_box() 

504 

505 # Post-reveal PA line 

506 post_text = self._line('post_reveal') 

507 post = Gtk.Label( 

508 label=post_text, 

509 wrap=True, 

510 wrap_mode=2, 

511 justify=Gtk.Justification.CENTER, 

512 ) 

513 post.add_css_class('post-text') 

514 page.append(post) 

515 

516 spacer1 = Gtk.Box() 

517 spacer1.set_size_request(-1, 48) 

518 page.append(spacer1) 

519 

520 # The sealed name 

521 name = Gtk.Label(label=result.get('hart_name', '')) 

522 name.add_css_class('sealed-name') 

523 page.append(name) 

524 

525 # HART tag 

526 tag = result.get('hart_tag', result.get('emoji_combo', '')) 

527 if tag: 

528 tag_label = Gtk.Label(label=tag) 

529 tag_label.add_css_class('sealed-tag') 

530 page.append(tag_label) 

531 

532 # Emoji 

533 emoji = result.get('emoji_combo', '') 

534 if emoji: 

535 emoji_label = Gtk.Label(label=emoji) 

536 emoji_label.add_css_class('emoji-display') 

537 page.append(emoji_label) 

538 

539 spacer2 = Gtk.Box() 

540 spacer2.set_size_request(-1, 48) 

541 page.append(spacer2) 

542 

543 # "Begin" button — closes the ceremony 

544 begin_btn = Gtk.Button(label='Begin') 

545 begin_btn.add_css_class('action-btn') 

546 begin_btn.add_css_class('action-btn-primary') 

547 begin_btn.connect('clicked', self._on_begin) 

548 page.append(begin_btn) 

549 

550 self.stack.add_named(page, 'sealed') 

551 self.stack.set_visible_child_name('sealed') 

552 

553 # ── Event handlers ────────────────────────────────────────── 

554 

555 def _on_language_selected(self, btn, lang_code): 

556 """User picked their language.""" 

557 result = self.session.advance( 

558 action='select_language', 

559 data={'language': lang_code}, 

560 ) 

561 # Show greeting, then auto-advance to passion question 

562 greeting = self._line('greeting') 

563 self._build_text_page( 

564 'greeting', greeting, 'pa-text', 

565 auto_advance_ms=4000, 

566 next_builder=self._show_passion, 

567 ) 

568 

569 def _show_passion(self): 

570 """Show the passion question.""" 

571 lang = self.session.language 

572 question = self._line('question_passion') 

573 options = [{ 

574 'key': p['key'], 

575 'label': p['labels'].get(lang, p['labels']['en']), 

576 } for p in PASSION_OPTIONS] 

577 

578 self._build_question_page('passion', question, options, 

579 self._on_passion_selected) 

580 

581 def _on_passion_selected(self, btn, key): 

582 """User selected their passion.""" 

583 result = self.session.advance(action='answer', data={'key': key}) 

584 

585 # Show acknowledgment 

586 ack = ACKNOWLEDGMENTS_PASSION.get(key, {}) 

587 ack_text = ack.get(self.session.language, ack.get('en', '')) 

588 self._build_text_page( 

589 'ack_passion', ack_text, 'ack-text', 

590 auto_advance_ms=3000, 

591 next_builder=self._show_escape, 

592 ) 

593 

594 def _show_escape(self): 

595 """Show the escape question.""" 

596 # Advance session past ack_passion 

597 self.session.advance() 

598 

599 lang = self.session.language 

600 question = self._line('question_escape') 

601 options = [{ 

602 'key': e['key'], 

603 'label': e['labels'].get(lang, e['labels']['en']), 

604 } for e in ESCAPE_OPTIONS] 

605 

606 self._build_question_page('escape', question, options, 

607 self._on_escape_selected) 

608 

609 def _on_escape_selected(self, btn, key): 

610 """User selected their escape.""" 

611 result = self.session.advance(action='answer', data={'key': key}) 

612 

613 # Show escape acknowledgment 

614 ack_text = ACKNOWLEDGMENT_ESCAPE.get( 

615 self.session.language, ACKNOWLEDGMENT_ESCAPE['en']) 

616 self._build_text_page( 

617 'ack_escape', ack_text, 'ack-text', 

618 auto_advance_ms=3000, 

619 next_builder=self._show_pre_reveal, 

620 ) 

621 

622 def _show_pre_reveal(self): 

623 """'I think I know you.'""" 

624 # Advance session past ack_escape 

625 self.session.advance() 

626 

627 pre_text = self._line('pre_reveal') 

628 self._build_text_page( 

629 'pre_reveal', pre_text, 'pre-reveal', 

630 auto_advance_ms=4000, 

631 next_builder=self._show_reveal, 

632 ) 

633 

634 def _show_reveal(self): 

635 """Generate and reveal the name.""" 

636 # Advance session to do the reveal 

637 result = self.session.advance() 

638 self._last_reveal = result 

639 self._build_reveal_page(result) 

640 

641 def _on_accept_name(self, btn): 

642 """Seal the name forever.""" 

643 result = self.session.advance(action='accept_name') 

644 if result.get('sealed'): 

645 self._build_sealed_page(result) 

646 else: 

647 # Error — show message and retry 

648 error = result.get('error', 'Something went wrong') 

649 self._build_text_page( 

650 'error', error, 'pa-text', 

651 auto_advance_ms=3000, 

652 next_builder=self._show_reveal, 

653 ) 

654 

655 def _on_try_another(self, btn): 

656 """Generate an alternative name.""" 

657 result = self.session.advance(action='try_another') 

658 self._last_reveal = result 

659 # Remove old reveal page and build new one 

660 old = self.stack.get_child_by_name('reveal') 

661 if old: 

662 self.stack.remove(old) 

663 self._build_reveal_page(result) 

664 

665 def _on_begin(self, btn): 

666 """Ceremony complete — close and start the desktop.""" 

667 self.close() 

668 

669 # ── Utilities ─────────────────────────────────────────────── 

670 

671 def _line(self, key): 

672 """Get a PA line in the current session language.""" 

673 lines = CONVERSATION_SCRIPT.get(key, {}) 

674 return lines.get(self.session.language, lines.get('en', '')) 

675 

676 def _schedule(self, ms, callback): 

677 """Schedule a callback after ms milliseconds.""" 

678 if self._pending_timeout: 

679 GLib.source_remove(self._pending_timeout) 

680 self._pending_timeout = GLib.timeout_add(ms, self._run_scheduled, callback) 

681 

682 def _run_scheduled(self, callback): 

683 """Run a scheduled callback (returns False to prevent repeat).""" 

684 self._pending_timeout = None 

685 callback() 

686 return False 

687 

688 

689class HARTOnboardingApp(Adw.Application): 

690 """GTK4/libadwaita application for the HART onboarding ceremony.""" 

691 

692 def __init__(self, user_id='1'): 

693 super().__init__(application_id='ai.hartos.onboarding') 

694 self.user_id = user_id 

695 

696 def do_activate(self): 

697 # Check if already onboarded 

698 if has_hart_name(self.user_id): 

699 profile = get_hart_profile(self.user_id) 

700 if profile: 

701 # Show identity card briefly, then quit 

702 self._show_identity_card(profile) 

703 return 

704 

705 # Run the ceremony 

706 win = HARTOnboardingWindow( 

707 user_id=self.user_id, 

708 application=self, 

709 ) 

710 win.present() 

711 

712 def _show_identity_card(self, profile): 

713 """Show existing HART identity (already onboarded).""" 

714 win = Adw.ApplicationWindow(application=self) 

715 win.set_default_size(600, 400) 

716 win.set_title('Your HART') 

717 

718 # Load CSS 

719 provider = Gtk.CssProvider() 

720 provider.load_from_string(_CSS) 

721 Gtk.StyleContext.add_provider_for_display( 

722 Gdk.Display.get_default(), 

723 provider, 

724 Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, 

725 ) 

726 

727 box = Gtk.Box( 

728 orientation=Gtk.Orientation.VERTICAL, 

729 halign=Gtk.Align.CENTER, 

730 valign=Gtk.Align.CENTER, 

731 spacing=8, 

732 ) 

733 box.add_css_class('ceremony-bg') 

734 

735 name = Gtk.Label(label=profile.get('name', '')) 

736 name.add_css_class('sealed-name') 

737 box.append(name) 

738 

739 tag = Gtk.Label(label=profile.get('hart_tag', profile.get('display', ''))) 

740 tag.add_css_class('sealed-tag') 

741 box.append(tag) 

742 

743 emoji = Gtk.Label(label=profile.get('emoji_combo', '')) 

744 emoji.add_css_class('emoji-display') 

745 box.append(emoji) 

746 

747 msg = Gtk.Label(label=f"Sealed {profile.get('sealed_at', '')[:10]}") 

748 msg.add_css_class('sealed-message') 

749 box.append(msg) 

750 

751 win.set_content(box) 

752 win.present() 

753 

754 

755def main(): 

756 import argparse 

757 parser = argparse.ArgumentParser(description='HART OS Onboarding') 

758 parser.add_argument('--user-id', default='1', help='User ID') 

759 parser.add_argument('--check', action='store_true', 

760 help='Check if already onboarded and exit') 

761 args = parser.parse_args() 

762 

763 if args.check: 

764 if has_hart_name(args.user_id): 

765 profile = get_hart_profile(args.user_id) 

766 if profile: 

767 print(f"Onboarded: {profile.get('hart_tag', profile.get('name'))}") 

768 sys.exit(0) 

769 print("Not onboarded") 

770 sys.exit(1) 

771 

772 app = HARTOnboardingApp(user_id=args.user_id) 

773 app.run(None) 

774 

775 

776if __name__ == '__main__': 

777 main()