Have something to say?

Tell us how we could make the product more useful to you.

Planned

Bug: New construction addresses blocked as out-of-service in online scheduler due to Mapbox geocoding failure

Objective Fix the online scheduler so new construction addresses that Mapbox cannot confidently geocode don't result in a false "Out of Service Area" block and a quote stuck in Blocked status. Give the service area check a reliable coordinate to work against, or surface a meaningful fallback path when geocoding fails rather than silently blocking the booking. Background Catherine Lemoine (SE/Residential) reported that a quote created via the online scheduler for 1105 Riverrun, Madison, Tennessee 37115 was blocked with status "Out of Service Area — Review Property Address." Madison, TN 37115 is confirmed inside the SE service area. Chris Scott identified this as a Mapbox geocoding failure — the new construction address doesn't exist in Mapbox's database yet, causing the geocoder to return either no result or an inaccurate coordinate. Chris noted the intended behavior is to fall back to zip code, but something broke in that chain. The online scheduler's address entry (AddressSearchInputFallbackForm) has two distinct paths: the autocomplete search path and the manual entry path. The geocoding fallback chain (full address → city/state/zip → zip code alone) exists only in the manual entry submitForm function. The autocomplete path (handleAutoCompleteClick) takes coordinates directly from Mapbox's suggestion result — no fallback runs if Mapbox returns a result with an imprecise coordinate, or if the user is forced into manual entry after a failed autocomplete. The service area check (service-area/find-containing) runs against address.lat and address.lng in PropertyStep.tsx. If those coordinates are wrong or represent a fallback location (e.g., a city centroid for a zip code that straddles the service area boundary), the property is silently blocked. There is no logging or diagnostic visibility into which geocode attempt produced the coordinates used, or what those coordinates were — making this class of failure impossible to identify without engineering investigation into Mapbox directly. New construction is a material inspection volume segment; any address newer than Mapbox's database is at risk of this failure pattern. Product Decisions Locked Behavior intent — The expected outcome for a valid address inside the service area is that the booking proceeds. A geocoding failure should never silently block a legitimate booking without a recoverable path. Open Fallback behavior — When Mapbox cannot return a high-confidence coordinate for a full address, should the system: (a) fall back to zip centroid and proceed if that centroid is inside the service area, (b) surface a manual override for internal schedulers, or (c) block and prompt the user to call in? The right answer may differ between the online-public and internal scheduler contexts. Logging — Should geocode attempt results (which tier succeeded, what coordinates were returned) be logged for observability? Or is a one-time Mapbox diagnostic on the specific address sufficient? Scope Frontend — The geocoding fallback chain in attik-frontend/src/components/forms/AddressSearchInputFallbackForm.tsx (submitForm) covers manual entry but not the autocomplete path. handleAutoCompleteClick takes foundAdd.center directly from the Mapbox Places response without any validation or fallback. If Mapbox returns coordinates that don't match the real property location (common for new construction), those bad coordinates propagate silently. The callMapboxPlaces and geocodeAddress utilities in attik-fronten

Linear 14 days ago

🏠

Main App

Planned

Payroll: show per-session clock in/out times inline in the hours view

Objective Allow payroll reviewers to see individual punch-in and punch-out timestamps for each employee across a date range without opening each day individually. Reduce friction in the payroll approval workflow by surfacing session-level detail inline rather than requiring per-record navigation. Background Tiffany Fisher reported via Google Chat that reviewing hours before approving payroll requires opening each day/record one at a time. No view currently shows individual session timestamps (clock-in/clock-out pairs) at a glance. The inspector/date range filter on the payroll hours view already exists and returns a daily total per employee — but only the aggregated hours value is surfaced, not the underlying sessions. The HoursData type (defined in ClockInOut.tsx) includes a sessions array of { clockIn: string, clockOut?: string } objects. The data is already returned from payroll/hours-history; this is a rendering gap, not a data gap. The ClockInOut dashboard widget already renders session-level history for the logged-in employee using this same sessions array — formatting clock-in/out times and per-session duration. That rendering logic is the direct reference for what needs to be added to the manager-facing hours view. Applies to all employee types (inspectors and office staff), not inspector-only. Product Decisions Locked Scope — Manager/payroll-reviewer-facing view only; no changes to the employee-facing Time Clock widget. Data source — HoursData.sessions is already returned from payroll/hours-history; no new backend endpoint needed. Placement — Session timestamps expand inline as sub-rows beneath each day row. No click or hover required to reveal them. Employee type — Applies to all employee types. The feature works the same whether one or multiple employees are displayed. Scope Frontend attik/apps/frontend/src/app/tools/hr/payroll/PayrollOverview.tsx — Primary manager-facing table. Inline session sub-rows would be added within the existing day-level row rendering. attik/apps/frontend/src/app/tools/hr/payroll/PayrollBase.tsx — Likely orchestrates the hours data fetch alongside payroll; relevant if data shape changes are threaded through. attik/apps/frontend/src/components/dashboard/ClockInOut/ClockInOut.tsx — Reference implementation. Contains the session rendering logic (sessions.map) using tzDayjs(session.clockIn).format('h:mm A') that can be extracted or adapted for the manager view. References payroll/hours-history — existing endpoint returning HoursData[] with sessions array ClockSession type: { clockIn: string; clockOut?: string } defined in ClockInOut.tsx

Linear 14 days ago

🏠

Main App

Planned

Add source filter to Repair Lists page (QwikFix, Thumbtack)

Objective Allow staff to filter the Repair Lists page by submission source (e.g. QwikFix, Thumbtack) so they can quickly identify which repair list submissions came from a specific lead channel without having to open individual records. Give teams a way to segment repair list activity by channel for operational and coaching purposes. Background The Repair Lists page (/inspections/repair-lists) currently supports filtering by status only — surfaced via StatusSelector.tsx and passed as a query param to GET /repair-list. There is no source or origin field on the filter UI or the underlying query. Shayna Turilli (SW) raised this on the SW/SE call on July 6, 2026 — she specifically mentioned wanting to pull a list of contacts who came through QwikFix vs Thumbtack to understand which agents are using the service, how frequently, and where education or follow-up is needed. Whether repair list records currently carry a source or origin field is an open question for engineering — no source field was found in a search of the repair list route or client component. If it doesn't exist, it would need to be stored at creation time. Product Decisions Open Source field existence — Does the SavedRepairList model currently store a source/origin field (e.g. QwikFix, Thumbtack, internal)? If not, how is source determined at the time a repair list is submitted — is it inferrable from a linked contact, quote, or integration record? Filter UI — Should source be a dropdown filter on the Repair Lists page alongside the existing status filter, or a separate column/tag on each row in SingleRepairListBar.tsx? Scope of sources — Which sources need to be surfaced initially? At minimum: QwikFix and Thumbtack. Are there others? Scope Backend apps/backend/src/routes/repairList.ts — Confirm whether source or origin is a filterable field on GET /repair-list; add if not present. SavedRepairList model — Confirm whether a source field exists and is populated at creation; if not, determine how to store it going forward. Frontend apps/frontend/src/app/tools/inspections/repair-lists/components/RepairListsClient.tsx — Currently passes status and _companyId params to GET /repair-list; would need a source param added. apps/frontend/src/app/tools/inspections/repair-lists/components/StatusSelector.tsx — Reference pattern for adding a source filter alongside the existing status filter. apps/frontend/src/app/tools/inspections/repair-lists/components/SingleRepairListBar.tsx — May need a source tag or label added to each row for visibility at a glance. References apps/backend/src/routes/repairList.ts apps/frontend/src/app/tools/inspections/repair-lists/components/RepairListsClient.tsx apps/frontend/src/app/tools/inspections/repair-lists/components/StatusSelector.tsx apps/frontend/src/app/tools/inspections/repair-lists/components/SingleRepairListBar.tsx Related: ATT-1480 (Add Repair Lists to main sidebar navigation)

Linear 14 days ago

🏠

Main App

Planned

Reorder job carries over _inspectorRequestedIds from source inspection

Objective When a job is reordered via the workorder Actions menu, the new inspection should start with a clean requested inspector state — no inspector should be pre-marked as requested on the reordered job. Prevent incorrect requested-inspector bonus payouts that result from the inherited flag being present on re-inspect jobs without explicit intent. Background Neil Chini (Axium/Colorado) surfaced a payroll discrepancy: inspector Michael was receiving $100 in requested-inspector bonus instead of the expected $75, with an extra $25 attributed to a component re-inspection at the same address (AXI1346). Investigation confirmed that AXI1346 was booked as a reordered job (a primary service, not an event), and that Michael was present in _inspectorRequestedIds on that new inspection — carried over silently from the original. The reorder flow navigates to /schedule?reorderFrom= (WorkorderActionsDropdown.tsx). NewSchedulingForm.tsx reads that query param and pre-populates the new inspection from the source job, including _inspectorRequestedIds. The requested flag has no meaningful relationship to the new inspection. It reflects a client or office preference on the original job. The reordered job is a distinct booking and must establish its own requested status independently. The downstream consumer of this field — calcBonus.ts — correctly counts any inspection where the employee is in _inspectorRequestedIds toward their requested-inspector bonus. The logic is sound; the data coming in is incorrect. Clearing should be silent — no UI notification to the scheduler. Product Decisions Locked Clear on reorder — _inspectorRequestedIds must not be copied from the source inspection to the reordered inspection. Silent clear — no toast, note, or visual indicator when the field is omitted. Scope Frontend — Scheduling attik/apps/frontend/src/components/scheduling/NewSchedulingForm.tsx — the reorderFrom pre-population logic is where the source inspection's fields are read and applied to the new scheduling form state. The fix is to exclude _inspectorRequestedIds when hydrating form state from the source job. Backend — Payroll (reference only, no change needed) attik/apps/backend/src/util/functions/payroll/calcBonus.ts — requested bonus calculation; correct as-is. attik/apps/backend/src/util/functions/payroll/payrollContext.ts — calcBonus called here per-inspector. References Reorder entry point: attik/apps/frontend/src/app/tools/inspections/[id]/components/WorkorderActionsDropdown.tsx Related: ATT-1164 — Preserve inspector request when rescheduling (distinct: reschedule should preserve the flag; reorder should clear it) Related: ATT-1738 — Reschedule job locks unrelated inspector

Linear 15 days ago

🏠

Main App

Planned

Fixed discount code distributes incorrect amount to primary service when combined with bundle on same order

Objective Ensure the invoice total correctly reflects the full applied discount when a fixed discount code is combined with a bundle on the same order. A $3 shortfall on invoice #TCI3667 (Axium Colorado, 7428 Coral Ridge Dr, 7/1/26) confirmed the bug is live and affecting real orders. Background Neil Chini (Axium/Colorado) identified a $3 discrepancy on invoice #TCI3667: the Military Discount ($30 fixed, applied to all 5 services) shows $102 in total savings rather than the expected $105. The order contains the VA Loan Complete bundle, which applies fixed per-service discounts to 3 of the 5 services (Radon, Sewer, Mold: -$25 each). The primary service (Home Inspection - VA Loan) receives a separate primary service bundle discount. Tracing the invoice line items: Radon (-$31), Sewer (-$31), Mold (-$31), WDI (-$6), and Home Inspection (-$3). The Military Discount's $6-per-service share is correctly applied to 4 of 5 services but produces only $3 on the Home Inspection — the primary service. The bug lives in calculateServicePrices.ts. For fixed discount codes, the function splits the total evenly (perService = round2(totalDiscount / eligibleServices.length)) and uses a "last eligible service gets the remainder" pattern to prevent cumulative rounding drift. However, the primary service also has a primaryDiscountAmount applied in an earlier pass, which modifies its preTaxAmount before the discount code pass runs. The combination of prior-pass price modification and the fixed-split rounding correction appears to produce an off-by-one allocation on the primary service. The result is a real invoice discrepancy: the client is billed $3 more than intended. Product Decisions Locked Correct behavior — The full fixed discount amount ($30) must be distributed across all eligible services such that the invoice total reflects the full discount. No rounding residual should result in under-discounting any service. Invoice accuracy is authoritative — The discrepancy is present in the invoice total ($1,083 instead of $1,080), not just the display; this is a billing accuracy bug, not a cosmetic one. Open Allocation method — The current even-split with last-service remainder correction is conceptually sound, but the interaction with pre-modified preTaxAmount on bundle+primary service combinations needs investigation. Should the fixed split use originalPreTaxAmount as the denominator base instead of the already-reduced preTaxAmount? Engineering should confirm whether the fix is isolated to the allocation order or requires a deeper change to how sequential discount passes interact. Scope Monorepo (attik) — Frontend Primary file: apps/frontend/src/util/functions/schedulingHelpers/calculateServicePrices.ts The fixed discount distribution block is the primary suspect. Focus on the interaction between the primaryDiscount pass (which modifies preTaxAmount on the primary service) and the subsequent fixed-code split, which uses preTaxAmount as the basis for even distribution. The isLast remainder logic uses findLastIndex over processedServices — verify that index is resolving correctly when the primary service is the first in the array and the last eligible service has already absorbed prior discount passes. Related: apps/frontend/src/components/scheduling/DiscountModel.tsx — UI for discount code application; not the source of the bug but relevant for confirming which service IDs are passed as eligible.

Linear 15 days ago

🏠

Main App

Planned

Display Referral Source on the Work Order

Objective Surface the Referral Source field on the Work Order so team members can see it while reviewing a job without leaving the page or exporting data. Eliminate the current friction of having to download a full inspections export and match by address or invoice number to find this value for a single job. Ensure parity between the booking form and the Work Order — Referral Source is the only booking-form field that disappears after a quote converts to an inspection. Background Referral Source is collected during the quote and online scheduling flows and stored on both the Quote and Inspection records. The field is already available in Reports Hub under both the Inspections and Quotes collections, labeled "Referral Source," and is filterable. No Work Order UI component currently renders referralSource — it was never added, not removed. The BookingDetailsBox component is the natural home for this field as it already groups booking-origin context (Scheduled By, Confirmed By, Canceled By). The InspectionFullyPopulated type already passes the inspection object into BookingDetailsBox, so referralSource is available in the component's props with no additional fetching required. Product Decisions Locked Display location — Show Referral Source in the existing BookingDetailsBox component on the Work Order, alongside the other booking context fields. Display behavior — Show the field only when a value is present; omit it entirely if referralSource is null or an empty string. Open Editability — Should Referral Source be read-only on the Work Order, or should team members be able to update it after booking? If editable, a PATCH to the inspection endpoint is straightforward, but the UX pattern (inline edit vs. modal) needs a decision. Read-only is the safe default. Scope Frontend (attik-frontend) src/app/tools/inspections/[id]/components/BookingDetailsBox.tsx — Primary change. Add a Referral Source row below the existing Scheduled/Confirmed/Canceled grid. Reads inspection.referralSource; renders conditionally when present. src/app/tools/inspections/[id]/components/WorkorderPage.tsx — No changes expected; inspection is already passed to BookingDetailsBox. References referralSource field definition in src/config/exportFieldDefinitions.ts (both inspectionFields and quoteFields) src/models/inspectionSchema.ts — confirms field stored on inspection src/util/functions/inspection/createInspection.ts — confirms field written at booking time

Linear 26 days ago

🏠

Main App

Planned

Add "Scheduled By" filter to inspection worklists

Objective Add a Scheduled By filter to the worklist filter bar on inspection-type worklists so schedulers can narrow a worklist to jobs they personally booked. Give office staff a self-service QC tool — replacing the "my orders" visibility they had in the Spectora widget — to review their own closed or open work without needing a separate worklist per person. Mirror the existing "Created By" filter already available on quote worklists; the underlying field and lookup mechanism are the same. Background Catherine Lemoine (SE/Residential) requested this as a scheduler productivity and QC feature. Schedulers previously used the Spectora widget to see "their own orders" — e.g. what they had closed that day — as a lightweight review step. That capability hasn't carried over into Attik worklists. The "Created By" filter on quote worklists already implements this pattern: the createdBy field on the quote document stores the booking staff member's employee ID, and createEmployeeStagesWithKeyword('createdBy') populates the resolved name in the response pipeline. The same createdBy field exists on inspection records; it is set at creation time in POST /worklist via res.locals.membership.employeeId || res.locals.membership._inspectorId || res.locals.membership._officeStaffId. The backend GET /worklist/:id/data handler already accepts a wlCreatedBy query param but only applies it when worklist.type === 'quotes'. Inspections are excluded. The frontend WorklistFiltersBar.tsx renders the "Created By" Autocomplete only inside the showQuoteFilters branch. The office staff list (useServer ({ url: 'office-staff' })) is also only fetched for quotes. Product Decisions Locked Filter label — Display as "Scheduled By" on inspection worklists (not "Created By"), to match the mental model schedulers use. Staff pool — All office staff (same unscoped list used for "Created By" on quotes); no role restriction. Scope — Inspection worklists only. Quote worklists already have "Created By." Event worklists are out of scope for this issue. Scope Backend — attik-backend/src/routes/worklist.ts In the ephemeral view filter block, extend the wlCreatedBy condition to apply when worklist.type === 'inspections', not just 'quotes'. The field path is createdBy on the inspection document — same as quotes. Frontend — attik-frontend/src/components/task-check/WorklistFiltersBar.tsx The officeStaff fetch (useServer ({ url: 'office-staff' })) currently conditions on worklistType === 'quotes'. Extend to also fetch when worklistType === 'inspections'. The "Created By" Autocomplete currently renders inside the showQuoteFilters block. Add a parallel "Scheduled By" Autocomplete inside the showInspectionFilters block, wired to the same createdBy / wlCreatedBy param but with the label "Scheduled By." The FilterValues interface, buildParams, hasActiveFilters, activeFilterCount, and clearAll already handle createdBy — verify they extend cleanly to the inspections path or adjust as needed. References ATT-78

Linear 26 days ago

🏠

Main App

Planned

BCC recipient support in email builder templates

Objective Allow template authors to configure BCC recipients on email builder templates, mirroring the existing CC configuration Prevent internal office or client success email addresses from being visible to clients on automated sends BCC addresses should be configurable as both role-based recipients (via ContactSelect) and manual email addresses (via ManualRecipientInput), matching the CC pattern Background Shayna Turilli raised this as a UX concern: when the office or client success team is CC'd on client-facing emails, their address is visible to the client in the email header — which reads as awkward and unprofessional A BCC workaround exists today for manually-sent emails but is not configurable on automated email templates The email builder already has full BCC infrastructure in place: EmailBuilderSchema defines bcc: [String], sendResendTemplate.ts passes bcc through to the Resend API, and handleEmailJob already includes bcc in CreateEmailOptions The Email Builder UI (EmailBuilderBase.tsx) renders side-by-side To/CC panels with role and manual address inputs — but has no BCC panel. The save payload in saveEmail() explicitly comments out bcc (// bcc: formData.bcc || []), and bccEmails does not yet exist on the schema — meaning BCC was likely started and intentionally deferred; engineering may have context on why ccEmails exists on EmailBuilderSchema for static CC addresses; a parallel bccEmails field is missing and will need to be added Product Decisions Locked BCC scope — Applies to email builder templates (automated sends) only; manual sends already support BCC BCC UI layout — Rendered as a collapsible section below the To/CC grid, not as a third column, since BCC will be used infrequently and a third column would add visual clutter Parity with CC — When expanded, BCC panel mirrors the CC panel: role-based recipients via ContactSelect + manual addresses via ManualRecipientInput Open Schema field naming — A bccEmails field needs to be added to EmailBuilderSchema to mirror ccEmails. Confirm naming convention before schema migration. Scope Backend (attik monorepo) apps/backend/src/models/emailBuilderSchema.ts — Add bccEmails: [String] field to EmailBuilderSchema, mirroring ccEmails apps/backend/src/routes/emailBuilder.ts — Ensure PATCH route accepts and persists bcc and bccEmails fields apps/backend/src/util/functions/emails/sendResendTemplate.ts — handleEmailJob already supports bcc in CreateEmailOptions; confirm bccEmails values are merged into the bcc array at send time, same as the cc + ccEmails pattern Frontend (attik-frontend) src/app/tools/action-flow/email-builder/[email_id]/EmailBuilderBase.tsx — Add collapsible BCC section below the To/CC grid; uncomment bcc: formData.bcc || [] in saveEmail(); add bccEmails to save payload, originalFormDataRef, and checkForChanges diff src/util/types/serverTypeCollection/emailBuilder.ts — Add bccEmails to the frontend type definition

Linear 27 days ago

🏠

Main App

Planned

Bug: Google Calendar OAuth connection failing with Error 400 (redirect_uri_mismatch)

Objective Restore the ability for inspectors to connect their Google Calendar via the OAuth flow in Account Settings The current OAuth flow returns Error 400: redirect_uri_mismatch for all users, blocking the integration entirely No inspector at any company can successfully complete the connection until resolved Background Adam Wright (RJ Inspections) and Rob Johnson (RJ Inspections) both attempted to connect via the "Connect Google Calendar" button in Account Settings and received Google's Error 400: redirect_uri_mismatch screen Adam initially suspected the issue was related to the "Google Account Linked" SSO badge also visible on the account settings page — confirmed via code review that these are two separate systems; the SSO badge (GoogleAccountLink.tsx → authClient.linkSocial()) has no relationship to the calendar OAuth flow The redirect URI sent to Google during the OAuth consent request is constructed at runtime in gcalOAuthClient.ts from the BETTER_AUTH_URL or SERVER_URL environment variable — if either resolves to a value not registered in the Google Cloud Console OAuth app config, Google rejects the request No inspector has a confirmed working calendar connection; previously appearing "linked" status in the admin view is attributable to the SSO link, not a calendar token The company-level integration toggle is not the cause — the Error 400 fires at Google's consent screen, after the backend has already generated the auth URL successfully Product Decisions Locked The "Google Account Linked" SSO badge and the "Connect Google Calendar" button are separate systems — this issue concerns the calendar OAuth flow only Open Redirect URI registration — Engineering needs to confirm what URI BETTER_AUTH_URL/SERVER_URL resolves to in production and verify it matches what's registered in the Google Cloud Console for the OAuth app (or GOOGLE_CAL_CLIENT_ID if a separate credential is in use) Scope Backend src/util/functions/googleCalendar/gcalOAuthClient.ts — constructs the redirect URI at runtime from BETTER_AUTH_URL or SERVER_URL; the resolved value in the deployed environment is the likely mismatch source src/routes/googleCalendar.ts — /auth-url endpoint generates the consent URL; /callback endpoint receives the redirect and exchanges the code for tokens Frontend src/app/tools/settings/integrations/google_calendar/GoogleCalendarConnect.tsx — renders the "Connect Google Calendar" button that initiates the flow src/app/tools/settings/account/page.tsx — hosts both GoogleAccountLink (SSO) and GoogleCalendarConnect (calendar) on the same page; the visual proximity of these two components contributed to user and operator confusion about connection state References Reported by Adam Wright and Rob Johnson, RJ Inspections Related: ATT-1357 (Google Calendar sync feature, now Done)

Linear 27 days ago

🏠

Main App

Planned

Bug: PriorityLab appointment not appearing in portal despite successful resync (Charleston)

Objective Ensure that when a PriorityLab sync completes and Attik shows a _priorityLabPropertyId, the corresponding appointment is actually visible in the PriorityLab portal. Prevent silent sync failures where the integrations tab reports a property ID and a recent sync timestamp but no appointment exists on the vendor side. Background Rein Manalad reported that order 1008600662 for Charleston Home Inspection does not appear in the PriorityLab portal despite the following being confirmed: The integrations tab on the work order shows a _priorityLabPropertyId under the PriorityLab Breeze integration. The inspector shown matches the inspector on the work order. A last-synced timestamp of 6:16 was present; Rein manually resynced on June 24 — the appointment still does not appear in the portal. The inspector has an active profile in the PriorityLab portal. The PriorityLab integration is enabled at the service level. A property address search in the PriorityLab portal returns no results for this order. This symptom profile matches ATT-1669 exactly: Attik shows a property ID (potentially from a prior sync) while the most recent sync either failed silently or wrote to the vendor without the appointment being findable. ATT-1669 was resolved in PR #513 on May 26, 2026. This report suggests the fix may not cover all cases, or a new edge case exists specific to the Charleston instance configuration. PostHog error tracking was checked on June 24, 2026 — no active or recently resolved issues matching PriorityLab sync errors were found. The failure is not generating a frontend-visible exception, which is consistent with a silent backend sync failure recorded in priorityLab.lastError rather than a thrown error reaching the client. This is a Credibility risk — a mold test appointment that doesn't appear in the lab portal is an operational miss, not just a display issue. Product Decisions Locked New issue — ATT-1669 was marked Done; this is filed as a separate report to keep history clean. Open Root cause — Backend logs for order 1008600662 need to be pulled to determine what CreateUpdateAppointment actually returned on the June 24 resync. Until then, whether this is a regression of ATT-1669 or a new edge case is unconfirmed. Charleston-specific config — It is unknown whether any configuration difference in the Charleston instance (inspector profile setup, service-level integration config, etc.) exposes a sync path not covered by the PR #513 fix. Scope Backend attik-backend/src/util/functions/priorityLab/syncPriorityLabAppointment.ts — primary sync path; check whether the June 24 resync attempt recorded a priorityLab.lastError or cleared it as a success. attik-backend/src/util/functions/priorityLab/priorityLabApi.ts — parseVendorResponse and createOrUpdateAppointment; verify the fix from PR #513 handles all response shapes being returned for this order. attik-backend/src/util/functions/priorityLab/priority

Linear 27 days ago

🏠

Main App

Planned

Bug: Add Equipment and Bulk Upload buttons inaccessible in large equipment categories

Objective Restore reliable access to the "Add Equipment" and "Bulk Upload" buttons in the Equipment Settings page when a category contains a high number of rows. Ensure action buttons at the bottom of each category section remain reachable regardless of category size. Background Jake Staron reported that after approximately 76–77 equipment rows within a single category, the red "Add Equipment" and yellow "Bulk Upload" buttons at the bottom of that category begin to get cut off; at 78 rows they are fully inaccessible without deleting active equipment rows above them. Ryan Wagner independently reproduced the issue at 77 rows — the buttons are not visible below the last entry. The cutoff is category-scoped, not page-wide — other categories with fewer rows are unaffected. Zoom level influences the threshold: going from 25% to 150% zoom increases visible rows by approximately one; 150% to 250% shrinks visibility by approximately the same. This zoom-proportional behavior points to a container overflow or fixed-height boundary issue rather than a hardcoded row limit. No workaround exists short of deleting active equipment rows, which is not acceptable in production. Jake confirmed this is not currently blocking operations — medium priority backlog placement is appropriate. Product Decisions Locked Priority — Medium; non-blocking per reporter and confirmed by Ryan. Open Fix approach — Should the category container grow dynamically with content (removing any fixed-height constraint), or should the action buttons be repositioned to a sticky/fixed location at the top or header of each category? Sticky buttons at the top of each category list is Ryan's preference, but final approach is left to engineering. Scope Frontend The equipment settings page lives at attik-frontend/src/app/tools/settings/equipment/page.tsx — entry point for the category list layout. Per-category list rendering is handled by attik-frontend/src/app/tools/settings/equipment/EquipmentSubList.tsx — the overflow or height constraint causing button clipping is most likely here. The "Bulk Upload" button trigger is in attik-frontend/src/app/tools/settings/equipment/BulkUploadModal.tsx — confirm whether the trigger button is rendered inside EquipmentSubList or separately. The "Add Equipment" modal entry point is attik-frontend/src/app/tools/settings/equipment/EquipmentModal.tsx. References Screenshot provided by Jake Staron showing buttons cut off at 78 rows — attach manually to this issue.

Linear 27 days ago

🏠

Main App

Planned

Add Past Jobs section to inspector mobile job screen

Objective Surface the Past Jobs section on the inspector mobile job screen so inspectors can see prior work at the same property without leaving the app. Match the content parity of the existing web work order PastInspectionsSection — same data, same address-matched logic. Position the section between Required Info and Payroll on the job detail screen. Background Ashley Assi requested the feature via Google Chat, noting it is not currently present in the mobile job view. The web work order already surfaces a PastInspectionsSection component (attik-frontend: src/components/scheduling/PastInspectionsSection.tsx) that matches prior jobs at the same property using the GET /past-jobs endpoint (attik-backend: src/routes/pastJobs.ts). The mobile job detail screen is composed in attik-mobile/app/(app)/inspection/[id].tsx, which already includes RequiredInfo and PayrollSection sections — the new section slots between them. No equivalent past-jobs component or hook exists in attik-mobile today. Product Decisions Locked Placement — Past Jobs section renders between Required Info and Payroll on the mobile job detail screen. Content parity — Display matches what is shown in the web work order's Past Jobs section; no mobile-specific subset. Data source — Reuse the existing GET /past-jobs backend endpoint (attik-backend: src/routes/pastJobs.ts); no new backend work required. Scope Mobile The job detail screen lives in attik-mobile/app/(app)/inspection/[id].tsx. The new PastJobsSection component should be inserted between the RequiredInfo and PayrollSection render positions in that file. A new component (e.g. attik-mobile/components/inspection/PastJobsSection.tsx) and a corresponding data hook (e.g. hooks/usePastJobs.ts) need to be created to fetch from GET /past-jobs using the current inspection's property address fields. The web reference implementation in attik-frontend/src/components/scheduling/PastInspectionsSection.tsx should be used as the authoritative model for data shape and display logic. Backend No changes required. The src/routes/pastJobs.ts endpoint is already in place and accepts zip, city, and street query params from the current inspection's property. References ATT-134 — Past Jobs or Quotes (web work order implementation, Done) ATT-1213 — Include unit number in Past Jobs & Quotes address matching (open refinement) ATT-1306 — Add search for past inspections in mobile app (related mobile history work, Done) attik-frontend/src/components/scheduling/PastInspectionsSection.tsx attik-backend/src/routes/pastJobs.ts

Linear 27 days ago

🏠

Main App

Planned

PDF-based termite reports cannot contribute items to the repair list

Objective Enable repair list functionality for jobs that include PDF-based termite (WDI) reports, so agents and clients can build and share a repair list from termite findings the same way they can from HTML inspection reports. Unblock SE and other markets that regularly include termite reports on multi-service jobs and are receiving agent requests for repair list access on those reports. Background The repair list feature works by reading individual defect items from Attik's structured HTML report format and allowing users to select and add them to a repair list. PDF-based reports — including termite/WDI forms like NPMA-33, state-specific WDI forms, and similar regulated PDF formats — have no selectable items because their content is rendered as a flat PDF rather than structured data. Catherine Lemoine (SE/Residential Inspector) raised this during the Implementation meeting on June 23, noting that a buyer's agent had asked about repair list access for termite findings that same day. She confirmed the issue affects multiple states, each with their own PDF form requirements. Chris Scott confirmed there is no current solution: the PDF structure does not expose item-by-item content to the repair list system. Two roadmap approaches were discussed: Option A — Pull form fields into actionable items: Build tooling that extracts structured data from the inspector's form input (as they fill out the PDF in Spectra) and surfaces those as selectable repair list items alongside the PDF export. This is the more complete long-term solution and aligns with the new Attik report writer direction. Option B — Stripped-down parallel template: Create a lightweight Attik-native report template alongside the PDF that captures key defect observations in structured form, allowing repair list selection from that template while the PDF serves as the official deliverable. Chris noted this is the current workaround at Scott's team for rental license inspections. Both approaches are on the roadmap. Chris noted Option A becomes significantly easier once Attik controls the data input layer (i.e. once the Attik report writer replaces Spectra for these form types), which is still in development. Catherine committed to communicating to affected agents that the team is aware and working on a solution. Product Decisions Locked No immediate workaround to ship — Converting toggles to dropdowns or other field-level hacks are not acceptable solutions. The fix must provide genuine repair list item selectability from termite report findings. Open Approach selection — Option A vs. Option B — Which roadmap path to pursue first? Option A (form field extraction → actionable items) is the better long-term solution but depends on Attik report writer progress. Option B (parallel stripped-down template) is shippable sooner but adds operational overhead for inspectors. To be decided by Chris and Ryan — likely after the report writer reaches a stable enough state to assess Option A feasibility. PDF form coverage — A full inventory of PDF form types in use across all markets is needed before implementation: NPMA-33, state-specific WDI forms, rental license inspections, four-point forms, etc. Scope will vary by approach chosen. Scope Frontend attik-frontend/src/app/client/reports/components/RepairListProvider.tsx — Manages repair list state and the add-to-list action; will need to support items sourced from non-defect-card origins (e.g. manually entered or form-extracted items) once an approach is chosen. attik-frontend/src/app/client/reports/components

Linear 27 days ago

🏠

Main App

Planned

Service area drawing tool UX improvements — collapsible panels, larger drag handles, fix map orientation reset

Objective Make the service area drawing and editing experience usable at normal screen sizes without requiring workarounds or excessive precision. Unblock brands like SE (Catherine Lemoine) who have pending service area changes they've been unable to complete due to the current UX limitations. Background The service area drawing tool in Settings suffers from a combination of layout and interaction problems that make it difficult to use in practice, particularly for editing existing areas rather than creating new ones. Catherine Lemoine (SE/Residential Inspector) raised this during the Implementation meeting on June 23, noting she had spent over an hour trying to make changes and gave up. She described the map canvas as extremely small on her screen because the service area name list panel and a total service area info box consume most of the available space. The specific issues raised across Catherine, Shayna Turilli (AJF), and Sean Garvey (Dwell Inspect AZ): Panels consume too much screen real estate — The service area names list and info box are fixed in place during edit/draw mode, leaving a very small map canvas to work with. Both panels should be collapsible or hidden when the user enters draw/edit mode so the map can expand to fill the screen. Drag handles and polygon points are too small — The dots marking polygon vertices are difficult to see and hard to click and drag accurately. They need to be larger and more visually distinct. Map can be accidentally reset to a flat/world view — Double-clicking the map canvas during editing can flip the map to a flat global projection, requiring the user to manually drag it back. This is a significant annoyance and should be prevented or at least made easily recoverable. Chris Scott agreed with all three improvements on the call and noted the screen is "not great" as-is, confirming the fixes are planned. Ryan and Chris also aligned on a modal option as an alternative layout approach — opening the drawing tool in a modal overlay that gives the full viewport to the map canvas. Product Decisions Locked Collapsible panels — The service area name list panel and the total service area info box should collapse or hide when the user enters draw/edit mode, maximizing map canvas space. Larger drag handles — Polygon vertex dots and drag handles should be made significantly larger and more visually distinct to improve precision. Prevent accidental map orientation reset — Double-click behavior that resets the map to a flat/world view should be suppressed or guarded against during draw/edit mode. Open Panel approach vs. modal — Should collapsed panels be implemented as collapsible sidebars within the existing page layout, or should the drawing tool open in a full-viewport modal? Both were discussed on the June 23 call. Modal may be cleaner but is more implementation work. To be decided by Chris and Ryan when the issue is assigned to an engineer. Scope Frontend attik-frontend/src/app/tools/settings/service-areas/ServiceAreaMap.tsx — The primary component for the service area drawing tool. All three UX improvements (panel layout, drag handle sizing, orientation lock) are centered here. attik-frontend/src/util/functions/mapbox/validateServiceAreaPolygon.ts — Polygon validation utility; relevant context for understanding how polygon points are managed, which informs the drag handle improvements. The settings page routing for service areas lives under attik-frontend/src/app/tools/settings/service-ar

Linear 27 days ago

🏠

Main App

Planned

Add trainee flag to inspector profiles — surface on work order and mobile app, auto-exclude from payroll splits

Objective Establish "trainee" as a first-class flag on the inspector profile so the system can recognize and act on trainee status across multiple surfaces. Surface the trainee indicator on the work order and in the inspector mobile app so all staff can immediately see when a job includes a trainee inspector. Automatically exclude inspectors flagged as trainees from payroll pay splits, eliminating the manual process of removing them from each job's payout line after the fact. Lay the groundwork for future features that can leverage the trainee flag (e.g. worklist conditions, action flow triggers, reporting) without retrofitting. Background When a trainee accompanies a lead inspector on a job, they are currently assigned to the job in Attik just like any other inspector. This causes two problems: their status as a trainee is invisible to anyone viewing the work order or the mobile app, and the payroll system automatically includes them in the pay split for that job. The current workaround is a two-step manual process: (1) toggle payroll off on the trainee's inspector profile so they don't generate their own payroll dropdown entry, then (2) manually X out their payout line for each job in the payroll overview. Neil Chini (Axium/Colorado) described this process during Office Hours on June 23 — with two active trainees, this requires manually removing payout lines across every job they appear on each pay period. Turning payroll off on the inspector profile is a blunt workaround: it prevents the trainee from ever appearing in payroll, but does not mark them as a trainee, does not communicate their status to other staff on the work order, and cannot be used to drive any future logic. Jim Duggan (Sherwood/Connecticut) noted his teams have handled trainee tracking entirely outside Attik via Google Forms, since there's no native way to track it. Ryan confirmed on the call that a dedicated trainee toggle is the right solution and that a flag on the profile — not a repurposed payroll toggle — is the correct implementation path. Product Decisions Locked Trainee is a flag on the inspector profile — isTrainee (or equivalent) is added to inspectorSchema.ts as a boolean. It is set in the inspector's profile settings, not per-job. Trainee badge on the work order — When a flagged trainee is assigned to a job, their inspector row on the work order displays a visible "Trainee" indicator so office staff and dispatchers can see the status at a glance. Trainee badge in the mobile app — The trainee indicator also appears on the inspection detail screen in the inspector app, so lead inspectors and other field staff are aware of who is shadowing. Auto-exclude from payroll splits — When payroll is calculated, inspectors with isTrainee: true are automatically excluded from the pay split for any job they are assigned to. No manual removal step required. Trainee flag is independent of the payroll-off toggle — The existing payroll-enabled toggle on the inspector profile remains separate. The trainee flag drives automatic payroll split exclusion through its own logic path, not by reusing the existing toggle. Trainee inspectors remain visible on the job — Being flagged as a trainee does not hide the inspector from the work order, the schedule, or any other surface. Visibility is preserved; only the payroll split behavior changes. Profile settings placement — The isTrainee toggle lives in the inspector profile settings directly above the existing payroll-enabled toggle, consistent with the grouping of inspector-level behavioral fl

Linear 27 days ago

🏠

Main App

Planned

Add "hide from inspector app" toggle to required info field settings

Objective Give admins a per-field setting to suppress a required info field from the inspector's mobile app without affecting its visibility on the internal order form or online scheduler. Reduce noise for inspectors in the field by allowing QA, admin, and office-facing fields to be hidden from the mobile view while remaining fully accessible to office staff. Background Required info fields that are toggle-type (boolean on/off) always appear in the inspector mobile app regardless of whether they're relevant to the inspector — because a toggle is never blank, it always renders as populated. Text, number, and dropdown fields only appear in the app when they have a value entered, so they naturally stay out of the way when empty. Toggles have no equivalent mechanism for suppression. Neil Chini (Axium/Colorado) raised this during Office Hours on June 23, noting that a large number of QA fields used by Gina Garlock's client care team appear on every inspector's mobile view even though inspectors have no use for them in the field. Inspectors find lockbox codes, gate codes, and property access fields relevant — QA completion fields are not. Ryan confirmed on the call that the only current option is all-or-nothing: either the field exists and shows up in the app, or it doesn't. There is no way to show a field to office staff while hiding it from the inspector app. Neil proposed a dedicated "hide from inspector app" toggle per field as the right solution. Ryan confirmed a new setting would be required. Product Decisions Locked Toggle-based control — The feature is a per-field toggle in service settings: "Hide from inspector app." When enabled, the field is suppressed from RequiredInfoGrid in the mobile app regardless of whether it has a value. Scope is mobile app only — This toggle does not affect internal order form or online scheduler visibility, which are governed separately. Applies to all field types uniformly — The "hide from inspector app" setting is available on all required info field types (toggle, text, number, dropdown), not limited to toggles. This is the cleaner long-term model even though toggles are the immediate pain point. Settings placement — The "hide from inspector app" toggle is placed inline on the per-field edit row in Settings → Required Service Info, alongside the existing required and hidden controls in RequiredInfoListItem. Scope Mobile (attik-mobile) attik-mobile/components/inspection/RequiredInfoGrid.tsx — renders required info fields on the inspector's job view. This component needs to filter out fields where hideFromMobile is true. attik-mobile/app/(app)/inspection/[id].tsx — the inspection detail screen that composes RequiredInfoGrid; will need to pass the updated field schema through. Backend attik-backend/src/models/requiredInfoSchema.ts — Mongoose schema for required info field definitions. A new hideFromMobile boolean property needs to be added here. Service routes that serve required info field definitions to the mobile app will need to include this property in their response payloads. Frontend (Settings) attik-frontend/src/app/tools/settings/required-service-info/RequiredInfoList.tsx and attik-frontend/src/app/tools/settings/required-service-info/RequiredInfoListItem.tsx — the settings UI where the new toggle control will be added inline alongside the existing required and hidden c

Linear 27 days ago

🏠

Main App

Planned

Additional report inspector block shows all job inspectors when report.inspectors is empty — Attik-native job with Spectora reports

Objective On Attik-native jobs that use Spectora reports, the Inspector block on the client report should show only the inspector(s) associated with that specific report — not all inspectors assigned to the Attik job. When a client opens an additional report (one associated with a charge that has its own assigned inspector), only that report's inspector should appear in the Inspector block. Background ATT-1606 fixed ReportHeader to prefer report.inspectors from the Spectora report JSON over Attik job inspectors, with Attik inspectors as a fallback when report.inspectors is empty. The fallback condition in ReportHeader.tsx (useAttikInspectorFallback) fires when spectoraInspectorRows.length === 0. When it fires, it passes all Attik job-level inspectors — not just the one assigned to the relevant charge. Spectora always populates report.inspectors on reports regardless of type. When the value is empty on an Attik-native job, the cause is in the display or data-passing layer, not Spectora sync. In page.tsx, inspectionInspectors is passed to ReportHeader as serverInspection._inspectorId — the full set of job-level Attik inspector IDs, unfiltered by charge or report. Reported June 23, 2026 by Sheryl Floyd via Google Chat: an additional report was showing both the primary HI and the additional service inspector in the Inspector block. Product Decisions Locked Report scope — Only the inspector(s) associated with the specific report should appear in the Inspector block. Inspectors assigned to other services on the same job should not appear. Fallback behavior — If report.inspectors is empty, fall back to the charge/event-assigned inspector from the Attik job (not all job-level inspectors). Spectora sync — report.inspectors is always populated by Spectora regardless of report type. Empty values indicate a display or data-passing issue, not a sync gap. No backend sync changes required. No Attik-native report path — This issue applies only to Spectora reports rendered on Attik-native jobs. The Attik native report renderer (/reports/v2/) is out of scope. Scope Frontend attik-frontend/src/app/client/reports/components/ReportHeader.tsx — The useAttikInspectorFallback branch passes all inspectionInspectors when spectoraInspectorRows is empty. This needs to be scoped to the charge/event-assigned inspector only. attik-frontend/src/app/client/reports/[slug]/page.tsx — inspectionInspectors is currently passed as serverInspection._inspectorId (all job-level inspectors). The charge/event assignment for this specific report needs to be resolved here or in a helper before being passed to ReportHeader. References ATT-1606 — Prior fix: prefer Spectora report.inspectors over Attik job inspectors in ReportHeader Attik-Field-Service-Solutions/attik-frontend#712 — ATT-1606 implementation Reported job: https://www.attik.ai/inspe

Linear 28 days ago

🏠

Main App

Planned

Dashboard Displays Wrong Company Data After Company Switch

Objective Fix a session/auth state desync that causes the dashboard to display data from a previously active company after switching instances. Prevent users from ever seeing data that doesn't belong to their current company context — even transiently — to protect operational trust and data integrity. Background Users with access to multiple company instances can switch between them via the company switcher in the navbar. Under certain conditions — most reliably triggered by logging in while the browser is mid-switch, or switching companies in rapid succession — the active company identity in the session becomes desynchronized from the data being fetched and rendered on the dashboard. The result: the URL and navbar may reflect Company A while the dashboard renders data from Company B. Clearing cookies does not resolve the issue. The state resets only when the user navigates to a third company and then back to the intended one, suggesting stale company context is held somewhere that a simple cookie clear doesn't flush. Confirmed reproducible by both Ryan and Chris on logout/login. Chris noted he had seen this previously and assumed it was isolated. Product Decisions Locked Correct behavior — Company context must be fully resolved and data re-fetched before the dashboard renders after any company switch or login event. Open Stale context location — Is the stale company identity held in a client-side store, a cached server session, or both? The workaround behavior suggests a client-side layer (globalContext, Zustand/React context, or cookie) is not fully flushed on switch. Needs investigation to confirm. Dashboard data fetch timing — Does the dashboard data fetch fire before the new session token is fully applied? If so, a loading/pending gate may be needed to prevent rendering while company context is still resolving. Switch-during-login race condition — Chris noted this is more likely to occur when a login and a company switch happen concurrently. Should the company switcher be gated or debounced during active login/session establishment? Scope Frontend src/actions/switchCompany.ts — Primary entry point for the company switch action; the most likely location where session state is updated or where a flush/reset should be triggered. src/app/tools/globalContext.tsx and src/app/admin/globalContext.tsx — Global context providers; if company identity is held here, these need to ensure stale state is cleared before re-render on switch. src/app/tools/notifications/switch-membership/page.tsx — Handles the membership switch notification flow; relevant if switch triggers a redirect that races with session resolution. src/components/navbar/NavUser.tsx — Navbar company switcher UI; relevant if the switch action is initiated here. Backend src/util/functions/betterAuth/auth.ts — Core auth configuration; relevant for understanding how company/session context is scoped per request. src/util/functions/betterAuth/shared/sessionHelpers.ts — Session helper utilities; likely involved in resolving or updating the active company on the session. src/util/functions/betterAuth/web/webAuthEndpoints.ts — Web auth endpoints; relevant if the company switch posts to or relies on an auth endpoint to re-establish context. src/routes/session.ts and src/routes/authenticate.ts — Session and authentication route handlers; relevant for understanding the full login + company resolution flow.

Linear 28 days ago

🏠

Main App

Capacity-based pricing modifier — surge pricing when schedule fills

Objective Allow companies to configure a pricing modifier that automatically increases service fees as daily schedule capacity fills toward a configurable threshold — applying demand-based pricing without manual intervention. Give brands a revenue lever that is already familiar from other industries (airlines, rideshare) and requires no staff action during the booking flow. Background Sean Garvey (SW/SE Region) proposed this concept in the June 22 regional call: as a day's available inspection slots fill toward 80–90% capacity, prices should increase automatically by a configurable amount — similar to airline seat pricing. Catherine Lemoine validated the idea and noted it should function as a pricing modifier, meaning it affects the client-facing charge that lands on the confirmed workorder. This is a distinct feature from day-of-week pricing (ATT-1304), which applies a static modifier based on calendar day. Capacity-based pricing is dynamic — the modifier fires based on real-time schedule fill rate, not a predetermined schedule rule. Payroll is out of scope for this issue. Because the modifier increases the client-facing service price, the existing commission and bonus mechanisms in payroll will naturally flow through the higher amount where applicable. This feature is related to ATT-1691 (configurable capacity calculation rules), which defines how slot availability is calculated across the company. That issue does not block this one, but the two are closely related and engineering should assess the dependency when picking this up. Product Decisions Locked Pricing modifier only — Capacity-based pricing applies to the client-facing service price only. Payroll is not directly modified by this trigger; inspector commissions and bonuses flow through existing mechanisms on the higher base price. Opt-in per company/service — Feature must be off by default and configurable per company and/or service. Capacity scope — Capacity is measured against total slots across the company for a given day. Tiered thresholds — Companies configure multiple tiers, each with its own capacity threshold percentage and modifier amount (e.g. +$25 at 70% capacity, +$50 at 90%). Not limited to a single threshold. Modifier amount type — Both flat dollar amounts and percentage-based increases are supported, consistent with the existing modifier system. Open Capacity data timing — At the moment a booking is priced, the system needs a real-time slot fill rate for the selected inspection date. Engineering needs to determine where that calculation lives and whether it's available server-side during price calculation without introducing meaningful latency or race conditions. Scope Backend src/routes/payroll.ts — Existing modifier loading pattern; capacity modifier will follow the same modifierSchema approach and needs a new modType for capacityThreshold. Engineering will need to identify where real-time company-wide slot fill rate is computed for a given day and whether that value can be passed into the pricing calculation at booking time. Frontend src/util/functions/schedulingHelpers/calculateServicePrices.ts — Core pricing engine; capacity modifier evaluation needs to be injected here alongside other modifier

Linear 28 days ago

🏠

Main App