Skip to content

Latest commit

 

History

History
403 lines (350 loc) · 70.1 KB

File metadata and controls

403 lines (350 loc) · 70.1 KB

PMDash

Multi-client property management SaaS dashboard for Aptly users. Provides KPI tracking, board connections, custom metrics, and AI-powered insights. Split into an admin backend (client management) and client-facing portal (their dashboard).

Architecture

  • Single-process server: Express + Vite middleware on port 5000
  • Frontend: React 19 + TypeScript + Vite (served via Express middleware in dev, static in prod)
  • Backend: Node.js + Express (API routes, Aptly proxy, AI proxy)
  • Database: PostgreSQL (Replit built-in) — stores users, clients, boards, metrics, metric results, metric history, metric snapshots, metric groups, metric assignments, metric group assignments
  • State Management: Zustand (fetches from API, caches locally; only activeClientId persisted to localStorage)
  • Styling: Tailwind CSS v4 + CSS custom properties for theming (light/dark mode, client branding). Font: Inter (Google Fonts, loaded in index.html with font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11' for stylistic alternates). Global CSS includes custom animations (fade-in, shimmer, float, gradient-shift), glassmorphism utilities (.glass, .glass-light), card hover effects (.card-hover), .btn-accent (primary buttons using var(--accent-color) with inset highlight and shadow), custom scrollbars, gradient border utility, and focus ring styles. All primary action buttons use .btn-accent so they respond to the company's custom accent color. Design tokens: --radius-sm/md/lg, --transition-fast/base, --shadow-sm/md/lg, --border-subtle, --bg-elevated, --accent-hover.
  • Theming: ThemeContext sets CSS variables on document root. Light/dark mode is a per-user preference (stored in users.theme_preference column, exposed via AuthUser.themePreference). Falls back to client branding theme, then defaults to dark. Company branding (logo, accent color, severity colors) is still company-wide on BrandingPage. Theme toggle lives on the Account page (PasswordPage.tsx). All client-facing pages use var(--bg-primary), var(--text-primary), etc. Dark mode: --bg-primary: #030712, --bg-secondary: #0a0f1e, --bg-tertiary: #111827, --text-secondary: #a1a7b4, --text-muted: #5a6178, --border-color: rgba(255,255,255,0.06). Light mode: white/light-gray backgrounds with dark text. Severity colors: clear #10b981, warning #f59e0b, critical #ef4444. AdminLayout uses same CSS variables. LoginPage/SignupPage use var(--bg-primary) with subtle radial gradients. AdminClientView uses themed CSS variables so it matches the client's branding when viewing their portal.
  • Mobile: Responsive at 768px breakpoint via useScreenMode hook (src/hooks/useScreenMode.ts). Both AdminLayout and ClientLayout switch to hamburger drawer + bottom tab bar on mobile. Global CSS includes .mobile-hide, .mobile-stack, .mobile-full, .mobile-compact utilities and 44px min touch targets for inputs/buttons. Desktop-only features (TV mode controls, grouping toggles) are hidden on mobile. MetricsPage condition builder shows "best on desktop" hint. All forms/cards stack vertically on mobile.
  • Large Desktop: Font scales via CSS media queries in src/index.css (base 16px → 17px at xl/1280px → 18px at 2xl/1536px). Layout padding and grid columns scale at xl: and 2xl: breakpoints across OverviewPage, MetricsPage, IntegrationsPage, AdminDashboard.
  • Modal Pattern: All create/add forms (UsersPage Add/Invite User, BoardsPage Connect/Bulk Add Board, MetricsPage New Group) use centered overlay modals: fixed inset-0 z-50 backdrop with rgba(0,0,0,0.5), centered max-w container, XMarkIcon close button in header, click-outside-to-close via onClick on backdrop + stopPropagation on inner container, max-h-[85vh] overflow-y-auto for scroll containment.
  • Error Handling & Stability: React ErrorBoundary (src/components/ErrorBoundary.tsx) wraps the app in main.tsx — catches render crashes and shows recovery UI with "Try Again" and "Reload Page" buttons. Global window.unhandledrejection listener in main.tsx logs uncaught async errors. All Zustand store fetch methods log errors with [Store] prefix and set connectionError state on network failures; ConnectionBanner component (src/components/ConnectionBanner.tsx) renders an amber dismissible warning in both ClientLayout and AdminLayout when server is unreachable; auto-clears when any fetch succeeds. Server has graceful shutdown: SIGTERM/SIGINT handlers close HTTP server and drain database pool before exit (10s forced timeout). External API calls in scheduler all have explicit axios timeouts (15-60s).
  • Charts: Recharts
  • Auth: JWT-based authentication with bcrypt password hashing (10 rounds). Email-based login: Users log in with their email address (stored lowercase in username column for case-insensitive matching). Optional display_name column for screen name (shown in sidebar, avatars, metric assignments). Login uses findByUsernameOrEmail() which matches against both username and email columns case-insensitively. Strong password policy: min 8 chars, must include uppercase, lowercase, number, and special character (validated both client-side and server-side via validatePassword()). JWT tokens expire after 24h. Token versioning: each user has a token_version column; incrementing it invalidates all existing tokens (checked in authMiddleware). Account lockout: 5 failed login attempts locks the account for 15 minutes (columns: failed_login_attempts, locked_until). Lockout resets on successful login. Admin can unlock via POST /api/auth/unlock/:userId. Session invalidation: admin can force-logout a user via POST /api/auth/invalidate-sessions/:userId; users can logout all other sessions via POST /api/auth/invalidate-my-sessions. Session idle timeout: Client-side idle detection (IdleTimeout.tsx) tracks mouse/keyboard/touch/scroll activity; after 30 minutes of inactivity shows a 60-second warning countdown, then auto-logs out. Forgot password flow: user submits email → server generates a 1-hour reset token (stored in password_reset_tokens table) → logs the reset link for admin to share → user visits /reset-password?token=... to set new password. Pages: ForgotPasswordPage.tsx, ResetPasswordPage.tsx. API: POST /api/auth/forgot-password, POST /api/auth/reset-password, GET /api/auth/validate-reset-token.
  • Security Headers: Helmet with CSP (production only — blocks inline scripts except Stripe/Google Fonts), HSTS (1 year, includeSubDomains, preload), strict Referrer-Policy, X-Content-Type-Options: nosniff, Permissions-Policy (camera/mic/geo disabled, payment self only). CORS locked to pmdash.io + Replit domains + Chrome extension origins in production. Chrome extension IDs can be restricted via CHROME_EXTENSION_IDS env var (comma-separated); if unset, all extension origins allowed. Server-side input sanitization strips <> from all API request bodies (except Stripe webhooks).
  • Monday.com & Asana Integrations: Board-based integrations like Aptly. Monday.com uses GraphQL API (https://api.monday.com/v2) with API token auth; Asana uses REST API (https://app.asana.com/api/1.0) with Personal Access Token. Both use the same board_connections table with source column ('aptly'/'monday'/'asana'), same evaluateAptlyMetric evaluation, same cardCache pattern in scheduler. Board data flattened to key-value cards. Monday: items_page pagination with cursor. Asana: tasks with offset pagination, custom fields via custom_{gid} keys, sections as board field. API proxy: /api/monday/schema/:boardId, /api/monday/boards/:boardId/items, /api/asana/schema/:projectId, /api/asana/projects/:projectId/tasks. Frontend: src/utils/mondayApi.ts, src/utils/asanaApi.ts. BoardsPage source selector (Aptly/Monday/Asana tabs). Colors: Monday=#6161FF, Asana=#F06A6A.
  • Chrome Extension: chrome-extension/ — Manifest V3 Chrome extension providing a floating KPI widget on supported PM tool websites (Aptly, Property Meld, RentEngine, RentVine, LeadSimple, ShowMojo, Buildium, Boom, Appfolio, RentManager, Doorloop, Guesty, QuickBooks, Monday.com, Asana). Users sign in via popup, then see a floating PMDash button on matched domains. Clicking it opens a panel showing source-filtered KPIs with color-coded severity. Shadow DOM isolates styles from host pages. Data cached 5 minutes. Badge shows alert count for warning/critical metrics. Settings page at /settings/chrome-extension (ChromeExtensionPage.tsx) — download button (serves public/pmdash-chrome-extension.zip via GET /api/chrome-extension/download), step-by-step installation instructions, supported tools list. Settings card in SettingsPage. WelcomeChecklist step links to this page.
  • SOC 2 Controls:
    • Comprehensive audit logging: All data mutations (CRUD on clients, users, boards, metrics, integrations, settings, webhook tokens, data exports) logged to audit_logs table with user_id, username, client_id, action, resource_type, resource_id, ip_address, details. Security events (failed logins, lockouts, password changes, session invalidations) also logged.
    • Global error handler: Express app.use((err, req, res, next)) middleware catches unhandled errors, logs to audit_logs, returns generic 500 in production (no stack trace leaks). process.on('unhandledRejection') and process.on('uncaughtException') handlers log to audit before crash.
    • Security & Audit page (AuditLogPage.tsx): Admin-only page at /audit-logs with two tabs: (1) Security Overview — stat cards for failed logins 24h/7d, locked accounts, password changes; suspicious IP list (5+ failed attempts); login activity bar chart (30d success/fail ratio); recent security events feed. (2) Audit Logs — paginated, filterable, searchable audit trail with CSV export. Filters: text search, action type dropdown, date range. API endpoints: GET /api/admin/audit-logs (paginated), GET /api/admin/audit-logs/export (CSV), GET /api/admin/security-summary. CSV export uses csvSafe() to prevent formula injection (prefixes =, +, -, @ with ').
    • Client Activity Log (ClientAuditLogPage.tsx): Client-scoped audit log viewer at /settings/activity-log. Accessible by client_admin users and admins viewing a client account (via /manage/:slug/settings/activity-log). Shows plaintext activity feed with search, action filter, date range, and pagination. API: GET /api/client/audit-logs (scoped by client_id, excludes IP addresses and other sensitive admin-only fields). Settings card added under "Activity Log" in company section.
    • Data retention policy: Automated daily cleanup job purges: audit_logs > 1 year, metric_history > 1 year, metric_snapshots > 1 year, analytics_events > 90 days, expired/used password_reset_tokens. Cleanup actions logged to audit_logs. Constants: DATA_RETENTION_DAYS in server/index.js.
  • User Analytics (UserAnalyticsPage.tsx): Admin-only dashboard at /analytics showing user behavior data. Tracks: page views, button clicks, session duration, bounce rate, device breakdown, most active users. Client-filterable + time range selector (7/14/30/60/90 days). Frontend tracking via useAnalytics hook (src/hooks/useAnalytics.ts) — auto-captures page views on route change, click events on buttons/links, batches events (max 50) and flushes every 5s or on 10+ events. Session ID generated per tab. API: POST /api/analytics/events (ingestion, batched), GET /api/admin/user-analytics (dashboard queries). DB: analytics_events table with indexes on created_at, client_id, event_type, session_id, page_path. Data retention: 90 days.
    • Demo Interest Tracking: Public unauthenticated endpoint POST /api/demo-analytics (rate-limited 60/min) tracks visitor interactions on all 3 demo pages (dashboard-builder, dashboard-preview, integration-flow). Events: demo_page_view, demo_source_click, demo_metric_add, demo_metric_remove, demo_filter_click, demo_group_change, demo_tv_toggle, demo_tab_click, demo_integration_click. Stored in existing analytics_events table with client_id IS NULL. Validated: event types allowlisted, paths restricted to /demo/*, metadata sanitized (10 keys max, primitive values only). Client-side: 2s batch flush + sendBeacon on page unload. Admin API: GET /api/admin/demo-interest returns source interest, metric interest, feature usage, page breakdown. Displayed in "Demo Interest Tracking" section of WebsiteAnalyticsPage.tsx.
    • Session idle timeout: 30-minute inactivity auto-logout with 60-second warning countdown (IdleTimeout.tsx). Configurable via IDLE_TIMEOUT_MS constant.
    • Live user presence: In-memory userPresence Map (server/index.js) tracks all authenticated users. Updated on every auth'd request via authMiddleware + explicit POST /api/heartbeat (60s interval from useAnalytics). Preserves pagePath across requests (only overwrites when new path provided). 5-minute TTL with cleanup interval. GET /api/admin/live-users returns active users with userId, clientId, role, username, displayName, pagePath, secondsAgo. AdminDashboard "Live Now" panel polls every 15s, groups users by client company with links, shows page path and role badges.
  • Billing: Stripe integration (live keys via STRIPE_SECRET_KEY/STRIPE_PUBLISHABLE_KEY env vars, falls back to Replit connector for dev). Free trial model: free accounts get 1 integration, 1 metric, 1 user, and auto-deactivate after 7 days of no logins (scheduler checks hourly; users.last_login + clients.is_active columns). Deactivated free accounts are blocked at login with reactivation message; paid accounts (active/trialing) auto-reactivate on login. Paid plan at $35/month unlocks unlimited everything. Server-side enforcement via checkFreePlanLimits() on POST /api/integrations, /api/metrics, /api/auth/register, /api/auth/invite. Signup creates account with subscription_status: 'incomplete' — user logs straight in (no Stripe redirect). SubscriptionGate lets incomplete users through (trial access); only blocks canceled/past_due/unpaid. Stripe Customer Portal for managing subscriptions. Webhook at /api/stripe/webhook (registered BEFORE express.json() middleware) handles checkout.session.completed, customer.subscription.created/updated/deleted, invoice.payment_failed, invoice.paid. Stripe data synced to stripe schema via stripe-replit-sync.
    • Payment failure handling: invoice.payment_failed webhook immediately marks client past_due + creates audit log; invoice.paid webhook auto-restores past_due/unpaid clients to active + reactivates if is_active was false; subscription updates auto-reactivate inactive clients when status returns to active
    • Cancellation pending: cancel_at_period_end tracked via clients.cancel_at_period_end column; when a subscription is set to cancel at period end, SubscriptionGate shows a sticky amber warning banner with "Keep Subscription" button (opens Stripe billing portal) and dismiss option; user still has full access until cancellation takes effect
    • SubscriptionGate UX: past_due shows amber-themed "Payment failed" screen with "Update Payment Method" button (opens Stripe billing portal); canceled shows red-themed "Subscription has ended" with resubscribe option; data explicitly preserved in messaging
    • Scheduler skip: Non-paying clients (canceled, unpaid, is_active=false) are skipped by the scheduler — no compute wasted on blocked accounts
    • Referral system: Users (client_admin) can generate unique 8-char referral codes via /settings/referrals (ReferralPage.tsx). New signups accept optional referralCode param (also via ?ref=CODE query string on signup URL). When a referred company subscribes (checkout.session.completed webhook), the referrer gets a $35 Stripe balance credit (one free month). DB tables: referral_codes (id, client_id, code, is_active), referral_redemptions (id, referral_code_id, referred_client_id, status [pending/qualified/credited], timestamps). API: POST/GET/DELETE /api/referrals/codes, GET /api/referrals/stats, GET /api/referrals/redemptions. Stripe credit applied via stripe.customers.createBalanceTransaction() with negative amount (-3500 cents). Stats track total referrals, qualified, credited, total saved ($35 per credit).
  • Partner Interest Form: Integration partners can submit interest via a form on the /integrations page (bottom section). Public POST /api/partner-interest endpoint (rate-limited 5/15min). Admin manages submissions at /partner-submissions (PartnerSubmissionsPage.tsx) — table with status badges (new/reviewed/contacted/declined), expandable detail rows, admin notes. DB: partner_submissions table. Admin nav: "Partner Requests" in sidebar.
  • Support Chat: Two-way messaging between clients and admin. Clients see a floating chat widget (bottom-right on all client pages, SupportChat.tsx in ClientLayout) — create conversations with subject, send/receive messages, 10s auto-poll. Admin has full inbox at /support (SupportInboxPage.tsx) — conversation list with unread badges, message thread view, reply, close/reopen. Unread count badge in admin sidebar polls every 30s. DB: support_conversations + support_messages tables. API: /api/support/* (user) and /api/admin/support/* (admin).
  • Public User Counter: Landing page shows "Join X+ property managers" counter when total user count >= 100. Fetches from GET /api/public/user-count. Animated count-up effect using IntersectionObserver. Hidden when under 100.
  • Integration Branding Overrides: Admin can customize integration names, logos, and colors via IntegrationBrandingPage (/settings/branding/integrations). Overrides stored in system_settings as integration_branding key. Applied everywhere: React pages use useIntegrationRegistry() hook / fetchIntegrationOverrides(). Demo pages receive overrides via server-side injection (BRANDING_OVERRIDES variable injected into /*__BRANDING_DATA__*/ placeholder). All 4 demo pages (dashboard-builder, configurator, live-metrics, integration-flow) respect branding overrides. XSS-safe: JSON escaped before inline injection.
  • Integrations Info Page: Public page at /integrations — detailed breakdown of each integration (tagline, description, available metrics, how it works, setup steps). Separated into "Live Integrations" and "Coming Soon" sections. Jump-nav buttons at top, anchor links from landing page integration cards. Respects admin branding overrides via fetchIntegrationOverrides(). Includes Partner Interest form at bottom. Each live integration card links to its dedicated template library page.
  • Integration Templates Pages: Public pages at /integrations/:type/templates (IntegrationTemplatesPage.tsx) — per-integration template library showing all metric templates with full explanations, threshold details, category filtering, "Why it matters" section with use cases, and an interactive custom metric configurator demo at the bottom. Available for: rentengine, propertymeld, boom, leadsimple, buildium, rentvine, column, quickbooks, guesty, showmojo. Templates fetched from public /api/metric-templates endpoint.
  • PM Ops Standards: Templates aligned with ProfitCoach 2024 PM Operations Standards (9 processes, 40+ metrics). Templates with an opsStandard field ({ process, metric, industryMedian, industryTop25 }) display a green "PM Ops Standard" badge with industry benchmarks in both MetricCatalog.tsx and IntegrationTemplatesPage.tsx. Processes covered: Maintenance Request (Speed to Repair, Validation, Schedule, Owner Approval, Callback Rate, Aging), Marketing (Days on Market, Days without Revenue, Speed to Lease Executed), Applications (Qualification Rate), Lease Renewal (Market Rate Determination, Renewal Decision Lead Time), Delinquency (Rent Delinquency Rate). Templates exist in both server/index.js (DEFAULT_METRIC_TEMPLATES) and src/utils/metricEngine.ts (client-side with API endpoint/computation details).
  • Setup Guide Page: Protected page at /guide (SetupGuidePage.tsx) — comprehensive 16-section setup and user guide for logged-in users. Features: sticky sidebar TOC with IntersectionObserver, styled tables, FAQ cards, responsive mobile drawer. Also available as raw markdown at /PMDash_Setup_Guide.md.
  • Knowledge Base: Public page at /kb (KnowledgeBasePage.tsx) — comprehensive searchable FAQ library with 70+ articles covering every aspect of PMDash. Features: full-text search with highlighting, 12 category sidebar (Getting Started, Integrations, Metrics, AI, Dashboard, Team, Branding, Billing, Security, Data, Account, Troubleshooting), per-integration filter pills (14 integrations), accordion FAQs with inline links to demos/login pages/templates, Quick Reference table, Interactive Demos grid, Integration Login Links grid. Mobile: floating category button + slide-out sidebar. Linked from all public page footers and sitemap.
  • Playbooks: Public resource hub at /playbooks (PlaybooksPage.tsx) with 8 tactical articles at /playbooks/:slug (PlaybookArticlePage.tsx). Articles: EOS L10 meetings, daily standups, rollout guide (with email/Slack templates), top PM metrics, team productivity tracking, severity thresholds, multi-source dashboards, TV mode office setup. Each article has structured sections with headings, paragraphs, and bullet lists. Index page shows cards with heroicon categories, descriptions, and read times. "Playbooks" link added to desktop nav (LandingPage, FeaturesPage) and mobile nav (all public pages) and footers.
  • Timezone-Aware Date Formatting: All date/time displays use src/utils/dateFormat.ts utility functions (formatDate, formatDateTime, formatTime, formatShortDate, formatRelativeTime, etc.) with IANA timezone support. Default timezone: America/New_York (EST). Client-facing pages use the client's configured timezone from clients.timezone column. Admin pages hardcode EST. BrandingPage allows client admins to set their timezone under "Branding & Appearance".
  • Integration Request Poll: Voting system for integration feature requests. Clients can suggest new integrations and upvote existing requests on the IntegrationsPage (IntegrationPoll component). Duplicate protection via case-insensitive name matching with fuzzy "similar" detection. Vote toggle (upvote/remove). Status badges (New/Under Review/Planned/Released). Admin results view at /integration-requests (IntegrationRequestsPage.tsx) — sortable table with vote counts, voter companies, status management dropdown, admin notes, expandable detail rows. DB tables: integration_requests (with name_lower unique index for CI dedup) + integration_request_votes (unique per user). API: GET/POST /api/integration-requests, POST /api/integration-requests/:id/vote, GET/PATCH /api/admin/integration-requests/:id.
  • App name: PMDash (formerly Aptly Dashboard). All user-facing branding uses "PMDash".

Two-Portal Architecture

Admin Portal (role: admin)

  • Layout: AdminLayout — gray-themed backend management console
  • Dashboard: Stats overview (total clients, stale metrics count, needs attention, churn risk count). Client Health section: stale metrics alert panel (metrics with no data in 2+ hours, amber styling); top 5 client scoreboard (ranked by composite score from logins, metrics, integrations, team size) — each client shows integration logos inline; client health monitor (color-coded health score bars — green healthy ≥70, amber warning 40-69, red at-risk <40, sorted worst-first) with integration icons per client; free accounts section also shows integration icons. Data from GET /api/admin/client-health (admin-only, parallel DB queries; includes integrationTypes array per client). Platform Analytics section (AdminAnalytics.tsx): MRR (live from Stripe active subscriptions), new clients (30d), cancellations, active integrations stat cards; Integration Adoption with adoption rate percentages and "X of Y" client counts; most tracked metrics ranked list with trend indicators (growing/declining/stable arrows); Tech Stack Combinations showing integration combos (e.g., "aptly + buildium: 5 clients") with logos and bar charts; Metrics by Source showing metrics grouped by integration source with unique/total counts; client growth bar chart (12 months). Data from GET /api/admin/analytics (admin-only, parallel DB queries + Stripe API; includes techStackCombos, metricSourceBreakdown, adoptionRate, metric trend data).
  • Client Management: Create/edit/delete clients, set API keys
  • Manage Client: /manage/:clientSlug/* — drill into a specific client using their URL-friendly slug (auto-generated from company name). Falls back to client ID for backwards compatibility. Slugs are stored in clients.slug column with unique index; auto-generated on create and updated on rename.
  • User Management: Create/delete users across all clients

Client Portal (roles: client_admin, client)

  • Layout: ClientLayout — blue-themed branded SaaS dashboard (sidebar: Dashboard, Metrics, Settings)

  • Dashboard: Metric overview with severity colors, issue counts

  • Settings: Split into two sections — Company (integrations, users, AI config, branding & appearance) and Personal (account & password, profile picture, theme toggle)

    • Branding: Logo upload with automatic color extraction (Canvas API + k-means clustering). When a logo is uploaded, dominant colors are extracted and displayed as a clickable palette for selecting the accent color. Colors section includes accent color and severity colors (clear/warning/critical).
    • Integrations (/settings/integrations): 8 integration cards with partner logos (public/integrations/*.png) and brand colors; enable/disable with API key management. INTEGRATION_REGISTRY in src/types/index.ts defines type, name, description, color, iconBg, logo, hasApiKey, metricsAvailable for each partner. Default brand colors: Aptly #00B8D9, RentEngine #10b981, Property Meld #1175CC, RentVine #22c55e, Appfolio #334F74, Boom #ec4899, LeadSimple #1a96c8, RentManager #3676BC, Buildium #1a6dff. RentVine uses HTTP Basic Auth (access key + secret) with account subdomain; config stored in integration.config as {accountSubdomain, accessKey, secret}; base URL: https://{subdomain}.rentvine.com/api/manager. 12 metric templates: Active Leases, Past Due Leases, Occupancy Rate, Open/Overdue Work Orders, Total Properties, Vacant Units, Pending/Approved Applications, Owner/Tenant Count, Avg Days Vacant. Custom fields auto-discovered via GET /api/rentvine/context which samples leases, units, properties, and work orders (10 records each); discovered fields appear in the metric builder dropdown with gear icon prefix and sample values power the value dropdown. extractCustomFields() merges custom field values into cardData for all RentVine metric computations (both run-metric and scheduler). Lease items include Balance, Rent, Start/End Date, Lease Type, Address, City, State in cardData. Buildium uses Client ID + Client Secret as HTTP headers (x-buildium-client-id, x-buildium-client-secret); config stored in integration.config as {clientId, clientSecret}; base URL: https://api.buildium.com/v1. 24 metric templates covering leases, maintenance, properties, tenants, applicants, financials. runBuildiumMetric() in scheduler.js handles all Buildium API calls. Endpoints: POST /api/buildium/test-connection, POST /api/buildium/run-metric, GET /api/buildium/context. Custom fields auto-discovered via GET /api/buildium/context which samples 8 entity types (rentals, units, tenants, leases, workorders, vendors, applicants, associations); returns fields with type detection and sample values. BuildiumCustomFieldBuilder.tsx provides a step-by-step UI: discover fields → select entity → pick field → configure computation (count, count_where, count_unique, sum, average) with optional filter conditions and thresholds. Custom field metrics stored with config JSONB column (entityEndpoint, customField, computation, optional filter/match settings). runBuildiumCustomFieldMetric() in scheduler.js handles execution — fetches all records from entity endpoint, applies filters, computes result. Available in Metric Store for paid Buildium clients. Column uses API key via HTTP Basic Auth (:apiKey encoded as base64); hasApiKey: true; config stores only API key (standard pattern). Base URL: https://api.column.com. 8 metric templates: Total Balance, Checking Balance, Pending Balance, Monthly ACH Volume, Monthly Income, Monthly Expenses, Net Cash Flow, Total Accounts. runColumnMetric() in scheduler.js with paginated ACH transfer fetching via columnGetAllTransfers(). Column API returns amounts in cents (divided by 100 in all metric runners and accounts endpoint). Endpoints: POST /api/column/test-connection, GET /api/column/accounts, POST /api/column/run-metric. Logo: public/integrations/columnbank.png. QuickBooks uses OAuth2 Client Credentials (Client ID + Client Secret + Realm ID/Company ID); config stored in integration.config as {clientId, clientSecret, realmId}; OAuth2 token endpoint: https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer; API base URL: https://quickbooks.api.intuit.com/v3/company/{realmId}. 13 metric templates: Total Revenue, Total Expenses, Net Income, Profit Margin, Accounts Receivable, Overdue Invoices, Open Invoices, Accounts Payable, Outstanding Bills, Overdue Bills, Cash on Hand, Monthly Revenue, Monthly Expenses. runQuickBooksMetric() in scheduler.js uses P&L reports and entity queries. Endpoints: POST /api/quickbooks/test-connection. Logo: public/integrations/quickbooks.svg, color: #2CA01C. Admin-editable: Super admins can override integration names, colors, and logos via /settings/integrations-branding — stored in system_settings as integration_branding JSON. All consumers use useIntegrationRegistry() hook (or fetchIntegrationOverrides() for landing page) to merge defaults with overrides. Cascades to landing page and all client accounts.
    • Aptly Boards (/settings/integrations/aptly): Board connections with schema sync. Field options for select-type fields (singleselect, stageselect, multiselect, tags) are auto-discovered from card data since Aptly's schema API doesn't return them. The server matches card data keys by both field UUID and display name (case-insensitive). POST /api/boards/sync-options batch-syncs all boards needing options; called automatically by the store after fetchBoards() when boards have select fields with no options. Metric builder shows dropdown selectors for these fields instead of text inputs.
  • Metrics: Dedicated metrics page with add/edit/run capabilities; supports multi-source metrics (Aptly + RentEngine + Property Meld + Boom + LeadSimple). Metric creation uses a stepped flow: Step 1 picks the software source, Step 2 picks context (Aptly board if multiple boards exist; LeadSimple pipeline/process type/general via GET /api/leadsimple/context), Step 3 is the metric builder form. LeadSimple context selection fetches pipelines and process types from the API; selected context stores _ls_pipeline_id or _ls_process_type_id in conditions to scope data fetching. Fields and templates are context-aware (pipeline shows deal fields, process_type shows process fields). Process type stage/step names are discovered via fallback chain: embedded steps → /process_types/{id}/steps/process_types/{id}/stages → sampling actual processes for stage_name. 15 LeadSimple templates: Active Deals, Deals by Stage, Stale Deals, New Deals (Recent), Won Deals, Lost Deals, Active Processes, Processes by Stage, Completed Processes, Overdue Tasks, Tasks Due Today, Total Contacts, Total Properties, Open Conversations, Pipeline Count. Custom fields are auto-discovered by sampling records from /deals, /contacts, /processes, /properties and extracting non-builtin field names (from custom_fields, custom_field_values, customFields, or custom_data on records); they appear in the condition field dropdown with a gear icon prefix and their sample values populate the value dropdown. The extractCustomFields() helper (in both server/index.js and scheduler.js) merges custom field values into cardData so post-fetch condition filtering works on them. Templates and editing skip directly to the builder. AI Builder: users can describe what they want to track in plain English; AI generates a full metric configuration (name, type, conditions, thresholds) that pre-fills the form for review/editing before saving. Uses getAIMetricFromDescription() in aiService.ts; gated behind AI configuration and paid plan. Features:

    • Metric Groups: Custom containers created by client_admin/admin to organize metrics; metrics can be placed into groups; groups are collapsible with user assignment
    • User Assignments: Multiple users can be assigned to individual metrics or entire groups; shown as avatar initials on metric cards
    • Metric Visibility: visibility column on metrics table (VARCHAR DEFAULT 'all'). Values: all (everyone sees it), assigned_only (only assigned users + admins), admin_only (only admins/client_admins). Backend: listByUserAssignments filters visibility for client-role users. Frontend: Visibility & Assignment section in metric builder form (both custom and template edit). Dashboard "View As" dropdown (admin/client_admin only) filters metrics by selected user's perspective.
  • Users (/settings/users): User management within their company (admin/client_admin only); accessible via Settings card; when creating a user, optional assignment to metrics and metric groups. Users are natively scoped to their client account — when in /manage/:clientId context or as client_admin, users are automatically created in and filtered to the current client with no client selector needed.

    • Data Export (/export): Export metric snapshot data as CSV or JSON. Supports date range, source, and metric filters. Preview table shows data before download. Nav link in client sidebar and admin client view.
    • Incoming Webhooks (integration card in /settings/integrations): Push metric data into PMDash from external services (Zapier, Make, n8n, etc.). Each client gets a unique webhook token. POST /api/webhooks/ingest/:token accepts single or batch (up to 100) JSON payloads with metric (name), value, total_cards, issue_count, date fields. Auto-creates metrics with source "webhook" if new. Upserts snapshots and metric_results. Token management via integration toggle (generate on enable, revoke on disable, regenerate within card). DB column: webhook_token on clients table. Old /settings/webhooks route redirects to /settings/integrations.
    • Overlap Review: Detects metrics with ≥50% field overlap on the same board. Shows a review banner with counts; clicking opens a dedicated modal where admins/client_admins can Resolve (dismiss) or Unresolve individual pairs, or Resolve All. Resolved overlaps are hidden from badges and banners. Stored in overlap_dismissals DB table with client-scoped API endpoints.
  • Onboarding: WelcomeChecklist component on OverviewPage shows a 7-step setup guide for client admins: read setup guide (opens /guide in new tab, tracked via localStorage pmdash_guide_visited_{clientId}) → connect integration → build first KPI → connect AI insights (skippable) → add Chrome extension (skippable, opens Chrome docs for loading unpacked, tracked via pmdash_extension_dismissed_{clientId}) → set branding → pick theme. Only visible to admin/client_admin roles. Shows on dashboard when < 3 steps done, moves to settings page when >= 3 done. Auto-hides when all steps complete.

  • Admin Dashboard Free Accounts: AdminDashboard.tsx shows free/unsubscribed accounts via a purple stat card, inline section (top 10), and expandable modal. Backend /api/admin/client-health returns freeAccounts array (clients where subscription_status is not active/trialing) with createdAt, activity data (logins, metrics, integrations, last login). Grid is 5-column layout on desktop (Clients, Free Accounts, Stale Metrics, Needs Attention, Churn Risk).

  • Features Page (/features): SEO-rich public page with hero, 8 feature cards (Custom Metric Builder, 14+ Integrations, AI-Powered Briefings, Severity Thresholds, Team Accountability, TV Display Mode, Chrome Extension, Enterprise Security), how-it-works steps, integration logos, FAQ accordion (7 questions), and CTA sections. Light/dark theme, AnimatedSection scroll animations, useDocumentMeta for SEO. FAQ structured data (FAQPage schema) in index.html. Linked from LandingPage and PressPage nav+footer. Sitemap includes /features, /press, /changelog, /demo/dashboard-builder.

  • Conversion Tracking (/conversion-funnel): Admin-only page tracking free-to-paid conversion funnel. DB: conversion_events table logs free_limit_hit, checkout_started, subscription_activated events. Server logs events at all 7 checkFreePlanLimits call sites, Stripe checkout creation, and checkout.session.completed webhook. API: GET /api/admin/conversion-funnel returns summary stats, limit breakdown by resource type, per-client upgrade journeys (with per-client limitHitsByType), and recent events feed. Frontend: summary cards, horizontal bar chart, client journeys table, event timeline. Time range selector (7d/30d/90d/all). Admin sidebar: "Conversions" with CurrencyDollarIcon.

  • Press Page (/press): Public press release page with company overview, key facts grid, and a single consolidated launch post with sections (The Problem, The Solution, How It Works, AI-Powered Insights, Chrome Extension, Built for Teams). Linked from landing page footer. Contact: support@pmdash.io. Nav links to Changelog.

  • Changelog (/changelog): Public timeline-style changelog showing daily product updates. Entries are hardcoded in src/pages/ChangelogPage.tsx in the changelog array. Each entry has a date, title, and bullet list of items. Should be updated no more than once per day when publishing. Linked from press page nav, changelog nav links to press, and landing page footer. Tracked by public analytics.

  • Website Analytics (/website-analytics): Public page traffic analytics for marketing pages. WebsiteAnalyticsPage.tsx with admin sidebar link. Tracks anonymous visitors via usePublicAnalytics.ts hook (mounted globally in App.tsx as <PublicTracker />). Tracks page views, clicks, scroll depth (25/50/75/100%), time on page, and exit events. Events sent to POST /api/public/analytics (rate-limited, no auth, stored with resource_type='public_website' in analytics_events table). Admin API at GET /api/admin/website-analytics provides: visitor count, page views, bounce rate, conversion funnel (landing → signup → clicked signup), daily traffic chart, top pages with avg time on page, top clicks, scroll depth per page, device breakdown, referrers. Supports days (7/30/90) and page filter params. Tracked pages: /welcome, /integrations, /signup, /login, /security, /privacy, /terms, /forgot-password, /press, /demo/*.

  • AI Recommendation Prompt: Admin-only section in Settings — not visible to any client role

  • Periodic AI Reports (/reports): Automatic AI-generated operations reports using each client's own AI provider/key. Four cadences, progressively more robust: Weekly (Fridays 1pm local — brief summary, top issues, trend direction, ~500 tokens), Monthly (last day of month — category breakdown, month-over-month trends, recommendations, ~750 tokens), Quarterly (end of quarter — deep trend analysis, strategic patterns, forecasting, ~1200 tokens), Yearly (Dec 31 — comprehensive year in review, wins/challenges, seasonal patterns, forward-looking, ~2000 tokens). Server-side: server/reportGenerator.jscheckAndGenerateReports() runs every 15 min via scheduler, checks each AI-configured client's local timezone, generates reports when timing matches. Deduplication via unique index on (client_id, report_type, period_start, period_end). callProviderAI() supports OpenAI/Claude/Gemini. DB: client_reports table (id, client_id, report_type, title, content, period_start, period_end, generated_at, metadata). API: GET /api/reports (list with filter/pagination), GET /api/reports/:id (full content), POST /api/reports/generate (on-demand, admin/client_admin only), DELETE /api/reports/:id. Frontend: ReportsPage.tsx with type filter tabs, expandable report cards with markdown rendering, "Generate Now" dropdown for admins, color-coded type badges (blue=weekly, purple=monthly, amber=quarterly, red=yearly). Nav: "Reports" with DocumentChartBarIcon in ClientLayout sidebar.

  • Stale Metrics AI Diagnostics (OverviewPage): Dashboard banner detects metrics with lastCalculated > 2h ago. Amber warning banner lists stale metrics with source/hours. "Diagnose with AI" button (if client has AI configured) sends stale context to callAI() for root-cause analysis. 1-minute interval timer for time-based detection. Banner auto-restores when stale metric set changes; diagnosis resets on client change.

  • Demo Pages (SEO/AIEO): Three server-rendered HTML pages served from Express before Vite middleware, at /demo/live-metrics, /demo/configurator, /demo/integration-flow. Pure HTML (no React) for full crawler indexability. Each has meta tags, Open Graph, JSON-LD structured data. All canonical/OG URLs use pmdash.io domain.

    • Live Metrics (server/demos/live-metrics.html): Interactive metric builder — empty dashboard + builder panel; user defines name, source, conditions (field/operator/value), thresholds, goal, then card appears live with auto-updating values and sparklines.
    • Configurator (server/demos/configurator.html): 3-step flow — pick tools → data field explorer + metric builder (name/conditions/thresholds/goal) → preview custom dashboard. No templates — everything user-defined.
    • Integration Flow (server/demos/integration-flow.html): Animated visualization of data flowing from PM tools through PMDash hub into KPI cards.
  • Integration Partners Context: Blanket (blankethomes.com) is a property retention and growth platform — Investor Dashboard (branded owner portal with AI asset management), Investor Marketplace (buy/sell investment properties, new owner leads), Retention Manager (owner sentiment/churn risk tracking), Referral Manager. NARPM 2025 Affiliate of the Year. Integrates with Appfolio, RentVine, Buildium, RentManager, Propertyware, Doorloop. Brand color: #6366f1. Key data fields: Owner Sentiment, Churn Risk Score, Door Count, Referral Status, Marketplace Listing, Owner Tenure, Portfolio Value.

Project Structure

├── server/
│   ├── index.js          # Express API + Vite middleware server (port 5000)
│   ├── db.js             # PostgreSQL database layer (pg Pool, CRUD helpers)
│   ├── stripeClient.js   # Stripe SDK client (Replit connector pattern)
│   ├── webhookHandlers.js # Stripe webhook event processing
│   ├── reportGenerator.js # Periodic AI report generation (weekly/monthly/quarterly/yearly)
│   ├── seedStripeProduct.js # One-time script to create Stripe product/price
│   └── .jwt_secret       # Persisted JWT signing key (gitignored)
├── src/
│   ├── main.tsx          # React entry point
│   ├── App.tsx           # Role-based router (admin vs client portals)
│   ├── index.css         # Tailwind imports
│   ├── components/
│   │   ├── AdminLayout.tsx    # Admin backend sidebar layout
│   │   ├── ClientLayout.tsx   # Client-facing SaaS sidebar layout
│   │   ├── BoardMetrics.tsx   # Inline metric management within a board
│   │   ├── CustomSelect.tsx   # Dark-themed custom dropdown
│   │   ├── SubscriptionGate.tsx # Blocks access when subscription inactive
│   │   ├── IdleTimeout.tsx      # Session idle timeout with warning countdown
│   │   └── ScoreboardConfigurator.tsx # Embedded 3-step interactive configurator (pick tools → build metrics → preview dashboard)
│   ├── pages/
│   │   ├── LandingPage.tsx          # Public marketing/landing page at /welcome (Rentvine-inspired redesign: scroll-triggered animations, dashboard mockup in hero, glassmorphic nav, badge pill, large CTAs, alternating section bgs, hover-lift cards, gradient CTA section)
│   │   ├── LoginPage.tsx           # Shared authentication page
│   │   ├── SignupPage.tsx          # Self-service client registration
│   │   ├── BillingPage.tsx         # Subscription management settings
│   │   ├── AdminDashboard.tsx      # Admin stats overview + client list
│   │   ├── AdminClientView.tsx     # Admin drills into a client (overview/integrations/settings)
│   │   ├── OverviewPage.tsx        # Client metrics dashboard
│   │   ├── IntegrationsPage.tsx    # Software integrations hub (7 cards)
│   │   ├── MetricsPage.tsx         # Dedicated metrics management (grouped by board)
│   │   ├── ClientsPage.tsx         # Client CRUD (admin only)
│   │   ├── BoardsPage.tsx          # Board connections (sub-view of Aptly integration)
│   │   ├── SettingsPage.tsx        # API keys, AI config, password change
│   │   ├── AuditLogPage.tsx        # Security & Audit (security dashboard + audit logs)
│   │   ├── ClientAuditLogPage.tsx  # Client-scoped activity log (Settings > Activity Log)
│   │   ├── UserAnalyticsPage.tsx   # User behavior analytics dashboard (admin only)
│   │   └── UsersPage.tsx           # User management
│   ├── stores/
│   │   ├── authStore.ts        # Authentication state
│   │   └── clientStore.ts      # Client/board/metric data
│   ├── types/
│   │   └── index.ts            # TypeScript interfaces
│   └── utils/
│       ├── aptlyApi.ts         # Aptly API client + resolveAptlyApiKey() helper
│       ├── metricEngine.ts     # Metric evaluation engine (handles Aptly money objects {amount,currency}, numeric extraction)
│       ├── aiService.ts        # AI provider integration
│       └── uuid.ts             # UUID generation
├── chrome-extension/
│   ├── manifest.json     # Chrome MV3 manifest
│   ├── popup.html/js/css # Extension popup (login/status)
│   ├── content.js/css    # Content script (floating KPI widget)
│   ├── icons/            # Extension icons (16/32/48/128px)
│   └── README.md         # Installation & usage guide
├── start.sh              # Startup script (runs node server/index.js)
├── vite.config.ts
├── tsconfig.json
├── index.html
└── package.json

Aptly API Key Resolution

All Aptly API calls use resolveAptlyApiKey(clientId, clients, integrations) from src/utils/aptlyApi.ts. This helper resolves the API key with fallback: client.apiKey → enabled Aptly integration's apiKey. Only enabled integrations (i.enabled === true) are considered for the fallback. Used universally in MetricsPage, BoardMetrics, OverviewPage, TVLayout, metricScheduler, and BoardsPage.

Aptly API Field Mapping

The Aptly API (https://app.getaptly.com/api) returns schema fields with this structure:

  • key → mapped to uuid (field identifier, e.g., "name", "description")
  • label → mapped to name (display name, e.g., "Title", "Description")
  • type → kept as type (e.g., "string", "date")

Two Metric Types

Countdown Metrics (default)

Issue-tracking metrics where the goal is to reach zero daily. Counts matching cards as "issues."

  • 0 issues = Green (clear) — metric is clean
  • 1-3 issues = Amber (warning) — needs attention
  • 4+ issues = Red (critical) — urgent action needed
  • Examples: Deposits Not Managed, Stagnant Applications, Overdue Maintenance
  • CIGHY templates (12): Applications Older Than 6 Days, Applications Not Touched Today, Vacancies Not Reviewed in 14 Days, Incomplete Security Deposit Dispositions Due Today, Move-Out Inspections Incomplete Within 2 Days, Keys Not Received for Today's Move-Outs, Make-Ready Inspections Overdue/Due Today, Make Readys Not Scheduled in 3 Days, Evictions Not Filed by Day 15, Lease Renewals Not Sent by Day 30, Lease Renewal Research Incomplete After 15 Days, Keys Not Received Within 10 Days of Court Date

Trend Metrics

Data/health tracking metrics that compute an aggregate value over matching cards. No "zero is the goal" — just data tracking with benchmark comparison.

  • Aggregation methods: count, average, sum, percentage
  • Display: Shows computed value with benchmark comparison and on-track/off-track indicator
  • Color scheme: Purple themed, benchmark comparison (green=on track, amber=near target, red=off target)
  • Examples: Average Days to Lease, Occupancy Rate, Total Active Units

Shared Behavior

  • Both types use the same condition system to filter cards
  • MetricResult includes issueCount (matched cards) + trendValue (computed aggregate for trends)
  • Card properties use field labels as keys (e.g., card["Due Date"]), not internal field keys
  • DB columns: metric_type (VARCHAR, default 'countdown'), trend_aggregation (VARCHAR)
  • metric_history table stores daily trend values; recorded once per day during end-of-day snapshot; retrievable via /api/metric-history/:metricId?days=N; displayed as toggleable Recharts AreaChart on Overview trend cards
  • metric_snapshots table stores full end-of-day metric outcomes (total_cards, issue_count, trend_value) per metric per day; recorded at ~11:45-11:59 PM in each client's local timezone (configured via clients.timezone column, default America/New_York); deduplicated by (metric_id, snapshot_date) unique index; API endpoints: GET /api/metric-snapshots/:metricId?days=N, GET /api/client-snapshots/:clientId?days=N
  • Timezone picker on BrandingPage allows admins to set client timezone for snapshot scheduling
  • Overview page splits into "Daily Zero Goals" (countdown) and "Performance Tracking" (trend) sections

RentEngine Integration

Full API integration with RentEngine leasing automation platform (https://app.rentengine.io/api/public/v1).

  • Auth: JWT Bearer token via developer portal
  • Rate limits: 20 requests / 5 seconds
  • Proxy endpoints: /api/rentengine/units, /api/rentengine/prospects, /api/rentengine/leasing-performance/:unitId
  • 13 metric templates defined in RENTENGINE_METRIC_TEMPLATES (metricEngine.ts):
    • Showings: Completion Rate, Missed Showings, Upcoming Showings
    • Leads: New Leads, Lead-to-Showing Conversion, Lead-to-Application Conversion, Ghosting Prospects, Leads vs Benchmark
    • Leasing: Days on Market, Pending Applications
    • Occupancy: Unhealthy Units, Available Units
    • Response Time: Outbound Communication Volume
  • Templates include source, apiEndpoint, apiFields, and computation metadata for future automated evaluation
  • Multi-source metrics: Metrics have a source field ('aptly' | 'rentengine' | 'propertymeld' | 'boom'). External integration metrics (RentEngine, Property Meld, Boom) don't require a board connection — the form hides board selector, field selectors, and filter conditions. DB board_connection_id is nullable; source column defaults to 'aptly'.

Property Meld Integration

Full API integration with Property Meld maintenance coordination platform (https://api.propertymeld.com/api/v2).

  • Auth: OAuth client_credentials flow. User enters Client ID + Client Secret on IntegrationsPage, which exchanges them for a Bearer access token via /api/v2/oauth/token/. Fresh token obtained on each metric run via getPropertyMeldToken().
  • Auth endpoint: /api/propertymeld/authenticate (POST) — server-side OAuth token exchange
  • Proxy endpoint: /api/propertymeld/run-metric (POST, requires auth)
  • Meld endpoint: /api/v2/meld/ (singular, not plural)
  • API values are UPPERCASE: Statuses: PENDING_ASSIGNMENT, PENDING_VENDOR, PENDING_COMPLETION, PENDING_MORE_MANAGEMENT_AVAILABILITY, PENDING_MORE_VENDOR_AVAILABILITY, COMPLETED, MANAGER_CANCELED, VENDOR_COULD_NOT_COMPLETE. Priorities: HIGH, MEDIUM, LOW. Owner approval: owner_approval_status param with OWNER_APPROVAL_REQUESTED. Completed date field: marked_complete (not completed). No days_open in API — computed from created and marked_complete. Vendor scheduling: check vendorappointment array.
  • Integration config: clientId, clientSecret (encrypted in config JSONB), tenantId (optional, for multi-tenant accounts). X-Multitenant-Id header sent when tenantId is set.
  • 12 metric templates defined in PROPERTYMELD_METRIC_TEMPLATES (metricEngine.ts):
    • Open Melds, Overdue Melds, Melds Due Today, Melds Due This Week
    • Unassigned Melds, High Priority Melds, Average Days to Complete
    • Melds Open Over 7 Days, Melds Open Over 14 Days
    • Melds Without Vendor Scheduling, Melds Completed This Month, Melds Awaiting Owner Approval
  • UI: Orange-themed badges (PM) on Overview and Metrics pages; orange tab in template selector; OAuth credential fields (Client ID + Client Secret) replace the old API Key field
  • Industry benchmarks: 3.4-6.8 days avg completion, <4 min scheduling time for top performers

Boom Integration (BoomScreen)

Full API integration with Boom tenant screening platform (https://api.sandbox.boompay.app).

  • Auth: Access key + secret key exchanged for bearer token via /partner/v1/authenticate. Keys stored in integration config, bearer token in apiKey.
  • Auth endpoint: /api/boom/authenticate (POST) — server-side token exchange
  • Proxy endpoint: /api/boom/run-metric (POST, requires auth)
  • 8 metric templates defined in BOOM_METRIC_TEMPLATES (metricEngine.ts):
    • Total Applications, Pending Applications, Approved Applications, Rejected Applications
    • Application Approval Rate, Total Properties, Active Magic Links, Available Units
  • Integration UI: IntegrationsPage shows access key + secret key fields with "Authenticate & Save Token" button; also accepts direct API token paste in API Key field
  • UI: Pink-themed badges on Metrics pages; pink tab in template selector
  • Category: screening — new metric category for tenant screening metrics

API Key Encryption

All API keys and secrets are encrypted at rest using AES-256-GCM. The ENCRYPTION_KEY environment variable (32-byte hex) is required.

  • Encrypted fields: clients.api_key, clients.ai_api_key, client_integrations.api_key
  • Format: Encrypted values stored as enc:<iv>:<authTag>:<ciphertext> in the database
  • Auto-migration: On startup, any plain-text keys are automatically detected and encrypted
  • Backwards compatible: The decrypt() function returns plain text unchanged if it doesn't start with enc:, and gracefully handles decryption failures
  • Implementation: encrypt()/decrypt() in server/db.js, called in formatClient/formatIntegration (decrypt) and create/update methods (encrypt)

External Metric Template Keys

External-source metrics (RentEngine, Property Meld, Boom) store a templateKey field that maps to the server-side runner. This allows users to rename metrics freely without breaking the runner lookup. The server looks up metric.templateKey first, then falls back to metric.name for backwards compatibility with older metrics. The templateKey is set automatically when creating a metric from a template.

Template Auto-Matching

Each Aptly template includes suggestedDateField, suggestedStatusField, and suggestedBoard arrays. When a user applies a template:

  1. matchBoardForTemplate() picks the best board by matching board names against suggestedBoard hints
  2. matchFieldToBoard() resolves each suggested field name against the actual board schema (exact match first, then partial/substring)
  3. suggestedConditions are populated with matched field names, operators, and values
  4. dateField and statusField are auto-resolved
  5. When both dateField and compareField are set, metric computes date difference between them. If useTodayIfEmpty is true, cards with empty compare field use today's date (for in-progress tracking)
  • Categories: deposits, inspections, applications, lease_renewals, maintenance, move_in_out, make_ready, evictions, screening, other, custom
  • Template sections in UI: Leasing, Move-In / Move-Out, Other

Multi-Screen Support

Three optimized screen modes detected via useScreenMode() hook (src/hooks/useScreenMode.ts):

TV Mode (/tv route)

  • Full-screen passive dashboard with no sidebar, no interactive buttons
  • Auto-refreshes all metrics every 5 minutes
  • Shows company logo, live clock, and refresh status in header
  • Large typography (text-6xl numbers) for wall-mounted displays
  • 4-5 column grid on large screens
  • No modals, no click handlers, no user avatars
  • Auto-rotation: When both countdown and trend metrics exist, automatically cycles between "Daily Zero Goals" and "Performance Tracking" every 90 seconds. Shows section indicator pills (blue/purple) and a progress bar. Section headers remain visible. AutoFit density scales per-section.

Mobile Mode (<=768px viewport)

  • All layouts (ClientLayout, AdminLayout, AdminClientView) use:
    • Hamburger menu in top bar, sidebar opens as overlay
    • Fixed bottom navigation bar with primary nav items
    • Content has pb-20 padding to clear bottom nav
    • Reduced padding (p-4 vs p-6)
  • OverviewPage: single-column grid, compact cards with smaller text
  • MetricsPage: stacked form fields, wrapped buttons, larger touch targets
  • SettingsPage: single-column severity color grid

Desktop Mode (>768px)

  • Auto-fit density: OverviewPage dynamically scales card sizing based on total metric count to avoid scrolling. autoFit config computed from filteredMetrics.length:
    • ≤6 metrics: standard (3 cols, p-5, text-4xl, summary cards shown)
    • 7-9 metrics: comfortable (3 cols, p-4, text-3xl)
    • 10-14 metrics: compact (4 cols, p-3, text-2xl, summary hidden)
    • 15-20 metrics: dense (5 cols, p-2.5, text-xl, assigned users hidden)
    • 21+ metrics: ultra-dense (6 cols, p-2, text-lg)
  • Scales: grid columns, card padding, font sizes, gap, section spacing, border radius, badge sizes, section headers

Key Features

  • Two-portal architecture: Admin backend + client-facing SaaS portal
  • Multi-client management: Admin creates clients; client_admin users manage their own company's API keys and users
  • Board connections: Connect Aptly boards by ID, auto-discover schema
  • Dedicated Metrics page: Metrics managed via a standalone Metrics tab, grouped by board; also accessible inline within BoardMetrics
  • Schema change detection: Detects added/removed/changed fields on sync
  • Pre-built metric templates: Deposits, inspections, applications, lease renewals, maintenance
  • Custom metric builder: Flexible conditions with multiple operators
  • AI insights: Locked until client provides their own API key (OpenAI/Claude/Gemini)
  • AI metric recommendations: Analyzes board fields against industry benchmarks (admin-editable prompt stored in system_settings table); accessible from Metrics page; AI Recommendation Prompt editor is admin-only and hidden from all client accounts
  • Settings page: Tab-based navigation hub with cards linking to sub-pages: Integrations, Users, AI Configuration (/settings/ai), Branding (/settings/branding), Account & Password (/settings/password). Each sub-page has back navigation. AI Recommendation Prompt only shown in admin portal when no client is selected. Admin-only "Platform Integrations" section (when no client selected) toggles coming-soon integrations globally via enabled_coming_soon_integrations system setting.
  • Admin AI Diagnostics: Admin can configure their own AI provider/key (stored encrypted in system_settings as admin_ai_provider + admin_ai_api_key). Settings UI in AISettingsPage when no client selected. "Diagnose with AI" button in the Stale Metrics modal (AdminDashboard.tsx) sends metric data to POST /api/admin/ai-diagnose for root-cause analysis. Access restricted to admin role. API key encrypted at rest using AES-256-GCM (same encryption as client AI keys). Context size limited to 10K chars. Settings read endpoint (GET /api/settings/:key) blocks non-admin access to admin AI keys via ADMIN_ONLY_SETTINGS allowlist.
  • Default metrics: When a paid client enables an integration with credentials, universal metric templates are auto-created as "default" metrics (is_default = true, is_enabled = false). Paid plans get all templates created; free plans get NO auto-created templates — they pick from the Metric Store catalog instead. Templates: RentEngine (13), PropertyMeld (12), Boom (8), LeadSimple (11), RentVine (12), Buildium (23), Column (8). Defined in DEFAULT_METRIC_TEMPLATES in server/index.js. Clients can toggle individual defaults on/off via a switch on MetricsPage (violet "Default" badge). Disabled defaults (is_enabled = false) are hidden from OverviewPage, skipped by scheduler, and hidden from free users on MetricsPage. Free plan metric flow: Free accounts start with NO metrics. They pick 1 metric from the Metric Store catalog (MetricCatalog.tsx). The catalog uses POST /api/metrics/add-from-template which creates the metric as enabled. Limit: 1 enabled metric. To swap, they must remove their current metric first. MetricsPage hides Templates/Custom builder buttons for free users — only shows "Add Metric" (catalog). Purple banner shows contextual messaging. createDefaultMetrics() skips entirely for free plan users. checkFreePlanLimits('metric') counts all enabled metrics (is_enabled = true).
  • Custom-only integrations in Metric Store: Connected integrations that have metricsAvailable: true but no pre-built templates (e.g., Aptly, Monday.com, Asana, ShowMojo) appear in the Metric Store sidebar as "custom-only" sources. When selected, they show a branded landing page with the integration logo, description, custom metric count, and a "Build Custom Metric" button that routes to the appropriate builder flow (board selection for board-based sources, builder for external sources). Board step is source-aware — filters boards by source column and shows source name dynamically. ShowMojo included in isExternalSource checks and INTEGRATION_FIELDS map.
  • Coming Soon integrations: RentVine, Appfolio, LeadSimple, RentManager are "Coming Soon" by default (metricsAvailable: false). Cards show grayed out with badge, no toggle for clients. Admin can globally enable each via Platform Integrations toggles in Settings (stored in system_settings as JSON).
  • Overview page tabs: When both countdown and trend metrics exist, a tab bar separates "Daily Zero Goals" and "Performance Tracking" into independent views. Tabs show badge counts. AutoFit density scales per-tab. TV mode shows both sections without tabs.
  • Role-based access: Three roles — admin (global), client_admin (manages users/API keys for their company), client (view-only within their company, only sees assigned metrics/groups)
  • User invite system: Admins/client_admins can invite users with auto-generated temp passwords; invite text is copy-pasteable; invited users must change their password on first login
  • JWT authentication: Backend auth with password hashing; mustChangePassword flag in JWT forces password change screen
  • Security hardening: Helmet middleware for HTTP security headers, rate limiting on auth endpoints (login 10/15min, register 5/hr, password change 5/15min), CORS with credentials
  • Audit logging: audit_logs table tracks login, login_failed, password_change, user_created, user_deleted, metric_created, metric_deleted, integration_added, integration_removed, settings_changed. db.auditLog() helper. Admin-only GET /api/audit-logs endpoint with pagination and filtering.
  • Security page: Public /security route showing PMDash's security practices (encryption, auth, RBAC, infrastructure, audit logging, compliance). Landing page includes trust badges section.
  • Client-side scheduler: Browser-based polling every 5 min for field changes, full recalc every 30 min
  • Server-side scheduler (server/scheduler.js): Runs every 15 min automatically for ALL clients (Aptly, RentEngine, PropertyMeld, Boom, LeadSimple, RentVine, Buildium, Column metrics); skips metrics run within last 10 min; first batch runs 60s after server start. Processes clients in batches of 5 concurrently (CLIENT_CONCURRENCY=5). Uses per-client result fetching instead of global listAll(). Overlap guard prevents concurrent scheduler runs. Includes automatic pruning: audit logs (90 days), metric history (365 days) — batched deletes of 1000 rows at a time to avoid table locks. Deactivates inactive free accounts hourly.

Demo Account

  • Persistent demo client: Auto-created on startup with is_demo = true flag; client ID de000000-0000-0000-0000-000000000001
  • Credentials: demo@pmdash.io / Demo1234! (client_admin role, must_change_password = false)
  • Reset: Admin-only POST /api/admin/reset-demo endpoint wipes all metrics, results, history, snapshots, assignments, groups, boards, integrations, branding, audit logs, and users — then recreates the demo user with default credentials. Triggered via "Reset Demo" button on AdminDashboard (violet themed)
  • Protection: Demo client cannot be deleted via DELETE /api/clients/:id or DELETE /api/my-company; exempt from scheduler deactivation of inactive free accounts
  • Onboarding: After reset, the WelcomeChecklist naturally reappears since all data is wiped
  • Badge: Violet "DEMO" badge displayed on the demo client card in AdminDashboard

Scalability

  • DB pool: 50 max connections, 10s connection timeout
  • Scheduler concurrency: 5 clients processed in parallel per batch cycle; overlap guard prevents stacking runs
  • API rate limiting: 120 requests/minute per IP on all /api/ routes (static assets excluded); auth endpoints have stricter limits
  • Data pruning: Audit logs pruned after 90 days, metric history after 365 days (batched deletes, runs daily)
  • Indexing: All foreign keys indexed (client_id, metric_id, board_connection_id, user_id, group_id); unique constraints on slugs, assignments, snapshots
  • Auth: Stateless JWT (no session store); no in-memory state that grows with client count
  • Delete account: Transactional cascade delete of all client data (metrics, results, history, snapshots, assignments, groups, boards, integrations, users)

API Endpoints

Auth

  • POST /api/auth/login - Login (includes mustChangePassword flag)
  • POST /api/auth/register - Register (admin or client_admin; client_admin scoped to own company)
  • POST /api/auth/invite - Invite user with auto-generated temp password (admin/client_admin)
  • GET /api/auth/users - List users (admin sees all or filtered by ?clientId=, client_admin sees own company)
  • GET /api/auth/me - Validate token / get current user
  • DELETE /api/auth/users/:id - Delete user (admin or client_admin within their company)
  • POST /api/auth/change-password - Change password (skips current password check for forced changes)

Audit Logs

  • GET /api/audit-logs - List audit logs (admin only, supports ?limit=, ?offset=, ?clientId=, ?action=)

Clients (require auth)

  • GET /api/clients - List clients (admin sees all, client users see own)
  • POST /api/clients - Create client (admin only)
  • PUT /api/clients/:id - Update client (admin or own client)
  • DELETE /api/clients/:id - Delete client (admin only, cascades boards/metrics)

Boards (require auth)

  • GET /api/boards - List boards (admin sees all, client users see own)
  • POST /api/boards - Create board connection
  • PUT /api/boards/:id - Update board
  • DELETE /api/boards/:id - Delete board (cascades metrics)

Metrics (require auth)

  • GET /api/metrics - List metrics
  • POST /api/metrics - Create metric
  • PUT /api/metrics/:id - Update metric
  • DELETE /api/metrics/:id - Delete metric

Metric Results (require auth)

  • GET /api/metric-results - List all metric results
  • PUT /api/metric-results/:metricId - Upsert metric result

Integrations (require auth)

  • GET /api/integrations?clientId= - List integrations for a client
  • POST /api/integrations - Create integration (admin/client_admin)
  • PUT /api/integrations/:id - Update integration (admin/client_admin)
  • DELETE /api/integrations/:id - Delete integration (admin/client_admin)

Aptly Proxy (all require auth)

  • GET /api/aptly/schema/:boardId - Get board schema (maps key→uuid, label→name)
  • GET /api/aptly/boards/:boardId/cards - Get board cards (paginated)

Settings (require auth)

  • GET /api/settings/:key - Get system setting value
  • PUT /api/settings/:key - Update system setting (admin only)

AI (requires auth)

  • POST /api/ai/insight - Generate AI insight (supports systemPrompt, maxTokens optional params; client provides their own key)
    • Models: OpenAI gpt-4o, Anthropic claude-sonnet-4-20250514, Google gemini-2.0-flash
    • System prompt support for all providers (OpenAI: system message, Claude: system field, Gemini: multi-turn preamble)
    • Max tokens default: 2000, timeout: 60s
  • AI Briefing: Dashboard-wide summary button that analyzes all metrics at once via getAIDashboardSummary() — generates operations briefing with health assessment, critical items, patterns, and action plan
  • Metric Insights: Per-metric AI analysis now includes up to 50 items with full field data, metric history trends, portfolio context (other metrics), group membership, and structured prompts for countdown vs trend metrics
  • AI Recommendations: Board field analysis via getAIMetricRecommendations() with admin-configurable system prompt containing industry benchmarks. Smart Board Recommendations: POST /api/ai/board-recommendations server-side endpoint analyzes board name + fields, checks ALL existing metrics (Aptly + PMS) for deduplication, biases toward countdown/daily-zeros metrics for process tracking, and asks for clarification if the board's purpose is unclear. Returns structured JSON recommendations or a clarification question. BoardAIRecommendations.tsx renders inline within expanded boards — shows AI suggestions with one-click "Add" or "Add All" buttons, clarification input when AI needs context, and "Set Up AI" CTA for unconfigured clients. Auto-triggered when a new board is connected.

Default Credentials

  • Username: admin
  • Password: admin123