appnqjDpqDniH3IRllocalStorage key _dlr_patlocalStorage key _dlr_dashcache_v1The Leadership Dashboard is a client-side HTML/JavaScript application. It runs entirely in the browser, connecting directly to Airtable via its REST API and to Gmail via a deployed Google Apps Script web app. There is no back-end server.
On load, the dashboard fetches 9 Airtable tables in parallel: Accounts, Fixed Costs, Tenancies, Transactions, Rental Units, Tenants, Categories, Sub-Categories, and Businesses. It also fires non-blocking fetches for invoices, Gmail sync, and Fintable status.
Those nine are not the whole list. The same load also reads Strategic Projects (tblHrpTMd5LNYn8v1, js/dashboard.js:185), Tasks and Prospects for the KPI compute (:487, :458), Team Members and AI Agents for the agent cards, and AI Recon Audit for the accuracy score. Opening the page also writes to Airtable, in two places: the Strategic KPI automation below, and the recon audit log in section 6.
Some Strategic Projects carry their own KPI compute code. It is written by hand and stored in the KPI Compute Code field on the project record in Airtable. Opening the dashboard runs that code in your browser and saves the answer back to Airtable. The page is not read-only.
loadDashboard() calls loadStrategicKpis() as its first action (js/dashboard.js:929), which reads the Strategic Projects table (:110, :185).runAutomatedKpis() (:514). Projects with a close date are skipped, so a closed quarter cannot drift away from its snapshot (:519).new Function() (:429). A safety check blocks obvious dangerous wording first (:410), and the note beside it in the code is honest that this is a speed bump rather than a wall. Only put compute code in that field if you wrote it yourself.:611-617): KPI Current, KPI Last Updated, KPI Last Updated By, and a KPI Detail JSON blob holding the workings, capped at about 95KB.loadDashboard() again, so the write repeats all day while the tab is open. The save is fire-and-forget: if Airtable rejects it, the only sign is a line in the browser console (:618), and nothing on screen tells you it failed. If the compute itself fails, the project row shows a red Compute failed badge (:709) with the reason on hover.localStorage (key _dlr_pat) and persists across browser sessions until you log out or clear storage.localStorage (not sessionStorage), so you stay logged in even after closing the browser. To log out, use the logout button or clear your browser’s local storage.api.airtable.com. No third-party server is involved.The dashboard uses a stale-while-revalidate caching strategy to provide instant page loads. On every successful data load, the full dataset is saved to localStorage under the key _dlr_dashcache_v1.
| Behaviour | What Happens |
|---|---|
| Cache hit (under 24 hours old) | The dashboard renders immediately from cached data. A blue Refreshing… badge appears (with the cache age, e.g. “showing data from 3h ago”). A fresh fetch runs in the background; when complete, the display silently updates and the badge disappears. |
| Cache miss or older than 24 hours | The loading overlay is shown while data fetches from scratch. No stale data is displayed. |
| Background refresh fails | The badge turns red with “Couldn’t refresh — showing saved data”. The cached data remains visible. |
| Auth failure | Cache is cleared and the login screen is shown. |
formatAge function rounds minutes, hours, and days. Cache is managed by loadDashCache, saveDashCache, and clearDashCache in js/dashboard.js.The Operations Hub uses a fixed left sidebar for navigation. Each page also has a standalone URL for deep-linking and sharing.
| Page | Purpose | Standalone URL |
|---|---|---|
| Leadership Dashboard | Financial overview, portfolio, cash flow, AI commentary | index.html#overview |
| CFVs | Cash flow void detection, chase tracking, comments | index.html#cfv |
| Accounts Payable Variable | Gmail invoices, AI matching, mark-as-paid, business assignment | index.html#invoices |
| Profit & Loss | Transaction-based P&L reporting | index.html#pnl |
| Inbound Comms | Email triage, AI labels, follow-up management | follow-up.html |
| Property Compliance | GSC, EICR, insurance status tracking | compliance.html |
| Site Map & Links | Page registry, version tracking, SOP sync status | index.html#sitemap |
switchTab(), which updates the URL hash and fires a data refresh for that page.The financial overview section shows five KPI cards. All figures are calculated from live Airtable data.
| Card | How It’s Calculated | Notes |
|---|---|---|
| Opening Balance | Sum of accGBP from Santander record (rec3LiEiifomEHlvy) + TNT Mgt Zempler record (recsR9QhRKYwgV8oP) |
Shown as a single £ total with per-account breakdown in subtitle |
| Monthly Income | Two figures shown as a range: amber = In Payment only; green = In Payment + CFV Actioned. Only tenancies where tenant status is active are included. | Click to expand — shows per-tenancy breakdown sorted by due day |
| Monthly Costs | Sum of costExpected for all active fixed costs (where costInactive is not set) |
Click to expand — shows per-cost breakdown sorted by due day |
| Monthly Operating Cushion | Monthly income minus monthly fixed costs. Two figures: amber = In Payment income minus costs; green = (In Payment + CFV Actioned) minus costs | Range reflects uncertainty from CFV Actioned tenancies. Distinct from gross profit (revenue − COGS) and operating profit (revenue − fixed − variable costs). |
| Operating Cushion Margin | Operating Cushion ÷ Monthly Income × 100, shown as a % range (amber to green) | Calculated independently for low and high operating cushion figures |
expandableCard() helper from js/shared.js. The chevron (▶) rotates when open.js/config.js)| Constant | Value | Purpose |
|---|---|---|
MAINT_TARGET_GBP | £1,000/month | Maintenance spend budget |
WAGES_TARGET_GBP | £1,500/month | Wages spend budget |
CFV_TARGET_GBP | £1,500/month | CFV exposure allowance |
CLEAR_PROFIT_TARGET | £10,000/month | Target clear profit after all variable costs |
variableCostReserve | £4,000/month | Sum of maintenance + wages + CFV budgets (calculated) |
requiredOperatingCushion | £14,000/month | Clear profit target + variable cost reserve (calculated) |
Above the financial KPI cards, the reconBar section displays two cards side by side:
txReconciled is false and txAccountAlias matches one of the monitored accounts (Santander or TNT Mgt Zempler). Each row shows date, label, and amount.triggerReconciliation(this), which runs the AI matching engine from js/reconciliation.js. A status span (id="reconStatus") shows progress.TABLES.reconAudit), one row per approved transaction where the AI made a suggestion. getReconAccuracyStats() returns a cached copy for an instant paint; refreshReconAccuracyStats() runs in the background on every dashboard load and pulls rows from the last 31 days only (server-side filterByFormula, so the payload stays small). Each write calls pruneStaleAudit() which deletes rows older than 35 days — this keeps the table ~1 month of data so reads never slow down. On first load after this change ships, migrateLocalReconLog() copies any historical entries from the legacy _recon_accuracy_log localStorage key into Airtable and then clears it.From the Rental Units table (tblM3mZCR5kiEdWMj):
unitPropName lookup field).unitStatus does NOT contain “void”.unitStatus contains “void” (case-insensitive). Expandable: shows unit display name.From the Tenancies table, filtered to active tenant status (tenStatus + isTenancyActive):
tenPayStatus = In Payment. Sorted by tenDueDay asc. Expandable with surname, ref, rent, due day.tenPayStatus = CFV. Amber text.tenPayStatus = CFV Actioned. Amber text.After the tenancy cards, the dashboard checks for potential CFVs not yet flagged in Airtable. The logic:
tenPayStatus = “In Payment” and active tenant status and rent > 0:daysOver = days since the current month’s due day (0 if not yet due).daysOver ≥ CFV_TOLERANCE_DAYS (= 2 days) AND tenPaidThisMonth is falsy AND no cfv_dismissed_[tenancyId] key in localStorage…If any potential CFVs are detected, an amber banner is inserted below the tenancy cards: “N potential CFVs detected — click to review”. Clicking the banner calls switchTab('cfv') to navigate directly to the CFV tab.
cfv_dismissed_[id] key set in localStorage) is excluded from the banner count. The CFV tab itself has separate dismiss logic. To clear a dismissal, remove the key from localStorage or wait for it to be cleared on “In Payment” status reload.This section compares actual spending over the last 31 days against monthly budget targets. Transactions are filtered to the 31-day window using txDate and matched to sub-categories by sub-category record IDs in the REC constants.
| KPI Card | Data Source | Budget | Traffic Light |
|---|---|---|---|
| Rental Income (31d) | Transactions with sub-category = REC.subRentalInc (recI8yCstyDP1Nd4b) |
No budget target (shown as actual) | Green (income) |
| Maintenance Spend (31d) | Transactions with sub-category = REC.subMaint (recWomXYQ3XTgMdrr). Absolute value of txReportAmount. |
£1,000 (MAINT_TARGET_GBP) |
Green < budget, amber ≤ budget × 1.1, red > budget × 1.1 |
| Wages Spend (31d) | Transactions with sub-category in REC.subOpexLabour or REC.subCOGSLabour. Absolute value. |
£1,500 (WAGES_TARGET_GBP) |
Same traffic light logic as maintenance |
| CFV Exposure | Sum of tenRent for all tenancies with status CFV or CFV Actioned (active tenants only) |
£1,500 (CFV_TARGET_GBP) |
Same traffic light logic |
| Target Operating Cushion | Best-case operating cushion (In Payment + CFV Actioned income minus fixed costs) vs required £14,000 | £14,000 (requiredOperatingCushion) |
Green if actual ≥ target, amber if below |
Built by buildCashFlow() in js/cashflow.js. Projects daily inflows and outflows for 31 days from today’s opening balance.
tenRent, tenDueDay, and tenPayFreq.tenantPayType contains “Universal Credit” are flagged as UC via the tenancyIsUC map.tblx5kvhzNEI5TFlS) are loaded with costDueDay, costFrequency, and costDueDateNext.The table has seven columns. There is no Net column (js/cashflow.js:719-726).
| Column | Description |
|---|---|
| Date | Calendar date for the row |
| Opening | Balance at the start of the day, with any earlier recommended withdrawals already taken off |
| In | Money in for the day, leaving out anything already reconciled or unticked |
| Out | Money out for the day, including the weekly commitments described below |
| Closing | Opening + In − Out − Withdraw. Carries forward as the next day’s Opening |
| Withdraw | Cash the withdrawal advisor says is safe to take that day |
| Account | Which accounts the day’s items sit on |
All the money columns are read from the same worked-through series (waProjected, js/cashflow.js:712-714), not from the raw Airtable rows, so each row reconciles exactly: Opening + In − Out − Withdraw = Closing. The table prints that same line under its totals row (:770).
The forecast is not a plain projection of Airtable data. Two things are applied on top of it before the table is drawn.
js/cashflow.js:459-465, applied at :491). That is £470 a week, roughly £2,000 a month, and it is not held in Airtable. It lives in this browser only, under the localStorage key _wa_settings (:1192). Edit the amounts and days in the controls above the table. A different browser, or a cleared cache, falls back to those defaults.:517-552). It only takes what still leaves the safety floor intact on that day and on every day after it. Once marked, the withdrawal is subtracted from every later Opening and Closing (:546-549), so the Closing balance you are reading already assumes you took that cash. The green Safe to withdraw (31-day cycle) figure above the table is the total of them (:629).Where the safety floor comes from: the largest single expected payment in, times 30%, scaled by how late payments have actually been running (analysePaymentLag(), :502-508), rounded up to the nearest £50 and never below £500. You can type over it in the floor box, which is also stored in _wa_settings.
:429), split between Santander and TNT Zempler when the two parts genuinely add up to the total (:519-524). Row 1 of the table shows the balance box above the table, which you can type over (:456). Once edited, your figure drives the whole forecast. A mismatch warning only appears when the two are more than £200 apart (:579), so a smaller gap passes with nothing said.The AI commentary section (id="aiCommentary") is generated in renderDashboard() using live KPI data. It covers six titled sections:
Below the AI commentary the dashboard works out how to split one payment across the cards. It is a single-payment allocator, not a weekly schedule. There is no Friday timetable and no £750 buffer any more. The card list is built inline in renderDashboard() (js/dashboard.js:1950-1990) and the split is redrawn by recalcCCStrategy() (:2046).
| Card | What counts as owed | Payment due day |
|---|---|---|
| American Express | The balance if it is positive, otherwise nothing (:1958) | 28th |
| Santander Credit Card | The balance if it is positive, otherwise nothing (:1959). The old £5,500 limit-minus-available sum is gone. | 14th |
| Lloyds Credit Card | The size of the balance ignoring the minus sign (:1957) | 21st |
Any card with nothing owed drops out of the list entirely (:1966).
:1991). You can type any amount over it and the split redraws as you type.:2049). This is the avalanche method: pay the minimum on everything, then put all the rest against the dearest card.:2054). If your figure is below the total of the minimums, the total of the minimums is used instead (:2052).:2055-2059).:2077-2085).:1978-1988). It averages those, divides by the balance and multiplies by twelve. If it finds none, the card shows 0% (no interest detected) and sorts last, even if it is really the dearest card you hold. The minimum payment follows the same fork: 1% of the balance plus the average interest where interest was found, otherwise a flat 2.5% of the balance, and never less than £25 (:1989).js/cashflow.js:914-935). That surplus is the best-case 31-day net change, which counts fixed costs only. Maintenance, wages, the CFV reserve and any cash you withdraw are not in it, and it assumes the same 31 days repeat every month. Treat the number of months as the floor, not the plan.The standalone Balance Calculator has gone, along with its own opening balance box and its Select All, Deselect All, Inflows Only and Outflows Only buttons. The what-if tool now sits inside the cash flow forecast itself: open any day in the table and every expected item has a tick box beside it (js/cashflow.js:670-673 for money in, :681-684 for money out).
toggleCFExclusion(), :1148).buildWhatIfLine(), :1159-1179). It only appears once something is unticked.localStorage under the key _cf_exclusions (:1131, :1155), not in sessionStorage, so a reload does not clear them. An untick is only dropped once its date has passed (:1137-1140). Anything you untick today keeps real income or real costs out of the forecast, quietly, for as long as its date is still in the future. If the forecast looks too good or too bad, open the days and look for unticked rows, or clear the _cf_exclusions key.| Parameter | Value | Purpose |
|---|---|---|
| Look-back | 5 days before expected date | Catches early payments |
| Look-ahead | 5 days after expected date | Catches late payments |
If the transaction’s Tenancy link points at this tenancy, that settles it and nothing else is checked. A transaction linked to a different tenancy is barred from clearing this one (js/cashflow.js:132-142, :155).
Two or more keywords from the tenant name or reference appear in the transaction description. Highest confidence.
One keyword plus an amount within 5p of the expected rent.
One keyword plus an amount within 30% of expected rent, which covers letting agent deductions.
There is no amount-only match any more. Every path needs at least one word in common (:161-174). A matching amount on its own is not evidence the same tenant paid: several tenancies share the same rent figure, and matching on amount was clearing the wrong tenancy’s forecast (:128-131, :175).
| Parameter | Value | Description |
|---|---|---|
| Refresh Interval | REFRESH_INTERVAL = 15 minutes | Timer set after each successful data load via setInterval(smartRefresh, REFRESH_INTERVAL) |
| Idle Threshold | IDLE_THRESHOLD = 15 minutes | User must be idle for 15 minutes before an auto-refresh fires |
| Activity Tracking | click, keydown, input events | Each event updates lastUserActivity timestamp |
| Deferred Refresh | If user is active | Sets refreshPending = true and schedules an idle check; fires as soon as user has been idle 15 min |
loadDashboard().script.google.com and open the existing project (or create new).markPaid actions.GMAIL_SCRIPT_URL in js/config.js.js/config.js. Only update it if you re-deploy the script to a new URL.tenPayStatus = “In Payment” or “CFV Actioned”, and that their linked Tenants record has an active status. Both conditions must be met for a tenancy to appear in income calculations.CLEAR_PROFIT_TARGET + MAINT_TARGET_GBP + WAGES_TARGET_GBP + CFV_TARGET_GBP = £10,000 + £1,000 + £1,500 + £1,500 = £14,000. If the displayed value differs, check js/config.js for the budget constants.js/cashflow.js:161-174). Links: if the transaction is linked to a different tenancy in Airtable, it is barred from clearing this one (:155). Check the date, the description and the Tenancy link on the transaction.tenPaidThisMonth (a formula field in Airtable) and the 2-day tolerance. If the formula is not updating, refresh Airtable or check the formula definition. The banner disappears as soon as tenPaidThisMonth is truthy or the payment status moves off “In Payment”.markUserActive().Four cards at the bottom of the KPI grid track the north star: getting up to 90% of repeatable operational work done by AI agents. Two of them measure the outcome.
| Card | What it shows |
|---|---|
| AI Agents | How many agents are live and testing, and how much of their work is waiting for an approval. |
| Agent Approvals | Work waiting for a yes right now, plus each agent's accuracy record per type of work. |
| Work Done by AI | The share of completed work (measured in time, not task count) done by AI over the last 30 days. Target: 90% of repeatable operational work. |
| AI Time & Money Saved | The same work expressed as hours handed to AI and the labour cost that saved, over the last 30 days and since AI go-live (1 Jun 2026). Includes a "doing the work of X of a full-time person" line and a trend against the 30 days before. |
What counts as AI work (both cards use the same rules): a task completed with an AI agent on the Team Member field, or work an agent prepared that was approved first time. Work that was sent back for changes never counts as AI work, whatever the links say, because a human had to redo it.
How the money figure is worked out: every completed task carries a time estimate (how long a person would need). AI hours are those estimates summed for AI work, then valued at £17.50 per hour: the UK office admin wage plus employer National Insurance and pension. The rate and the go-live date live in js/config.js (AI_LABOUR_RATE_GBP_PER_HOUR, AI_WORK_EPOCH), so changing them is one edit. Figures are whole pounds; pence on an estimate would be false precision.
Trust guards: both cards publish the share of completed tasks carrying a time estimate and warn below 80% (a task without an estimate is invisible to the sums). An empty query shows "no data", never a fake 0% or £0. The health bar's "AI Time & Money Saved card computed" check confirms the card loaded on every sync.
| Version | Date | Changes |
|---|---|---|
| 2.94 | 3 Sep 2026 | Documentation-drift corrections. Added Strategic KPI Automation (the dashboard runs compute code held in Airtable and writes KPI values back on every load and every refresh) and corrected the list of tables touched. Rewrote the credit card section against the live avalanche allocator, including the estimated APR and the best-case “months to clear”. Replaced the forecast column table with the seven real columns and added the Withdrawal Advisor and weekly commitments. Replaced the retired Balance Calculator with the in-row what-if tick boxes and corrected the storage claim (localStorage, not sessionStorage). Removed the retired amount-only inflow match and added the linked-tenancy priority. |
| 2.93 | 28 Aug 2026 | Added AI Workforce & Labour Saved section covering the four agent cards, including the new AI Time & Money Saved card (hours + £ saved, 30-day and since go-live, FTE framing, trend). |
| 2.9 | 16 Apr 2026 | Updated budget values (wages £1,500 not £3,000; required GP £16,000 not £17,500). Added: stale-while-revalidate cache, income/profit range display, AI Reconciliation Accuracy card, Run Reconciliation button, 31-Day Operational Metrics section, CFV alert banner, budget progress bars with target markers, credit card repayment plan detail, 9 Airtable tables fetched. Corrected: auth stored in localStorage (not sessionStorage). |
| 2.3 | 12 Apr 2026 | Added Platform Navigation, Balance Calculator, Smart Refresh, mobile responsive, Google Apps Script setup sections. |
| 2.0 | Mar 2026 | Restructured into modular JS files. Sidebar navigation replaces horizontal tabs. |