@rubytech/create-realagent-code 0.1.510 → 0.1.512

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.
Files changed (54) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/package.json +3 -0
  3. package/payload/platform/plugins/admin/skills/deck-pages/SKILL.md +2 -2
  4. package/payload/platform/plugins/admin/skills/skill-builder/SKILL.md +1 -1
  5. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +10 -0
  6. package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +15 -34
  7. package/payload/platform/plugins/business-assistant/skills/e-sign/stamp.mjs +269 -0
  8. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/profile.md +5 -5
  9. package/payload/platform/plugins/memory/skills/conversation-archive-enrich/SKILL.md +2 -2
  10. package/payload/platform/plugins/memory/skills/document-ingest/SKILL.md +1 -1
  11. package/payload/platform/plugins/x-import/skills/x-import/SKILL.md +1 -1
  12. package/payload/platform/scripts/check-no-esm-require.mjs +6 -0
  13. package/payload/platform/scripts/logs-read.sh +117 -36
  14. package/payload/platform/scripts/logs-read.test.sh +234 -0
  15. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  16. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +13 -0
  17. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  18. package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts +7 -3
  19. package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts.map +1 -1
  20. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js +17 -11
  21. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js.map +1 -1
  22. package/payload/premium-plugins/real-agent/plugins/brochures/skills/brand-design/SKILL.md +1 -1
  23. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/copy.md +2 -2
  24. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/images.md +2 -2
  25. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/index-landing.md +2 -2
  26. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/index.html +3 -3
  27. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/placeholders.md +16 -16
  28. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/registers.md +1 -1
  29. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/seller-brief.md +3 -3
  30. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/structure.md +3 -3
  31. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/template.html +5 -6
  32. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-extract/SKILL.md +1 -1
  33. package/payload/premium-plugins/real-agent/plugins/estate-business/skills/exp-partnership/SKILL.md +1 -1
  34. package/payload/premium-plugins/real-agent/plugins/estate-business/skills/exp-partnership/references/12-reasons.md +1 -1
  35. package/payload/premium-plugins/real-agent/plugins/estate-business/skills/exp-partnership/references/business-partnership.md +1 -1
  36. package/payload/premium-plugins/real-agent/plugins/estate-coaching/skills/bespoke-coaching/references/performance-framework.md +1 -2
  37. package/payload/premium-plugins/real-agent/plugins/estate-teaching/skills/content-directory/SKILL.md +1 -1
  38. package/payload/premium-plugins/real-agent/plugins/estate-teaching/skills/content-directory/references/module-delivery.md +1 -1
  39. package/payload/premium-plugins/real-agent/plugins/leads/skills/prospecting/references/database-matching.md +1 -1
  40. package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/SKILL.md +3 -3
  41. package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/references/render.py +1 -1
  42. package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/references/template-inputs.schema.json +1 -1
  43. package/payload/premium-plugins/real-agent/plugins/vendors/skills/vendor-communication/references/seller-engagement.md +4 -4
  44. package/payload/premium-plugins/real-agent/plugins/vendors/skills/vendor-communication/references/vendor-updates.md +2 -2
  45. package/payload/server/maxy-edge.js +6 -2
  46. package/payload/server/public/assets/admin-6QN-k3zQ.js +1 -0
  47. package/payload/server/public/assets/admin-6QN-k3zQ.js.br +0 -0
  48. package/payload/server/public/assets/admin-6QN-k3zQ.js.gz +0 -0
  49. package/payload/server/public/index.html +1 -1
  50. package/payload/server/server.js +777 -349
  51. package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/references/__tests__/adherence.test.py +0 -373
  52. package/payload/server/public/assets/admin-DGr1HuPg.js +0 -1
  53. package/payload/server/public/assets/admin-DGr1HuPg.js.br +0 -0
  54. package/payload/server/public/assets/admin-DGr1HuPg.js.gz +0 -0
@@ -4,7 +4,7 @@
4
4
 
5
5
  This file is the substitution contract — the exhaustive list of every token used across both templates, what fills it, what format it takes, and what voice the copy tokens need. Style and composition guidance lives in `copy.md` (and is referenced inline from each `<!-- REPLACE: ... -->` comment in the templates). The two files are complementary: this one says **what** each token is; `copy.md` says **how** the copy should read.
6
6
 
7
- Worked examples below quote the validated Sparrows Farm reference brochure at `/Users/neo/estate-agents/muvin/properties/henham-road-debden-green-hamperden-end-cb11-3lz-949931/output/brochure.html` that brochure is the canonical "what good looks like." Read it whenever a placeholder's intent is unclear.
7
+ Worked examples below use illustrative values, not any one agent's data. When a placeholder's intent is unclear, open a brochure already rendered under `~/estate-agents/<brand-slug>/properties/<property-slug>/output/brochure.html` on this install and read the token in place.
8
8
 
9
9
  ## How to use this file
10
10
 
@@ -19,9 +19,9 @@ These tokens are used everywhere — cover, opener, folio strips, footer, alt te
19
19
 
20
20
  | Token | Source | Format / length | Notes |
21
21
  |---|---|---|---|
22
- | `{{ property_name }}` | `property.json -> property_name`, or `address.line_1` if name is null | Title Case, ~10-30 chars | Distinct house name preferred ("Sparrows Farm", "Agincourt House"); falls back to the street ("12 Henham Road") when the property has no separate name. CSS renders it ALL-CAPS on the cover — source is Title Case. |
23
- | `{{ property_slug }}` | `property.json -> source.url` (slug part) | lowercase-kebab | Used in every image filename pattern: `images/{{ property_slug }}-NN.webp`. Examples: `sparrows-farm`, `agincourt-house`. |
24
- | `{{ address_line_1 }}` | `property.json -> address.line_1` | Title Case | Street or first address line ("Henham Road", "Llanddewi Rhydderch"). |
22
+ | `{{ property_name }}` | `property.json -> property_name`, or `address.line_1` if name is null | Title Case, ~10-30 chars | Distinct house name preferred ("Elmfield House", "Wrenbury Hall"); falls back to the street ("12 Elmfield Road") when the property has no separate name. CSS renders it ALL-CAPS on the cover — source is Title Case. |
23
+ | `{{ property_slug }}` | `property.json -> source.url` (slug part) | lowercase-kebab | Used in every image filename pattern: `images/{{ property_slug }}-NN.webp`. Examples: `elmfield-house`, `wrenbury-hall`. |
24
+ | `{{ address_line_1 }}` | `property.json -> address.line_1` | Title Case | Street or first address line ("Elmfield Road", "Wrenbury Lane"). |
25
25
  | `{{ locality }}` | `property.json -> address.locality` | Title Case | Village / town district. |
26
26
  | `{{ town }}` | `property.json -> address.town` | Title Case | Nearest town. |
27
27
  | `{{ county }}` | `property.json -> address.county` | Title Case | County. |
@@ -30,11 +30,11 @@ These tokens are used everywhere — cover, opener, folio strips, footer, alt te
30
30
  | `{{ folio_strip }}` | Composed | `{{ property_name }} · {{ locality }}` | Repeats on every interior page's left folio slot. |
31
31
  | `{{ folio_mid_NN }}` | Per-page chapter title | "Contents" / "Of place & pedigree" / "A home at ease" / etc. | Used in `<em>` of folio-mid. Match the chapter the page belongs to (intermissions inherit their parent chapter's mid-label). |
32
32
 
33
- Worked example (Sparrows Farm):
34
- - `property_name` = "Sparrows Farm"
35
- - `property_slug` = "sparrows-farm"
36
- - `address_subtitle` = `Henham Road <em>·</em> Hamperden End <em>·</em> Essex <em>·</em> CB11&nbsp;3LZ`
37
- - `folio_strip` = "Sparrows Farm · Hamperden End"
33
+ Worked example (Elmfield House):
34
+ - `property_name` = "Elmfield House"
35
+ - `property_slug` = "elmfield-house"
36
+ - `address_subtitle` = `Elmfield Road <em>·</em> Marlow <em>·</em> Buckinghamshire <em>·</em> SL7&nbsp;1AA`
37
+ - `folio_strip` = "Elmfield House · Marlow"
38
38
 
39
39
  ## Category 2 — Brand pack
40
40
 
@@ -43,15 +43,15 @@ From the brand pack at `<brand_dir>/{description.md, DESIGN.md}` and `property.j
43
43
  | Token | Source | Format | Notes |
44
44
  |---|---|---|---|
45
45
  | `{{ brand_slug }}` | `<brand_dir>` basename | lowercase-kebab | Used in logo filenames: `images/{{ brand_slug }}-logo-light.png` (dark surfaces) and `images/{{ brand_slug }}-logo-dark.png` (light surfaces). Naming convention is the **colour of the art**, not the surface — see `images.md` → "Brand logo". |
46
- | `{{ brand_name }}` | `description.md` first line | Title Case | "Muvin", "Beacons Real Estate". |
46
+ | `{{ brand_name }}` | `description.md` first line | Title Case | The agent's trading name, e.g. "Hallow & Crest". |
47
47
  | `{{ brand_town }}` | `description.md` officials block | Title Case | Branch town. |
48
48
  | `{{ brand_county }}` | `description.md` officials block | Title Case | |
49
49
  | `{{ brand_year }}` | Current year | YYYY | Used in the index.html footer copyright. |
50
50
  | `{{ office_address_line_1 }}` | `description.md` registered office line | "25 Bell Street" | Street, first line of the postal address. |
51
- | `{{ office_address_line_2 }}` | `description.md` registered office line | "Sawbridgeworth, Hertfordshire CM21 9AR" | Locality + county + postcode. |
52
- | `{{ office_phone }}` | `property.json -> agent.phone` | Display format | "01873 377 575". |
53
- | `{{ office_phone_tel }}` | Composed | `tel:` link format | "+441873377575" (no spaces, with country code). |
54
- | `{{ office_email }}` | `property.json -> agent.email` | lowercase | "sales@beaconsrealestate.com". |
51
+ | `{{ office_address_line_2 }}` | `description.md` registered office line | "Marlow, Buckinghamshire SL7 1AA" | Locality + county + postcode. |
52
+ | `{{ office_phone }}` | `property.json -> agent.phone` | Display format | "01628 000 000". |
53
+ | `{{ office_phone_tel }}` | Composed | `tel:` link format | "+441628000000" (no spaces, with country code). |
54
+ | `{{ office_email }}` | `property.json -> agent.email` | lowercase | "sales@example.com". |
55
55
  | `{{ email_subject_enquiry }}` | Composed | `{{ property_name }} — enquiry` | Used in mailto: subject param (URL-encode spaces if needed). |
56
56
  | `{{ email_subject_viewing }}` | Composed | `{{ property_name }} — viewing request` | Same as above. |
57
57
 
@@ -64,7 +64,7 @@ From `property.json -> agent`.
64
64
  | `{{ agent_first_name }}` | First name | Title Case | Goes in `<em>` on the back page contact cell. |
65
65
  | `{{ agent_surname }}` | Last name (possibly hyphenated) | Title Case | Plain weight after the first name. |
66
66
 
67
- Worked example (Sparrows Farm): `{{ agent_first_name }}` = "Adam", `{{ agent_surname }}` = "Mackay".
67
+ Worked example (Elmfield House): `{{ agent_first_name }}` = "Jordan", `{{ agent_surname }}` = "Hale".
68
68
 
69
69
  ## Category 4 — Listing references
70
70
 
@@ -74,7 +74,7 @@ From `property.json -> source` and `property.json -> media`.
74
74
  |---|---|---|---|
75
75
  | `{{ listing_url }}` | `source.url` | full URL | Used in QR + listing CTAs. |
76
76
  | `{{ listing_id }}` | `source.listing_id` | numeric | |
77
- | `{{ listing_display }}` | Composed | "{{ brand domain }} &middot; listing {{ listing_id }}" | Short, scannable text for QR caption (e.g. "beaconsrealestate.co.uk · listing 1424919"). |
77
+ | `{{ listing_display }}` | Composed | "{{ brand domain }} &middot; listing {{ listing_id }}" | Short, scannable text for QR caption (e.g. "example.co.uk · listing 100000"). |
78
78
  | `{{ video_url }}` | `media.video_tours[0]` | full URL | Empty string if no video; in that case drop the video QR per the back-page rules. |
79
79
  | `{{ video_short_url }}` | derived | "youtu.be/<id>" or equivalent | Visible URL under the video QR. |
80
80
 
@@ -25,7 +25,7 @@ The signal is the user's wording, not the property. Don't infer premium from a h
25
25
 
26
26
  ## Type-role overrides
27
27
 
28
- The branded register's font-family and palette swap alone produces a folio painted in the brand's colours but still wearing the template's editorial conventions — italic display, small-caps decks, gold hairlines under every heading, dropcap, italic emphasis on `<em>`. For brands whose live identity does not use those conventions (Cooper Adams' upright TrajanPro, eXp UK's Montserrat, Knight-Frank-adjacent vs corporate-residential), that's a mismatch. The brochure reads as a country-house folio with brand paint, not as a brand document.
28
+ The branded register's font-family and palette swap alone produces a folio painted in the brand's colours but still wearing the template's editorial conventions — italic display, small-caps decks, gold hairlines under every heading, dropcap, italic emphasis on `<em>`. For brands whose live identity does not use those conventions (an upright serif such as TrajanPro, a geometric sans such as Montserrat, prestige-country vs corporate-residential), that's a mismatch. The brochure reads as a country-house folio with brand paint, not as a brand document.
29
29
 
30
30
  To close the gap without leaving branded territory, the brand pack's `DESIGN.md` may declare a `## Type roles` block. Each row maps a named brochure type-role to a brand styling decision. The build pipeline reads these and emits a single override CSS block — see `build.md → Type-role overlay generation` for the recipe map.
31
31
 
@@ -22,7 +22,7 @@ The 20 questions cluster into four uses: **voice setters** (Q5, Q19, Q20 — set
22
22
  | **Q10.** Design details others notice | Strip-cell captions (page 4); attr-grid features (page 7 feature page) |
23
23
  | **Q11.** What you love about the local area | Garden & ground page text panels (rear garden, front & approach, ancillary) |
24
24
  | **Q12.** Hidden gems nearby | Garden & ground secondary panel; possibly a footnote on the back page |
25
- | **Q13.** Commute / transport | Opener spec line (e.g. "*Essex · CB11 3LZ · 50 minutes from Liverpool Street*") if commutable |
25
+ | **Q13.** Commute / transport | Opener spec line (e.g. "*&lt;county&gt; · &lt;postcode&gt; · 50 minutes from &lt;terminus&gt;*") if commutable |
26
26
  | **Q14.** Community vibe | Garden & ground tone; back-page tagline supporting line |
27
27
  | **Q15.** Who this home suits best | Buyer-avatar tone (reinforces Q7) |
28
28
  | **Q16.** Hoped-for next buyer | Tone of the back-page CTA ("Arrange a viewing of …") |
@@ -37,9 +37,9 @@ Sellers' answers are conversational. The brochure's register is editorial. Most
37
37
 
38
38
  `[VERBATIM]` is a contract: the answer is preserved exactly, in italic, as a pull-quote on a chapter page or as the back-page tagline. Use it sparingly — at most one or two per brochure. The Q20 answer is the most common candidate; Q4 and Q5 occasionally.
39
39
 
40
- ## Worked example — Sparrows Farm
40
+ ## Worked example — Elmfield House
41
41
 
42
- (Hypothetical — Sparrows Farm shipped without a brief; a real example would source from the seller.)
42
+ (Hypothetical — Elmfield House shipped without a brief; a real example would source from the seller.)
43
43
 
44
44
  > **Q5.** A buyer should feel that the home is *settled*. Six hundred years of beam and stone with a kitchen that wants to be cooked in.
45
45
  >
@@ -89,7 +89,7 @@ If a brochure has multiple floor plans (per-floor breakdowns), tune the layout t
89
89
 
90
90
  Property listings in this register often have several distinct outdoor or auxiliary buildings, each photographed separately. The brochure must keep them distinct in copy *and* match each photo to the right structure. The recurring failure mode is collapsing them into a single noun ("the cedar pavilion", "the garden complex") that means different things on different pages.
91
91
 
92
- For Henham/Sparrows-Farm-style country homes the typical glossary is:
92
+ For period country homes the typical glossary is:
93
93
 
94
94
  | Feature | Description | Photo cue |
95
95
  |---|---|---|
@@ -142,7 +142,7 @@ The back page includes two QR codes, one for the video tour and one for the list
142
142
 
143
143
  1. The QR image itself (for print scanning).
144
144
  2. A short label above describing what it links to ("Watch the *video tour*", "View the *full listing*").
145
- 3. **A visible, underlined URL beneath the label** — short and human-readable (e.g. `youtu.be/8V7jjS5ZyeI`, `muvin.co.uk · listing 949931`).
145
+ 3. **A visible, underlined URL beneath the label** — short and human-readable (e.g. `youtu.be/<video-id>`, `example.co.uk · listing 100000`).
146
146
  4. **The whole stack wrapped in an `<a>` tag with `target="_blank" rel="noopener"`** so the QR image, label and URL are all clickable in the digital PDF and on the web.
147
147
 
148
148
  Markup:
@@ -194,7 +194,7 @@ The brochure includes a location map **only when the source listing supplies one
194
194
 
195
195
  ### How the conditional works
196
196
 
197
- `property-extract` screenshots whatever map element exists on the listing — provider-agnostic by design. Google Maps iframes (Loop CRM / Muvin), Mapbox or Leaflet div+canvas builds, static-map `<img>` tags from Rightmove-style backends, Apple/Bing embeds — they all land as `<property_dir>/images/map.webp` and `media.map: "map.webp"` in `property.json`. When no map element is detected on the listing, both `media.map` and (typically) `address.geo` are `null`. See **Map — extract or omit** in the property-extract SKILL for the heuristic detector and the screenshot contract.
197
+ `property-extract` screenshots whatever map element exists on the listing — provider-agnostic by design. Google Maps iframes (Loop CRM tenants), Mapbox or Leaflet div+canvas builds, static-map `<img>` tags from Rightmove-style backends, Apple/Bing embeds — they all land as `<property_dir>/images/map.webp` and `media.map: "map.webp"` in `property.json`. When no map element is detected on the listing, both `media.map` and (typically) `address.geo` are `null`. See **Map — extract or omit** in the property-extract SKILL for the heuristic detector and the screenshot contract.
198
198
 
199
199
  The template ships with a marker-bounded conditional block on page 10 (Plan & particulars), in the `.spec-side` column above the EPC bars:
200
200
 
@@ -36,8 +36,7 @@
36
36
  references/placeholders.md
37
37
  Each <!-- REPLACE: ... --> inline comment in this file gives the
38
38
  short version (what fills the token, how long, what voice). The
39
- placeholders.md file gives the long version with worked examples
40
- from the validated muvin Sparrows Farm reference brochure.
39
+ placeholders.md file gives the long version with worked examples.
41
40
 
42
41
  Re-skin: agents override the design tokens in :root from their
43
42
  brand DESIGN.md per the property-brochure SKILL ("registers" — see
@@ -45,7 +44,7 @@
45
44
 
46
45
  Image filenames follow the pattern {{ property_slug }}-NN.webp,
47
46
  where {{ property_slug }} is the literal slug from the property
48
- directory (e.g. "sparrows-farm" or "agincourt-house") and NN is
47
+ directory (e.g. "elmfield-house" or "wrenbury-hall") and NN is
49
48
  the zero-padded image index per references/images.md.
50
49
  ═══════════════════════════════════════════════════ */
51
50
  :root {
@@ -1422,9 +1421,9 @@
1422
1421
  <div class="cover-title-line">
1423
1422
  <span class="flank"></span>
1424
1423
  <!-- REPLACE {{ property_name }}: the property's distinct house name
1425
- (e.g. "Sparrows Farm", "Agincourt House") OR — if the property has
1424
+ (e.g. "Elmfield House", "Wrenbury Hall") OR — if the property has
1426
1425
  no separate name — the street as a tracked-caps proper noun
1427
- (e.g. "12 Henham Road"). All-caps display via CSS; the source
1426
+ (e.g. "12 Elmfield Road"). All-caps display via CSS; the source
1428
1427
  should be Title Case. Capped at ~28 characters to fit between
1429
1428
  the gold flank rules. See placeholders.md for the picking rule. -->
1430
1429
  <h1 class="cover-title">{{ property_name }}</h1>
@@ -2354,7 +2353,7 @@
2354
2353
 
2355
2354
  <!-- REPLACE the agent + office cells. {{ agent_first_name }} sits in
2356
2355
  <em>; {{ agent_surname }} follows in normal weight. {{ office_phone }}
2357
- is the display format ("01873 377 575"); {{ office_email }} the
2356
+ is the display format ("01628 000 000"); {{ office_email }} the
2358
2357
  contact email; {{ office_address_line_1 }} and
2359
2358
  {{ office_address_line_2 }} the postal lines. -->
2360
2359
  <div class="back-contact-grid">
@@ -15,7 +15,7 @@ allowed-tools:
15
15
 
16
16
  Turn one publicly-accessible property listing URL into a self-contained property assets directory that `property-brochure` can consume without re-visiting the source site.
17
17
 
18
- The contract is **portal-agnostic**: the output shape is the same whether the source is an agent's own site (Muvin / Loop CRM tenants, custom builds) or a portal (Rightmove, Zoopla, OnTheMarket, OpenRent). The static-HTML probe is the cheap default; when it returns no asset URLs or the data is tab-bound, fall back to a headless browser. See **Process — high level**.
18
+ The contract is **portal-agnostic**: the output shape is the same whether the source is an agent's own site (Loop CRM tenants, custom builds) or a portal (Rightmove, Zoopla, OnTheMarket, OpenRent). The static-HTML probe is the cheap default; when it returns no asset URLs or the data is tab-bound, fall back to a headless browser. See **Process — high level**.
19
19
 
20
20
  ## Outcome contract
21
21
 
@@ -13,7 +13,7 @@ Real Agency Club operates within the eXp UK framework. Members benefit from unde
13
13
 
14
14
  2. **Lead with the problem.** Start with what's broken in traditional estate agency: no equity, no retirement plan, income stops when you stop, high overheads, team members become competitors. Then show how eXp addresses each one.
15
15
 
16
- 3. **Always defer commercial specifics to Adam.** You can explain the model, share general figures, and point to resources. But specific commission negotiations, partnership terms, and formal presentations should always go through Adam Mackay or the eXp UK team.
16
+ 3. **Always defer commercial specifics to a human.** You can explain the model, share general figures, and point to resources. But specific commission negotiations, partnership terms, and formal presentations should always go through the account's own partnership lead or the eXp UK team.
17
17
 
18
18
  ## What You Should Know
19
19
 
@@ -1,6 +1,6 @@
1
1
  # 12 Reasons a Business Should Consider Partnering With eXp
2
2
 
3
- Adapted from Adam Mackay's "12 Days of Christmas" guide for Real Agency.
3
+ Adapted from a "12 Days of Christmas" partnership guide.
4
4
 
5
5
  ## 1. You Keep Your Brand
6
6
  Partnering doesn't mean losing your identity. Your brand, listings, data, and reputation stay yours. eXp is the platform underneath, not the face of your business.
@@ -89,4 +89,4 @@ You can leave with 2 weeks' notice and go back to how you operated previously. N
89
89
 
90
90
  ## Important
91
91
 
92
- All commercial terms, partnership specifics, and formal presentations should go through Adam Mackay and the eXp UK team. This reference is for education — not for making commitments.
92
+ All commercial terms, partnership specifics, and formal presentations should go through the account's own partnership lead and the eXp UK team. This reference is for education — not for making commitments.
@@ -1,5 +1,4 @@
1
1
  # Performance Coaching Framework
2
- *Source: BackleyBlack (Steve Backley OBE & Roger Black MBE) + Adam Mackay & Jamie Fisher*
3
2
 
4
3
  ## 45 Core Behavioural Characteristics of Top Estate Agents
5
4
 
@@ -106,4 +105,4 @@ When coaching a member:
106
105
  - Use the **45 characteristics** as a diagnostic — ask them to pick their top 3 strengths and top 3 development areas
107
106
  - Use the **10 PCDEs** for a structured self-assessment (monthly check-in)
108
107
  - Use the **Mind-Setting phases** as a progressive programme — members work through at their own pace
109
- - Always attribute: "This framework comes from Steve Backley and Roger Black Olympic athletes who've translated performance principles into business"
108
+ - Always attribute the framework to whoever the install's own content directory credits for it; never present it as the agent's own invention
@@ -34,6 +34,6 @@ A member asks about available content, wants to learn something, requests a spec
34
34
  | Agency Blueprint | Setting up and structuring an agency | New agents, career changers |
35
35
  | Real Marketing | Social media, content creation, personal brand | Brand builders, content creators |
36
36
  | Real Business | Systems, structure, profit, scaling | Growth-stage agents |
37
- | Real Coaching | Mindset, performance (Backley & Black) | Anyone wanting mental edge |
37
+ | Real Coaching | Mindset, performance | Anyone wanting mental edge |
38
38
  | Podcast | Conversations with working agents | Inspiration, peer learning |
39
39
  | Resources | Templates, scripts, SOPs, tools | Practical application |
@@ -34,7 +34,7 @@ If the member asks for a topic without specifying a module, search across all pi
34
34
 
35
35
  1. **Introduce** — What the module covers, who it's from, why it matters for this member specifically. Reference their profile context.
36
36
 
37
- 2. **Teach the core concept** — Present the key insight or framework. Use the contributor's voice and perspective. "Adam's approach to this is..." or "Steve Backley's framework here is..."
37
+ 2. **Teach the core concept** — Present the key insight or framework. Use the contributor's voice and perspective, naming them as the module's own metadata credits them: "<contributor>'s approach to this is..."
38
38
 
39
39
  3. **Check understanding** — Ask the member to reflect. "How does this compare to how you currently handle...?" or "What would that look like for your patch?"
40
40
 
@@ -13,7 +13,7 @@ When a new property goes live, proactively match it against registered buyers. A
13
13
 
14
14
  ## Outreach Message Principles
15
15
 
16
- - **Personal** — reference their requirements: "I know you mentioned you were looking for a 3-bed with a garden in the Abergavenny area..."
16
+ - **Personal** — reference their requirements: "I know you mentioned you were looking for a 3-bed with a garden in the <town> area..."
17
17
  - **Enthusiastic but honest** — share what makes this property a good match for them specifically
18
18
  - **Low pressure** — "I thought this might be worth a look" not "You need to see this before it's gone"
19
19
  - **Call to action** — offer a viewing with specific time options
@@ -36,8 +36,8 @@ Schema requires `brand.font_display` to be set for `on-brand` to engage — a mi
36
36
 
37
37
  ## Inputs (never invented)
38
38
 
39
- - **`address`** — full UK address ending in a valid postcode (e.g. `9 Phoenix Drive, Bishop's Stortford, CM23 2UJ`). Skill derives the postcode by matching the UK postcode regex on the final token group; stops if no match.
40
- - **`brand`** — agent design tokens, same shape as property-market-report: `name`, `tagline`, hex tokens `primary` / `primary_dark` / `accent` / `paper` / `paper_banded` / `rule`. Optional `logo_light` / `logo_dark` filesystem paths. Resolved from the agent's `DESIGN.md` (e.g. `/estate-agents/muvin/DESIGN.md`); read the canonical hex values directly, never substitute "close" colours. Only colour tokens are consumed; typography/spacing/radius tokens in the agent's `DESIGN.md` are ignored by design.
39
+ - **`address`** — full UK address ending in a valid postcode (e.g. `12 Elmfield Road, Marlow, SL7 1AA`). Skill derives the postcode by matching the UK postcode regex on the final token group; stops if no match.
40
+ - **`brand`** — agent design tokens, same shape as property-market-report: `name`, `tagline`, hex tokens `primary` / `primary_dark` / `accent` / `paper` / `paper_banded` / `rule`. Optional `logo_light` / `logo_dark` filesystem paths. Resolved from the agent's `DESIGN.md` (`~/estate-agents/<brand-slug>/DESIGN.md`); read the canonical hex values directly, never substitute "close" colours. Only colour tokens are consumed; typography/spacing/radius tokens in the agent's `DESIGN.md` are ignored by design.
41
41
  - **`subject`** — paste-in details about the property being valued:
42
42
  - `cover_hero` (filesystem path or URL to a photo of the subject — required for the cover).
43
43
  - Optional `beds`, `type`, `tenure`, `condition` (well / average / needs work).
@@ -76,7 +76,7 @@ EPC certificate fields are operator paste-in — see `subject.total_floor_area_m
76
76
 
77
77
  ## Output
78
78
 
79
- Directory under `~/estate-agents/<brand-slug>/preval/<address-slug>/` (e.g. `~/estate-agents/muvin/preval/11-phoenix-drive-cm23-2uj/`) containing:
79
+ Directory under `~/estate-agents/<brand-slug>/preval/<address-slug>/` (e.g. `~/estate-agents/<brand-slug>/preval/12-elmfield-road-sl7-1aa/`) containing:
80
80
 
81
81
  - `index.html` — single self-contained file.
82
82
  - `img/` — copied logos and the subject hero image.
@@ -312,7 +312,7 @@ def _street_token(address: str) -> str | None:
312
312
  """First non-stopword token from the address's street component.
313
313
 
314
314
  Used to detect cover_hero photos sourced from a sister property folder.
315
- "9 Phoenix Drive, Bishop's Stortford, CM23 2UJ" → "phoenix".
315
+ "12 Elmfield Road, Marlow, SL7 1AA" → "elmfield".
316
316
  """
317
317
  if not address:
318
318
  return None
@@ -12,7 +12,7 @@
12
12
  "out_dir": { "type": "string", "description": "Absolute path to the output directory (created if missing)." },
13
13
  "filename_stem": { "type": "string", "description": "PDF filename stem; .pdf appended by the renderer." },
14
14
  "address": { "type": "string", "description": "Full UK address as the operator gave it." },
15
- "postcode": { "type": "string", "description": "Postcode derived from the address, e.g. 'CM23 2UJ'." },
15
+ "postcode": { "type": "string", "description": "Postcode derived from the address, e.g. 'SL7 1AA'." },
16
16
  "generated": { "type": "string", "description": "Human date, e.g. '19 May 2026'." },
17
17
  "variant": { "type": "string", "enum": ["premium", "on-brand"], "description": "Render variant. 'premium' (default) keeps the editorial Cormorant/Lora/Inter typography lock and only applies the brand's colours + logo. 'on-brand' overrides the type system with brand.font_display + brand.font_body — the pack follows the agent's identity end-to-end. Decision rule: if the operator says 'premium' use premium; if brand.font_display + brand.font_body are both set and the operator did not say premium, default to 'on-brand'; otherwise premium." },
18
18
 
@@ -15,16 +15,16 @@ Convert a seller enquiry into a booked market appraisal with Alex. Understand th
15
15
  - Bereavement — handle with extra sensitivity
16
16
  3. **Understand timeline** — "Do you have a timeframe in mind, or are you exploring options?"
17
17
  4. **Understand their position** — "Will you be buying as well, or just selling?"
18
- 5. **Book the appraisal** — "Alex would love to come and see the property. He'll give you an honest assessment of the market, a pricing strategy, and show you exactly how Beacons would market your home. When works best for you?"
18
+ 5. **Book the appraisal** — "We'd love to come and see the property. You'll get an honest assessment of the market, a pricing strategy, and see exactly how we'd market your home. When works best for you?"
19
19
 
20
20
  ## Switching Agents
21
21
 
22
- Many Beacons clients come after frustration with another agent. Handle this carefully:
22
+ Many clients come after frustration with another agent. Handle this carefully:
23
23
 
24
24
  1. **Listen first** — let them vent if they need to. Acknowledge the frustration without fuelling it
25
25
  2. **Never name or criticise the other agent** — "That sounds really frustrating" is enough
26
- 3. **Focus forward** — explain what Beacons does differently:
27
- - One point of contact (Alex, not a rotating team)
26
+ 3. **Focus forward** — explain what we do differently:
27
+ - One point of contact, not a rotating team
28
28
  - Quality marketing for every property (photography, video, drone, social media)
29
29
  - Proactive communication (you'll never have to chase)
30
30
  - Track record of turning around stale listings
@@ -2,11 +2,11 @@
2
2
 
3
3
  ## Purpose
4
4
 
5
- Keep vendors informed and confident throughout the marketing period. The number one complaint about estate agents is silence — Beacons doesn't do silence.
5
+ Keep vendors informed and confident throughout the marketing period. The number one complaint about estate agents is silence — we don't do silence.
6
6
 
7
7
  ## Update Cadence
8
8
 
9
- - **Weekly minimum** — every vendor should hear from Beacons at least once a week
9
+ - **Weekly minimum** — every vendor should hear from us at least once a week
10
10
  - **After every viewing** — feedback relayed within 24-48 hours
11
11
  - **After any portal activity spike** — if a listing gets a burst of saves or clicks, share it
12
12
  - **When there's nothing to report** — say so. "No viewings this week, but here's what we're doing next" is infinitely better than silence
@@ -577,6 +577,10 @@ function classifyUpstreamError(err) {
577
577
  function formatUpstreamErrorLine(args) {
578
578
  return `[edge] ts=${(/* @__PURE__ */ new Date()).toISOString()} upstream http error path=${proxyPathname(args.url)} err=${args.errMessage} err-class=${args.errClass} upstream=${args.upstream}`;
579
579
  }
580
+ function formatUpstreamUpgradeLine(args) {
581
+ const err = args.errMessage === void 0 ? "" : ` err=${args.errMessage}`;
582
+ return `[edge] ts=${(/* @__PURE__ */ new Date()).toISOString()} upstream upgrade ${args.kind} path=${proxyPathname(args.url)}${err}`;
583
+ }
580
584
  var HTML_ESCAPES = { "<": "&lt;", ">": "&gt;", "&": "&amp;", '"': "&quot;", "'": "&#39;" };
581
585
  function escapeHtml(s) {
582
586
  return String(s).replace(/[<>&"']/g, (c) => HTML_ESCAPES[c] ?? c);
@@ -792,7 +796,7 @@ function forwardUpgrade(req, clientSocket, head) {
792
796
  upstream.once("close", () => teardown("upstream-close"));
793
797
  clientSocket.once("error", () => teardown("client-error"));
794
798
  upstream.once("error", (err) => {
795
- console.error(`[edge] upstream upgrade error path=${req.url} err=${err.message}`);
799
+ console.error(formatUpstreamUpgradeLine({ kind: "error", url: req.url ?? "", errMessage: err.message }));
796
800
  try {
797
801
  clientSocket.write("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
798
802
  } catch {
@@ -800,7 +804,7 @@ function forwardUpgrade(req, clientSocket, head) {
800
804
  teardown("upstream-error");
801
805
  });
802
806
  upstream.once("timeout", () => {
803
- console.error(`[edge] upstream upgrade timeout path=${req.url}`);
807
+ console.error(formatUpstreamUpgradeLine({ kind: "timeout", url: req.url ?? "" }));
804
808
  try {
805
809
  clientSocket.write("HTTP/1.1 504 Gateway Timeout\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
806
810
  } catch {
@@ -0,0 +1 @@
1
+ import{o as e}from"./chunk-CltuBf4Z.js";import{B as t,D as n,K as r,V as i,W as a,a as o,l as s,m as c,p as l,s as u,v as ee}from"./useSubAccountSwitcher-CJKjEPNN.js";import{n as d,t as f}from"./AdminLoginScreens-bksN0XPe.js";import{s as p}from"./useVoiceRecorder-ChnBrzbZ.js";import{a as m,b as h,f as te,t as g,y as ne}from"./AdminShell-BMPEWNZk.js";import{a as _,i as v,n as y,r as re}from"./page-BWFHRIAH.js";import{t as b}from"./chevron-left-DqYv3oFh.js";import{t as x}from"./chevron-right-CQfPGsFb.js";import{t as S}from"./file-text-DMKw3nIk.js";import"./graph-labels-BZgd0L6n.js";var C=a(),w=e(r(),1),T=t();function E({onOpen:e}){return(0,T.jsx)(`footer`,{className:`admin-footer`,children:(0,T.jsxs)(`div`,{className:`powered-by`,role:`button`,tabIndex:0,onClick:e,onKeyDown:t=>{(t.key===`Enter`||t.key===` `)&&(t.preventDefault(),e())},"aria-label":`Powered by Claude Code — show details`,children:[(0,T.jsx)(`img`,{src:`/brand/claude.png`,alt:`Claude`,className:`powered-by-icon`}),(0,T.jsx)(`span`,{children:`Powered by Claude Code`})]})})}function D({sessionId:e,projectDir:t,sessionKey:n,contactName:r,onBack:i}){return(0,T.jsxs)(`div`,{className:`wa-web`,children:[(0,T.jsxs)(`header`,{className:`wa-web-header`,children:[i&&(0,T.jsx)(`button`,{type:`button`,className:`wa-web-back`,onClick:i,"aria-label":`Back`,children:(0,T.jsx)(b,{size:20})}),(0,T.jsx)(`span`,{className:`wa-web-avatar`,"aria-hidden":!0,children:(0,T.jsx)(_,{size:20})}),(0,T.jsx)(`div`,{className:`wa-web-titles`,children:(0,T.jsx)(`span`,{className:`wa-web-name`,children:r})})]}),(0,T.jsx)(`div`,{className:`wa-web-body`,children:(0,T.jsx)(p,{sessionId:e,projectDir:t,sessionKey:n,forceDeliveredOnly:!0})})]})}var O=6e4,k=60*O,A=24*k,j=O,M=[`Jan`,`Feb`,`Mar`,`Apr`,`May`,`Jun`,`Jul`,`Aug`,`Sep`,`Oct`,`Nov`,`Dec`],N=[`Sun`,`Mon`,`Tue`,`Wed`,`Thu`,`Fri`,`Sat`];function P(e){return e<10?`0${e}`:String(e)}function F(e){return new Date(e.getFullYear(),e.getMonth(),e.getDate()).getTime()}function I(e,t){if(!Number.isFinite(e)||e<=0||!Number.isFinite(t))return`—`;let n=t-e;if(n<-j)return`—`;let r=n<0?0:n;if(r<O)return`just now`;if(r<k)return`${Math.floor(r/O)}m`;if(r<A)return`${Math.floor(r/k)}h`;let i=new Date(t),a=new Date(e),o=Math.round((F(i)-F(a))/A);if(o===1)return`yesterday`;if(o>=2&&o<=6)return N[a.getDay()];let s=P(a.getDate()),c=M[a.getMonth()];return a.getFullYear()===i.getFullYear()?`${s} ${c}`:`${s} ${c} ${a.getFullYear()}`}var ie={whatsapp:`WhatsApp`,telegram:`Telegram`,webchat:`Web chat`};function L({variant:e=`row`,count:t=1}){return(0,T.jsx)(T.Fragment,{children:Array.from({length:t},(t,n)=>(0,T.jsx)(`span`,{className:`dashboard-skeleton dashboard-skeleton--${e}`,"data-testid":`dashboard-skeleton`,"aria-hidden":`true`},n))})}var ae=6e4;function oe(e){return e<12?`morning`:e<18?`afternoon`:`evening`}function R(e){return e&&e.trim().split(/\s+/)[0]||`there`}function se(e){if(!e)return`everything is running normally.`;let t=e.cpuPct,n=e.memUsedPct;return t!==null&&t>=.98||n!==null&&n>=.98?`system load is critical.`:t!==null&&t>=.9||n!==null&&n>=.9?`system load is high.`:`everything is running normally.`}function z(e){return e==null?`—`:`${Math.round(e*100)}%`}function B(e){return e.operatorName??e.whatsappName??e.senderId??e.title}function V(e,t){if(!e)return!1;let n=Date.parse(e);if(!Number.isFinite(n))return!1;let r=new Date(n),i=new Date(t);return r.getFullYear()===i.getFullYear()&&r.getMonth()===i.getMonth()&&r.getDate()===i.getDate()}function H(e){return e?e.replace(/^claude-/,``):null}var U=new Set([`sessions`,`conversations`,`tasks`,`artefacts`,`skills`,`stats`]),W=!1;function G(e,t,n,r){let i=e=>t.get(e)??null,a=r?r.getEntriesByType(`navigation`)[0]:void 0,o=r?r.getEntriesByType(`resource`).filter(e=>new URL(e.name,`http://x`).pathname.startsWith(`/assets/`)):[],s=e=>e===void 0||!Number.isFinite(e)?null:Math.round(e),c=e=>e?s(e):null;return{batchMs:e,sessionsMs:i(`sessions`),conversationsMs:i(`conversations`),tasksMs:i(`tasks`),artefactsMs:i(`artefacts`),skillsMs:i(`skills`),statsMs:i(`stats`),failed:[...U].filter(e=>n.has(e)).join(`,`)||`none`,ttfbMs:s(a?.responseStart),domContentLoadedMs:c(a?.domContentLoadedEventEnd),loadMs:c(a?.loadEventEnd),assetCount:o.length,assetBytes:o.reduce((e,t)=>e+(t.transferSize||0),0),assetMs:o.length?s(Math.max(...o.map(e=>e.responseEnd))):null}}function K({cacheKey:e,userName:t,onSelectWhatsappConversation:r}){let a=c(e),[l,d]=(0,w.useState)(null),[f,p]=(0,w.useState)(null),[m,g]=(0,w.useState)(null),_=(0,w.useRef)(!1),[v,y]=(0,w.useState)(null),[b,C]=(0,w.useState)(null),[E,D]=(0,w.useState)(null),[O,k]=(0,w.useState)(null),[A,j]=(0,w.useState)(new Set),[M,N]=(0,w.useState)(()=>new Set),P=(0,w.useCallback)(e=>{N(t=>t.has(e)?t:new Set(t).add(e))},[]),F=Date.now(),U=(0,w.useRef)(!1),K=(0,w.useRef)(new Map),q=(0,w.useRef)(new Set),J=(0,w.useRef)(null);(0,w.useEffect)(()=>{if(!e)return;let t=!1;async function n(e,n,r,i){let o=Date.now(),s=typeof n==`string`?n:`unresolved`;try{typeof n!=`string`&&(s=await n());let c=await a(s);if(!c.ok)throw Error(`status ${c.status}`);let l=await c.json();t||i(r(l)),console.info(`[admin-ui] op=dashboard-fetch slice=${e} url=${s} outcome=ok ms=${Date.now()-o}`)}catch(t){q.current.add(e),console.error(`[admin-ui] dashboard-fetch-failed slice=${e} url=${s} ms=${Date.now()-o} reason=${t instanceof Error?t.message:String(t)}`)}finally{K.current.set(e,Date.now()-o),P(e)}}async function r(){if(!J.current){let e=await a(`/api/admin/accounts`);if(!e.ok)throw Error(`accounts status ${e.status}`);let t=await e.json(),n=t.accounts??[],r=t.activeAccountId??null,i=n.find(e=>e.accountId===r);if(!i)throw Error(`accounts: active ${r??`none`} absent from list of ${n.length}`);let o=i.isHouse||n.length===1;J.current={accountId:i.accountId,isHouse:o}}let{accountId:e,isHouse:t}=J.current;return console.info(`[admin-ui] op=dashboard-channels-source source=${t?`all`:`scoped`} account=${e.slice(0,8)}`),t?`/api/whatsapp-reader/conversations-all`:`/api/whatsapp-reader/conversations`}let o={"sidebar-sessions":()=>n(`sessions`,`/api/admin/sidebar-sessions`,e=>e.sessions??[],d),"conversations-all":()=>n(`conversations`,r,e=>e.conversations??[],p),"tasks-list":()=>n(`tasks`,`/api/admin/tasks-list`,e=>e,C),"sidebar-artefacts":()=>n(`artefacts`,`/api/admin/sidebar-artefacts`,e=>e.artefacts??[],D),skills:()=>n(`skills`,`/api/admin/skills`,e=>e.skills??[],k)};async function s(){if(U.current)return;U.current=!0;let e=Date.now();try{await Promise.all([...Object.values(o).map(e=>e()),n(`stats`,`/api/admin/system-stats`,e=>e,y)])}finally{U.current=!1;let t=Date.now()-e;if(console.info(`[admin-ui] op=dashboard-refresh-settled ms=${t}`),!W){W=!0;try{let e=G(t,K.current,q.current,typeof performance>`u`?null:performance);fetch(`/api/_client-error`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({kind:`event`,source:`dashboard-load`,...e}),keepalive:!0}).catch(()=>{})}catch{}}}}s();let c=null;try{c=new EventSource(i(`/api/admin/events`,e)),c.onmessage=e=>{if(document.visibilityState===`visible`)try{let{resource:t}=JSON.parse(e.data),n=t?o[t]:void 0;n&&n()}catch{}},c.onerror=()=>{console.error(`[admin-ui] dashboard-events stream error`)}}catch(e){console.error(`[admin-ui] dashboard-events unavailable reason=${e instanceof Error?e.message:String(e)}`)}let l=setInterval(()=>{document.visibilityState===`visible`&&s()},ae),u=()=>{document.visibilityState===`visible`&&s()};return document.addEventListener(`visibilitychange`,u),()=>{t=!0,U.current=!1,clearInterval(l),c?.close(),document.removeEventListener(`visibilitychange`,u)}},[e,a]);let Y=(0,w.useMemo)(()=>(l??[]).filter(e=>!e.isSubagent),[l]),X=(0,w.useMemo)(()=>Y.filter(e=>e.live),[Y]),Z=(0,w.useMemo)(()=>(f??[]).filter(e=>V(e.lastMessageAt,F)).length,[f,F]),ce=b?.open.length??0,Q=(0,w.useMemo)(()=>{let e=new Map;for(let t of f??[]){let n=e.get(t.channel)??[];n.push(t),e.set(t.channel,n)}return[...e.entries()].map(([e,t])=>{let n=t.reduce((e,t)=>t.lastMessageAt&&(!e||Date.parse(t.lastMessageAt)>Date.parse(e.lastMessageAt??``))?t:e,null);return{channel:e,count:t.length,preview:n?B(n):``,rows:t}})},[f]);(0,w.useEffect)(()=>{_.current||Q.length!==0&&(_.current=!0,g(Q[0].channel))},[Q]);let le=(0,w.useCallback)(async e=>{if(!A.has(e)){j(t=>new Set(t).add(e));try{let t=await a(`/api/admin/session-stop`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({sessionId:e})});if(!t.ok){console.error(`[admin-ui] dashboard-session-stop-failed sessionId=${e} status=${t.status}`);return}console.info(`[admin-ui] dashboard-session-stop sessionId=${e}`),d(t=>(t??[]).map(t=>t.sessionId===e?{...t,live:!1}:t))}catch(t){console.error(`[admin-ui] dashboard-session-stop-failed sessionId=${e} reason=${t instanceof Error?t.message:String(t)}`)}finally{j(t=>{let n=new Set(t);return n.delete(e),n})}}},[a,A]),ue=(0,w.useCallback)(t=>{if(!e||!t.downloadPath){console.error(`[admin-ui] dashboard-artefact-download-blocked id=${t.id} reason=${e?`not-downloadable`:`no-cache-key`}`);return}re(e,t.downloadPath,t.downloadRoot??`data`)},[e]),$=oe(new Date(F).getHours());return(0,T.jsxs)(`div`,{className:`dashboard-home`,children:[(0,T.jsx)(`p`,{className:`dashboard-greeting`,children:M.has(`stats`)?`Good ${$}, ${R(t)} — ${se(v)}`:`Good ${$}, ${R(t)}`}),(0,T.jsxs)(`div`,{className:`dashboard-stats`,children:[(0,T.jsxs)(`div`,{className:`admin-stat-card dashboard-stat-card`,children:[(0,T.jsx)(`span`,{className:`dashboard-stat-label`,children:`Live sessions`}),M.has(`sessions`)?(0,T.jsxs)(T.Fragment,{children:[(0,T.jsx)(`span`,{className:`dashboard-stat-value`,children:X.length}),(0,T.jsxs)(`span`,{className:`dashboard-stat-delta`,children:[`of `,Y.length,` total`]})]}):(0,T.jsx)(L,{variant:`value`})]}),(0,T.jsxs)(`div`,{className:`admin-stat-card dashboard-stat-card`,children:[(0,T.jsx)(`span`,{className:`dashboard-stat-label`,children:`Messages today`}),M.has(`conversations`)?(0,T.jsx)(`span`,{className:`dashboard-stat-value`,children:Z}):(0,T.jsx)(L,{variant:`value`})]}),(0,T.jsxs)(`a`,{className:`admin-stat-card dashboard-stat-card dashboard-stat-card--link`,href:`/tasks`,children:[(0,T.jsx)(`span`,{className:`dashboard-stat-label`,children:`Open tasks`}),M.has(`tasks`)?(0,T.jsx)(`span`,{className:`dashboard-stat-value`,children:ce}):(0,T.jsx)(L,{variant:`value`})]}),(0,T.jsxs)(`div`,{className:`admin-stat-card dashboard-stat-card`,children:[(0,T.jsx)(`span`,{className:`dashboard-stat-label`,children:`Host CPU`}),M.has(`stats`)?(0,T.jsxs)(T.Fragment,{children:[(0,T.jsx)(`span`,{className:`dashboard-stat-value`,children:z(v?.cpuPct??null)}),(0,T.jsxs)(`span`,{className:`dashboard-stat-delta`,children:[`RAM `,z(v?.memUsedPct??null)]})]}):(0,T.jsx)(L,{variant:`value`})]})]}),(0,T.jsxs)(`div`,{className:`dashboard-columns`,children:[(0,T.jsxs)(`div`,{className:`dashboard-col`,children:[(0,T.jsx)(`span`,{className:`admin-section-label dashboard-col-label`,children:`Live now`}),M.has(`sessions`)?X.length===0?(0,T.jsx)(`div`,{className:`admin-card dashboard-empty`,children:`No live sessions right now.`}):X.map(e=>{let t=[e.sessionId.slice(0,8),H(e.model),I(Date.parse(e.startedAt),F)].filter(Boolean).join(` · `);return(0,T.jsxs)(`div`,{className:`admin-card dashboard-live-card`,children:[(0,T.jsx)(`span`,{className:`admin-status-dot is-live`}),(0,T.jsxs)(`div`,{className:`dashboard-live-main`,children:[(0,T.jsx)(`span`,{className:`dashboard-live-name`,children:e.title}),(0,T.jsx)(`span`,{className:`dashboard-live-meta`,children:t})]}),(0,T.jsx)(`a`,{className:`admin-btn-cta dashboard-open-btn`,href:`/chat?session=${encodeURIComponent(e.sessionId)}`,children:`Open`}),(0,T.jsx)(`button`,{type:`button`,className:`dashboard-stop-btn`,"aria-label":`Stop ${e.title}`,disabled:A.has(e.sessionId),onClick:()=>void le(e.sessionId),children:(0,T.jsx)(ee,{size:12})})]},e.sessionId)}):(0,T.jsx)(L,{variant:`card`,count:2}),(0,T.jsx)(`span`,{className:`admin-section-label dashboard-col-label`,children:`Recent artefacts`}),(0,T.jsx)(`div`,{className:`admin-card dashboard-artefacts`,children:M.has(`artefacts`)?(E??[]).length===0?(0,T.jsx)(`div`,{className:`dashboard-empty`,children:`No recent artefacts.`}):(E??[]).slice(0,6).map(e=>(0,T.jsxs)(`button`,{type:`button`,className:`admin-row dashboard-artefact-row`,onClick:()=>ue(e),children:[(0,T.jsx)(`span`,{className:`dashboard-artefact-icon`,children:e.kind===`agent-template`?(0,T.jsx)(h,{size:16}):(0,T.jsx)(S,{size:16})}),(0,T.jsx)(`div`,{className:`dashboard-artefact-body`,children:(0,T.jsx)(`span`,{className:`dashboard-artefact-name`,children:e.name})}),(0,T.jsx)(`span`,{className:`dashboard-artefact-time`,children:I(Date.parse(e.updatedAt),F)})]},e.id)):(0,T.jsx)(L,{count:3})}),(0,T.jsx)(`a`,{className:`admin-section-label dashboard-col-label dashboard-skills-label`,href:`/skills`,children:`Skills`}),(0,T.jsx)(`div`,{className:`admin-card dashboard-skills`,children:M.has(`skills`)?(O??[]).length===0?(0,T.jsx)(`div`,{className:`dashboard-empty`,children:`No skills yet.`}):(O??[]).map(e=>(0,T.jsxs)(`a`,{className:`admin-row dashboard-skill-row`,href:`/skills`,children:[(0,T.jsx)(`span`,{className:`dashboard-skill-icon`,children:(0,T.jsx)(te,{size:16})}),(0,T.jsxs)(`div`,{className:`dashboard-skill-body`,children:[(0,T.jsx)(`span`,{className:`dashboard-skill-name`,children:e.skill}),(0,T.jsx)(`span`,{className:`dashboard-skill-plugin`,children:e.plugin})]})]},`${e.plugin}/${e.skill}`)):(0,T.jsx)(L,{count:2})})]}),(0,T.jsxs)(`div`,{className:`dashboard-col`,children:[(0,T.jsx)(`span`,{className:`admin-section-label dashboard-col-label`,children:`Channels`}),M.has(`conversations`)?Q.length===0?(0,T.jsx)(`div`,{className:`admin-card dashboard-empty`,children:`No channel activity.`}):Q.map(e=>{let t=m===e.channel;return(0,T.jsxs)(`div`,{className:`admin-card dashboard-channel-card`,children:[(0,T.jsxs)(`button`,{type:`button`,className:`dashboard-channel-head`,"aria-expanded":t,onClick:()=>g(t=>t===e.channel?null:e.channel),children:[(0,T.jsx)(`span`,{className:`dashboard-channel-glyph`,children:e.channel===`whatsapp`||e.channel===`telegram`?(0,T.jsx)(s,{channel:e.channel,size:16}):(0,T.jsx)(n,{size:16})}),(0,T.jsx)(`span`,{className:`dashboard-channel-name`,children:ie[e.channel]??e.channel}),(0,T.jsx)(`span`,{className:`dashboard-channel-badge`,children:e.count}),(0,T.jsx)(`span`,{className:`dashboard-channel-chevron`,children:t?(0,T.jsx)(ne,{size:14}):(0,T.jsx)(x,{size:14})})]}),t?(0,T.jsx)(`div`,{className:`dashboard-channel-convs`,children:e.rows.map(e=>(0,T.jsxs)(`button`,{type:`button`,className:`admin-row dashboard-channel-conv-row`,onClick:()=>r(e),title:e.title,children:[(0,T.jsx)(`span`,{className:`dashboard-channel-conv-name`,children:o(e)}),(0,T.jsx)(`span`,{className:`dashboard-channel-conv-tag`,children:e.isHouse?`House`:`Client`}),e.lastMessageAt&&(0,T.jsx)(`span`,{className:`dashboard-channel-conv-time`,children:I(Date.parse(e.lastMessageAt),F)})]},u(e)))}):e.preview&&(0,T.jsx)(`span`,{className:`dashboard-channel-preview`,children:e.preview})]},e.channel)}):(0,T.jsx)(L,{variant:`card`,count:2})]})]})]})}function q(){let e=d(),[t,n]=(0,w.useState)(!1);(0,w.useEffect)(()=>{if(!t)return;let e=e=>{e.key===`Escape`&&n(!1)};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[t]);let r=(0,w.useCallback)(async()=>{let t=e.claudeInfo!=null;if(n(!0),t){console.info(`[admin-ui] claude-info-open cached=true ms=0`);return}let r=Date.now();if(!e.cacheKey){console.error(`[admin-ui] claude-info-fetch-skipped reason=no-session-key`);return}try{let t=await fetch(`/api/admin/claude-info?session_key=${encodeURIComponent(e.cacheKey)}`);if(t.ok){let n=await t.json();e.setClaudeInfo(n)}console.info(`[admin-ui] claude-info-open cached=false ms=${Date.now()-r}`)}catch(e){console.error(`[admin-ui] claude-info-fetch-failed ms=${Date.now()-r} error="${e instanceof Error?e.message:String(e)}"`)}},[e]),i=(0,w.useCallback)(({code:t,path:n})=>{console.warn(`[admin-auth] outcome=chat-tab-logout-on-401 code=${t} path=${n}`),e.handleLogout()},[e]);return e.appState===`chat`?(0,T.jsx)(l,{cacheKey:e.cacheKey,onSessionExpired:i,surface:`chat`,children:(0,T.jsx)(g,{cacheKey:e.cacheKey,businessName:e.businessName,sessionId:e.sessionId,onLogout:e.handleLogout,onDisconnect:e.handleDisconnect,disconnecting:e.disconnecting,claudeConnected:e.claudeConnected,onConnectClaude:()=>e.setAppState(`connect-claude`),userName:e.userName,userAvatar:e.userAvatar,role:e.role,footer:(0,T.jsxs)(T.Fragment,{children:[(0,T.jsx)(E,{onOpen:r}),(0,T.jsx)(m,{show:t,onClose:()=>n(!1),claudeInfo:e.claudeInfo,messages:[],sessionElapsed:0,sessionId:e.sessionId,cacheKey:e.cacheKey})]}),children:t=>(0,T.jsx)(T.Fragment,{children:t.dataOpen&&e.cacheKey?(0,T.jsx)(y,{cacheKey:e.cacheKey}):t.selectedWhatsapp&&e.cacheKey?t.selectedWhatsapp.source===`store`&&t.selectedWhatsapp.remoteJid&&t.selectedWhatsapp.accountId?(0,T.jsx)(v,{accountId:t.selectedWhatsapp.accountId,remoteJid:t.selectedWhatsapp.remoteJid,contactName:o(t.selectedWhatsapp),sessionKey:e.cacheKey,onBack:t.onClearWhatsapp},u(t.selectedWhatsapp)):(0,T.jsx)(D,{sessionId:t.selectedWhatsapp.sessionId,projectDir:t.selectedWhatsapp.projectDir,sessionKey:e.cacheKey,contactName:o(t.selectedWhatsapp),onBack:t.onClearWhatsapp},u(t.selectedWhatsapp)):(0,T.jsx)(K,{cacheKey:e.cacheKey,userName:e.userName,onSelectWhatsappConversation:t.onSelectWhatsappConversation})})})}):(0,T.jsx)(f,{auth:e})}(0,C.createRoot)(document.getElementById(`root`)).render((0,T.jsx)(q,{}));
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Real Agent</title>
7
7
  <link rel="icon" href="/favicon.ico">
8
- <script type="module" crossorigin src="/assets/admin-DGr1HuPg.js"></script>
8
+ <script type="module" crossorigin src="/assets/admin-6QN-k3zQ.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/chunk-CltuBf4Z.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/useSubAccountSwitcher-CJKjEPNN.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/graph-labels-BZgd0L6n.js">