estatehelm 1.0.8 → 1.0.9
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/dist/index.js +2647 -430
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -933,6 +933,16 @@ async function deriveWrapKey(recoveryKeyBytes, serverWrapSecret, info = "hearthc
|
|
|
933
933
|
return wrapKey;
|
|
934
934
|
}
|
|
935
935
|
|
|
936
|
+
// ../types/src/contacts.ts
|
|
937
|
+
function getContactDisplayName(contact, fallback = "") {
|
|
938
|
+
const personName = [contact.first_name, contact.last_name].filter(Boolean).join(" ");
|
|
939
|
+
const companyName = contact.company_name || "";
|
|
940
|
+
if (personName && companyName) {
|
|
941
|
+
return `${personName} / ${companyName}`;
|
|
942
|
+
}
|
|
943
|
+
return personName || companyName || fallback;
|
|
944
|
+
}
|
|
945
|
+
|
|
936
946
|
// ../types/src/keys.ts
|
|
937
947
|
var DEFAULT_KEY_BUNDLE_ALG = "ECDH-P-521";
|
|
938
948
|
|
|
@@ -1725,45 +1735,7 @@ async function getPrivateKey() {
|
|
|
1725
1735
|
// src/server.ts
|
|
1726
1736
|
var import_server = require("@modelcontextprotocol/sdk/server/index.js");
|
|
1727
1737
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
1728
|
-
var
|
|
1729
|
-
|
|
1730
|
-
// src/filter.ts
|
|
1731
|
-
var REDACTION_RULES = {
|
|
1732
|
-
// Password entries
|
|
1733
|
-
password: ["password", "notes"],
|
|
1734
|
-
// Identity documents
|
|
1735
|
-
identity: ["password", "recoveryKey", "securityAnswers"],
|
|
1736
|
-
// Financial accounts
|
|
1737
|
-
bank_account: ["accountNumber", "routingNumber"],
|
|
1738
|
-
investment: ["accountNumber"],
|
|
1739
|
-
// Access codes
|
|
1740
|
-
access_code: ["code", "pin"],
|
|
1741
|
-
// Credentials (show last 4 of document number)
|
|
1742
|
-
credential: ["documentNumber"]
|
|
1743
|
-
};
|
|
1744
|
-
var PARTIAL_REDACTION_FIELDS = ["documentNumber", "accountNumber"];
|
|
1745
|
-
var REDACTED = "[REDACTED]";
|
|
1746
|
-
function redactEntity(entity, entityType, mode) {
|
|
1747
|
-
if (mode === "full") {
|
|
1748
|
-
return entity;
|
|
1749
|
-
}
|
|
1750
|
-
const fieldsToRedact = REDACTION_RULES[entityType] || [];
|
|
1751
|
-
if (fieldsToRedact.length === 0) {
|
|
1752
|
-
return entity;
|
|
1753
|
-
}
|
|
1754
|
-
const redacted = { ...entity };
|
|
1755
|
-
for (const field of fieldsToRedact) {
|
|
1756
|
-
if (field in redacted && redacted[field] != null) {
|
|
1757
|
-
const value = redacted[field];
|
|
1758
|
-
if (PARTIAL_REDACTION_FIELDS.includes(field) && typeof value === "string" && value.length > 4) {
|
|
1759
|
-
redacted[field] = `****${value.slice(-4)}`;
|
|
1760
|
-
} else {
|
|
1761
|
-
redacted[field] = REDACTED;
|
|
1762
|
-
}
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
return redacted;
|
|
1766
|
-
}
|
|
1738
|
+
var import_types42 = require("@modelcontextprotocol/sdk/types.js");
|
|
1767
1739
|
|
|
1768
1740
|
// ../cache-sqlite/src/sqliteStore.ts
|
|
1769
1741
|
var import_better_sqlite3 = __toESM(require("better-sqlite3"));
|
|
@@ -2478,194 +2450,2623 @@ async function downloadAndDecryptFile(client, householdId, fileId, entityId, ent
|
|
|
2478
2450
|
mimeType
|
|
2479
2451
|
);
|
|
2480
2452
|
return {
|
|
2481
|
-
|
|
2482
|
-
dataBase64: base64Encode(decrypted.
|
|
2453
|
+
bytes: decrypted.bytes,
|
|
2454
|
+
dataBase64: base64Encode(decrypted.bytes),
|
|
2483
2455
|
mimeType: decrypted.mimeType,
|
|
2484
2456
|
fileName: fileInfo.fileName
|
|
2485
2457
|
};
|
|
2486
2458
|
}
|
|
2487
2459
|
|
|
2488
|
-
// src/
|
|
2489
|
-
async function
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
const
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
const
|
|
2499
|
-
if (!
|
|
2500
|
-
|
|
2501
|
-
process.exit(1);
|
|
2460
|
+
// src/resources/households.ts
|
|
2461
|
+
async function listHouseholds() {
|
|
2462
|
+
return getHouseholds();
|
|
2463
|
+
}
|
|
2464
|
+
async function getHousehold(householdId) {
|
|
2465
|
+
const households = await getHouseholds();
|
|
2466
|
+
return households.find((h) => h.id === householdId) || null;
|
|
2467
|
+
}
|
|
2468
|
+
async function getHouseholdSummary(householdId, privateKey, getDecryptedEntities2) {
|
|
2469
|
+
const households = await getHouseholds();
|
|
2470
|
+
const household = households.find((h) => h.id === householdId);
|
|
2471
|
+
if (!household) {
|
|
2472
|
+
return null;
|
|
2502
2473
|
}
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2474
|
+
const entityTypes = [
|
|
2475
|
+
"pet",
|
|
2476
|
+
"property",
|
|
2477
|
+
"vehicle",
|
|
2478
|
+
"contact",
|
|
2479
|
+
"insurance",
|
|
2480
|
+
"bank_account",
|
|
2481
|
+
"investment",
|
|
2482
|
+
"subscription",
|
|
2483
|
+
"maintenance_task",
|
|
2484
|
+
"password",
|
|
2485
|
+
"access_code"
|
|
2486
|
+
];
|
|
2487
|
+
const counts = {};
|
|
2488
|
+
for (const type of entityTypes) {
|
|
2489
|
+
const entities = await getDecryptedEntities2(householdId, type, privateKey);
|
|
2490
|
+
counts[type] = entities.length;
|
|
2510
2491
|
}
|
|
2511
|
-
|
|
2512
|
-
{
|
|
2513
|
-
|
|
2514
|
-
|
|
2492
|
+
return {
|
|
2493
|
+
household: {
|
|
2494
|
+
id: household.id,
|
|
2495
|
+
name: household.name
|
|
2515
2496
|
},
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2497
|
+
counts,
|
|
2498
|
+
totalEntities: Object.values(counts).reduce((a, b) => a + b, 0)
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
// src/filter.ts
|
|
2503
|
+
var REDACTION_RULES = {
|
|
2504
|
+
// Password entries
|
|
2505
|
+
password: ["password", "notes"],
|
|
2506
|
+
// Identity documents
|
|
2507
|
+
identity: ["password", "recovery_key", "security_answers"],
|
|
2508
|
+
// Financial accounts
|
|
2509
|
+
bank_account: ["account_number", "routing_number"],
|
|
2510
|
+
investment: ["account_number"],
|
|
2511
|
+
// Access codes
|
|
2512
|
+
access_code: ["code", "pin"],
|
|
2513
|
+
// Credentials (show last 4 of document number)
|
|
2514
|
+
credential: ["document_number"]
|
|
2515
|
+
};
|
|
2516
|
+
var PARTIAL_REDACTION_FIELDS = ["document_number", "account_number"];
|
|
2517
|
+
var REDACTED = "[REDACTED]";
|
|
2518
|
+
function redactEntity(entity, entityType, mode) {
|
|
2519
|
+
if (mode === "full") {
|
|
2520
|
+
return entity;
|
|
2521
|
+
}
|
|
2522
|
+
const fieldsToRedact = REDACTION_RULES[entityType] || [];
|
|
2523
|
+
if (fieldsToRedact.length === 0) {
|
|
2524
|
+
return entity;
|
|
2525
|
+
}
|
|
2526
|
+
const redacted = { ...entity };
|
|
2527
|
+
for (const field of fieldsToRedact) {
|
|
2528
|
+
if (field in redacted && redacted[field] != null) {
|
|
2529
|
+
const value = redacted[field];
|
|
2530
|
+
if (PARTIAL_REDACTION_FIELDS.includes(field) && typeof value === "string" && value.length > 4) {
|
|
2531
|
+
redacted[field] = `****${value.slice(-4)}`;
|
|
2532
|
+
} else {
|
|
2533
|
+
redacted[field] = REDACTED;
|
|
2521
2534
|
}
|
|
2522
2535
|
}
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2536
|
+
}
|
|
2537
|
+
return redacted;
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
// ../list-data/src/computedFields.ts
|
|
2541
|
+
function getExpiryStatus(days) {
|
|
2542
|
+
if (days < 0) return "expired";
|
|
2543
|
+
if (days <= 30) return "expiring_soon";
|
|
2544
|
+
return "ok";
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
// ../utils/src/dateHelpers.ts
|
|
2548
|
+
function calculateDetailedAge(dateOfBirth) {
|
|
2549
|
+
if (!dateOfBirth) return null;
|
|
2550
|
+
const birth = new Date(dateOfBirth);
|
|
2551
|
+
const today = /* @__PURE__ */ new Date();
|
|
2552
|
+
let years = today.getFullYear() - birth.getFullYear();
|
|
2553
|
+
let months = today.getMonth() - birth.getMonth();
|
|
2554
|
+
if (today.getDate() < birth.getDate()) {
|
|
2555
|
+
months--;
|
|
2556
|
+
}
|
|
2557
|
+
if (months < 0) {
|
|
2558
|
+
years--;
|
|
2559
|
+
months += 12;
|
|
2560
|
+
}
|
|
2561
|
+
const fractionalYears = years + months / 12;
|
|
2562
|
+
return { years, months, fractionalYears };
|
|
2563
|
+
}
|
|
2564
|
+
function calculateCatHumanYears(catAge) {
|
|
2565
|
+
if (catAge <= 0) return 0;
|
|
2566
|
+
if (catAge < 1) return Math.round(catAge * 15);
|
|
2567
|
+
if (catAge < 2) return Math.round(15 + (catAge - 1) * 9);
|
|
2568
|
+
return Math.round(24 + (catAge - 2) * 4);
|
|
2569
|
+
}
|
|
2570
|
+
function calculateDogHumanYears(dogAge, size = "medium") {
|
|
2571
|
+
if (dogAge <= 0) return 0;
|
|
2572
|
+
if (dogAge < 1) return Math.round(dogAge * 15);
|
|
2573
|
+
if (dogAge < 2) return Math.round(15 + (dogAge - 1) * 9);
|
|
2574
|
+
const yearlyRate = size === "small" ? 4 : size === "large" ? 6 : 5;
|
|
2575
|
+
return Math.round(24 + (dogAge - 2) * yearlyRate);
|
|
2576
|
+
}
|
|
2577
|
+
function parseDateLocal(dateStr) {
|
|
2578
|
+
const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
|
2579
|
+
if (match) {
|
|
2580
|
+
const [, year, month, day] = match;
|
|
2581
|
+
return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
|
2582
|
+
}
|
|
2583
|
+
return new Date(dateStr);
|
|
2584
|
+
}
|
|
2585
|
+
function daysUntil(dateStr) {
|
|
2586
|
+
const targetDate = parseDateLocal(dateStr);
|
|
2587
|
+
const today = /* @__PURE__ */ new Date();
|
|
2588
|
+
today.setHours(0, 0, 0, 0);
|
|
2589
|
+
const diffTime = targetDate.getTime() - today.getTime();
|
|
2590
|
+
return Math.ceil(diffTime / (1e3 * 60 * 60 * 24));
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
// ../utils/src/search/searchEngine.ts
|
|
2594
|
+
var DEFAULT_OPTIONS = {
|
|
2595
|
+
minQueryLength: 1,
|
|
2596
|
+
maxPerGroup: 5,
|
|
2597
|
+
maxTotal: 25,
|
|
2598
|
+
includeTypes: [],
|
|
2599
|
+
excludeTypes: []
|
|
2600
|
+
};
|
|
2601
|
+
function scoreMatch(query, text) {
|
|
2602
|
+
if (!query || !text) return 0;
|
|
2603
|
+
const queryLower = query.toLowerCase().trim();
|
|
2604
|
+
const textLower = text.toLowerCase();
|
|
2605
|
+
if (queryLower.length === 0) return 0;
|
|
2606
|
+
if (textLower === queryLower) {
|
|
2607
|
+
return 100;
|
|
2608
|
+
}
|
|
2609
|
+
if (textLower.startsWith(queryLower)) {
|
|
2610
|
+
return 75;
|
|
2611
|
+
}
|
|
2612
|
+
const words = textLower.split(/\s+/);
|
|
2613
|
+
for (const word of words) {
|
|
2614
|
+
if (word.startsWith(queryLower)) {
|
|
2615
|
+
return 50;
|
|
2567
2616
|
}
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2617
|
+
}
|
|
2618
|
+
if (textLower.includes(queryLower)) {
|
|
2619
|
+
return 25;
|
|
2620
|
+
}
|
|
2621
|
+
return 0;
|
|
2622
|
+
}
|
|
2623
|
+
function scoreEntity(entity, query) {
|
|
2624
|
+
const matchedFields = [];
|
|
2625
|
+
let bestScore = 0;
|
|
2626
|
+
const nameScore = scoreMatch(query, entity.name);
|
|
2627
|
+
if (nameScore > 0) {
|
|
2628
|
+
matchedFields.push("name");
|
|
2629
|
+
bestScore = Math.max(bestScore, nameScore);
|
|
2630
|
+
}
|
|
2631
|
+
if (entity.subtitle) {
|
|
2632
|
+
const subtitleScore = scoreMatch(query, entity.subtitle);
|
|
2633
|
+
if (subtitleScore > 0) {
|
|
2634
|
+
matchedFields.push("subtitle");
|
|
2635
|
+
bestScore = Math.max(bestScore, subtitleScore * 0.9);
|
|
2575
2636
|
}
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2637
|
+
}
|
|
2638
|
+
if (entity.keywords && entity.keywords.length > 0) {
|
|
2639
|
+
for (const keyword of entity.keywords) {
|
|
2640
|
+
const keywordScore = scoreMatch(query, keyword);
|
|
2641
|
+
if (keywordScore > 0) {
|
|
2642
|
+
if (!matchedFields.includes("keywords")) {
|
|
2643
|
+
matchedFields.push("keywords");
|
|
2644
|
+
}
|
|
2645
|
+
bestScore = Math.max(bestScore, keywordScore * 0.8);
|
|
2584
2646
|
}
|
|
2585
|
-
} else if (parsed.householdId && parsed.entityType) {
|
|
2586
|
-
const entities = await getDecryptedEntities(parsed.householdId, parsed.entityType, privateKey);
|
|
2587
|
-
content = entities.map((e) => redactEntity(e, parsed.entityType, privacyMode));
|
|
2588
|
-
} else {
|
|
2589
|
-
throw new Error(`Unsupported resource: ${uri}`);
|
|
2590
2647
|
}
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2648
|
+
}
|
|
2649
|
+
return { score: bestScore, matchedFields };
|
|
2650
|
+
}
|
|
2651
|
+
function searchEntities(entities, query, options) {
|
|
2652
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
2653
|
+
const trimmedQuery = query.trim();
|
|
2654
|
+
if (trimmedQuery.length < opts.minQueryLength) {
|
|
2655
|
+
return [];
|
|
2656
|
+
}
|
|
2657
|
+
let filteredEntities = entities;
|
|
2658
|
+
if (opts.includeTypes && opts.includeTypes.length > 0) {
|
|
2659
|
+
filteredEntities = filteredEntities.filter(
|
|
2660
|
+
(e) => opts.includeTypes.includes(e.entityType)
|
|
2661
|
+
);
|
|
2662
|
+
}
|
|
2663
|
+
if (opts.excludeTypes && opts.excludeTypes.length > 0) {
|
|
2664
|
+
filteredEntities = filteredEntities.filter(
|
|
2665
|
+
(e) => !opts.excludeTypes.includes(e.entityType)
|
|
2666
|
+
);
|
|
2667
|
+
}
|
|
2668
|
+
if (trimmedQuery.length === 0) {
|
|
2669
|
+
const results2 = filteredEntities.map((entity) => ({
|
|
2670
|
+
entity,
|
|
2671
|
+
score: 1,
|
|
2672
|
+
// Default score for unfiltered results
|
|
2673
|
+
matchedFields: []
|
|
2674
|
+
}));
|
|
2675
|
+
results2.sort((a, b) => (a.entity?.name || "").localeCompare(b.entity?.name || ""));
|
|
2676
|
+
return results2.slice(0, opts.maxTotal);
|
|
2677
|
+
}
|
|
2678
|
+
const results = [];
|
|
2679
|
+
for (const entity of filteredEntities) {
|
|
2680
|
+
const { score, matchedFields } = scoreEntity(entity, trimmedQuery);
|
|
2681
|
+
if (score > 0) {
|
|
2682
|
+
results.push({ entity, score, matchedFields });
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
results.sort((a, b) => {
|
|
2686
|
+
if (b.score !== a.score) {
|
|
2687
|
+
return b.score - a.score;
|
|
2688
|
+
}
|
|
2689
|
+
return (a.entity?.name || "").localeCompare(b.entity?.name || "");
|
|
2600
2690
|
});
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2691
|
+
return results.slice(0, opts.maxTotal);
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
// ../utils/src/search/entityIndexer.ts
|
|
2695
|
+
function formatLabel(value) {
|
|
2696
|
+
if (!value) return "";
|
|
2697
|
+
return value.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
2698
|
+
}
|
|
2699
|
+
function getVehicleName(vehicle) {
|
|
2700
|
+
if (vehicle.name) return vehicle.name;
|
|
2701
|
+
const parts = [vehicle.year, vehicle.make, vehicle.model].filter(Boolean);
|
|
2702
|
+
return parts.length > 0 ? parts.join(" ") : "Vehicle";
|
|
2703
|
+
}
|
|
2704
|
+
function getContactName(contact) {
|
|
2705
|
+
const fullName = [contact.first_name, contact.last_name].filter(Boolean).join(" ");
|
|
2706
|
+
if (fullName) return fullName;
|
|
2707
|
+
if (contact.company_name) return contact.company_name;
|
|
2708
|
+
return "Contact";
|
|
2709
|
+
}
|
|
2710
|
+
function getSubscriptionName(sub) {
|
|
2711
|
+
return sub.custom_name || formatLabel(sub.provider) || "Subscription";
|
|
2712
|
+
}
|
|
2713
|
+
function getServiceName(service) {
|
|
2714
|
+
return service.service_name || service.name || "Service";
|
|
2715
|
+
}
|
|
2716
|
+
function getPropertyName(property) {
|
|
2717
|
+
if (property.name) return property.name;
|
|
2718
|
+
if (property.street_address) {
|
|
2719
|
+
return property.city ? `${property.street_address}, ${property.city}` : property.street_address;
|
|
2720
|
+
}
|
|
2721
|
+
return "Property";
|
|
2722
|
+
}
|
|
2723
|
+
function getPetName(pet) {
|
|
2724
|
+
return pet.name || "Pet";
|
|
2725
|
+
}
|
|
2726
|
+
function getAccessCodeName(code) {
|
|
2727
|
+
return code.name || code.description || "Access Code";
|
|
2728
|
+
}
|
|
2729
|
+
function getFinancialAccountName(account) {
|
|
2730
|
+
return account.name || account.account_name || account.nickname || (account.institution ? `${account.institution} Account` : "Financial Account");
|
|
2731
|
+
}
|
|
2732
|
+
function getInsuranceName(policy) {
|
|
2733
|
+
return policy.name || policy.policy_name || policy.provider || formatLabel(policy.type) || "Insurance";
|
|
2734
|
+
}
|
|
2735
|
+
function getValuableName(valuable) {
|
|
2736
|
+
return valuable.name || valuable.item_name || valuable.description || "Valuable";
|
|
2737
|
+
}
|
|
2738
|
+
function getMaintenanceTaskName(task) {
|
|
2739
|
+
return task.name || task.title || task.task_name || "Maintenance Task";
|
|
2740
|
+
}
|
|
2741
|
+
function getDeviceName(device) {
|
|
2742
|
+
return device.name || device.device_name || "Device";
|
|
2743
|
+
}
|
|
2744
|
+
function getPetVetVisitName(visit) {
|
|
2745
|
+
return visit.name || visit.clinic_name || visit.reason || "Vet Visit";
|
|
2746
|
+
}
|
|
2747
|
+
function getPetHealthName(record) {
|
|
2748
|
+
return record.name || (record.record_type ? formatLabel(record.record_type) : "Health Record");
|
|
2749
|
+
}
|
|
2750
|
+
function getVehicleMaintenanceName(record) {
|
|
2751
|
+
return record.name || record.service_type || record.description || "Maintenance Record";
|
|
2752
|
+
}
|
|
2753
|
+
function getVehicleServiceVisitName(visit) {
|
|
2754
|
+
return visit.name || visit.shop_name || visit.description || "Service Visit";
|
|
2755
|
+
}
|
|
2756
|
+
function getEntityDisplayName(entityType, data) {
|
|
2757
|
+
switch (entityType) {
|
|
2758
|
+
case "property":
|
|
2759
|
+
return getPropertyName(data);
|
|
2760
|
+
case "vehicle":
|
|
2761
|
+
return getVehicleName(data);
|
|
2762
|
+
case "pet":
|
|
2763
|
+
return getPetName(data);
|
|
2764
|
+
case "contact":
|
|
2765
|
+
return getContactName(data);
|
|
2766
|
+
case "subscription":
|
|
2767
|
+
return getSubscriptionName(data);
|
|
2768
|
+
case "service":
|
|
2769
|
+
return getServiceName(data);
|
|
2770
|
+
case "insurance":
|
|
2771
|
+
return getInsuranceName(data);
|
|
2772
|
+
case "valuable":
|
|
2773
|
+
return getValuableName(data);
|
|
2774
|
+
case "financial_account":
|
|
2775
|
+
return getFinancialAccountName(data);
|
|
2776
|
+
case "access_code":
|
|
2777
|
+
return getAccessCodeName(data);
|
|
2778
|
+
case "maintenance_task":
|
|
2779
|
+
return getMaintenanceTaskName(data);
|
|
2780
|
+
case "device":
|
|
2781
|
+
return getDeviceName(data);
|
|
2782
|
+
case "pet_vet_visit":
|
|
2783
|
+
return getPetVetVisitName(data);
|
|
2784
|
+
case "pet_health":
|
|
2785
|
+
return getPetHealthName(data);
|
|
2786
|
+
case "vehicle_maintenance":
|
|
2787
|
+
return getVehicleMaintenanceName(data);
|
|
2788
|
+
case "vehicle_service_visit":
|
|
2789
|
+
return getVehicleServiceVisitName(data);
|
|
2790
|
+
case "home_improvement":
|
|
2791
|
+
return data.name || data.title || data.project_name || "Home Improvement";
|
|
2792
|
+
case "credential":
|
|
2793
|
+
return data.name || data.title || "Credential";
|
|
2794
|
+
case "password":
|
|
2795
|
+
return data.name || data.site_name || data.title || "Password";
|
|
2796
|
+
case "legal":
|
|
2797
|
+
return data.name || data.title || "Legal Document";
|
|
2798
|
+
case "military_record":
|
|
2799
|
+
return data.name || data.title || "Military Record";
|
|
2800
|
+
case "education_record":
|
|
2801
|
+
return data.name || data.institution || data.title || "Education Record";
|
|
2802
|
+
case "travel":
|
|
2803
|
+
return data.name || data.document_type || "Travel Document";
|
|
2804
|
+
case "identity":
|
|
2805
|
+
return data.name || data.document_type || "Identity Document";
|
|
2806
|
+
case "document":
|
|
2807
|
+
return data.name || data.title || data.file_name || "Document";
|
|
2808
|
+
default:
|
|
2809
|
+
return data.name || data.title || entityType;
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
function indexProperty(property) {
|
|
2813
|
+
const subtitle = property.city && property.state ? `${property.city}, ${property.state}` : property.street_address || "Property";
|
|
2814
|
+
return {
|
|
2815
|
+
id: property.id,
|
|
2816
|
+
entityType: "property",
|
|
2817
|
+
name: property.name,
|
|
2818
|
+
subtitle,
|
|
2819
|
+
// Synonyms: house, home for natural language searches
|
|
2820
|
+
keywords: [property.street_address, property.city, property.state, "house", "home"].filter(Boolean),
|
|
2821
|
+
icon: "Home",
|
|
2822
|
+
route: `/property?id=${property.id}`,
|
|
2823
|
+
routeParams: { id: property.id }
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
2826
|
+
function indexVehicle(vehicle) {
|
|
2827
|
+
const subtitle = formatLabel(vehicle.vehicle_type) || "Vehicle";
|
|
2828
|
+
const synonyms = [];
|
|
2829
|
+
if (vehicle.vehicle_type === "car" || vehicle.vehicle_type === "sedan" || vehicle.vehicle_type === "suv" || vehicle.vehicle_type === "truck") {
|
|
2830
|
+
synonyms.push("car", "auto", "automobile");
|
|
2831
|
+
}
|
|
2832
|
+
if (vehicle.vehicle_type === "motorcycle") {
|
|
2833
|
+
synonyms.push("bike", "motorbike");
|
|
2834
|
+
}
|
|
2835
|
+
if (vehicle.vehicle_type === "boat") {
|
|
2836
|
+
synonyms.push("watercraft");
|
|
2837
|
+
}
|
|
2838
|
+
const keywords = [vehicle.make, vehicle.model, vehicle.license_plate, ...synonyms].filter(Boolean);
|
|
2839
|
+
return {
|
|
2840
|
+
id: vehicle.id,
|
|
2841
|
+
entityType: "vehicle",
|
|
2842
|
+
name: getVehicleName(vehicle),
|
|
2843
|
+
subtitle,
|
|
2844
|
+
keywords,
|
|
2845
|
+
icon: "Car",
|
|
2846
|
+
route: `/vehicle?id=${vehicle.id}`,
|
|
2847
|
+
routeParams: { id: vehicle.id }
|
|
2848
|
+
};
|
|
2849
|
+
}
|
|
2850
|
+
function indexPet(pet) {
|
|
2851
|
+
const speciesLabel = formatLabel(pet.species);
|
|
2852
|
+
const subtitle = pet.breed ? `${speciesLabel} - ${pet.breed}` : speciesLabel || "Pet";
|
|
2853
|
+
const synonyms = [];
|
|
2854
|
+
if (pet.species === "dog" || pet.species === "canine") {
|
|
2855
|
+
synonyms.push("dog", "puppy", "pup");
|
|
2856
|
+
} else if (pet.species === "cat" || pet.species === "feline") {
|
|
2857
|
+
synonyms.push("cat", "kitty", "kitten");
|
|
2858
|
+
} else if (pet.species === "bird") {
|
|
2859
|
+
synonyms.push("bird", "parrot");
|
|
2860
|
+
} else if (pet.species === "fish") {
|
|
2861
|
+
synonyms.push("fish", "aquarium");
|
|
2862
|
+
}
|
|
2863
|
+
return {
|
|
2864
|
+
id: pet.id,
|
|
2865
|
+
entityType: "pet",
|
|
2866
|
+
name: pet.name,
|
|
2867
|
+
subtitle,
|
|
2868
|
+
keywords: [pet.breed, pet.species, ...synonyms].filter(Boolean),
|
|
2869
|
+
icon: "PawPrint",
|
|
2870
|
+
route: `/pet?id=${pet.id}`,
|
|
2871
|
+
routeParams: { id: pet.id }
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
function indexContact(contact, linkedEntities) {
|
|
2875
|
+
const subtitle = contact.specialty ? `${formatLabel(contact.type)} - ${contact.specialty}` : formatLabel(contact.type) || "Contact";
|
|
2876
|
+
const linkedNames = linkedEntities?.map((e) => e.name) || [];
|
|
2877
|
+
const linkedRoles = linkedEntities?.map((e) => formatLabel(e.role)) || [];
|
|
2878
|
+
return {
|
|
2879
|
+
id: contact.id,
|
|
2880
|
+
entityType: "contact",
|
|
2881
|
+
name: getContactName(contact),
|
|
2882
|
+
subtitle,
|
|
2883
|
+
keywords: [
|
|
2884
|
+
contact.company_name,
|
|
2885
|
+
contact.first_name,
|
|
2886
|
+
contact.last_name,
|
|
2887
|
+
contact.specialty,
|
|
2888
|
+
...linkedNames,
|
|
2889
|
+
...linkedRoles
|
|
2890
|
+
].filter(Boolean),
|
|
2891
|
+
icon: "Phone",
|
|
2892
|
+
route: `/contact?id=${contact.id}`,
|
|
2893
|
+
routeParams: { id: contact.id }
|
|
2894
|
+
};
|
|
2895
|
+
}
|
|
2896
|
+
function indexSubscription(sub) {
|
|
2897
|
+
const subtitle = formatLabel(sub.category) || "Subscription";
|
|
2898
|
+
const displayName = getSubscriptionName(sub);
|
|
2899
|
+
const keywords = [];
|
|
2900
|
+
if (sub.provider) {
|
|
2901
|
+
keywords.push(sub.provider);
|
|
2902
|
+
const formatted = formatLabel(sub.provider);
|
|
2903
|
+
if (formatted !== sub.provider) {
|
|
2904
|
+
keywords.push(formatted);
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
if (sub.custom_name) {
|
|
2908
|
+
keywords.push(sub.custom_name);
|
|
2909
|
+
}
|
|
2910
|
+
if (sub.category) {
|
|
2911
|
+
keywords.push(sub.category);
|
|
2912
|
+
}
|
|
2913
|
+
const formattedProvider = formatLabel(sub.provider);
|
|
2914
|
+
const searchableName = sub.provider && formattedProvider !== displayName ? `${displayName} (${formattedProvider})` : displayName;
|
|
2915
|
+
return {
|
|
2916
|
+
id: sub.id,
|
|
2917
|
+
entityType: "subscription",
|
|
2918
|
+
name: searchableName,
|
|
2919
|
+
subtitle,
|
|
2920
|
+
keywords,
|
|
2921
|
+
icon: "CreditCard",
|
|
2922
|
+
route: `/subscription?id=${sub.id}`,
|
|
2923
|
+
routeParams: { id: sub.id }
|
|
2924
|
+
};
|
|
2925
|
+
}
|
|
2926
|
+
function indexService(service) {
|
|
2927
|
+
const subtitle = formatLabel(service.service_type) || "Service";
|
|
2928
|
+
return {
|
|
2929
|
+
id: service.id,
|
|
2930
|
+
entityType: "service",
|
|
2931
|
+
name: getServiceName(service),
|
|
2932
|
+
subtitle,
|
|
2933
|
+
keywords: [service.provider, service.service_type].filter(Boolean),
|
|
2934
|
+
icon: "Wrench",
|
|
2935
|
+
route: `/service?id=${service.id}`,
|
|
2936
|
+
routeParams: { id: service.id }
|
|
2937
|
+
};
|
|
2938
|
+
}
|
|
2939
|
+
function indexInsurance(policy, coveredAssetNames) {
|
|
2940
|
+
const subtitle = formatLabel(policy.type) || "Insurance";
|
|
2941
|
+
const keywords = [policy.policy_number, policy.type, ...coveredAssetNames || []].filter(Boolean);
|
|
2942
|
+
return {
|
|
2943
|
+
id: policy.id,
|
|
2944
|
+
entityType: "insurance",
|
|
2945
|
+
name: policy.provider || "Insurance Policy",
|
|
2946
|
+
subtitle,
|
|
2947
|
+
keywords,
|
|
2948
|
+
icon: "Shield",
|
|
2949
|
+
route: `/insurance?id=${policy.id}`,
|
|
2950
|
+
routeParams: { id: policy.id }
|
|
2951
|
+
};
|
|
2952
|
+
}
|
|
2953
|
+
function indexValuable(valuable) {
|
|
2954
|
+
const categoryLabel = valuable.category === "other" && valuable.other_category ? valuable.other_category : formatLabel(valuable.category);
|
|
2955
|
+
const subtitle = categoryLabel || "Valuable";
|
|
2956
|
+
return {
|
|
2957
|
+
id: valuable.id,
|
|
2958
|
+
entityType: "valuable",
|
|
2959
|
+
name: valuable.name,
|
|
2960
|
+
subtitle,
|
|
2961
|
+
keywords: [valuable.category, valuable.other_category].filter(Boolean),
|
|
2962
|
+
icon: "Gem",
|
|
2963
|
+
route: `/valuables?id=${valuable.id}`,
|
|
2964
|
+
routeParams: { id: valuable.id }
|
|
2965
|
+
};
|
|
2966
|
+
}
|
|
2967
|
+
function indexFinancialAccount(account, ownerNames) {
|
|
2968
|
+
const typeLabel = formatLabel(account.account_type) || "Financial Account";
|
|
2969
|
+
let subtitle = typeLabel;
|
|
2970
|
+
if (ownerNames && ownerNames.length > 0) {
|
|
2971
|
+
subtitle = `${typeLabel} \u2022 ${ownerNames.join(", ")}`;
|
|
2972
|
+
}
|
|
2973
|
+
return {
|
|
2974
|
+
id: account.id,
|
|
2975
|
+
entityType: "financial_account",
|
|
2976
|
+
name: account.nickname || account.institution || "Financial Account",
|
|
2977
|
+
subtitle,
|
|
2978
|
+
keywords: [account.institution, account.account_type, ...ownerNames || []].filter(Boolean),
|
|
2979
|
+
icon: "Landmark",
|
|
2980
|
+
route: `/financial?id=${account.id}`,
|
|
2981
|
+
routeParams: { id: account.id }
|
|
2982
|
+
};
|
|
2983
|
+
}
|
|
2984
|
+
function indexCredential(credential, personName) {
|
|
2985
|
+
const subtypeLabel = formatLabel(credential.credential_subtype);
|
|
2986
|
+
const typeLabel = subtypeLabel || formatLabel(credential.credential_type) || "Credential";
|
|
2987
|
+
const subtitle = personName ? `${typeLabel} \u2022 ${personName}` : typeLabel;
|
|
2988
|
+
const synonyms = ["id", "identification"];
|
|
2989
|
+
const subtype = credential.credential_subtype?.toLowerCase() || "";
|
|
2990
|
+
const type = credential.credential_type?.toLowerCase() || "";
|
|
2991
|
+
if (subtype.includes("passport")) {
|
|
2992
|
+
synonyms.push("passport", "travel document");
|
|
2993
|
+
}
|
|
2994
|
+
if (subtype.includes("driver") || subtype.includes("license")) {
|
|
2995
|
+
synonyms.push("license", "drivers license", "DL", "driving license");
|
|
2996
|
+
}
|
|
2997
|
+
if (subtype.includes("ssn") || subtype.includes("social_security")) {
|
|
2998
|
+
synonyms.push("social security", "SSN", "social security number");
|
|
2999
|
+
}
|
|
3000
|
+
if (type.includes("professional") || type.includes("license")) {
|
|
3001
|
+
synonyms.push("license", "certification", "cert");
|
|
3002
|
+
}
|
|
3003
|
+
if (subtype.includes("birth") || subtype.includes("certificate")) {
|
|
3004
|
+
synonyms.push("birth certificate", "certificate");
|
|
3005
|
+
}
|
|
3006
|
+
return {
|
|
3007
|
+
id: credential.id,
|
|
3008
|
+
entityType: "credential",
|
|
3009
|
+
name: credential.name,
|
|
3010
|
+
subtitle,
|
|
3011
|
+
keywords: [credential.credential_type, credential.credential_subtype, credential.issuing_authority, personName, ...synonyms].filter(Boolean),
|
|
3012
|
+
icon: "CreditCard",
|
|
3013
|
+
route: `/credentials?id=${credential.id}`,
|
|
3014
|
+
routeParams: { id: credential.id }
|
|
3015
|
+
};
|
|
3016
|
+
}
|
|
3017
|
+
function indexPetVetVisit(visit, petName) {
|
|
3018
|
+
const procedureNames = visit.procedures?.map((p) => p.name).filter(Boolean).join(", ");
|
|
3019
|
+
let subtitle = petName || "Vet Visit";
|
|
3020
|
+
if (petName && procedureNames) {
|
|
3021
|
+
subtitle = `${petName} \u2022 ${procedureNames}`;
|
|
3022
|
+
} else if (procedureNames) {
|
|
3023
|
+
subtitle = procedureNames;
|
|
3024
|
+
}
|
|
3025
|
+
return {
|
|
3026
|
+
id: visit.id,
|
|
3027
|
+
entityType: "pet_vet_visit",
|
|
3028
|
+
name: visit.name || `${petName || "Pet"} - Vet Visit`,
|
|
3029
|
+
subtitle,
|
|
3030
|
+
keywords: [petName, ...visit.procedures?.map((p) => p.name).filter(Boolean) || []].filter(Boolean),
|
|
3031
|
+
icon: "Stethoscope",
|
|
3032
|
+
route: `/pet?tab=health&id=${visit.id}`,
|
|
3033
|
+
routeParams: { tab: "health", id: visit.id }
|
|
3034
|
+
};
|
|
3035
|
+
}
|
|
3036
|
+
function indexVehicleService(service, vehicleName) {
|
|
3037
|
+
const serviceNames = service.services?.map((s) => s.name).filter(Boolean).join(", ");
|
|
3038
|
+
let subtitle = vehicleName || "Service Record";
|
|
3039
|
+
if (vehicleName && serviceNames) {
|
|
3040
|
+
subtitle = `${vehicleName} \u2022 ${serviceNames}`;
|
|
3041
|
+
} else if (serviceNames) {
|
|
3042
|
+
subtitle = serviceNames;
|
|
3043
|
+
}
|
|
3044
|
+
return {
|
|
3045
|
+
id: service.id,
|
|
3046
|
+
entityType: "vehicle_service",
|
|
3047
|
+
name: service.name || `${vehicleName || "Vehicle"} - Service`,
|
|
3048
|
+
subtitle,
|
|
3049
|
+
keywords: [vehicleName, ...service.services?.map((s) => s.name).filter(Boolean) || []].filter(Boolean),
|
|
3050
|
+
icon: "FileText",
|
|
3051
|
+
route: `/vehicle-maintenance?id=${service.id}`,
|
|
3052
|
+
routeParams: { id: service.id }
|
|
3053
|
+
};
|
|
3054
|
+
}
|
|
3055
|
+
function indexMaintenanceTask(task, assetName) {
|
|
3056
|
+
const subtitle = assetName ? `${formatLabel(task.category)} - ${assetName}` : formatLabel(task.category) || "Maintenance";
|
|
3057
|
+
return {
|
|
3058
|
+
id: task.id,
|
|
3059
|
+
entityType: "maintenance_task",
|
|
3060
|
+
name: task.name || task.title || "Maintenance Task",
|
|
3061
|
+
subtitle,
|
|
3062
|
+
keywords: [task.category].filter(Boolean),
|
|
3063
|
+
icon: "ClipboardList",
|
|
3064
|
+
route: `/maintenance?id=${task.id}`,
|
|
3065
|
+
routeParams: { id: task.id }
|
|
3066
|
+
};
|
|
3067
|
+
}
|
|
3068
|
+
function indexLegalDocument(doc, residentNames) {
|
|
3069
|
+
const typeLabel = formatLabel(doc.type) || "Legal Document";
|
|
3070
|
+
let subtitle = typeLabel;
|
|
3071
|
+
if (residentNames && residentNames.length > 0) {
|
|
3072
|
+
subtitle = `${typeLabel} \u2022 ${residentNames.join(", ")}`;
|
|
3073
|
+
}
|
|
3074
|
+
const synonyms = ["doc", "document", "legal", "paperwork"];
|
|
3075
|
+
return {
|
|
3076
|
+
id: doc.id,
|
|
3077
|
+
entityType: "legal_document",
|
|
3078
|
+
name: doc.name,
|
|
3079
|
+
subtitle,
|
|
3080
|
+
keywords: [doc.type, ...residentNames || [], ...synonyms].filter(Boolean),
|
|
3081
|
+
icon: "FileText",
|
|
3082
|
+
route: `/legal?id=${doc.id}`,
|
|
3083
|
+
routeParams: { id: doc.id }
|
|
3084
|
+
};
|
|
3085
|
+
}
|
|
3086
|
+
function indexAccessCode(code, propertyName) {
|
|
3087
|
+
const subtitle = propertyName ? `${formatLabel(code.code_type)} - ${propertyName}` : formatLabel(code.code_type) || "Access Code";
|
|
3088
|
+
return {
|
|
3089
|
+
id: code.id,
|
|
3090
|
+
entityType: "access_code",
|
|
3091
|
+
name: code.name,
|
|
3092
|
+
subtitle,
|
|
3093
|
+
keywords: [code.code_type].filter(Boolean),
|
|
3094
|
+
icon: "Key",
|
|
3095
|
+
route: `/property?tab=access&id=${code.id}`,
|
|
3096
|
+
routeParams: { tab: "access", id: code.id }
|
|
3097
|
+
};
|
|
3098
|
+
}
|
|
3099
|
+
function indexDevice(device) {
|
|
3100
|
+
const subtitle = device.brand ? `${formatLabel(device.device_type)} - ${device.brand}` : formatLabel(device.device_type) || "Device";
|
|
3101
|
+
return {
|
|
3102
|
+
id: device.id,
|
|
3103
|
+
entityType: "device",
|
|
3104
|
+
name: device.name,
|
|
3105
|
+
subtitle,
|
|
3106
|
+
keywords: [device.device_type, device.brand, device.model].filter(Boolean),
|
|
3107
|
+
icon: "Wifi",
|
|
3108
|
+
route: `/device?id=${device.id}`,
|
|
3109
|
+
routeParams: { id: device.id }
|
|
3110
|
+
};
|
|
3111
|
+
}
|
|
3112
|
+
function indexPerson(person) {
|
|
3113
|
+
const subtitle = formatLabel(person.relationship_type) || formatLabel(person.person_type) || "Person";
|
|
3114
|
+
return {
|
|
3115
|
+
id: person.id,
|
|
3116
|
+
entityType: "person",
|
|
3117
|
+
name: person.name,
|
|
3118
|
+
subtitle,
|
|
3119
|
+
keywords: [person.person_type, person.relationship_type].filter(Boolean),
|
|
3120
|
+
icon: "User",
|
|
3121
|
+
route: `/people?id=${person.id}`,
|
|
3122
|
+
routeParams: { id: person.id }
|
|
3123
|
+
};
|
|
3124
|
+
}
|
|
3125
|
+
function indexHealthRecord(record, ownerName) {
|
|
3126
|
+
const subtitle = ownerName ? `${formatLabel(record.record_type)} - ${ownerName}` : formatLabel(record.record_type) || "Health Record";
|
|
3127
|
+
return {
|
|
3128
|
+
id: record.id,
|
|
3129
|
+
entityType: "health_record",
|
|
3130
|
+
name: record.name,
|
|
3131
|
+
subtitle,
|
|
3132
|
+
keywords: [record.record_type, record.provider].filter(Boolean),
|
|
3133
|
+
icon: "Heart",
|
|
3134
|
+
route: `/records?type=health_record&id=${record.id}`,
|
|
3135
|
+
routeParams: { id: record.id }
|
|
3136
|
+
};
|
|
3137
|
+
}
|
|
3138
|
+
function indexEducationRecord(record, personName) {
|
|
3139
|
+
const subtitle = personName ? `${formatLabel(record.record_type)} - ${personName}` : record.institution || formatLabel(record.record_type) || "Education Record";
|
|
3140
|
+
return {
|
|
3141
|
+
id: record.id,
|
|
3142
|
+
entityType: "education_record",
|
|
3143
|
+
name: record.name,
|
|
3144
|
+
subtitle,
|
|
3145
|
+
keywords: [record.record_type, record.institution, record.level, record.field_of_study].filter(Boolean),
|
|
3146
|
+
icon: "GraduationCap",
|
|
3147
|
+
route: `/records?type=education_record&id=${record.id}`,
|
|
3148
|
+
routeParams: { id: record.id }
|
|
3149
|
+
};
|
|
3150
|
+
}
|
|
3151
|
+
function indexMilitaryRecord(record, personName) {
|
|
3152
|
+
const branchLabel = formatLabel(record.branch);
|
|
3153
|
+
const subtitle = personName ? `${branchLabel || formatLabel(record.record_type)} - ${personName}` : branchLabel || formatLabel(record.record_type) || "Military Record";
|
|
3154
|
+
return {
|
|
3155
|
+
id: record.id,
|
|
3156
|
+
entityType: "military_record",
|
|
3157
|
+
name: record.name,
|
|
3158
|
+
subtitle,
|
|
3159
|
+
keywords: [record.record_type, record.branch, record.rank].filter(Boolean),
|
|
3160
|
+
icon: "Medal",
|
|
3161
|
+
route: `/records?type=military_record&id=${record.id}`,
|
|
3162
|
+
routeParams: { id: record.id }
|
|
3163
|
+
};
|
|
3164
|
+
}
|
|
3165
|
+
function indexHomeImprovement(improvement, propertyName) {
|
|
3166
|
+
const subtitle = propertyName ? `${formatLabel(improvement.improvement_type)} - ${propertyName}` : formatLabel(improvement.improvement_type) || "Home Improvement";
|
|
3167
|
+
return {
|
|
3168
|
+
id: improvement.id,
|
|
3169
|
+
entityType: "home_improvement",
|
|
3170
|
+
name: improvement.name,
|
|
3171
|
+
subtitle,
|
|
3172
|
+
keywords: [improvement.improvement_type].filter(Boolean),
|
|
3173
|
+
icon: "Hammer",
|
|
3174
|
+
route: `/home-improvement/${improvement.id}`,
|
|
3175
|
+
routeParams: { id: improvement.id }
|
|
3176
|
+
};
|
|
3177
|
+
}
|
|
3178
|
+
function indexTaxYear(taxYear, filerNames) {
|
|
3179
|
+
let subtitle = taxYear.country || "Tax Year";
|
|
3180
|
+
if (filerNames && filerNames.length > 0) {
|
|
3181
|
+
subtitle = filerNames.join(", ");
|
|
3182
|
+
}
|
|
3183
|
+
return {
|
|
3184
|
+
id: taxYear.id,
|
|
3185
|
+
entityType: "tax_year",
|
|
3186
|
+
name: `${taxYear.year} Tax Year`,
|
|
3187
|
+
subtitle,
|
|
3188
|
+
keywords: [taxYear.country, String(taxYear.year), ...filerNames || []].filter(Boolean),
|
|
3189
|
+
icon: "Receipt",
|
|
3190
|
+
route: `/taxes?id=${taxYear.id}`,
|
|
3191
|
+
routeParams: { id: taxYear.id }
|
|
3192
|
+
};
|
|
3193
|
+
}
|
|
3194
|
+
function indexMembershipRecord(record, personName) {
|
|
3195
|
+
const typeLabel = formatLabel(record.membership_type);
|
|
3196
|
+
let subtitle = typeLabel || "Membership";
|
|
3197
|
+
if (personName) {
|
|
3198
|
+
subtitle = record.level ? `${typeLabel} \u2022 ${record.level} \u2022 ${personName}` : `${typeLabel} \u2022 ${personName}`;
|
|
3199
|
+
} else if (record.level) {
|
|
3200
|
+
subtitle = `${typeLabel} \u2022 ${record.level}`;
|
|
3201
|
+
}
|
|
3202
|
+
return {
|
|
3203
|
+
id: record.id,
|
|
3204
|
+
entityType: "membership_record",
|
|
3205
|
+
name: record.name,
|
|
3206
|
+
subtitle,
|
|
3207
|
+
keywords: [record.membership_type, record.membership_number, record.level, record.alliance, personName].filter(Boolean),
|
|
3208
|
+
icon: "Ticket",
|
|
3209
|
+
route: `/records?type=membership_record&id=${record.id}`,
|
|
3210
|
+
routeParams: { id: record.id }
|
|
3211
|
+
};
|
|
3212
|
+
}
|
|
3213
|
+
function indexAllEntities(data) {
|
|
3214
|
+
const entities = [];
|
|
3215
|
+
const propertyNames = /* @__PURE__ */ new Map();
|
|
3216
|
+
const vehicleNames = /* @__PURE__ */ new Map();
|
|
3217
|
+
const petNames = /* @__PURE__ */ new Map();
|
|
3218
|
+
const personNames = /* @__PURE__ */ new Map();
|
|
3219
|
+
data.properties?.forEach((p) => propertyNames.set(p.id, p.name));
|
|
3220
|
+
data.vehicles?.forEach((v) => vehicleNames.set(v.id, getVehicleName(v)));
|
|
3221
|
+
data.pets?.forEach((p) => petNames.set(p.id, p.name));
|
|
3222
|
+
data.people?.forEach((p) => personNames.set(p.id, p.name));
|
|
3223
|
+
const contactLinkedEntities = /* @__PURE__ */ new Map();
|
|
3224
|
+
const addContactLink = (contactId, name, role) => {
|
|
3225
|
+
const existing = contactLinkedEntities.get(contactId) || [];
|
|
3226
|
+
existing.push({ name, role });
|
|
3227
|
+
contactLinkedEntities.set(contactId, existing);
|
|
3228
|
+
};
|
|
3229
|
+
data.pets?.forEach((pet) => {
|
|
3230
|
+
pet.contact_relationships?.forEach((rel) => {
|
|
3231
|
+
addContactLink(rel.contact_id, pet.name, rel.role);
|
|
3232
|
+
});
|
|
3233
|
+
});
|
|
3234
|
+
data.vehicles?.forEach((vehicle) => {
|
|
3235
|
+
const name = getVehicleName(vehicle);
|
|
3236
|
+
vehicle.contact_relationships?.forEach((rel) => {
|
|
3237
|
+
addContactLink(rel.contact_id, name, rel.role);
|
|
3238
|
+
});
|
|
3239
|
+
});
|
|
3240
|
+
data.properties?.forEach((property) => {
|
|
3241
|
+
property.contact_relationships?.forEach((rel) => {
|
|
3242
|
+
addContactLink(rel.contact_id, property.name, rel.role);
|
|
3243
|
+
});
|
|
3244
|
+
});
|
|
3245
|
+
data.properties?.forEach((p) => entities.push(indexProperty(p)));
|
|
3246
|
+
data.vehicles?.forEach((v) => entities.push(indexVehicle(v)));
|
|
3247
|
+
data.pets?.forEach((p) => entities.push(indexPet(p)));
|
|
3248
|
+
data.contacts?.forEach((c) => {
|
|
3249
|
+
const linkedEntities = contactLinkedEntities.get(c.id);
|
|
3250
|
+
entities.push(indexContact(c, linkedEntities));
|
|
3251
|
+
});
|
|
3252
|
+
data.subscriptions?.forEach((s) => entities.push(indexSubscription(s)));
|
|
3253
|
+
data.services?.forEach((s) => entities.push(indexService(s)));
|
|
3254
|
+
data.insurancePolicies?.forEach((policy) => {
|
|
3255
|
+
const coveredAssetNames = [];
|
|
3256
|
+
policy.relationships?.forEach((rel) => {
|
|
3257
|
+
if (rel.entity_type === "property") {
|
|
3258
|
+
const name = propertyNames.get(rel.entity_id);
|
|
3259
|
+
if (name) coveredAssetNames.push(name);
|
|
3260
|
+
} else if (rel.entity_type === "vehicle") {
|
|
3261
|
+
const name = vehicleNames.get(rel.entity_id);
|
|
3262
|
+
if (name) coveredAssetNames.push(name);
|
|
3263
|
+
} else if (rel.entity_type === "pet") {
|
|
3264
|
+
const name = petNames.get(rel.entity_id);
|
|
3265
|
+
if (name) coveredAssetNames.push(name);
|
|
3266
|
+
}
|
|
3267
|
+
});
|
|
3268
|
+
if (policy.property_id) {
|
|
3269
|
+
const name = propertyNames.get(policy.property_id);
|
|
3270
|
+
if (name && !coveredAssetNames.includes(name)) coveredAssetNames.push(name);
|
|
3271
|
+
}
|
|
3272
|
+
if (policy.vehicle_id) {
|
|
3273
|
+
const name = vehicleNames.get(policy.vehicle_id);
|
|
3274
|
+
if (name && !coveredAssetNames.includes(name)) coveredAssetNames.push(name);
|
|
3275
|
+
}
|
|
3276
|
+
if (policy.pet_id) {
|
|
3277
|
+
const name = petNames.get(policy.pet_id);
|
|
3278
|
+
if (name && !coveredAssetNames.includes(name)) coveredAssetNames.push(name);
|
|
3279
|
+
}
|
|
3280
|
+
entities.push(indexInsurance(policy, coveredAssetNames.length > 0 ? coveredAssetNames : void 0));
|
|
3281
|
+
});
|
|
3282
|
+
data.valuables?.forEach((v) => entities.push(indexValuable(v)));
|
|
3283
|
+
data.financialAccounts?.forEach((a) => {
|
|
3284
|
+
const ownerNames = a.owner_ids?.map((id) => personNames.get(id)).filter(Boolean);
|
|
3285
|
+
entities.push(indexFinancialAccount(a, ownerNames));
|
|
3286
|
+
});
|
|
3287
|
+
data.credentials?.forEach((c) => {
|
|
3288
|
+
const personName = c.person_id ? personNames.get(c.person_id) : void 0;
|
|
3289
|
+
entities.push(indexCredential(c, personName));
|
|
3290
|
+
});
|
|
3291
|
+
data.petVetVisits?.forEach((v) => {
|
|
3292
|
+
const petName = v.pet_id ? petNames.get(v.pet_id) : void 0;
|
|
3293
|
+
entities.push(indexPetVetVisit(v, petName));
|
|
3294
|
+
});
|
|
3295
|
+
data.vehicleServices?.forEach((s) => {
|
|
3296
|
+
const vehicleName = s.vehicle_id ? vehicleNames.get(s.vehicle_id) : void 0;
|
|
3297
|
+
entities.push(indexVehicleService(s, vehicleName));
|
|
3298
|
+
});
|
|
3299
|
+
data.maintenanceTasks?.forEach((t) => {
|
|
3300
|
+
let assetName;
|
|
3301
|
+
if (t.property_id) assetName = propertyNames.get(t.property_id);
|
|
3302
|
+
else if (t.vehicle_id) assetName = vehicleNames.get(t.vehicle_id);
|
|
3303
|
+
entities.push(indexMaintenanceTask(t, assetName));
|
|
3304
|
+
});
|
|
3305
|
+
data.legalDocuments?.forEach((d) => {
|
|
3306
|
+
const residentNames = d.resident_ids?.map((id) => personNames.get(id)).filter(Boolean);
|
|
3307
|
+
entities.push(indexLegalDocument(d, residentNames));
|
|
3308
|
+
});
|
|
3309
|
+
data.accessCodes?.forEach((c) => {
|
|
3310
|
+
const propertyName = c.property_id ? propertyNames.get(c.property_id) : void 0;
|
|
3311
|
+
entities.push(indexAccessCode(c, propertyName));
|
|
3312
|
+
});
|
|
3313
|
+
data.devices?.forEach((d) => entities.push(indexDevice(d)));
|
|
3314
|
+
data.people?.forEach((p) => entities.push(indexPerson(p)));
|
|
3315
|
+
data.healthRecords?.forEach((r) => {
|
|
3316
|
+
let ownerName;
|
|
3317
|
+
if (r.person_id) ownerName = personNames.get(r.person_id);
|
|
3318
|
+
else if (r.pet_id) ownerName = petNames.get(r.pet_id);
|
|
3319
|
+
entities.push(indexHealthRecord(r, ownerName));
|
|
3320
|
+
});
|
|
3321
|
+
data.educationRecords?.forEach((r) => {
|
|
3322
|
+
const personName = r.person_id ? personNames.get(r.person_id) : void 0;
|
|
3323
|
+
entities.push(indexEducationRecord(r, personName));
|
|
3324
|
+
});
|
|
3325
|
+
data.militaryRecords?.forEach((r) => {
|
|
3326
|
+
const personName = r.person_id ? personNames.get(r.person_id) : void 0;
|
|
3327
|
+
entities.push(indexMilitaryRecord(r, personName));
|
|
3328
|
+
});
|
|
3329
|
+
data.membershipRecords?.forEach((r) => {
|
|
3330
|
+
const personName = r.person_id ? personNames.get(r.person_id) : void 0;
|
|
3331
|
+
entities.push(indexMembershipRecord(r, personName));
|
|
3332
|
+
});
|
|
3333
|
+
data.homeImprovements?.forEach((i) => {
|
|
3334
|
+
const propertyName = i.property_id ? propertyNames.get(i.property_id) : void 0;
|
|
3335
|
+
entities.push(indexHomeImprovement(i, propertyName));
|
|
3336
|
+
});
|
|
3337
|
+
data.taxYears?.forEach((t) => {
|
|
3338
|
+
const filerNames = t.filer_ids?.map((id) => personNames.get(id)).filter(Boolean);
|
|
3339
|
+
entities.push(indexTaxYear(t, filerNames));
|
|
3340
|
+
});
|
|
3341
|
+
return entities;
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3344
|
+
// ../list-data/src/adapters/contactAdapter.ts
|
|
3345
|
+
function getContactComputedFields(contact) {
|
|
3346
|
+
const computed = {
|
|
3347
|
+
displayName: getContactDisplayName(contact)
|
|
3348
|
+
};
|
|
3349
|
+
if (contact.type) {
|
|
3350
|
+
computed.formattedType = contact.type.charAt(0).toUpperCase() + contact.type.slice(1).replace(/_/g, " ");
|
|
3351
|
+
}
|
|
3352
|
+
return computed;
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
// ../list-data/src/adapters/insuranceAdapter.ts
|
|
3356
|
+
function getInsuranceComputedFields(policy) {
|
|
3357
|
+
const computed = {
|
|
3358
|
+
displayName: policy.provider || "Insurance Policy"
|
|
3359
|
+
};
|
|
3360
|
+
if (policy.expiration_date) {
|
|
3361
|
+
computed.daysUntilExpiry = daysUntil(policy.expiration_date);
|
|
3362
|
+
computed.expiryStatus = getExpiryStatus(computed.daysUntilExpiry);
|
|
3363
|
+
}
|
|
3364
|
+
if (policy.type) {
|
|
3365
|
+
computed.formattedType = policy.type.charAt(0).toUpperCase() + policy.type.slice(1);
|
|
3366
|
+
}
|
|
3367
|
+
return computed;
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
// ../list-data/src/adapters/petAdapter.ts
|
|
3371
|
+
function getPetComputedFields(pet) {
|
|
3372
|
+
const computed = {
|
|
3373
|
+
displayName: pet.name
|
|
3374
|
+
};
|
|
3375
|
+
const detailedAge = calculateDetailedAge(pet.date_of_birth);
|
|
3376
|
+
if (detailedAge !== null && !pet.memorialized_at) {
|
|
3377
|
+
if (detailedAge.years < 1) {
|
|
3378
|
+
const months = detailedAge.months || 1;
|
|
3379
|
+
computed.age = `${months} month${months !== 1 ? "s" : ""}`;
|
|
3380
|
+
} else {
|
|
3381
|
+
computed.age = `${detailedAge.years} year${detailedAge.years !== 1 ? "s" : ""}`;
|
|
3382
|
+
}
|
|
3383
|
+
if (pet.species === "cat") {
|
|
3384
|
+
computed.humanYears = calculateCatHumanYears(detailedAge.fractionalYears);
|
|
3385
|
+
} else if (pet.species === "dog") {
|
|
3386
|
+
let size = "medium";
|
|
3387
|
+
const weightHistory = pet.weight_history || [];
|
|
3388
|
+
const latestWeight = weightHistory.length > 0 ? [...weightHistory].sort((a, b) => b.date.localeCompare(a.date))[0] : null;
|
|
3389
|
+
const weightInLbs = latestWeight ? latestWeight.unit === "kg" ? latestWeight.weight * 2.20462 : latestWeight.weight : pet.weight_lbs;
|
|
3390
|
+
if (weightInLbs) {
|
|
3391
|
+
if (weightInLbs < 20) size = "small";
|
|
3392
|
+
else if (weightInLbs > 50) size = "large";
|
|
3393
|
+
}
|
|
3394
|
+
computed.humanYears = calculateDogHumanYears(detailedAge.fractionalYears, size);
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
if (pet.species) {
|
|
3398
|
+
computed.formattedType = pet.species.charAt(0).toUpperCase() + pet.species.slice(1).replace(/_/g, " ");
|
|
3399
|
+
}
|
|
3400
|
+
return computed;
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
// ../list-data/src/adapters/vehicleAdapter.ts
|
|
3404
|
+
function getVehicleComputedFields(vehicle) {
|
|
3405
|
+
const parts = [vehicle.year, vehicle.make, vehicle.model].filter(Boolean);
|
|
3406
|
+
const displayName = parts.length > 0 ? parts.join(" ") : vehicle.name || "Vehicle";
|
|
3407
|
+
const computed = {
|
|
3408
|
+
displayName
|
|
3409
|
+
};
|
|
3410
|
+
if (vehicle.registration_expiration) {
|
|
3411
|
+
const monthNum = parseInt(vehicle.registration_expiration, 10);
|
|
3412
|
+
if (monthNum >= 1 && monthNum <= 12) {
|
|
3413
|
+
const now = /* @__PURE__ */ new Date();
|
|
3414
|
+
const currentYear = now.getFullYear();
|
|
3415
|
+
const currentMonth = now.getMonth() + 1;
|
|
3416
|
+
let targetYear = currentYear;
|
|
3417
|
+
if (monthNum < currentMonth) {
|
|
3418
|
+
targetYear = currentYear + 1;
|
|
3419
|
+
}
|
|
3420
|
+
const expirationDate = new Date(targetYear, monthNum, 0);
|
|
3421
|
+
const daysUntil2 = Math.ceil((expirationDate.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24));
|
|
3422
|
+
computed.daysUntilExpiry = daysUntil2;
|
|
3423
|
+
if (daysUntil2 < 0) {
|
|
3424
|
+
computed.expiryStatus = "expired";
|
|
3425
|
+
} else if (daysUntil2 <= 30) {
|
|
3426
|
+
computed.expiryStatus = "expiring_soon";
|
|
3427
|
+
} else {
|
|
3428
|
+
computed.expiryStatus = "ok";
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
const vehicleType = vehicle.vehicle_type || vehicle.type;
|
|
3433
|
+
if (vehicleType) {
|
|
3434
|
+
computed.formattedType = vehicleType.charAt(0).toUpperCase() + vehicleType.slice(1);
|
|
3435
|
+
}
|
|
3436
|
+
return computed;
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
// ../list-data/src/data/propertyMaintenanceTemplates.json
|
|
3440
|
+
var propertyMaintenanceTemplates_default = [
|
|
3441
|
+
{
|
|
3442
|
+
id: "smoke-detector-maintenance",
|
|
3443
|
+
name: "Smoke & CO Detector Maintenance",
|
|
3444
|
+
description: "Monthly: Test all detectors. Annually: Replace batteries",
|
|
3445
|
+
category: "safety",
|
|
3446
|
+
applicableTo: "property",
|
|
3447
|
+
conditions: {},
|
|
3448
|
+
defaultFrequency: {
|
|
3449
|
+
type: "recurring",
|
|
3450
|
+
interval: "monthly"
|
|
3451
|
+
},
|
|
3452
|
+
priority: "critical",
|
|
3453
|
+
skillLevel: "diy_easy",
|
|
3454
|
+
estimatedTimeMinutes: 15,
|
|
3455
|
+
whyImportant: "Life-saving devices must be functional - test monthly, replace batteries yearly"
|
|
3456
|
+
},
|
|
3457
|
+
{
|
|
3458
|
+
id: "hvac-filter-replacement",
|
|
3459
|
+
name: "Replace HVAC Air Filter",
|
|
3460
|
+
description: "Change air filter in heating/cooling system to maintain efficiency",
|
|
3461
|
+
category: "hvac",
|
|
3462
|
+
applicableTo: "property",
|
|
3463
|
+
conditions: {
|
|
3464
|
+
hasAC: true
|
|
3465
|
+
},
|
|
3466
|
+
defaultFrequency: {
|
|
3467
|
+
type: "recurring",
|
|
3468
|
+
interval: "monthly"
|
|
3469
|
+
},
|
|
3470
|
+
priority: "important",
|
|
3471
|
+
skillLevel: "diy_easy",
|
|
3472
|
+
estimatedTimeMinutes: 10,
|
|
3473
|
+
whyImportant: "Dirty filters reduce efficiency by up to 15% and worsen air quality"
|
|
3474
|
+
},
|
|
3475
|
+
{
|
|
3476
|
+
id: "furnace-inspection",
|
|
3477
|
+
name: "Furnace Inspection & Service",
|
|
3478
|
+
description: "Professional inspection and cleaning of gas furnace",
|
|
3479
|
+
category: "hvac",
|
|
3480
|
+
applicableTo: "property",
|
|
3481
|
+
conditions: {
|
|
3482
|
+
hasGasFurnace: true
|
|
3483
|
+
},
|
|
3484
|
+
defaultFrequency: {
|
|
3485
|
+
type: "seasonal",
|
|
3486
|
+
season: "fall",
|
|
3487
|
+
monthsNorthern: [9],
|
|
3488
|
+
monthsSouthern: [3]
|
|
3489
|
+
},
|
|
3490
|
+
priority: "important",
|
|
3491
|
+
skillLevel: "professional",
|
|
3492
|
+
estimatedTimeMinutes: 90,
|
|
3493
|
+
whyImportant: "Ensures safe operation and prevents carbon monoxide leaks"
|
|
3494
|
+
},
|
|
3495
|
+
{
|
|
3496
|
+
id: "ac-service",
|
|
3497
|
+
name: "Air Conditioning Service",
|
|
3498
|
+
description: "Professional AC inspection and cleaning",
|
|
3499
|
+
category: "hvac",
|
|
3500
|
+
applicableTo: "property",
|
|
3501
|
+
conditions: {
|
|
3502
|
+
hasAC: true
|
|
3503
|
+
},
|
|
3504
|
+
defaultFrequency: {
|
|
3505
|
+
type: "seasonal",
|
|
3506
|
+
season: "spring",
|
|
3507
|
+
monthsNorthern: [4, 5],
|
|
3508
|
+
monthsSouthern: [10, 11]
|
|
3509
|
+
},
|
|
3510
|
+
priority: "important",
|
|
3511
|
+
skillLevel: "professional",
|
|
3512
|
+
estimatedTimeMinutes: 90,
|
|
3513
|
+
whyImportant: "Maintains efficiency and prevents breakdowns in summer heat"
|
|
3514
|
+
},
|
|
3515
|
+
{
|
|
3516
|
+
id: "gutter-cleaning-fall",
|
|
3517
|
+
name: "Clean Gutters (Fall)",
|
|
3518
|
+
description: "Remove leaves and debris from gutters and downspouts",
|
|
3519
|
+
category: "exterior",
|
|
3520
|
+
applicableTo: "property",
|
|
3521
|
+
conditions: {
|
|
3522
|
+
hasGutters: true,
|
|
3523
|
+
propertyType: ["single_family", "townhouse"]
|
|
3524
|
+
},
|
|
3525
|
+
defaultFrequency: {
|
|
3526
|
+
type: "seasonal",
|
|
3527
|
+
season: "fall",
|
|
3528
|
+
monthsNorthern: [10, 11],
|
|
3529
|
+
monthsSouthern: [4, 5]
|
|
3530
|
+
},
|
|
3531
|
+
priority: "important",
|
|
3532
|
+
skillLevel: "diy_moderate",
|
|
3533
|
+
estimatedTimeMinutes: 120,
|
|
3534
|
+
whyImportant: "Clogged gutters can cause water damage to foundation and roof"
|
|
3535
|
+
},
|
|
3536
|
+
{
|
|
3537
|
+
id: "gutter-cleaning-spring",
|
|
3538
|
+
name: "Clean Gutters (Spring)",
|
|
3539
|
+
description: "Remove debris accumulated over winter",
|
|
3540
|
+
category: "exterior",
|
|
3541
|
+
applicableTo: "property",
|
|
3542
|
+
conditions: {
|
|
3543
|
+
hasGutters: true,
|
|
3544
|
+
propertyType: ["single_family", "townhouse"]
|
|
3545
|
+
},
|
|
3546
|
+
defaultFrequency: {
|
|
3547
|
+
type: "seasonal",
|
|
3548
|
+
season: "spring",
|
|
3549
|
+
monthsNorthern: [4, 5],
|
|
3550
|
+
monthsSouthern: [10, 11]
|
|
3551
|
+
},
|
|
3552
|
+
priority: "important",
|
|
3553
|
+
skillLevel: "diy_moderate",
|
|
3554
|
+
estimatedTimeMinutes: 120,
|
|
3555
|
+
whyImportant: "Spring runoff needs clear gutters to prevent water damage"
|
|
3556
|
+
},
|
|
3557
|
+
{
|
|
3558
|
+
id: "roof-moss-removal",
|
|
3559
|
+
name: "Remove Roof Moss",
|
|
3560
|
+
description: "Clean moss and debris from roof shingles",
|
|
3561
|
+
category: "roof",
|
|
3562
|
+
applicableTo: "property",
|
|
3563
|
+
conditions: {
|
|
3564
|
+
propertyType: ["single_family", "townhouse"],
|
|
3565
|
+
climate: ["marine", "humid_subtropical", "humid_continental"]
|
|
3566
|
+
},
|
|
3567
|
+
defaultFrequency: {
|
|
3568
|
+
type: "recurring",
|
|
3569
|
+
interval: "yearly"
|
|
3570
|
+
},
|
|
3571
|
+
priority: "important",
|
|
3572
|
+
skillLevel: "professional",
|
|
3573
|
+
estimatedTimeMinutes: 240,
|
|
3574
|
+
whyImportant: "Moss damages shingles and reduces roof lifespan significantly"
|
|
3575
|
+
},
|
|
3576
|
+
{
|
|
3577
|
+
id: "roof-inspection",
|
|
3578
|
+
name: "Inspect Roof",
|
|
3579
|
+
description: "Check for damaged shingles, leaks, and wear",
|
|
3580
|
+
category: "roof",
|
|
3581
|
+
applicableTo: "property",
|
|
3582
|
+
conditions: {
|
|
3583
|
+
propertyType: ["single_family", "townhouse"]
|
|
3584
|
+
},
|
|
3585
|
+
defaultFrequency: {
|
|
3586
|
+
type: "recurring",
|
|
3587
|
+
interval: "biannually"
|
|
3588
|
+
},
|
|
3589
|
+
priority: "important",
|
|
3590
|
+
skillLevel: "professional",
|
|
3591
|
+
estimatedTimeMinutes: 60,
|
|
3592
|
+
whyImportant: "Early detection of roof damage prevents expensive interior water damage"
|
|
3593
|
+
},
|
|
3594
|
+
{
|
|
3595
|
+
id: "pool-chemistry-check",
|
|
3596
|
+
name: "Check Pool Chemistry",
|
|
3597
|
+
description: "Test and balance pH, chlorine, and alkalinity levels",
|
|
3598
|
+
category: "pool",
|
|
3599
|
+
applicableTo: "property",
|
|
3600
|
+
conditions: {
|
|
3601
|
+
hasPool: true
|
|
3602
|
+
},
|
|
3603
|
+
defaultFrequency: {
|
|
3604
|
+
type: "recurring",
|
|
3605
|
+
interval: "weekly"
|
|
3606
|
+
},
|
|
3607
|
+
priority: "important",
|
|
3608
|
+
skillLevel: "diy_easy",
|
|
3609
|
+
estimatedTimeMinutes: 30,
|
|
3610
|
+
whyImportant: "Prevents algae growth and keeps water safe for swimming"
|
|
3611
|
+
},
|
|
3612
|
+
{
|
|
3613
|
+
id: "pool-filter-cleaning",
|
|
3614
|
+
name: "Clean Pool Filter",
|
|
3615
|
+
description: "Backwash or clean pool filter system",
|
|
3616
|
+
category: "pool",
|
|
3617
|
+
applicableTo: "property",
|
|
3618
|
+
conditions: {
|
|
3619
|
+
hasPool: true
|
|
3620
|
+
},
|
|
3621
|
+
defaultFrequency: {
|
|
3622
|
+
type: "recurring",
|
|
3623
|
+
interval: "monthly"
|
|
3624
|
+
},
|
|
3625
|
+
priority: "important",
|
|
3626
|
+
skillLevel: "diy_moderate",
|
|
3627
|
+
estimatedTimeMinutes: 45,
|
|
3628
|
+
whyImportant: "Keeps water circulation efficient and clear"
|
|
3629
|
+
},
|
|
3630
|
+
{
|
|
3631
|
+
id: "pool-winterization",
|
|
3632
|
+
name: "Winterize Pool",
|
|
3633
|
+
description: "Close pool for winter: drain, add antifreeze, install cover",
|
|
3634
|
+
category: "pool",
|
|
3635
|
+
applicableTo: "property",
|
|
3636
|
+
conditions: {
|
|
3637
|
+
hasPool: true,
|
|
3638
|
+
winterSeverity: ["moderate", "severe"]
|
|
3639
|
+
},
|
|
3640
|
+
defaultFrequency: {
|
|
3641
|
+
type: "seasonal",
|
|
3642
|
+
season: "fall",
|
|
3643
|
+
monthsNorthern: [10],
|
|
3644
|
+
monthsSouthern: [4]
|
|
3645
|
+
},
|
|
3646
|
+
priority: "critical",
|
|
3647
|
+
skillLevel: "professional",
|
|
3648
|
+
estimatedTimeMinutes: 180,
|
|
3649
|
+
whyImportant: "Prevents freeze damage to pool equipment and plumbing"
|
|
3650
|
+
},
|
|
3651
|
+
{
|
|
3652
|
+
id: "pool-opening",
|
|
3653
|
+
name: "Open Pool for Summer",
|
|
3654
|
+
description: "Remove cover, refill, balance chemicals, start equipment",
|
|
3655
|
+
category: "pool",
|
|
3656
|
+
applicableTo: "property",
|
|
3657
|
+
conditions: {
|
|
3658
|
+
hasPool: true,
|
|
3659
|
+
winterSeverity: ["moderate", "severe"]
|
|
3660
|
+
},
|
|
3661
|
+
defaultFrequency: {
|
|
3662
|
+
type: "seasonal",
|
|
3663
|
+
season: "spring",
|
|
3664
|
+
monthsNorthern: [4, 5],
|
|
3665
|
+
monthsSouthern: [10, 11]
|
|
3666
|
+
},
|
|
3667
|
+
priority: "important",
|
|
3668
|
+
skillLevel: "professional",
|
|
3669
|
+
estimatedTimeMinutes: 180,
|
|
3670
|
+
whyImportant: "Proper opening prevents damage and ensures safe swimming"
|
|
3671
|
+
},
|
|
3672
|
+
{
|
|
3673
|
+
id: "sprinkler-system-blowout",
|
|
3674
|
+
name: "Blow Out Sprinkler System",
|
|
3675
|
+
description: "Clear water from sprinkler lines to prevent freeze damage",
|
|
3676
|
+
category: "landscaping",
|
|
3677
|
+
applicableTo: "property",
|
|
3678
|
+
conditions: {
|
|
3679
|
+
hasSprinklerSystem: true,
|
|
3680
|
+
winterSeverity: ["moderate", "severe"]
|
|
3681
|
+
},
|
|
3682
|
+
defaultFrequency: {
|
|
3683
|
+
type: "seasonal",
|
|
3684
|
+
season: "fall",
|
|
3685
|
+
monthsNorthern: [10],
|
|
3686
|
+
monthsSouthern: [4]
|
|
3687
|
+
},
|
|
3688
|
+
priority: "critical",
|
|
3689
|
+
skillLevel: "professional",
|
|
3690
|
+
estimatedTimeMinutes: 60,
|
|
3691
|
+
whyImportant: "Frozen water in lines can cause $1000+ in repairs"
|
|
3692
|
+
},
|
|
3693
|
+
{
|
|
3694
|
+
id: "sprinkler-system-startup",
|
|
3695
|
+
name: "Start Up Sprinkler System",
|
|
3696
|
+
description: "Turn on system, check for leaks, adjust spray patterns",
|
|
3697
|
+
category: "landscaping",
|
|
3698
|
+
applicableTo: "property",
|
|
3699
|
+
conditions: {
|
|
3700
|
+
hasSprinklerSystem: true,
|
|
3701
|
+
winterSeverity: ["moderate", "severe"]
|
|
3702
|
+
},
|
|
3703
|
+
defaultFrequency: {
|
|
3704
|
+
type: "seasonal",
|
|
3705
|
+
season: "spring",
|
|
3706
|
+
monthsNorthern: [4, 5],
|
|
3707
|
+
monthsSouthern: [10, 11]
|
|
3708
|
+
},
|
|
3709
|
+
priority: "important",
|
|
3710
|
+
skillLevel: "diy_moderate",
|
|
3711
|
+
estimatedTimeMinutes: 90,
|
|
3712
|
+
whyImportant: "Catch leaks early before they waste water and money"
|
|
3713
|
+
},
|
|
3714
|
+
{
|
|
3715
|
+
id: "chimney-cleaning-inspection",
|
|
3716
|
+
name: "Chimney Cleaning & Inspection",
|
|
3717
|
+
description: "Remove creosote buildup and inspect for damage",
|
|
3718
|
+
category: "safety",
|
|
3719
|
+
applicableTo: "property",
|
|
3720
|
+
conditions: {
|
|
3721
|
+
hasFireplace: true
|
|
3722
|
+
},
|
|
3723
|
+
defaultFrequency: {
|
|
3724
|
+
type: "recurring",
|
|
3725
|
+
interval: "yearly"
|
|
3726
|
+
},
|
|
3727
|
+
priority: "important",
|
|
3728
|
+
skillLevel: "professional",
|
|
3729
|
+
estimatedTimeMinutes: 120,
|
|
3730
|
+
whyImportant: "Prevents chimney fires caused by creosote buildup"
|
|
3731
|
+
},
|
|
3732
|
+
{
|
|
3733
|
+
id: "septic-tank-pumping",
|
|
3734
|
+
name: "Pump Septic Tank",
|
|
3735
|
+
description: "Professional septic tank pumping and inspection",
|
|
3736
|
+
category: "plumbing",
|
|
3737
|
+
applicableTo: "property",
|
|
3738
|
+
conditions: {
|
|
3739
|
+
propertyType: ["single_family"],
|
|
3740
|
+
hasSepticSystem: true
|
|
3741
|
+
},
|
|
3742
|
+
defaultFrequency: {
|
|
3743
|
+
type: "recurring",
|
|
3744
|
+
customMonths: 36
|
|
3745
|
+
},
|
|
3746
|
+
priority: "critical",
|
|
3747
|
+
skillLevel: "professional",
|
|
3748
|
+
estimatedTimeMinutes: 120,
|
|
3749
|
+
whyImportant: "Prevents backup and expensive drain field replacement ($10,000+)"
|
|
3750
|
+
},
|
|
3751
|
+
{
|
|
3752
|
+
id: "water-heater-flush",
|
|
3753
|
+
name: "Flush Water Heater",
|
|
3754
|
+
description: "Drain sediment from water heater tank",
|
|
3755
|
+
category: "plumbing",
|
|
3756
|
+
applicableTo: "property",
|
|
3757
|
+
conditions: {},
|
|
3758
|
+
defaultFrequency: {
|
|
3759
|
+
type: "recurring",
|
|
3760
|
+
interval: "yearly"
|
|
3761
|
+
},
|
|
3762
|
+
priority: "routine",
|
|
3763
|
+
skillLevel: "diy_moderate",
|
|
3764
|
+
estimatedTimeMinutes: 60,
|
|
3765
|
+
whyImportant: "Removes sediment buildup that reduces efficiency and lifespan"
|
|
3766
|
+
},
|
|
3767
|
+
{
|
|
3768
|
+
id: "deck-staining",
|
|
3769
|
+
name: "Stain/Seal Deck",
|
|
3770
|
+
description: "Apply protective stain or sealant to wood deck",
|
|
3771
|
+
category: "exterior",
|
|
3772
|
+
applicableTo: "property",
|
|
3773
|
+
conditions: {
|
|
3774
|
+
hasDeck: true,
|
|
3775
|
+
deckMaterial: ["wood"]
|
|
3776
|
+
},
|
|
3777
|
+
defaultFrequency: {
|
|
3778
|
+
type: "recurring",
|
|
3779
|
+
customMonths: 24
|
|
3780
|
+
},
|
|
3781
|
+
priority: "important",
|
|
3782
|
+
skillLevel: "diy_moderate",
|
|
3783
|
+
estimatedTimeMinutes: 480,
|
|
3784
|
+
whyImportant: "Protects wood from rot and extends deck life by years"
|
|
3785
|
+
},
|
|
3786
|
+
{
|
|
3787
|
+
id: "driveway-sealing",
|
|
3788
|
+
name: "Seal Asphalt Driveway",
|
|
3789
|
+
description: "Apply sealcoat to protect asphalt surface",
|
|
3790
|
+
category: "exterior",
|
|
3791
|
+
applicableTo: "property",
|
|
3792
|
+
conditions: {
|
|
3793
|
+
hasDriveway: true,
|
|
3794
|
+
drivewayMaterial: ["asphalt"]
|
|
3795
|
+
},
|
|
3796
|
+
defaultFrequency: {
|
|
3797
|
+
type: "recurring",
|
|
3798
|
+
customMonths: 24
|
|
3799
|
+
},
|
|
3800
|
+
priority: "routine",
|
|
3801
|
+
skillLevel: "diy_moderate",
|
|
3802
|
+
estimatedTimeMinutes: 360,
|
|
3803
|
+
whyImportant: "Prevents cracks and extends driveway life significantly"
|
|
3804
|
+
},
|
|
3805
|
+
{
|
|
3806
|
+
id: "power-wash-exterior",
|
|
3807
|
+
name: "Power Wash Home Exterior",
|
|
3808
|
+
description: "Clean siding, walkways, and deck to remove dirt and mildew",
|
|
3809
|
+
category: "exterior",
|
|
3810
|
+
applicableTo: "property",
|
|
3811
|
+
conditions: {
|
|
3812
|
+
propertyType: ["single_family", "townhouse"]
|
|
3813
|
+
},
|
|
3814
|
+
defaultFrequency: {
|
|
3815
|
+
type: "recurring",
|
|
3816
|
+
interval: "yearly"
|
|
3817
|
+
},
|
|
3818
|
+
priority: "routine",
|
|
3819
|
+
skillLevel: "diy_moderate",
|
|
3820
|
+
estimatedTimeMinutes: 240,
|
|
3821
|
+
whyImportant: "Prevents mildew damage and maintains curb appeal"
|
|
3822
|
+
},
|
|
3823
|
+
{
|
|
3824
|
+
id: "window-caulking",
|
|
3825
|
+
name: "Inspect & Recaulk Windows",
|
|
3826
|
+
description: "Check window caulking and reapply where needed",
|
|
3827
|
+
category: "exterior",
|
|
3828
|
+
applicableTo: "property",
|
|
3829
|
+
conditions: {
|
|
3830
|
+
propertyType: ["single_family", "townhouse"]
|
|
3831
|
+
},
|
|
3832
|
+
defaultFrequency: {
|
|
3833
|
+
type: "recurring",
|
|
3834
|
+
customMonths: 24
|
|
3835
|
+
},
|
|
3836
|
+
priority: "routine",
|
|
3837
|
+
skillLevel: "diy_moderate",
|
|
3838
|
+
estimatedTimeMinutes: 180,
|
|
3839
|
+
whyImportant: "Prevents water infiltration and improves energy efficiency"
|
|
3840
|
+
},
|
|
3841
|
+
{
|
|
3842
|
+
id: "dryer-vent-cleaning",
|
|
3843
|
+
name: "Clean Dryer Vent",
|
|
3844
|
+
description: "Remove lint buildup from dryer vent ductwork",
|
|
3845
|
+
category: "appliances",
|
|
3846
|
+
applicableTo: "property",
|
|
3847
|
+
conditions: {},
|
|
3848
|
+
defaultFrequency: {
|
|
3849
|
+
type: "recurring",
|
|
3850
|
+
interval: "yearly"
|
|
3851
|
+
},
|
|
3852
|
+
priority: "important",
|
|
3853
|
+
skillLevel: "diy_moderate",
|
|
3854
|
+
estimatedTimeMinutes: 45,
|
|
3855
|
+
whyImportant: "Prevents fire hazard and improves dryer efficiency"
|
|
3856
|
+
},
|
|
3857
|
+
{
|
|
3858
|
+
id: "refrigerator-coil-cleaning",
|
|
3859
|
+
name: "Clean Refrigerator Coils",
|
|
3860
|
+
description: "Vacuum dust from refrigerator condenser coils",
|
|
3861
|
+
category: "appliances",
|
|
3862
|
+
applicableTo: "property",
|
|
3863
|
+
conditions: {},
|
|
3864
|
+
defaultFrequency: {
|
|
3865
|
+
type: "recurring",
|
|
3866
|
+
interval: "biannually"
|
|
3867
|
+
},
|
|
3868
|
+
priority: "routine",
|
|
3869
|
+
skillLevel: "diy_easy",
|
|
3870
|
+
estimatedTimeMinutes: 30,
|
|
3871
|
+
whyImportant: "Improves efficiency and extends refrigerator lifespan"
|
|
3872
|
+
},
|
|
3873
|
+
{
|
|
3874
|
+
id: "lawn-mowing",
|
|
3875
|
+
name: "Mow Lawn",
|
|
3876
|
+
description: "Cut grass to maintain healthy lawn appearance",
|
|
3877
|
+
category: "landscaping",
|
|
3878
|
+
applicableTo: "property",
|
|
3879
|
+
conditions: {
|
|
3880
|
+
propertyType: ["single_family", "townhouse"],
|
|
3881
|
+
lawnSize: ["small", "medium"]
|
|
3882
|
+
},
|
|
3883
|
+
defaultFrequency: {
|
|
3884
|
+
type: "recurring",
|
|
3885
|
+
interval: "weekly"
|
|
3886
|
+
},
|
|
3887
|
+
priority: "routine",
|
|
3888
|
+
skillLevel: "diy_easy",
|
|
3889
|
+
estimatedTimeMinutes: 60,
|
|
3890
|
+
whyImportant: "Keeps lawn healthy and property looking well-maintained"
|
|
3891
|
+
},
|
|
3892
|
+
{
|
|
3893
|
+
id: "lawn-fertilizing-spring",
|
|
3894
|
+
name: "Fertilize Lawn (Spring)",
|
|
3895
|
+
description: "Apply spring fertilizer to promote growth",
|
|
3896
|
+
category: "landscaping",
|
|
3897
|
+
applicableTo: "property",
|
|
3898
|
+
conditions: {
|
|
3899
|
+
propertyType: ["single_family", "townhouse"],
|
|
3900
|
+
lawnSize: ["small", "medium"]
|
|
3901
|
+
},
|
|
3902
|
+
defaultFrequency: {
|
|
3903
|
+
type: "seasonal",
|
|
3904
|
+
season: "spring",
|
|
3905
|
+
monthsNorthern: [4, 5],
|
|
3906
|
+
monthsSouthern: [10, 11]
|
|
3907
|
+
},
|
|
3908
|
+
priority: "routine",
|
|
3909
|
+
skillLevel: "diy_easy",
|
|
3910
|
+
estimatedTimeMinutes: 90,
|
|
3911
|
+
whyImportant: "Promotes healthy growth and green color"
|
|
3912
|
+
},
|
|
3913
|
+
{
|
|
3914
|
+
id: "lawn-fertilizing-fall",
|
|
3915
|
+
name: "Fertilize Lawn (Fall)",
|
|
3916
|
+
description: "Apply fall fertilizer to strengthen roots",
|
|
3917
|
+
category: "landscaping",
|
|
3918
|
+
applicableTo: "property",
|
|
3919
|
+
conditions: {
|
|
3920
|
+
propertyType: ["single_family", "townhouse"],
|
|
3921
|
+
lawnSize: ["small", "medium"]
|
|
3922
|
+
},
|
|
3923
|
+
defaultFrequency: {
|
|
3924
|
+
type: "seasonal",
|
|
3925
|
+
season: "fall",
|
|
3926
|
+
monthsNorthern: [9, 10],
|
|
3927
|
+
monthsSouthern: [3, 4]
|
|
3928
|
+
},
|
|
3929
|
+
priority: "routine",
|
|
3930
|
+
skillLevel: "diy_easy",
|
|
3931
|
+
estimatedTimeMinutes: 90,
|
|
3932
|
+
whyImportant: "Strengthens roots for winter and early spring growth"
|
|
3933
|
+
},
|
|
3934
|
+
{
|
|
3935
|
+
id: "lawn-aeration",
|
|
3936
|
+
name: "Aerate Lawn",
|
|
3937
|
+
description: "Core aerate lawn to improve water and nutrient absorption",
|
|
3938
|
+
category: "landscaping",
|
|
3939
|
+
applicableTo: "property",
|
|
3940
|
+
conditions: {
|
|
3941
|
+
propertyType: ["single_family", "townhouse"],
|
|
3942
|
+
lawnSize: ["medium"]
|
|
3943
|
+
},
|
|
3944
|
+
defaultFrequency: {
|
|
3945
|
+
type: "recurring",
|
|
3946
|
+
interval: "yearly"
|
|
3947
|
+
},
|
|
3948
|
+
priority: "routine",
|
|
3949
|
+
skillLevel: "diy_moderate",
|
|
3950
|
+
estimatedTimeMinutes: 120,
|
|
3951
|
+
whyImportant: "Reduces soil compaction and promotes healthier grass"
|
|
3952
|
+
},
|
|
3953
|
+
{
|
|
3954
|
+
id: "tree-trimming",
|
|
3955
|
+
name: "Trim Trees & Shrubs",
|
|
3956
|
+
description: "Prune dead branches and shape trees/shrubs",
|
|
3957
|
+
category: "landscaping",
|
|
3958
|
+
applicableTo: "property",
|
|
3959
|
+
conditions: {
|
|
3960
|
+
propertyType: ["single_family", "townhouse"],
|
|
3961
|
+
treeCount: ["few"]
|
|
3962
|
+
},
|
|
3963
|
+
defaultFrequency: {
|
|
3964
|
+
type: "recurring",
|
|
3965
|
+
interval: "yearly"
|
|
3966
|
+
},
|
|
3967
|
+
priority: "routine",
|
|
3968
|
+
skillLevel: "professional",
|
|
3969
|
+
estimatedTimeMinutes: 180,
|
|
3970
|
+
whyImportant: "Prevents damage from falling branches and promotes healthy growth"
|
|
3971
|
+
},
|
|
3972
|
+
{
|
|
3973
|
+
id: "well-pump-testing",
|
|
3974
|
+
name: "Test Well Pump",
|
|
3975
|
+
description: "Check well pump operation and water quality",
|
|
3976
|
+
category: "plumbing",
|
|
3977
|
+
applicableTo: "property",
|
|
3978
|
+
conditions: {
|
|
3979
|
+
hasWell: true
|
|
3980
|
+
},
|
|
3981
|
+
defaultFrequency: {
|
|
3982
|
+
type: "recurring",
|
|
3983
|
+
interval: "yearly"
|
|
3984
|
+
},
|
|
3985
|
+
priority: "important",
|
|
3986
|
+
skillLevel: "professional",
|
|
3987
|
+
estimatedTimeMinutes: 90,
|
|
3988
|
+
whyImportant: "Catches pump issues early before complete failure"
|
|
3989
|
+
},
|
|
3990
|
+
{
|
|
3991
|
+
id: "basement-sump-pump-test",
|
|
3992
|
+
name: "Test Sump Pump",
|
|
3993
|
+
description: "Pour water into sump pit to ensure pump activates properly",
|
|
3994
|
+
category: "plumbing",
|
|
3995
|
+
applicableTo: "property",
|
|
3996
|
+
conditions: {
|
|
3997
|
+
hasBasement: true
|
|
3998
|
+
},
|
|
3999
|
+
defaultFrequency: {
|
|
4000
|
+
type: "recurring",
|
|
4001
|
+
interval: "quarterly"
|
|
4002
|
+
},
|
|
4003
|
+
priority: "important",
|
|
4004
|
+
skillLevel: "diy_easy",
|
|
4005
|
+
estimatedTimeMinutes: 15,
|
|
4006
|
+
whyImportant: "Prevents basement flooding during heavy rain"
|
|
4007
|
+
},
|
|
4008
|
+
{
|
|
4009
|
+
id: "solar-panel-cleaning",
|
|
4010
|
+
name: "Clean Solar Panels",
|
|
4011
|
+
description: "Remove dirt, pollen, bird droppings, and debris from solar panels",
|
|
4012
|
+
category: "solar",
|
|
4013
|
+
applicableTo: "property",
|
|
4014
|
+
conditions: {
|
|
4015
|
+
hasSolarPanels: true
|
|
4016
|
+
},
|
|
4017
|
+
defaultFrequency: {
|
|
4018
|
+
type: "recurring",
|
|
4019
|
+
interval: "yearly"
|
|
4020
|
+
},
|
|
4021
|
+
priority: "important",
|
|
4022
|
+
skillLevel: "diy_moderate",
|
|
4023
|
+
estimatedTimeMinutes: 60,
|
|
4024
|
+
whyImportant: "Dirty panels can reduce energy output by 20-25%"
|
|
4025
|
+
},
|
|
4026
|
+
{
|
|
4027
|
+
id: "solar-panel-visual-inspection",
|
|
4028
|
+
name: "Inspect Solar Panels",
|
|
4029
|
+
description: "Check panels for cracks, hotspots, discoloration, loose connections, and debris buildup",
|
|
4030
|
+
category: "solar",
|
|
4031
|
+
applicableTo: "property",
|
|
4032
|
+
conditions: {
|
|
4033
|
+
hasSolarPanels: true
|
|
4034
|
+
},
|
|
4035
|
+
defaultFrequency: {
|
|
4036
|
+
type: "recurring",
|
|
4037
|
+
interval: "biannually"
|
|
4038
|
+
},
|
|
4039
|
+
priority: "important",
|
|
4040
|
+
skillLevel: "diy_easy",
|
|
4041
|
+
estimatedTimeMinutes: 30,
|
|
4042
|
+
whyImportant: "Early detection of damage prevents costly repairs and output loss"
|
|
4043
|
+
},
|
|
4044
|
+
{
|
|
4045
|
+
id: "solar-inverter-inspection",
|
|
4046
|
+
name: "Check Solar Inverter",
|
|
4047
|
+
description: "Verify inverter is functioning properly, check status lights and error codes",
|
|
4048
|
+
category: "solar",
|
|
4049
|
+
applicableTo: "property",
|
|
4050
|
+
conditions: {
|
|
4051
|
+
hasSolarPanels: true
|
|
4052
|
+
},
|
|
4053
|
+
defaultFrequency: {
|
|
4054
|
+
type: "recurring",
|
|
4055
|
+
interval: "monthly"
|
|
4056
|
+
},
|
|
4057
|
+
priority: "important",
|
|
4058
|
+
skillLevel: "diy_easy",
|
|
4059
|
+
estimatedTimeMinutes: 10,
|
|
4060
|
+
whyImportant: "Inverter failure stops all energy production - catch issues early"
|
|
4061
|
+
},
|
|
4062
|
+
{
|
|
4063
|
+
id: "solar-system-professional-inspection",
|
|
4064
|
+
name: "Professional Solar System Inspection",
|
|
4065
|
+
description: "Full system inspection including wiring, connections, mounting, and performance analysis",
|
|
4066
|
+
category: "solar",
|
|
4067
|
+
applicableTo: "property",
|
|
4068
|
+
conditions: {
|
|
4069
|
+
hasSolarPanels: true
|
|
4070
|
+
},
|
|
4071
|
+
defaultFrequency: {
|
|
4072
|
+
type: "recurring",
|
|
4073
|
+
customMonths: 24
|
|
4074
|
+
},
|
|
4075
|
+
priority: "important",
|
|
4076
|
+
skillLevel: "professional",
|
|
4077
|
+
estimatedTimeMinutes: 120,
|
|
4078
|
+
whyImportant: "Ensures system operates at peak efficiency and maintains warranty"
|
|
4079
|
+
},
|
|
4080
|
+
{
|
|
4081
|
+
id: "home-battery-visual-inspection",
|
|
4082
|
+
name: "Inspect Home Battery System",
|
|
4083
|
+
description: "Check battery unit for damage, leaks, unusual sounds, and ensure ventilation is clear",
|
|
4084
|
+
category: "solar",
|
|
4085
|
+
applicableTo: "property",
|
|
4086
|
+
conditions: {
|
|
4087
|
+
hasHomeBattery: true
|
|
4088
|
+
},
|
|
4089
|
+
defaultFrequency: {
|
|
4090
|
+
type: "recurring",
|
|
4091
|
+
interval: "quarterly"
|
|
4092
|
+
},
|
|
4093
|
+
priority: "important",
|
|
4094
|
+
skillLevel: "diy_easy",
|
|
4095
|
+
estimatedTimeMinutes: 15,
|
|
4096
|
+
whyImportant: "Early detection of battery issues prevents safety hazards and costly damage"
|
|
4097
|
+
},
|
|
4098
|
+
{
|
|
4099
|
+
id: "home-battery-firmware-update",
|
|
4100
|
+
name: "Check Battery Firmware Updates",
|
|
4101
|
+
description: "Check manufacturer app for firmware updates and apply if available",
|
|
4102
|
+
category: "solar",
|
|
4103
|
+
applicableTo: "property",
|
|
4104
|
+
conditions: {
|
|
4105
|
+
hasHomeBattery: true
|
|
4106
|
+
},
|
|
4107
|
+
defaultFrequency: {
|
|
4108
|
+
type: "recurring",
|
|
4109
|
+
interval: "quarterly"
|
|
4110
|
+
},
|
|
4111
|
+
priority: "routine",
|
|
4112
|
+
skillLevel: "diy_easy",
|
|
4113
|
+
estimatedTimeMinutes: 15,
|
|
4114
|
+
whyImportant: "Updates improve performance, fix bugs, and enhance safety features"
|
|
4115
|
+
},
|
|
4116
|
+
{
|
|
4117
|
+
id: "home-battery-professional-inspection",
|
|
4118
|
+
name: "Professional Battery System Inspection",
|
|
4119
|
+
description: "Full inspection of battery connections, performance testing, and state of health analysis",
|
|
4120
|
+
category: "solar",
|
|
4121
|
+
applicableTo: "property",
|
|
4122
|
+
conditions: {
|
|
4123
|
+
hasHomeBattery: true
|
|
4124
|
+
},
|
|
4125
|
+
defaultFrequency: {
|
|
4126
|
+
type: "recurring",
|
|
4127
|
+
interval: "yearly"
|
|
4128
|
+
},
|
|
4129
|
+
priority: "important",
|
|
4130
|
+
skillLevel: "professional",
|
|
4131
|
+
estimatedTimeMinutes: 90,
|
|
4132
|
+
whyImportant: "Ensures safe operation and optimal battery health for longevity"
|
|
4133
|
+
},
|
|
4134
|
+
{
|
|
4135
|
+
id: "window-cleaning",
|
|
4136
|
+
name: "Clean Windows",
|
|
4137
|
+
description: "Clean interior and exterior windows for improved visibility and appearance",
|
|
4138
|
+
category: "interior",
|
|
4139
|
+
applicableTo: "property",
|
|
4140
|
+
conditions: {},
|
|
4141
|
+
defaultFrequency: {
|
|
4142
|
+
type: "recurring",
|
|
4143
|
+
interval: "biannually"
|
|
4144
|
+
},
|
|
4145
|
+
priority: "routine",
|
|
4146
|
+
skillLevel: "diy_moderate",
|
|
4147
|
+
estimatedTimeMinutes: 180,
|
|
4148
|
+
whyImportant: "Clean windows improve natural light and curb appeal"
|
|
4149
|
+
},
|
|
4150
|
+
{
|
|
4151
|
+
id: "carpet-cleaning",
|
|
4152
|
+
name: "Carpet Cleaning",
|
|
4153
|
+
description: "Deep clean carpets to remove dirt, stains, and allergens",
|
|
4154
|
+
category: "interior",
|
|
4155
|
+
applicableTo: "property",
|
|
4156
|
+
conditions: {
|
|
4157
|
+
hasCarpets: true
|
|
4158
|
+
},
|
|
4159
|
+
defaultFrequency: {
|
|
4160
|
+
type: "recurring",
|
|
4161
|
+
interval: "yearly"
|
|
4162
|
+
},
|
|
4163
|
+
priority: "routine",
|
|
4164
|
+
skillLevel: "professional",
|
|
4165
|
+
estimatedTimeMinutes: 240,
|
|
4166
|
+
whyImportant: "Extends carpet life, improves air quality, and removes allergens"
|
|
4167
|
+
}
|
|
4168
|
+
];
|
|
4169
|
+
|
|
4170
|
+
// ../list-data/src/data/vehicleMaintenanceTemplates.json
|
|
4171
|
+
var vehicleMaintenanceTemplates_default = [
|
|
4172
|
+
{
|
|
4173
|
+
id: "oil-change",
|
|
4174
|
+
name: "Oil Change",
|
|
4175
|
+
description: "Change engine oil and oil filter",
|
|
4176
|
+
category: "engine",
|
|
4177
|
+
applicableTo: "vehicle",
|
|
4178
|
+
conditions: {
|
|
4179
|
+
vehicleType: ["car", "rv"],
|
|
4180
|
+
engineType: ["gasoline", "diesel", "hybrid", "plugin_hybrid"]
|
|
4181
|
+
},
|
|
4182
|
+
defaultFrequency: {
|
|
4183
|
+
type: "recurring",
|
|
4184
|
+
interval: "quarterly"
|
|
4185
|
+
},
|
|
4186
|
+
priority: "critical",
|
|
4187
|
+
skillLevel: "diy_moderate",
|
|
4188
|
+
estimatedTimeMinutes: 45,
|
|
4189
|
+
whyImportant: "Regular oil changes prevent engine wear and extend vehicle life. Most critical maintenance task."
|
|
4190
|
+
},
|
|
4191
|
+
{
|
|
4192
|
+
id: "tire-rotation",
|
|
4193
|
+
name: "Tire Rotation",
|
|
4194
|
+
description: "Rotate tires to ensure even wear",
|
|
4195
|
+
category: "tires",
|
|
4196
|
+
applicableTo: "vehicle",
|
|
4197
|
+
conditions: {
|
|
4198
|
+
vehicleType: ["car", "rv"]
|
|
4199
|
+
},
|
|
4200
|
+
defaultFrequency: {
|
|
4201
|
+
type: "recurring",
|
|
4202
|
+
interval: "biannually"
|
|
4203
|
+
},
|
|
4204
|
+
priority: "important",
|
|
4205
|
+
skillLevel: "diy_moderate",
|
|
4206
|
+
estimatedTimeMinutes: 60,
|
|
4207
|
+
whyImportant: "Even tire wear extends tire life by up to 20% and improves safety and fuel efficiency."
|
|
4208
|
+
},
|
|
4209
|
+
{
|
|
4210
|
+
id: "brake-inspection",
|
|
4211
|
+
name: "Brake Inspection",
|
|
4212
|
+
description: "Inspect brake pads, rotors, and fluid",
|
|
4213
|
+
category: "brakes",
|
|
4214
|
+
applicableTo: "vehicle",
|
|
4215
|
+
conditions: {
|
|
4216
|
+
vehicleType: ["car", "motorcycle", "rv"]
|
|
4217
|
+
},
|
|
4218
|
+
defaultFrequency: {
|
|
4219
|
+
type: "recurring",
|
|
4220
|
+
interval: "biannually"
|
|
4221
|
+
},
|
|
4222
|
+
priority: "critical",
|
|
4223
|
+
skillLevel: "professional",
|
|
4224
|
+
estimatedTimeMinutes: 45,
|
|
4225
|
+
whyImportant: "Brake failure is life-threatening. Regular inspection prevents accidents and costly repairs."
|
|
4226
|
+
},
|
|
4227
|
+
{
|
|
4228
|
+
id: "air-filter-replacement",
|
|
4229
|
+
name: "Replace Engine Air Filter",
|
|
4230
|
+
description: "Replace engine air filter for optimal performance",
|
|
4231
|
+
category: "engine",
|
|
4232
|
+
applicableTo: "vehicle",
|
|
4233
|
+
conditions: {
|
|
4234
|
+
vehicleType: ["car", "rv"],
|
|
4235
|
+
engineType: ["gasoline", "diesel", "hybrid", "plugin_hybrid"]
|
|
4236
|
+
},
|
|
4237
|
+
defaultFrequency: {
|
|
4238
|
+
type: "recurring",
|
|
4239
|
+
interval: "yearly"
|
|
4240
|
+
},
|
|
4241
|
+
priority: "routine",
|
|
4242
|
+
skillLevel: "diy_easy",
|
|
4243
|
+
estimatedTimeMinutes: 15,
|
|
4244
|
+
whyImportant: "Clean air filter improves fuel economy by up to 10% and engine performance."
|
|
4245
|
+
},
|
|
4246
|
+
{
|
|
4247
|
+
id: "cabin-air-filter",
|
|
4248
|
+
name: "Replace Cabin Air Filter",
|
|
4249
|
+
description: "Replace cabin air filter for clean interior air",
|
|
4250
|
+
category: "interior",
|
|
4251
|
+
applicableTo: "vehicle",
|
|
4252
|
+
conditions: {
|
|
4253
|
+
vehicleType: ["car", "rv"]
|
|
4254
|
+
},
|
|
4255
|
+
defaultFrequency: {
|
|
4256
|
+
type: "recurring",
|
|
4257
|
+
interval: "yearly"
|
|
4258
|
+
},
|
|
4259
|
+
priority: "routine",
|
|
4260
|
+
skillLevel: "diy_easy",
|
|
4261
|
+
estimatedTimeMinutes: 10,
|
|
4262
|
+
whyImportant: "Filters out pollen, dust, and pollutants for healthier cabin air."
|
|
4263
|
+
},
|
|
4264
|
+
{
|
|
4265
|
+
id: "battery-check",
|
|
4266
|
+
name: "Battery Test",
|
|
4267
|
+
description: "Test battery voltage and terminals, clean corrosion",
|
|
4268
|
+
category: "electrical",
|
|
4269
|
+
applicableTo: "vehicle",
|
|
4270
|
+
conditions: {
|
|
4271
|
+
vehicleType: ["car", "rv"]
|
|
4272
|
+
},
|
|
4273
|
+
defaultFrequency: {
|
|
4274
|
+
type: "recurring",
|
|
4275
|
+
interval: "biannually"
|
|
4276
|
+
},
|
|
4277
|
+
priority: "important",
|
|
4278
|
+
skillLevel: "diy_easy",
|
|
4279
|
+
estimatedTimeMinutes: 20,
|
|
4280
|
+
whyImportant: "Prevents unexpected breakdowns. Most batteries last 3-5 years."
|
|
4281
|
+
},
|
|
4282
|
+
{
|
|
4283
|
+
id: "coolant-flush",
|
|
4284
|
+
name: "Coolant Flush",
|
|
4285
|
+
description: "Drain and replace engine coolant/antifreeze",
|
|
4286
|
+
category: "engine",
|
|
4287
|
+
applicableTo: "vehicle",
|
|
4288
|
+
conditions: {
|
|
4289
|
+
vehicleType: ["car", "rv"],
|
|
4290
|
+
engineType: ["gasoline", "diesel", "hybrid", "plugin_hybrid"]
|
|
4291
|
+
},
|
|
4292
|
+
defaultFrequency: {
|
|
4293
|
+
type: "recurring",
|
|
4294
|
+
interval: "biannually"
|
|
4295
|
+
},
|
|
4296
|
+
priority: "important",
|
|
4297
|
+
skillLevel: "diy_moderate",
|
|
4298
|
+
estimatedTimeMinutes: 90,
|
|
4299
|
+
whyImportant: "Prevents engine overheating and corrosion. Critical for engine longevity."
|
|
4300
|
+
},
|
|
4301
|
+
{
|
|
4302
|
+
id: "transmission-service",
|
|
4303
|
+
name: "Transmission Service",
|
|
4304
|
+
description: "Change transmission fluid and filter",
|
|
4305
|
+
category: "drivetrain",
|
|
4306
|
+
applicableTo: "vehicle",
|
|
4307
|
+
conditions: {
|
|
4308
|
+
vehicleType: ["car", "rv"],
|
|
4309
|
+
transmissionType: ["automatic", "cvt"]
|
|
4310
|
+
},
|
|
4311
|
+
defaultFrequency: {
|
|
4312
|
+
type: "recurring",
|
|
4313
|
+
interval: "biannually"
|
|
4314
|
+
},
|
|
4315
|
+
priority: "important",
|
|
4316
|
+
skillLevel: "professional",
|
|
4317
|
+
estimatedTimeMinutes: 120,
|
|
4318
|
+
whyImportant: "Extends transmission life. Transmission replacement can cost $3,000-$8,000."
|
|
4319
|
+
},
|
|
4320
|
+
{
|
|
4321
|
+
id: "spark-plugs",
|
|
4322
|
+
name: "Replace Spark Plugs",
|
|
4323
|
+
description: "Replace spark plugs for optimal ignition",
|
|
4324
|
+
category: "engine",
|
|
4325
|
+
applicableTo: "vehicle",
|
|
4326
|
+
conditions: {
|
|
4327
|
+
vehicleType: ["car", "rv"],
|
|
4328
|
+
engineType: ["gasoline", "hybrid", "plugin_hybrid"]
|
|
4329
|
+
},
|
|
4330
|
+
defaultFrequency: {
|
|
4331
|
+
type: "recurring",
|
|
4332
|
+
interval: "yearly"
|
|
4333
|
+
},
|
|
4334
|
+
priority: "routine",
|
|
4335
|
+
skillLevel: "diy_moderate",
|
|
4336
|
+
estimatedTimeMinutes: 60,
|
|
4337
|
+
whyImportant: "Improves fuel efficiency, reduces emissions, and ensures smooth engine operation."
|
|
4338
|
+
},
|
|
4339
|
+
{
|
|
4340
|
+
id: "wiper-blades",
|
|
4341
|
+
name: "Replace Wiper Blades",
|
|
4342
|
+
description: "Replace windshield wiper blades",
|
|
4343
|
+
category: "exterior",
|
|
4344
|
+
applicableTo: "vehicle",
|
|
4345
|
+
conditions: {
|
|
4346
|
+
vehicleType: ["car", "rv"]
|
|
4347
|
+
},
|
|
4348
|
+
defaultFrequency: {
|
|
4349
|
+
type: "recurring",
|
|
4350
|
+
interval: "yearly"
|
|
4351
|
+
},
|
|
4352
|
+
priority: "routine",
|
|
4353
|
+
skillLevel: "diy_easy",
|
|
4354
|
+
estimatedTimeMinutes: 10,
|
|
4355
|
+
whyImportant: "Essential for visibility and safety in rain and snow."
|
|
4356
|
+
},
|
|
4357
|
+
{
|
|
4358
|
+
id: "wheel-alignment",
|
|
4359
|
+
name: "Wheel Alignment",
|
|
4360
|
+
description: "Check and adjust wheel alignment",
|
|
4361
|
+
category: "tires",
|
|
4362
|
+
applicableTo: "vehicle",
|
|
4363
|
+
conditions: {
|
|
4364
|
+
vehicleType: ["car"]
|
|
4365
|
+
},
|
|
4366
|
+
defaultFrequency: {
|
|
4367
|
+
type: "recurring",
|
|
4368
|
+
interval: "yearly"
|
|
4369
|
+
},
|
|
4370
|
+
priority: "routine",
|
|
4371
|
+
skillLevel: "professional",
|
|
4372
|
+
estimatedTimeMinutes: 60,
|
|
4373
|
+
whyImportant: "Prevents uneven tire wear and improves handling and fuel economy."
|
|
4374
|
+
},
|
|
4375
|
+
{
|
|
4376
|
+
id: "timing-belt",
|
|
4377
|
+
name: "Timing Belt Replacement",
|
|
4378
|
+
description: "Replace timing belt (if applicable)",
|
|
4379
|
+
category: "engine",
|
|
4380
|
+
applicableTo: "vehicle",
|
|
4381
|
+
conditions: {
|
|
4382
|
+
vehicleType: ["car"],
|
|
4383
|
+
engineType: ["gasoline", "diesel"]
|
|
4384
|
+
},
|
|
4385
|
+
defaultFrequency: {
|
|
4386
|
+
type: "recurring",
|
|
4387
|
+
interval: "yearly"
|
|
4388
|
+
},
|
|
4389
|
+
priority: "critical",
|
|
4390
|
+
skillLevel: "professional",
|
|
4391
|
+
estimatedTimeMinutes: 240,
|
|
4392
|
+
whyImportant: "Timing belt failure can cause catastrophic engine damage. Check your owner's manual for mileage interval (typically 60k-100k miles)."
|
|
4393
|
+
},
|
|
4394
|
+
{
|
|
4395
|
+
id: "differential-service",
|
|
4396
|
+
name: "Differential Service",
|
|
4397
|
+
description: "Change differential fluid",
|
|
4398
|
+
category: "drivetrain",
|
|
4399
|
+
applicableTo: "vehicle",
|
|
4400
|
+
conditions: {
|
|
4401
|
+
vehicleType: ["car"],
|
|
4402
|
+
driveType: ["awd", "4wd", "rwd"]
|
|
4403
|
+
},
|
|
4404
|
+
defaultFrequency: {
|
|
4405
|
+
type: "recurring",
|
|
4406
|
+
interval: "biannually"
|
|
4407
|
+
},
|
|
4408
|
+
priority: "routine",
|
|
4409
|
+
skillLevel: "professional",
|
|
4410
|
+
estimatedTimeMinutes: 90,
|
|
4411
|
+
whyImportant: "Maintains drivetrain efficiency and prevents costly differential failure."
|
|
4412
|
+
},
|
|
4413
|
+
{
|
|
4414
|
+
id: "motorcycle-chain-maintenance",
|
|
4415
|
+
name: "Chain Cleaning & Lubrication",
|
|
4416
|
+
description: "Clean and lubricate motorcycle chain",
|
|
4417
|
+
category: "drivetrain",
|
|
4418
|
+
applicableTo: "vehicle",
|
|
4419
|
+
conditions: {
|
|
4420
|
+
vehicleType: ["motorcycle"]
|
|
4421
|
+
},
|
|
4422
|
+
defaultFrequency: {
|
|
4423
|
+
type: "recurring",
|
|
4424
|
+
interval: "monthly"
|
|
4425
|
+
},
|
|
4426
|
+
priority: "important",
|
|
4427
|
+
skillLevel: "diy_easy",
|
|
4428
|
+
estimatedTimeMinutes: 30,
|
|
4429
|
+
whyImportant: "Prevents chain wear and ensures safe operation. Chain failure can be dangerous."
|
|
4430
|
+
},
|
|
4431
|
+
{
|
|
4432
|
+
id: "bicycle-tune-up",
|
|
4433
|
+
name: "Bicycle Tune-Up",
|
|
4434
|
+
description: "Check brakes, gears, tire pressure, chain lubrication",
|
|
4435
|
+
category: "general",
|
|
4436
|
+
applicableTo: "vehicle",
|
|
4437
|
+
conditions: {
|
|
4438
|
+
vehicleType: ["bicycle"]
|
|
4439
|
+
},
|
|
4440
|
+
defaultFrequency: {
|
|
4441
|
+
type: "recurring",
|
|
4442
|
+
interval: "quarterly"
|
|
4443
|
+
},
|
|
4444
|
+
priority: "important",
|
|
4445
|
+
skillLevel: "diy_easy",
|
|
4446
|
+
estimatedTimeMinutes: 45,
|
|
4447
|
+
whyImportant: "Ensures safe and efficient riding. Prevents accidents and costly repairs."
|
|
4448
|
+
},
|
|
4449
|
+
{
|
|
4450
|
+
id: "ev-battery-check",
|
|
4451
|
+
name: "EV Battery Health Check",
|
|
4452
|
+
description: "Professional battery health diagnostic",
|
|
4453
|
+
category: "electrical",
|
|
4454
|
+
applicableTo: "vehicle",
|
|
4455
|
+
conditions: {
|
|
4456
|
+
vehicleType: ["car"],
|
|
4457
|
+
engineType: ["electric", "plugin_hybrid"]
|
|
4458
|
+
},
|
|
4459
|
+
defaultFrequency: {
|
|
4460
|
+
type: "recurring",
|
|
4461
|
+
interval: "yearly"
|
|
4462
|
+
},
|
|
4463
|
+
priority: "important",
|
|
4464
|
+
skillLevel: "professional",
|
|
4465
|
+
estimatedTimeMinutes: 45,
|
|
4466
|
+
whyImportant: "Monitors battery degradation and ensures warranty compliance."
|
|
4467
|
+
},
|
|
4468
|
+
{
|
|
4469
|
+
id: "winter-tire-swap",
|
|
4470
|
+
name: "Winter Tire Installation",
|
|
4471
|
+
description: "Swap to winter tires for cold weather",
|
|
4472
|
+
category: "tires",
|
|
4473
|
+
applicableTo: "vehicle",
|
|
4474
|
+
conditions: {
|
|
4475
|
+
vehicleType: ["car"],
|
|
4476
|
+
hasWinterTires: true
|
|
4477
|
+
},
|
|
4478
|
+
defaultFrequency: {
|
|
4479
|
+
type: "recurring",
|
|
4480
|
+
interval: "yearly"
|
|
4481
|
+
},
|
|
4482
|
+
priority: "important",
|
|
4483
|
+
skillLevel: "diy_moderate",
|
|
4484
|
+
estimatedTimeMinutes: 90,
|
|
4485
|
+
whyImportant: "Winter tires dramatically improve traction and safety in snow and ice."
|
|
4486
|
+
},
|
|
4487
|
+
{
|
|
4488
|
+
id: "boat-engine-oil",
|
|
4489
|
+
name: "Boat Engine Oil Change",
|
|
4490
|
+
description: "Change engine oil and filter for boat motor",
|
|
4491
|
+
category: "engine",
|
|
4492
|
+
applicableTo: "vehicle",
|
|
4493
|
+
conditions: {
|
|
4494
|
+
vehicleType: ["boat"]
|
|
4495
|
+
},
|
|
4496
|
+
defaultFrequency: {
|
|
4497
|
+
type: "recurring",
|
|
4498
|
+
interval: "yearly"
|
|
4499
|
+
},
|
|
4500
|
+
priority: "critical",
|
|
4501
|
+
skillLevel: "diy_moderate",
|
|
4502
|
+
estimatedTimeMinutes: 60,
|
|
4503
|
+
whyImportant: "Marine engines operate in harsh conditions. Regular oil changes prevent corrosion and extend engine life."
|
|
4504
|
+
},
|
|
4505
|
+
{
|
|
4506
|
+
id: "boat-hull-inspection",
|
|
4507
|
+
name: "Hull Inspection & Cleaning",
|
|
4508
|
+
description: "Inspect hull for damage, clean barnacles and growth",
|
|
4509
|
+
category: "exterior",
|
|
4510
|
+
applicableTo: "vehicle",
|
|
4511
|
+
conditions: {
|
|
4512
|
+
vehicleType: ["boat"]
|
|
4513
|
+
},
|
|
4514
|
+
defaultFrequency: {
|
|
4515
|
+
type: "recurring",
|
|
4516
|
+
interval: "biannually"
|
|
4517
|
+
},
|
|
4518
|
+
priority: "important",
|
|
4519
|
+
skillLevel: "professional",
|
|
4520
|
+
estimatedTimeMinutes: 180,
|
|
4521
|
+
whyImportant: "Hull growth reduces fuel efficiency and speed. Damage can lead to costly repairs or sinking."
|
|
4522
|
+
},
|
|
4523
|
+
{
|
|
4524
|
+
id: "boat-winterization",
|
|
4525
|
+
name: "Winterization",
|
|
4526
|
+
description: "Prepare boat for winter storage: drain water, stabilize fuel, protect engine",
|
|
4527
|
+
category: "general",
|
|
4528
|
+
applicableTo: "vehicle",
|
|
4529
|
+
conditions: {
|
|
4530
|
+
vehicleType: ["boat"]
|
|
4531
|
+
},
|
|
4532
|
+
defaultFrequency: {
|
|
4533
|
+
type: "recurring",
|
|
4534
|
+
interval: "yearly"
|
|
4535
|
+
},
|
|
4536
|
+
priority: "critical",
|
|
4537
|
+
skillLevel: "professional",
|
|
4538
|
+
estimatedTimeMinutes: 240,
|
|
4539
|
+
whyImportant: "Prevents freeze damage to engine and systems. Skipping winterization can cause thousands in repairs."
|
|
4540
|
+
},
|
|
4541
|
+
{
|
|
4542
|
+
id: "boat-battery-maintenance",
|
|
4543
|
+
name: "Battery Check & Charging",
|
|
4544
|
+
description: "Test battery, check terminals, maintain charge",
|
|
4545
|
+
category: "electrical",
|
|
4546
|
+
applicableTo: "vehicle",
|
|
4547
|
+
conditions: {
|
|
4548
|
+
vehicleType: ["boat"]
|
|
4549
|
+
},
|
|
4550
|
+
defaultFrequency: {
|
|
4551
|
+
type: "recurring",
|
|
4552
|
+
interval: "quarterly"
|
|
4553
|
+
},
|
|
4554
|
+
priority: "important",
|
|
4555
|
+
skillLevel: "diy_easy",
|
|
4556
|
+
estimatedTimeMinutes: 30,
|
|
4557
|
+
whyImportant: "Dead battery leaves you stranded on water. Marine batteries need regular charging and maintenance."
|
|
4558
|
+
},
|
|
4559
|
+
{
|
|
4560
|
+
id: "boat-safety-equipment",
|
|
4561
|
+
name: "Safety Equipment Inspection",
|
|
4562
|
+
description: "Check life jackets, fire extinguishers, flares, first aid kit",
|
|
4563
|
+
category: "safety",
|
|
4564
|
+
applicableTo: "vehicle",
|
|
4565
|
+
conditions: {
|
|
4566
|
+
vehicleType: ["boat"]
|
|
4567
|
+
},
|
|
4568
|
+
defaultFrequency: {
|
|
4569
|
+
type: "recurring",
|
|
4570
|
+
interval: "yearly"
|
|
4571
|
+
},
|
|
4572
|
+
priority: "critical",
|
|
4573
|
+
skillLevel: "diy_easy",
|
|
4574
|
+
estimatedTimeMinutes: 45,
|
|
4575
|
+
whyImportant: "Required by law and essential for safety. Expired flares and damaged life jackets can be life-threatening."
|
|
4576
|
+
},
|
|
4577
|
+
{
|
|
4578
|
+
id: "boat-bilge-pump",
|
|
4579
|
+
name: "Bilge Pump Test",
|
|
4580
|
+
description: "Test bilge pump operation and clean intake",
|
|
4581
|
+
category: "safety",
|
|
4582
|
+
applicableTo: "vehicle",
|
|
4583
|
+
conditions: {
|
|
4584
|
+
vehicleType: ["boat"]
|
|
4585
|
+
},
|
|
4586
|
+
defaultFrequency: {
|
|
4587
|
+
type: "recurring",
|
|
4588
|
+
interval: "quarterly"
|
|
4589
|
+
},
|
|
4590
|
+
priority: "critical",
|
|
4591
|
+
skillLevel: "diy_easy",
|
|
4592
|
+
estimatedTimeMinutes: 20,
|
|
4593
|
+
whyImportant: "Bilge pump failure can lead to sinking. Regular testing ensures it works when needed."
|
|
4594
|
+
},
|
|
4595
|
+
{
|
|
4596
|
+
id: "boat-propeller-inspection",
|
|
4597
|
+
name: "Propeller Inspection",
|
|
4598
|
+
description: "Check propeller for damage, dings, fishing line",
|
|
4599
|
+
category: "drivetrain",
|
|
4600
|
+
applicableTo: "vehicle",
|
|
4601
|
+
conditions: {
|
|
4602
|
+
vehicleType: ["boat"]
|
|
4603
|
+
},
|
|
4604
|
+
defaultFrequency: {
|
|
4605
|
+
type: "recurring",
|
|
4606
|
+
interval: "biannually"
|
|
4607
|
+
},
|
|
4608
|
+
priority: "important",
|
|
4609
|
+
skillLevel: "diy_easy",
|
|
4610
|
+
estimatedTimeMinutes: 30,
|
|
4611
|
+
whyImportant: "Damaged props reduce performance and fuel efficiency. Can damage transmission if left unchecked."
|
|
4612
|
+
},
|
|
4613
|
+
{
|
|
4614
|
+
id: "boat-fuel-system",
|
|
4615
|
+
name: "Fuel System Maintenance",
|
|
4616
|
+
description: "Add fuel stabilizer, check fuel lines and water separator",
|
|
4617
|
+
category: "engine",
|
|
4618
|
+
applicableTo: "vehicle",
|
|
4619
|
+
conditions: {
|
|
4620
|
+
vehicleType: ["boat"]
|
|
4621
|
+
},
|
|
4622
|
+
defaultFrequency: {
|
|
4623
|
+
type: "recurring",
|
|
4624
|
+
interval: "yearly"
|
|
4625
|
+
},
|
|
4626
|
+
priority: "important",
|
|
4627
|
+
skillLevel: "diy_moderate",
|
|
4628
|
+
estimatedTimeMinutes: 60,
|
|
4629
|
+
whyImportant: "Marine fuel degrades quickly. Prevents engine damage from bad fuel and water contamination."
|
|
4630
|
+
}
|
|
4631
|
+
];
|
|
4632
|
+
|
|
4633
|
+
// ../list-data/src/adapters/maintenanceAdapter.ts
|
|
4634
|
+
var ALL_MAINTENANCE_TEMPLATES = [
|
|
4635
|
+
...propertyMaintenanceTemplates_default,
|
|
4636
|
+
...vehicleMaintenanceTemplates_default
|
|
4637
|
+
];
|
|
4638
|
+
|
|
4639
|
+
// ../list-data/src/adapters/subscriptionAdapter.ts
|
|
4640
|
+
function getSubscriptionComputedFields(sub) {
|
|
4641
|
+
const computed = {
|
|
4642
|
+
displayName: sub.name || sub.provider || "Subscription"
|
|
4643
|
+
};
|
|
4644
|
+
if (sub.expiration_date) {
|
|
4645
|
+
computed.daysUntilExpiry = daysUntil(sub.expiration_date);
|
|
4646
|
+
computed.expiryStatus = getExpiryStatus(computed.daysUntilExpiry);
|
|
4647
|
+
}
|
|
4648
|
+
if (sub.category) {
|
|
4649
|
+
computed.formattedType = sub.category.charAt(0).toUpperCase() + sub.category.slice(1).replace(/_/g, " ");
|
|
4650
|
+
}
|
|
4651
|
+
return computed;
|
|
4652
|
+
}
|
|
4653
|
+
|
|
4654
|
+
// ../list-data/src/adapters/credentialAdapter.ts
|
|
4655
|
+
function getCredentialComputedFields(credential) {
|
|
4656
|
+
const computed = {
|
|
4657
|
+
displayName: credential.name || credential.credential_type || "Credential"
|
|
4658
|
+
};
|
|
4659
|
+
if (credential.expiration_date) {
|
|
4660
|
+
computed.daysUntilExpiry = daysUntil(credential.expiration_date);
|
|
4661
|
+
computed.expiryStatus = getExpiryStatus(computed.daysUntilExpiry);
|
|
4662
|
+
}
|
|
4663
|
+
if (credential.credential_type) {
|
|
4664
|
+
computed.formattedType = credential.credential_type.charAt(0).toUpperCase() + credential.credential_type.slice(1).replace(/_/g, " ");
|
|
4665
|
+
}
|
|
4666
|
+
return computed;
|
|
4667
|
+
}
|
|
4668
|
+
|
|
4669
|
+
// src/enrichment.ts
|
|
4670
|
+
function getComputedFields(entity, entityType) {
|
|
4671
|
+
switch (entityType) {
|
|
4672
|
+
case "pet":
|
|
4673
|
+
return getPetComputedFields(entity);
|
|
4674
|
+
case "vehicle":
|
|
4675
|
+
return getVehicleComputedFields(entity);
|
|
4676
|
+
case "insurance":
|
|
4677
|
+
return getInsuranceComputedFields(entity);
|
|
4678
|
+
case "subscription":
|
|
4679
|
+
return getSubscriptionComputedFields(entity);
|
|
4680
|
+
case "contact":
|
|
4681
|
+
return getContactComputedFields(entity);
|
|
4682
|
+
case "credential":
|
|
4683
|
+
return getCredentialComputedFields(entity);
|
|
4684
|
+
default:
|
|
4685
|
+
return {
|
|
4686
|
+
displayName: getEntityDisplayName(entityType, entity)
|
|
4687
|
+
};
|
|
4688
|
+
}
|
|
4689
|
+
}
|
|
4690
|
+
function enrichEntity(entity, entityType) {
|
|
4691
|
+
const computed = getComputedFields(entity, entityType);
|
|
4692
|
+
return { ...entity, _computed: computed };
|
|
4693
|
+
}
|
|
4694
|
+
function enrichEntities(entities, entityType) {
|
|
4695
|
+
return entities.map((entity) => enrichEntity(entity, entityType));
|
|
4696
|
+
}
|
|
4697
|
+
|
|
4698
|
+
// src/resources/entities.ts
|
|
4699
|
+
var ENTITY_TYPES = [
|
|
4700
|
+
"pet",
|
|
4701
|
+
"property",
|
|
4702
|
+
"vehicle",
|
|
4703
|
+
"contact",
|
|
4704
|
+
"insurance",
|
|
4705
|
+
"bank_account",
|
|
4706
|
+
"investment",
|
|
4707
|
+
"subscription",
|
|
4708
|
+
"maintenance_task",
|
|
4709
|
+
"password",
|
|
4710
|
+
"access_code",
|
|
4711
|
+
"document",
|
|
4712
|
+
"medical",
|
|
4713
|
+
"prescription",
|
|
4714
|
+
"credential",
|
|
4715
|
+
"utility"
|
|
4716
|
+
];
|
|
4717
|
+
async function getEntities(householdId, entityType, privateKey, privacyMode) {
|
|
4718
|
+
const entities = await getDecryptedEntities(householdId, entityType, privateKey);
|
|
4719
|
+
return enrichEntities(entities, entityType).map(
|
|
4720
|
+
(entity) => redactEntity(entity, entityType, privacyMode)
|
|
4721
|
+
);
|
|
4722
|
+
}
|
|
4723
|
+
async function getExpiringItems(days, householdId, privateKey) {
|
|
4724
|
+
const households = await getHouseholds();
|
|
4725
|
+
const searchHouseholds = householdId ? households.filter((h) => h.id === householdId) : households;
|
|
4726
|
+
const now = /* @__PURE__ */ new Date();
|
|
4727
|
+
const cutoff = new Date(now.getTime() + days * 24 * 60 * 60 * 1e3);
|
|
4728
|
+
const expiring = [];
|
|
4729
|
+
for (const household of searchHouseholds) {
|
|
4730
|
+
const insurance = await getDecryptedEntities(household.id, "insurance", privateKey);
|
|
4731
|
+
for (const policy of insurance) {
|
|
4732
|
+
if (policy.expiration_date) {
|
|
4733
|
+
const expires = new Date(policy.expiration_date);
|
|
4734
|
+
if (expires <= cutoff) {
|
|
4735
|
+
expiring.push({
|
|
4736
|
+
householdId: household.id,
|
|
4737
|
+
householdName: household.name,
|
|
4738
|
+
type: "insurance",
|
|
4739
|
+
name: policy.name || policy.provider || policy.policy_number,
|
|
4740
|
+
expiresAt: policy.expiration_date,
|
|
4741
|
+
daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
|
|
4742
|
+
});
|
|
4743
|
+
}
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4746
|
+
const vehicles = await getDecryptedEntities(household.id, "vehicle", privateKey);
|
|
4747
|
+
for (const vehicle of vehicles) {
|
|
4748
|
+
const vehicleName = `${vehicle.year || ""} ${vehicle.make || ""} ${vehicle.model || ""}`.trim() || "Vehicle";
|
|
4749
|
+
if (vehicle.registration_expiration) {
|
|
4750
|
+
const expires = new Date(vehicle.registration_expiration);
|
|
4751
|
+
if (expires <= cutoff) {
|
|
4752
|
+
expiring.push({
|
|
4753
|
+
householdId: household.id,
|
|
4754
|
+
householdName: household.name,
|
|
4755
|
+
type: "vehicle_registration",
|
|
4756
|
+
name: vehicleName,
|
|
4757
|
+
expiresAt: vehicle.registration_expiration,
|
|
4758
|
+
daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
|
|
4759
|
+
});
|
|
4760
|
+
}
|
|
4761
|
+
}
|
|
4762
|
+
if (vehicle.tabs_expiration) {
|
|
4763
|
+
const expires = new Date(vehicle.tabs_expiration);
|
|
4764
|
+
if (expires <= cutoff) {
|
|
4765
|
+
expiring.push({
|
|
4766
|
+
householdId: household.id,
|
|
4767
|
+
householdName: household.name,
|
|
4768
|
+
type: "vehicle_tabs",
|
|
4769
|
+
name: vehicleName,
|
|
4770
|
+
expiresAt: vehicle.tabs_expiration,
|
|
4771
|
+
daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
|
|
4772
|
+
});
|
|
4773
|
+
}
|
|
4774
|
+
}
|
|
4775
|
+
}
|
|
4776
|
+
const subscriptions = await getDecryptedEntities(household.id, "subscription", privateKey);
|
|
4777
|
+
for (const sub of subscriptions) {
|
|
4778
|
+
if (sub.expiration_date) {
|
|
4779
|
+
const expires = new Date(sub.expiration_date);
|
|
4780
|
+
if (expires <= cutoff) {
|
|
4781
|
+
expiring.push({
|
|
4782
|
+
householdId: household.id,
|
|
4783
|
+
householdName: household.name,
|
|
4784
|
+
type: "subscription",
|
|
4785
|
+
name: sub.name || sub.custom_name || sub.provider || sub.service_name,
|
|
4786
|
+
expiresAt: sub.expiration_date,
|
|
4787
|
+
daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
|
|
4788
|
+
});
|
|
4789
|
+
}
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4792
|
+
const credentials = await getDecryptedEntities(household.id, "credential", privateKey);
|
|
4793
|
+
for (const cred of credentials) {
|
|
4794
|
+
if (cred.expiration_date) {
|
|
4795
|
+
const expires = new Date(cred.expiration_date);
|
|
4796
|
+
if (expires <= cutoff) {
|
|
4797
|
+
expiring.push({
|
|
4798
|
+
householdId: household.id,
|
|
4799
|
+
householdName: household.name,
|
|
4800
|
+
type: "credential",
|
|
4801
|
+
name: cred.name || cred.credential_type,
|
|
4802
|
+
expiresAt: cred.expiration_date,
|
|
4803
|
+
daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
|
|
4804
|
+
});
|
|
4805
|
+
}
|
|
4806
|
+
}
|
|
4807
|
+
}
|
|
4808
|
+
}
|
|
4809
|
+
expiring.sort((a, b) => a.daysUntil - b.daysUntil);
|
|
4810
|
+
return expiring;
|
|
4811
|
+
}
|
|
4812
|
+
|
|
4813
|
+
// src/tools/search.ts
|
|
4814
|
+
async function buildEntitiesData(householdId, privateKey) {
|
|
4815
|
+
const entityTypeMapping = {
|
|
4816
|
+
property: "properties",
|
|
4817
|
+
vehicle: "vehicles",
|
|
4818
|
+
pet: "pets",
|
|
4819
|
+
contact: "contacts",
|
|
4820
|
+
subscription: "subscriptions",
|
|
4821
|
+
service: "services",
|
|
4822
|
+
insurance: "insurancePolicies",
|
|
4823
|
+
valuable: "valuables",
|
|
4824
|
+
bank_account: "financialAccounts",
|
|
4825
|
+
investment: "financialAccounts",
|
|
4826
|
+
credential: "credentials",
|
|
4827
|
+
maintenance_task: "maintenanceTasks",
|
|
4828
|
+
access_code: "accessCodes",
|
|
4829
|
+
device: "devices",
|
|
4830
|
+
person: "people",
|
|
4831
|
+
pet_vet_visit: "petVetVisits",
|
|
4832
|
+
vehicle_service: "vehicleServices",
|
|
4833
|
+
legal: "legalDocuments",
|
|
4834
|
+
health_record: "healthRecords",
|
|
4835
|
+
education_record: "educationRecords",
|
|
4836
|
+
military_record: "militaryRecords",
|
|
4837
|
+
membership_record: "membershipRecords",
|
|
4838
|
+
home_improvement: "homeImprovements",
|
|
4839
|
+
tax_year: "taxYears"
|
|
4840
|
+
};
|
|
4841
|
+
const data = {};
|
|
4842
|
+
const entityTypes = Object.keys(entityTypeMapping);
|
|
4843
|
+
for (const type of entityTypes) {
|
|
4844
|
+
try {
|
|
4845
|
+
const entities = await getDecryptedEntities(householdId, type, privateKey);
|
|
4846
|
+
if (entities.length > 0) {
|
|
4847
|
+
const propName = entityTypeMapping[type];
|
|
4848
|
+
const existing = data[propName] || [];
|
|
4849
|
+
data[propName] = [...existing, ...entities];
|
|
4850
|
+
}
|
|
4851
|
+
} catch {
|
|
4852
|
+
}
|
|
4853
|
+
}
|
|
4854
|
+
return data;
|
|
4855
|
+
}
|
|
4856
|
+
async function executeSearch(params, privateKey, privacyMode) {
|
|
4857
|
+
const { query, householdId, entityType } = params;
|
|
4858
|
+
const households = await getHouseholds();
|
|
4859
|
+
const searchHouseholds = householdId ? households.filter((h) => h.id === householdId) : households;
|
|
4860
|
+
const results = [];
|
|
4861
|
+
for (const household of searchHouseholds) {
|
|
4862
|
+
const entitiesData = await buildEntitiesData(household.id, privateKey);
|
|
4863
|
+
const searchableEntities = indexAllEntities(entitiesData);
|
|
4864
|
+
const searchResults = searchEntities(searchableEntities, query, {
|
|
4865
|
+
maxTotal: 50,
|
|
4866
|
+
includeTypes: entityType ? [entityType] : void 0
|
|
4867
|
+
});
|
|
4868
|
+
for (const result of searchResults) {
|
|
4869
|
+
const type = result.entity.entityType;
|
|
4870
|
+
const entityId = result.entity.id;
|
|
4871
|
+
const entities = await getDecryptedEntities(household.id, type, privateKey);
|
|
4872
|
+
const fullEntity = entities.find((e) => e.id === entityId);
|
|
4873
|
+
if (fullEntity) {
|
|
4874
|
+
const enriched = enrichEntity(fullEntity, type);
|
|
4875
|
+
const redacted = redactEntity(enriched, type, privacyMode);
|
|
4876
|
+
results.push({
|
|
4877
|
+
householdId: household.id,
|
|
4878
|
+
householdName: household.name,
|
|
4879
|
+
entityType: type,
|
|
4880
|
+
entity: redacted,
|
|
4881
|
+
score: result.score,
|
|
4882
|
+
matchedFields: result.matchedFields
|
|
4883
|
+
});
|
|
4884
|
+
}
|
|
4885
|
+
}
|
|
4886
|
+
}
|
|
4887
|
+
results.sort((a, b) => b.score - a.score);
|
|
4888
|
+
return results;
|
|
4889
|
+
}
|
|
4890
|
+
|
|
4891
|
+
// src/tools/files.ts
|
|
4892
|
+
async function executeFileDownload(client, params) {
|
|
4893
|
+
const { fileId, householdId, entityId, entityType } = params;
|
|
4894
|
+
try {
|
|
4895
|
+
const result = await downloadAndDecryptFile(
|
|
4896
|
+
client,
|
|
4897
|
+
householdId,
|
|
4898
|
+
fileId,
|
|
4899
|
+
entityId,
|
|
4900
|
+
entityType
|
|
4901
|
+
);
|
|
4902
|
+
return {
|
|
4903
|
+
success: true,
|
|
4904
|
+
fileName: result.fileName,
|
|
4905
|
+
mimeType: result.mimeType,
|
|
4906
|
+
size: result.bytes.length,
|
|
4907
|
+
data: result.dataBase64
|
|
4908
|
+
};
|
|
4909
|
+
} catch (err) {
|
|
4910
|
+
return {
|
|
4911
|
+
success: false,
|
|
4912
|
+
error: err.message
|
|
4913
|
+
};
|
|
4914
|
+
}
|
|
4915
|
+
}
|
|
4916
|
+
|
|
4917
|
+
// src/server.ts
|
|
4918
|
+
async function startServer(mode) {
|
|
4919
|
+
const config = loadConfig();
|
|
4920
|
+
const privacyMode = mode || config.defaultMode;
|
|
4921
|
+
console.error(`[MCP] Starting server in ${privacyMode} mode`);
|
|
4922
|
+
const client = await getAuthenticatedClient();
|
|
4923
|
+
if (!client) {
|
|
4924
|
+
console.error("[MCP] Not logged in. Run: estatehelm login");
|
|
4925
|
+
process.exit(1);
|
|
4926
|
+
}
|
|
4927
|
+
const privateKey = await getPrivateKey();
|
|
4928
|
+
if (!privateKey) {
|
|
4929
|
+
console.error("[MCP] Failed to load encryption keys. Run: estatehelm login");
|
|
4930
|
+
process.exit(1);
|
|
4931
|
+
}
|
|
4932
|
+
await initCache();
|
|
4933
|
+
console.error("[MCP] Checking for updates...");
|
|
4934
|
+
const synced = await syncIfNeeded(client, privateKey);
|
|
4935
|
+
if (synced) {
|
|
4936
|
+
console.error("[MCP] Cache updated");
|
|
4937
|
+
} else {
|
|
4938
|
+
console.error("[MCP] Cache is up to date");
|
|
4939
|
+
}
|
|
4940
|
+
const server = new import_server.Server(
|
|
4941
|
+
{
|
|
4942
|
+
name: "estatehelm",
|
|
4943
|
+
version: "1.0.0"
|
|
4944
|
+
},
|
|
4945
|
+
{
|
|
4946
|
+
capabilities: {
|
|
4947
|
+
resources: {},
|
|
4948
|
+
tools: {},
|
|
4949
|
+
prompts: {}
|
|
4950
|
+
}
|
|
4951
|
+
}
|
|
4952
|
+
);
|
|
4953
|
+
server.setRequestHandler(import_types42.ListResourcesRequestSchema, async () => {
|
|
4954
|
+
const households = await listHouseholds();
|
|
4955
|
+
const resources = [
|
|
4956
|
+
{
|
|
4957
|
+
uri: "estatehelm://households",
|
|
4958
|
+
name: "All Households",
|
|
4959
|
+
description: "List of all households you have access to",
|
|
4960
|
+
mimeType: "application/json"
|
|
4961
|
+
}
|
|
4962
|
+
];
|
|
4963
|
+
for (const household of households) {
|
|
4964
|
+
resources.push({
|
|
4965
|
+
uri: `estatehelm://households/${household.id}`,
|
|
4966
|
+
name: household.name,
|
|
4967
|
+
description: `Household: ${household.name}`,
|
|
4968
|
+
mimeType: "application/json"
|
|
4969
|
+
});
|
|
4970
|
+
for (const type of ENTITY_TYPES) {
|
|
4971
|
+
resources.push({
|
|
4972
|
+
uri: `estatehelm://households/${household.id}/${type}`,
|
|
4973
|
+
name: `${household.name} - ${formatEntityType(type)}`,
|
|
4974
|
+
description: `${formatEntityType(type)} in ${household.name}`,
|
|
4975
|
+
mimeType: "application/json"
|
|
4976
|
+
});
|
|
4977
|
+
}
|
|
4978
|
+
}
|
|
4979
|
+
return { resources };
|
|
4980
|
+
});
|
|
4981
|
+
server.setRequestHandler(import_types42.ReadResourceRequestSchema, async (request) => {
|
|
4982
|
+
const uri = request.params.uri;
|
|
4983
|
+
const parsed = parseResourceUri(uri);
|
|
4984
|
+
if (!parsed) {
|
|
4985
|
+
throw new Error(`Invalid resource URI: ${uri}`);
|
|
4986
|
+
}
|
|
4987
|
+
let content;
|
|
4988
|
+
if (parsed.type === "households" && !parsed.householdId) {
|
|
4989
|
+
content = await listHouseholds();
|
|
4990
|
+
} else if (parsed.type === "households" && parsed.householdId && !parsed.entityType) {
|
|
4991
|
+
content = await getHousehold(parsed.householdId);
|
|
4992
|
+
if (!content) {
|
|
4993
|
+
throw new Error(`Household not found: ${parsed.householdId}`);
|
|
4994
|
+
}
|
|
4995
|
+
} else if (parsed.householdId && parsed.entityType) {
|
|
4996
|
+
content = await getEntities(parsed.householdId, parsed.entityType, privateKey, privacyMode);
|
|
4997
|
+
} else {
|
|
4998
|
+
throw new Error(`Unsupported resource: ${uri}`);
|
|
4999
|
+
}
|
|
5000
|
+
return {
|
|
5001
|
+
contents: [
|
|
5002
|
+
{
|
|
5003
|
+
uri,
|
|
5004
|
+
mimeType: "application/json",
|
|
5005
|
+
text: JSON.stringify(content, null, 2)
|
|
5006
|
+
}
|
|
5007
|
+
]
|
|
5008
|
+
};
|
|
5009
|
+
});
|
|
5010
|
+
server.setRequestHandler(import_types42.ListToolsRequestSchema, async () => {
|
|
5011
|
+
return {
|
|
5012
|
+
tools: [
|
|
5013
|
+
{
|
|
5014
|
+
name: "search_entities",
|
|
5015
|
+
description: "Search across all entities in EstateHelm. Returns results with computed fields (age, days until expiry, etc.)",
|
|
5016
|
+
inputSchema: {
|
|
5017
|
+
type: "object",
|
|
5018
|
+
properties: {
|
|
5019
|
+
query: {
|
|
5020
|
+
type: "string",
|
|
5021
|
+
description: "Search query"
|
|
5022
|
+
},
|
|
5023
|
+
householdId: {
|
|
5024
|
+
type: "string",
|
|
5025
|
+
description: "Optional: Limit search to a specific household"
|
|
5026
|
+
},
|
|
5027
|
+
entityType: {
|
|
5028
|
+
type: "string",
|
|
5029
|
+
description: "Optional: Limit search to a specific entity type"
|
|
5030
|
+
}
|
|
5031
|
+
},
|
|
5032
|
+
required: ["query"]
|
|
5033
|
+
}
|
|
5034
|
+
},
|
|
5035
|
+
{
|
|
5036
|
+
name: "get_household_summary",
|
|
5037
|
+
description: "Get a summary of a household including counts and key dates",
|
|
5038
|
+
inputSchema: {
|
|
5039
|
+
type: "object",
|
|
5040
|
+
properties: {
|
|
5041
|
+
householdId: {
|
|
5042
|
+
type: "string",
|
|
5043
|
+
description: "The household ID"
|
|
5044
|
+
}
|
|
5045
|
+
},
|
|
5046
|
+
required: ["householdId"]
|
|
5047
|
+
}
|
|
5048
|
+
},
|
|
5049
|
+
{
|
|
5050
|
+
name: "get_expiring_items",
|
|
5051
|
+
description: "Get items expiring within a given number of days (insurance, vehicle registration, credentials, subscriptions)",
|
|
5052
|
+
inputSchema: {
|
|
5053
|
+
type: "object",
|
|
5054
|
+
properties: {
|
|
5055
|
+
days: {
|
|
5056
|
+
type: "number",
|
|
5057
|
+
description: "Number of days to look ahead (default: 30)"
|
|
5058
|
+
},
|
|
5059
|
+
householdId: {
|
|
5060
|
+
type: "string",
|
|
5061
|
+
description: "Optional: Limit to a specific household"
|
|
5062
|
+
}
|
|
5063
|
+
}
|
|
5064
|
+
}
|
|
5065
|
+
},
|
|
5066
|
+
{
|
|
5067
|
+
name: "get_file",
|
|
5068
|
+
description: "Download and decrypt a file attachment. Returns base64-encoded file data that can be saved to disk.",
|
|
5069
|
+
inputSchema: {
|
|
2669
5070
|
type: "object",
|
|
2670
5071
|
properties: {
|
|
2671
5072
|
fileId: {
|
|
@@ -2687,231 +5088,68 @@ async function startServer(mode) {
|
|
|
2687
5088
|
},
|
|
2688
5089
|
required: ["fileId", "householdId", "entityId", "entityType"]
|
|
2689
5090
|
}
|
|
5091
|
+
},
|
|
5092
|
+
{
|
|
5093
|
+
name: "refresh",
|
|
5094
|
+
description: "Force refresh of cached data from the server",
|
|
5095
|
+
inputSchema: {
|
|
5096
|
+
type: "object",
|
|
5097
|
+
properties: {}
|
|
5098
|
+
}
|
|
2690
5099
|
}
|
|
2691
5100
|
]
|
|
2692
5101
|
};
|
|
2693
5102
|
});
|
|
2694
|
-
server.setRequestHandler(
|
|
5103
|
+
server.setRequestHandler(import_types42.CallToolRequestSchema, async (request) => {
|
|
2695
5104
|
const { name, arguments: args } = request.params;
|
|
2696
5105
|
switch (name) {
|
|
2697
5106
|
case "search_entities": {
|
|
2698
|
-
const
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
const entityTypes = entityType ? [entityType] : [
|
|
2704
|
-
"pet",
|
|
2705
|
-
"property",
|
|
2706
|
-
"vehicle",
|
|
2707
|
-
"contact",
|
|
2708
|
-
"insurance",
|
|
2709
|
-
"bank_account",
|
|
2710
|
-
"investment",
|
|
2711
|
-
"subscription",
|
|
2712
|
-
"maintenance_task",
|
|
2713
|
-
"password",
|
|
2714
|
-
"access_code"
|
|
2715
|
-
];
|
|
2716
|
-
for (const type of entityTypes) {
|
|
2717
|
-
const entities = await getDecryptedEntities(household.id, type, privateKey);
|
|
2718
|
-
const matches = entities.filter((e) => searchEntity(e, query));
|
|
2719
|
-
for (const match of matches) {
|
|
2720
|
-
results.push({
|
|
2721
|
-
householdId: household.id,
|
|
2722
|
-
householdName: household.name,
|
|
2723
|
-
entityType: type,
|
|
2724
|
-
entity: redactEntity(match, type, privacyMode)
|
|
2725
|
-
});
|
|
2726
|
-
}
|
|
2727
|
-
}
|
|
2728
|
-
}
|
|
5107
|
+
const results = await executeSearch(
|
|
5108
|
+
args,
|
|
5109
|
+
privateKey,
|
|
5110
|
+
privacyMode
|
|
5111
|
+
);
|
|
2729
5112
|
return {
|
|
2730
|
-
content: [
|
|
2731
|
-
{
|
|
2732
|
-
type: "text",
|
|
2733
|
-
text: JSON.stringify(results, null, 2)
|
|
2734
|
-
}
|
|
2735
|
-
]
|
|
5113
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
|
|
2736
5114
|
};
|
|
2737
5115
|
}
|
|
2738
5116
|
case "get_household_summary": {
|
|
2739
5117
|
const { householdId } = args;
|
|
2740
|
-
const
|
|
2741
|
-
|
|
2742
|
-
if (!household) {
|
|
5118
|
+
const summary = await getHouseholdSummary(householdId, privateKey, getDecryptedEntities);
|
|
5119
|
+
if (!summary) {
|
|
2743
5120
|
throw new Error(`Household not found: ${householdId}`);
|
|
2744
5121
|
}
|
|
2745
|
-
const entityTypes = [
|
|
2746
|
-
"pet",
|
|
2747
|
-
"property",
|
|
2748
|
-
"vehicle",
|
|
2749
|
-
"contact",
|
|
2750
|
-
"insurance",
|
|
2751
|
-
"bank_account",
|
|
2752
|
-
"investment",
|
|
2753
|
-
"subscription",
|
|
2754
|
-
"maintenance_task",
|
|
2755
|
-
"password",
|
|
2756
|
-
"access_code"
|
|
2757
|
-
];
|
|
2758
|
-
const counts = {};
|
|
2759
|
-
for (const type of entityTypes) {
|
|
2760
|
-
const entities = await getDecryptedEntities(householdId, type, privateKey);
|
|
2761
|
-
counts[type] = entities.length;
|
|
2762
|
-
}
|
|
2763
|
-
const summary = {
|
|
2764
|
-
household: {
|
|
2765
|
-
id: household.id,
|
|
2766
|
-
name: household.name
|
|
2767
|
-
},
|
|
2768
|
-
counts,
|
|
2769
|
-
totalEntities: Object.values(counts).reduce((a, b) => a + b, 0)
|
|
2770
|
-
};
|
|
2771
5122
|
return {
|
|
2772
|
-
content: [
|
|
2773
|
-
{
|
|
2774
|
-
type: "text",
|
|
2775
|
-
text: JSON.stringify(summary, null, 2)
|
|
2776
|
-
}
|
|
2777
|
-
]
|
|
5123
|
+
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
2778
5124
|
};
|
|
2779
5125
|
}
|
|
2780
5126
|
case "get_expiring_items": {
|
|
2781
5127
|
const { days = 30, householdId } = args;
|
|
2782
|
-
const
|
|
2783
|
-
const searchHouseholds = householdId ? households.filter((h) => h.id === householdId) : households;
|
|
2784
|
-
const now = /* @__PURE__ */ new Date();
|
|
2785
|
-
const cutoff = new Date(now.getTime() + days * 24 * 60 * 60 * 1e3);
|
|
2786
|
-
const expiring = [];
|
|
2787
|
-
for (const household of searchHouseholds) {
|
|
2788
|
-
const insurance = await getDecryptedEntities(household.id, "insurance", privateKey);
|
|
2789
|
-
for (const policy of insurance) {
|
|
2790
|
-
if (policy.expiration_date) {
|
|
2791
|
-
const expires = new Date(policy.expiration_date);
|
|
2792
|
-
if (expires <= cutoff) {
|
|
2793
|
-
expiring.push({
|
|
2794
|
-
householdId: household.id,
|
|
2795
|
-
householdName: household.name,
|
|
2796
|
-
type: "insurance",
|
|
2797
|
-
name: policy.name || policy.policy_number,
|
|
2798
|
-
expiresAt: policy.expiration_date,
|
|
2799
|
-
daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
|
|
2800
|
-
});
|
|
2801
|
-
}
|
|
2802
|
-
}
|
|
2803
|
-
}
|
|
2804
|
-
const vehicles = await getDecryptedEntities(household.id, "vehicle", privateKey);
|
|
2805
|
-
for (const vehicle of vehicles) {
|
|
2806
|
-
const vehicleName = `${vehicle.year || ""} ${vehicle.make || ""} ${vehicle.model || ""}`.trim();
|
|
2807
|
-
if (vehicle.registration_expiration) {
|
|
2808
|
-
const expires = new Date(vehicle.registration_expiration);
|
|
2809
|
-
if (expires <= cutoff) {
|
|
2810
|
-
expiring.push({
|
|
2811
|
-
householdId: household.id,
|
|
2812
|
-
householdName: household.name,
|
|
2813
|
-
type: "vehicle_registration",
|
|
2814
|
-
name: vehicleName,
|
|
2815
|
-
expiresAt: vehicle.registration_expiration,
|
|
2816
|
-
daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
|
|
2817
|
-
});
|
|
2818
|
-
}
|
|
2819
|
-
}
|
|
2820
|
-
if (vehicle.tabs_expiration) {
|
|
2821
|
-
const expires = new Date(vehicle.tabs_expiration);
|
|
2822
|
-
if (expires <= cutoff) {
|
|
2823
|
-
expiring.push({
|
|
2824
|
-
householdId: household.id,
|
|
2825
|
-
householdName: household.name,
|
|
2826
|
-
type: "vehicle_tabs",
|
|
2827
|
-
name: vehicleName,
|
|
2828
|
-
expiresAt: vehicle.tabs_expiration,
|
|
2829
|
-
daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
|
|
2830
|
-
});
|
|
2831
|
-
}
|
|
2832
|
-
}
|
|
2833
|
-
}
|
|
2834
|
-
const subscriptions = await getDecryptedEntities(household.id, "subscription", privateKey);
|
|
2835
|
-
for (const sub of subscriptions) {
|
|
2836
|
-
if (sub.expiration_date) {
|
|
2837
|
-
const expires = new Date(sub.expiration_date);
|
|
2838
|
-
if (expires <= cutoff) {
|
|
2839
|
-
expiring.push({
|
|
2840
|
-
householdId: household.id,
|
|
2841
|
-
householdName: household.name,
|
|
2842
|
-
type: "subscription",
|
|
2843
|
-
name: sub.name || sub.service_name,
|
|
2844
|
-
expiresAt: sub.expiration_date,
|
|
2845
|
-
daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
|
|
2846
|
-
});
|
|
2847
|
-
}
|
|
2848
|
-
}
|
|
2849
|
-
}
|
|
2850
|
-
}
|
|
2851
|
-
expiring.sort((a, b) => a.daysUntil - b.daysUntil);
|
|
5128
|
+
const expiring = await getExpiringItems(days, householdId, privateKey);
|
|
2852
5129
|
return {
|
|
2853
|
-
content: [
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
5130
|
+
content: [{ type: "text", text: JSON.stringify(expiring, null, 2) }]
|
|
5131
|
+
};
|
|
5132
|
+
}
|
|
5133
|
+
case "get_file": {
|
|
5134
|
+
const result = await executeFileDownload(client, args);
|
|
5135
|
+
return {
|
|
5136
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
2859
5137
|
};
|
|
2860
5138
|
}
|
|
2861
5139
|
case "refresh": {
|
|
2862
5140
|
const synced2 = await syncIfNeeded(client, privateKey, true);
|
|
2863
5141
|
return {
|
|
2864
|
-
content: [
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
}
|
|
2869
|
-
]
|
|
5142
|
+
content: [{
|
|
5143
|
+
type: "text",
|
|
5144
|
+
text: synced2 ? "Cache refreshed with latest data" : "Cache was already up to date"
|
|
5145
|
+
}]
|
|
2870
5146
|
};
|
|
2871
5147
|
}
|
|
2872
|
-
case "get_file": {
|
|
2873
|
-
const { fileId, householdId, entityId, entityType } = args;
|
|
2874
|
-
try {
|
|
2875
|
-
const result = await downloadAndDecryptFile(
|
|
2876
|
-
client,
|
|
2877
|
-
householdId,
|
|
2878
|
-
fileId,
|
|
2879
|
-
entityId,
|
|
2880
|
-
entityType
|
|
2881
|
-
);
|
|
2882
|
-
return {
|
|
2883
|
-
content: [
|
|
2884
|
-
{
|
|
2885
|
-
type: "text",
|
|
2886
|
-
text: JSON.stringify({
|
|
2887
|
-
success: true,
|
|
2888
|
-
fileName: result.fileName,
|
|
2889
|
-
mimeType: result.mimeType,
|
|
2890
|
-
size: result.data.length,
|
|
2891
|
-
data: result.dataBase64
|
|
2892
|
-
}, null, 2)
|
|
2893
|
-
}
|
|
2894
|
-
]
|
|
2895
|
-
};
|
|
2896
|
-
} catch (err) {
|
|
2897
|
-
return {
|
|
2898
|
-
content: [
|
|
2899
|
-
{
|
|
2900
|
-
type: "text",
|
|
2901
|
-
text: JSON.stringify({
|
|
2902
|
-
success: false,
|
|
2903
|
-
error: err.message
|
|
2904
|
-
}, null, 2)
|
|
2905
|
-
}
|
|
2906
|
-
]
|
|
2907
|
-
};
|
|
2908
|
-
}
|
|
2909
|
-
}
|
|
2910
5148
|
default:
|
|
2911
5149
|
throw new Error(`Unknown tool: ${name}`);
|
|
2912
5150
|
}
|
|
2913
5151
|
});
|
|
2914
|
-
server.setRequestHandler(
|
|
5152
|
+
server.setRequestHandler(import_types42.ListPromptsRequestSchema, async () => {
|
|
2915
5153
|
return {
|
|
2916
5154
|
prompts: [
|
|
2917
5155
|
{
|
|
@@ -2944,12 +5182,12 @@ async function startServer(mode) {
|
|
|
2944
5182
|
]
|
|
2945
5183
|
};
|
|
2946
5184
|
});
|
|
2947
|
-
server.setRequestHandler(
|
|
5185
|
+
server.setRequestHandler(import_types42.GetPromptRequestSchema, async (request) => {
|
|
2948
5186
|
const { name, arguments: args } = request.params;
|
|
2949
5187
|
switch (name) {
|
|
2950
5188
|
case "household_summary": {
|
|
2951
5189
|
const householdId = args?.householdId;
|
|
2952
|
-
const households = await
|
|
5190
|
+
const households = await listHouseholds();
|
|
2953
5191
|
const household = householdId ? households.find((h) => h.id === householdId) : households[0];
|
|
2954
5192
|
if (!household) {
|
|
2955
5193
|
throw new Error("No household found");
|
|
@@ -2974,7 +5212,7 @@ async function startServer(mode) {
|
|
|
2974
5212
|
role: "user",
|
|
2975
5213
|
content: {
|
|
2976
5214
|
type: "text",
|
|
2977
|
-
text: `What items are expiring in the next ${days} days? Include insurance policies, vehicle registrations, subscriptions, and any other items with expiration dates.`
|
|
5215
|
+
text: `What items are expiring in the next ${days} days? Include insurance policies, vehicle registrations, credentials (passports, licenses), subscriptions, and any other items with expiration dates.`
|
|
2978
5216
|
}
|
|
2979
5217
|
}
|
|
2980
5218
|
]
|
|
@@ -3010,27 +5248,6 @@ function parseResourceUri(uri) {
|
|
|
3010
5248
|
function formatEntityType(type) {
|
|
3011
5249
|
return type.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
3012
5250
|
}
|
|
3013
|
-
function searchEntity(entity, query) {
|
|
3014
|
-
const lowerQuery = query.toLowerCase();
|
|
3015
|
-
const searchFields = [
|
|
3016
|
-
"name",
|
|
3017
|
-
"title",
|
|
3018
|
-
"description",
|
|
3019
|
-
"notes",
|
|
3020
|
-
"make",
|
|
3021
|
-
"model",
|
|
3022
|
-
"policyNumber",
|
|
3023
|
-
"serviceName",
|
|
3024
|
-
"username",
|
|
3025
|
-
"email"
|
|
3026
|
-
];
|
|
3027
|
-
for (const field of searchFields) {
|
|
3028
|
-
if (entity[field] && String(entity[field]).toLowerCase().includes(lowerQuery)) {
|
|
3029
|
-
return true;
|
|
3030
|
-
}
|
|
3031
|
-
}
|
|
3032
|
-
return false;
|
|
3033
|
-
}
|
|
3034
5251
|
|
|
3035
5252
|
// src/index.ts
|
|
3036
5253
|
var program = new import_commander.Command();
|