Client Profitability is an admin-only page in Signal (Power Digital's internal BI web app) at /profitability that shows, per client and month, whether the revenue Power Digital collects covers an estimate of the people-hours spent serving that client. Revenue is finance's Vena contract data (the same revenue backbone as Signal's Summary dashboard); the cost side is hours that delivery employees self-report in the weekly Pulse check-in survey inside the Nova platform (question 1006), costed at a flat blended rate of $175/hour (a hardcoded code constant, not payroll data). The page shows eight KPI cards (including a "Pulse Coverage" gauge that self-audits how many delivery employees actually submitted hours), a worst-to-best margin bar chart, a revenue-vs-cost chart, a sortable per-client detail table, and an Excel export. It is read-only: nothing on the page changes any data. Visible to Admin tier and above.
Field | Value |
Route | /profitability |
Access | Admin+ (AdminRoute: isAdmin or isGandalf; others redirected to /summary) |
Nav section | "Client Health" (link flagged adminOnly; section itself is visible to non-admins) |
Primary data sources | Nova platform (weekly Pulse check-in hours — question 1006; client names; employee roster and client-team assignments); Vena contract revenue (finance); Signal's own records (manual lead corrections that shape the "delivery employee" denominator); derived cost at a hardcoded $175/hour |
Writes | None user-initiated. Passive: each page view logs a usage-telemetry event to Signal's own records (signal_usage_events, module profitability). Excel export is a client-side download only. |
Key code refs | src/views/ClientProfitabilityView.tsx, src/App.tsx (route + nav), src/services/api.ts (getClientProfitability, getPulseCoverage), server/index.ts (GET /api/client-profitability, GET /api/pulse-coverage, HOURLY_RATE, decryptLaravel), server/vena-kpi-engine.ts (getFinanceData, enrichRevenue, computeMonths), src/components/GlobalSearch.tsx (deep link) |
Last verified | staging @ 98df0c4, verified 2026-07-07 |
Purpose
Signal's Client Profitability page (/profitability) exists because Power Digital bills clients on retainers (MRR), but the true cost of serving a client is the delivery team's time — which is not captured in any finance system. The page joins the two sides of the equation:
Revenue: contracted monthly revenue per client from finance's Vena contract data — the identical data path used by Signal's Summary dashboard, so revenue figures reconcile across Signal.
Cost (estimate): hours that employees self-report per client in the weekly Pulse check-in survey inside the Nova platform (question 1006), multiplied by a flat blended rate of $175/hour (HOURLY_RATE code constant in server/index.ts as of Jul 2026).
The result is a per-client P&L proxy answering: "Which clients are we losing money on? Which are we over-servicing? Is margin healthy across the book?" The page also self-audits its own input quality with a Pulse Coverage KPI, because the cost side is only as trustworthy as the share of delivery employees who actually submitted their hours.
Important framing: the cost figure is an estimate, not payroll. It does not use actual salaries, contractor costs, or media/outsource pass-through costs. Typical users are executives and delivery/ops leadership doing monthly account-economics or resourcing reviews.
Access & visibility
The Client Profitability route (/profitability) is wrapped in the AdminRoute guard in src/App.tsx. The guard admits users where isAdmin OR isGandalf is true and redirects everyone else to /summary. Gandalf is Signal's tier above Admin; Sauron (super-admin) auto-sets both flags, so all higher tiers can see the page.
How the Admin flag is granted: isAdmin comes from Signal's own user records (Supabase app_users), read by src/context/AuthContext.tsx and managed on Signal's General Access screen (/users, Gandalf-only).
Nav visibility: the "Client Profitability" link sits in the "Client Health" sidebar section alongside Portfolio Review, POC Management, Churn Risk, Division Forecast, and Service Expansion. The section itself is visible to non-admins, but the Client Profitability link is flagged adminOnly — non-admins see the section without this link.
Deep link: Signal's Global Search offers a "Client Profitability" quick link when searching a client name — it navigates to /profitability?search=<client name>, which pre-selects that client in the Client filter (the URL parameter is then cleared). Keyword searches for "profit", "profitability", or "margin" also surface the page as a navigation result.
Endpoint-level note: the page's main data endpoint (GET /api/client-profitability) carries no auth middleware in the server code as of this verification; the companion GET /api/pulse-coverage endpoint requires authenticated admin. Access enforcement for the page is at the UI route level.
Local dev bypass: VITE_DEV_BYPASS_AUTH=true grants all roles.
The SHOW_AI_AGENTS feature flag does not affect this page.
Data sources & lineage
Everything on Signal's Client Profitability page is computed from two API calls made on page load (GET /api/client-profitability and GET /api/pulse-coverage) plus the app-wide employee/account data already loaded in DataContext. Origin systems are listed below — the warehouse and server caches are transport, not sources.
On-screen element | Origin system | Code path | Refresh | Caveat |
Hours per client per month; distinct-employee counts; non-client hours | Nova platform (weekly Pulse check-in survey, question 1006 — hours per client, entered by employees in Nova, not in Signal) | GET /api/client-profitability in server/index.ts: reads all Pulse answers for question 1006, decrypts each (decryptLaravel with NOVA_APP_KEY), groups by client and calendar month of submission date | Fetched fresh on every page load (no server cache on the Pulse side); underlying replication latency from Nova is not defined in the app repo | Answers are encrypted at rest; rows that fail to decrypt are silently skipped. Per-submission hours above 250 are clamped to 250 as input errors (code constant as of Jul 2026). Weekly submissions accumulate into the month of their submission date |
Revenue per client per month ("Revenue (MRR)") | Vena (finance's planning system; contract revenue written by finance — Signal reads only) | Same endpoint → getFinanceData() → enrichRevenue() in server/vena-kpi-engine.ts — the identical path used by /api/assignments and the Summary view | Vena contract data refreshes weekly (Blueprint Data Load); Signal's server caches the contract dataset up to 4 hours (default TTL, code constant as of Jul 2026) and the month list 30 minutes | VABO rows excluded; churned clients (churn date on/before month end) excluded; if a month has logged hours but no Vena revenue row for that client, the latest available month's revenue is substituted as an MRR proxy |
Rows for clients with revenue but zero logged hours | Vena (revenue) + absence of Pulse hours | Same endpoint, step that adds revenue-only keys for months that have hours data | Same as revenue | These clients appear with 0 hours, $0 cost, and 100% margin (green) — which can simply mean nobody logged time against them |
Client display names | Nova platform (client records) | Same endpoint; name lookup against Nova's non-deleted client list | Per page load | Fallback chain: Nova client record name → name embedded in the Pulse answer → "Client <id>" |
Cost, Profit, Margin (table columns, KPI cards, both charts) | Derived in code: cost = hours × $175 (HOURLY_RATE = 175, code constant in server/index.ts as of Jul 2026); profit = revenue − cost; margin = profit ÷ revenue | Server computes per row; view aggregates per client for the selected filter | Recomputed on filter change | Not from payroll or salary data. The rate is served in the API response (hourlyRate) and shown in the Employee Cost card subtitle; it is not configurable in any UI or table |
Pulse Coverage KPI — numerator (who submitted) | Nova platform (Pulse submission records — a submission counts even with zero hours) | GET /api/pulse-coverage (authenticated admin) in server/index.ts; the view matches submissions by submission month | Per page load | Pulse weeks run Tuesday → Monday, but this KPI attributes a submission to the calendar month of its submission date (month-based attribution replaced week-based attribution in Apr 2026) |
Pulse Coverage KPI — denominator ("delivery employees") | Nova platform (employee roster + client-team assignments) merged with Signal's own records (manual lead corrections entered by users in Signal's Manage Leads screen, team_assignments) | DataContext (employees, accounts) loaded at app start; view counts employees who lead at least one retained account | App load; roster served with a 1-hour server cache | "Delivery employee" = anyone listed as a lead on ≥1 retained account. Editing leads in Manage Leads — and, since Jul 2026, live Nova platform AD/Sr AD assignments that override Signal's cached copy for non-consulting single-division clients — changes this denominator |
Hours Distribution KPI (client vs non-client hours) | Nova platform (the nonClientHours field of the same Pulse question 1006 answers) | GET /api/client-profitability returns nonClientHoursByMonth; view computes client-facing share | Per page load | Non-client hours are month totals across all submitters; they are not attributable to a client |
Excel export | The table currently on screen (no server call) | ExportButton component, client-side | Instant | Produces signal-profitability.xlsx; respects the active month/client filters and current aggregation |
Page-view telemetry (invisible) | Write to Signal's own records (signal_usage_events) | usePageViewTracking in src/hooks/useUsageTracking.ts, module profitability | On every route change | Passive logging of user email; feeds Signal's internal Analytics view |
Metrics & definitions
All metrics below appear on Signal's Client Profitability page (/profitability). Server math lives in GET /api/client-profitability (server/index.ts); filter-scoped aggregation happens in the browser (src/views/ClientProfitabilityView.tsx).
Revenue (MRR) — total contracted monthly revenue across the clients in view, from finance's Vena contract data (VABO excluded, churned clients excluded — same filters as Signal's Summary view). If a client has logged hours in a month with no Vena revenue row, the latest available month's revenue is carried in as a proxy.
Employee Cost — total reported client hours × $175/hour (HOURLY_RATE code constant as of Jul 2026). The card subtitle shows the formula ("N hrs × $175") so the assumption stays visible. This is a blended estimate, not payroll.
Net Profit — Revenue (MRR) minus Employee Cost. Colored by the blended margin tier (below).
Avg Margin — blended margin: (total profit ÷ total revenue) × 100 across clients in view. Color tiers (code constants as of Jul 2026): red below 0%, orange 0–40%, green at 40%+. The same tiers color the Net Profit card, the margin chart bars, and the detail-table rows/cells. Whether 40% is an official company margin target is not defined in the app repo.
Per-client margin — (client revenue − client cost) ÷ client revenue × 100. A client with revenue but zero logged hours shows 100% margin; a client-month with cost but $0 revenue would compute −100%, but the view excludes $0-revenue clients entirely.
Clients Analyzed — count of distinct clients in view after filters (month, client, and the always-on "revenue > $0" filter).
Unprofitable — count of clients whose profit is negative in the selected period; the card turns red when the count is above zero.
Pulse Coverage — % of delivery employees who submitted a Pulse check-in at least once in the selected month (any submission counts, even with zero hours). "Delivery employee" = an employee listed as lead on at least one retained account (per Nova assignments plus Signal's Manage Leads corrections). Color tiers (code constants as of Jul 2026): red under 50%, orange under 75%, green at 75%+. Subtitle shows "X of Y delivery employees". This is the data-trust gauge for the whole page — low coverage means costs are understated.
Hours Distribution — % of all reported hours that were client-facing: client hours ÷ (client hours + non-client hours), with both totals in the subtitle. Non-client hours come from the same Pulse question's nonClientHours field.
Hours (table column) — sum of self-reported hours against that client in the period. Individual submissions above 250 hours are clamped to 250 as presumed input errors (code constant as of Jul 2026).
Employees (table column) — distinct employees who logged hours against the client in that month. In "All Months" mode this column sums the per-month distinct counts, so one person active for six months counts as 6.
250-hour clamp — MAX_HOURS_PER_SUBMISSION = 250 (code constant as of Jul 2026): any single client-line in one Pulse submission above 250 hours is capped at 250 (e.g. "2620" entered instead of "26.20").
How it's used
Real workflows on Signal's Client Profitability page (/profitability, Admin+ only):
Monthly margin review. Open the page (defaults to the most recent month with hours data), scan the Unprofitable KPI and the reddest bars at the top of the Client Profit Margins chart, then use the detail table's default sort (Profit ascending — worst first). Decision: which accounts need a staffing reduction, scope renegotiation, or price increase.
Investigate one client before a renewal or QBR. Pick the client in the Client dropdown, or deep-link from Global Search (/profitability?search=<client>). Compare revenue vs cost and how many employees are logging time. Decision: is the account over-serviced relative to its retainer, and by how much per month?
Validate the data before trusting it. Check the Pulse Coverage KPI first. If it is orange or red (under 75%), costs are understated and "profitable" clients may not be — chase submission compliance (Signal's Pulse Coverage view is the drill-down) before making staffing calls.
Spot over-servicing patterns across the book. Sort the table by Hours or Employees descending and cross-check margin. A mid-size retainer with many people logging time is a resourcing smell. Decision: consolidate the team or rescope.
Track non-billable drag. Watch the Hours Distribution KPI month over month: if the client-facing share drops, delivery time is leaking into internal/non-client work.
Export for finance or an offsite. Apply the month (and optionally client) filter, click Export, and hand signal-profitability.xlsx (Client, Revenue, Hours, Employees, Cost, Profit, Margin %) to finance for deeper modeling with real salary data.
What users can change here
Signal's Client Profitability page (/profitability) is read-only. There are no write paths from the page: no edits to Signal's own records, no warehouse writes, no Nova writes. The only user actions are filtering (Month dropdown, Client dropdown, Clear Filters), sorting the detail table, and the client-side Excel export.
Automatic usage logging: visiting /profitability writes a page_view event (module profitability) into Signal's own usage records (signal_usage_events) via the app-wide usage-tracking hook. This is telemetry for Signal's internal Analytics dashboard, not user-editable data.
Everything that shapes the numbers is changed elsewhere:
Hours — employees submit the weekly Pulse check-in (question 1006) in the Nova platform, not in Signal.
The delivery-employee denominator — edit lead assignments in Signal's Manage Leads screen (writes to Signal's team_assignments records; AD/Sr AD assignments can also change directly in the Nova platform and flow in on load).
Revenue — finance's Vena contract data; Signal never writes it.
The $175/hour rate, the 40% margin tier, the 250-hour clamp, and the coverage color cutoffs — code constants; changing any of them requires a code change and deploy.
Caveats & data quality
Known caveats for Signal's Client Profitability page (/profitability), verified against staging @ 98df0c4 (Jul 2026):
Cost is an estimate, not payroll. "Employee Cost" = self-reported Pulse hours × a flat $175/hour blended rate (code constant as of Jul 2026). It ignores actual salaries, seniority mix, contractor costs, and media/outsource pass-through. Who set $175 and what it represents (fully loaded cost vs target bill rate) is not defined in the app repo.
Clients with zero logged hours look perfectly profitable. The server includes clients that have Vena revenue but no Pulse hours in a month; they render with 0 hours, $0 cost, and a green 100% margin. A 100% margin usually means "nobody logged time", not "free to serve". Always read margins alongside the Hours column and the Pulse Coverage KPI.
Low Pulse Coverage silently understates cost. Every unsubmitted week is missing cost. Treat margins as upper bounds whenever the Pulse Coverage KPI is below green (75%).
MRR fallback can overstate or backfill revenue. Months where a client has logged hours but no Vena revenue row silently borrow the latest available month's revenue as a proxy — useful when finance data lags, but it means current-month revenue per client is not always literally that month's contract value.
Current-month numbers are partial by construction. Pulse is weekly (weeks run Tuesday → Monday) and Vena data for the in-flight month lags; the picture firms up after month end. Base decisions on a closed month.
"All Months" mode has aggregation quirks. The Employees column sums per-month distinct counts (people are double-counted across months), and Pulse Coverage becomes "% of delivery employees who ever submitted".
Hours land in the month of the submission date. A Pulse submitted in the first days of a month can describe work from the last week of the prior month; the code attributes it to the submission month. The Pulse Coverage KPI switched to the same month-based attribution in Apr 2026 (older screenshots may show slightly different percentages).
The 250-hour clamp masks big typos but keeps them at 250. A "2620 hours" typo becomes 250 hours of cost, not zero.
Undecryptable Pulse rows are silently dropped. Answers are decrypted server-side with the NOVA_APP_KEY environment secret; rows that fail to decrypt are skipped with only a server log, so totals can shift if key/format issues occur upstream.
Endpoint auth asymmetry. GET /api/client-profitability has no auth middleware in the server code (page access is enforced by the UI AdminRoute), while GET /api/pulse-coverage requires an authenticated admin. Whether the unauthenticated endpoint is intentional is not defined in the app repo.
Delivery-employee denominator moved under Nova's control (Jul 2026). Live Nova platform AD/Sr AD assignments now override Signal's cached lead records for non-consulting, single-division clients on app load — so the Pulse Coverage denominator can shift when someone edits an AD directly in Nova, not just via Signal's Manage Leads.
Thresholds are opinionated code constants (as of Jul 2026): 40% green-margin bar, 50%/75% coverage cutoffs, 250-hour clamp, $175 rate. None are user-tunable at runtime, and none are documented as official finance policy in the app repo.
Dead-ish response field. The endpoint still returns submittersByMonth and the view stores it, but the Pulse Coverage KPI is computed from /api/pulse-coverage submissions instead — the field is unused for display as of Jul 2026.
Related views
Pulse Coverage (/pulse-coverage, Admin+): the drill-down for this page's Pulse Coverage KPI — per-employee, per-week submission detail from the same GET /api/pulse-coverage endpoint. Fixing coverage there improves this page's cost accuracy.
Pulse Summary (/predictions/pulse-summary, Admin+) and Submit Pulse Data (/pulse/submit, Team Lead+): the Signal-side windows into the same weekly Pulse survey; the hours themselves are entered in the Nova platform.
Summary (/summary): shares the identical revenue backbone (finance's Vena contract data via getFinanceData/enrichRevenue, VABO excluded, churn-filtered), so revenue figures reconcile between the two pages.
Manage Leads (/leads, Team Lead+): defines who counts as a "delivery employee" for the Pulse Coverage denominator (lead corrections stored in Signal's own team_assignments records).
Divisional P&Ls (/divisional-pl, Gandalf-only): the finance-grade P&L view with real personnel cost lines; Client Profitability is the lighter per-client estimate.
Client Health siblings (Portfolio Review, Churn Risk, Division Forecast, Service Expansion): same nav section, complementary lenses on account health; no data flows between them and Client Profitability.
Global Search: deep-links a searched client straight into this page pre-filtered (/profitability?search=<client>).
FAQ
Q: Who can see the Client Profitability page? A: Admins and above. The /profitability route requires Signal's Admin flag (or the higher Gandalf tier); anyone else is silently redirected to the Summary page, and non-admins do not see the "Client Profitability" link in the Client Health nav section. The Admin flag is managed by a Gandalf-tier user on Signal's General Access screen (/users). If you believe you need access, ask a Signal Gandalf admin (or your manager) — this is an in-app role, not an authentication issue.
Q: Where does the cost number come from? Is it real payroll? A: No. Cost = the hours employees self-report per client in the weekly Pulse check-in survey (question 1006, filled out in the Nova platform) × a flat blended rate of $175/hour. The rate is a hardcoded code constant (as of Jul 2026) — not salaries, not contractor invoices, not media spend. Treat Cost, Profit, and Margin as directional estimates. For finance-grade cost lines, use Signal's Divisional P&Ls view.
Q: Why does a client show 100% margin? A: Almost always because nobody logged Pulse hours against that client in the selected month. The page includes clients that have revenue but zero reported hours, and zero hours means $0 estimated cost, which computes to a 100% margin (green). Check the Hours column: if it is 0, the margin says nothing about how expensive the client is to serve. Also check the Pulse Coverage KPI — low coverage understates cost across the whole page.
Q: What is the Pulse Coverage KPI and why should I care? A: It is the share of "delivery employees" (anyone who leads at least one retained account, per Nova assignments plus Signal's Manage Leads corrections) who submitted a Pulse check-in at least once in the selected month — any submission counts, even with zero hours. Color cutoffs: red under 50%, orange under 75%, green at 75%+ (code constants as of Jul 2026). It matters because the entire cost side of the page comes from those submissions: at 60% coverage, roughly 40% of delivery time is invisible, so every margin on the page is optimistic.
Q: Why don't this page's numbers match the middle of the current month? A: Two structural lags. Pulse is a weekly survey (weeks run Tuesday → Monday), so mid-month cost is only partially reported; and finance's Vena revenue for the in-flight month can lag, in which case the page substitutes the latest available month's revenue as a proxy for clients that have hours. The picture is reliable after a month closes — use the previous month for decisions.
Q: An employee logged an absurd number of hours — does that blow up the cost? A: Partially protected. Any single client line above 250 hours in one Pulse submission is clamped to 250 (code constant as of Jul 2026), which catches typos like "2620" instead of "26.20". But the clamped value still contributes 250 hours (≈ $43,750 at the $175 rate), so a known typo is worth flagging to the Signal team even though the damage is capped.
Q: Can I change the $175/hour rate or the 40% margin threshold? A: Not from the app. Both are code constants (HOURLY_RATE = 175 in server/index.ts; the 40% green-margin cutoff in src/views/ClientProfitabilityView.tsx, as of Jul 2026). Changing them requires a code change and deploy by the Signal team. Neither value is documented in the app repo as an official finance policy — confirm with finance before quoting them as company standards.
Q: How do I fix wrong hours or a missing client on this page? A: Hours are entered in the weekly Pulse check-in inside the Nova platform, not in Signal — corrections happen there (the next submission cycle, or via the Nova team). If a whole client is missing, it either had no revenue in the period (the page hides $0-revenue clients) or nobody logged hours against it while it also lacked a Vena revenue row. Nothing on the Client Profitability page itself is editable.
Q: Why did the "X of Y delivery employees" denominator change when nobody touched Manage Leads? A: Since Jul 2026, Signal merges live AD/Sr AD assignments from the Nova platform on app load, and for non-consulting single-division clients those override Signal's cached lead records. So an assignment edit made directly in Nova can add or remove people from the "delivery employee" set (leads on retained accounts) without any change inside Signal.
Q: Does the revenue here match the Summary dashboard and Vena? A: It uses the identical server path as the Summary view (finance's Vena contract data with VABO excluded and churned clients filtered out), so it reconciles with Summary. Two local differences: clients with $0 revenue in the period are hidden entirely, and hours-months missing a Vena revenue row borrow the latest month's revenue as an MRR proxy. For client revenue/performance questions beyond Signal, ask iris (iris.novapower.io).
Q: Can I export this data? A: Yes — the Export button in the filters bar downloads signal-profitability.xlsx with the detail table as currently filtered and aggregated (columns: Client, Revenue, Hours, Employees, Cost, Profit, Margin %). The export is generated in the browser from what is on screen; there is no server-side report.
