Reviews can be per-shift now, the reviewee can post a response under any review, and there's a dispute path moderated by admins. Two-factor authentication is live in account settings. Plus the off-session 3DS / SCA stuck-charge surface from earlier in the day.
Tier-3 strategic moat #1. The morning's rate-badge embed seeded this (per-business + opt-in surface). This batch brings the cross-business going-rate signal inline, in the two places where a rate is actually being decided: the business posting a shift, and the shifter looking at an open broadcast.
GET /v1/going_rate. Takes a geographic filter (city_id, state_id, country_id) and an optional shiftrole_id. Returns min, p25, median, p75, max, mean, sample size, currency, and the time window (default 90 days, env-tuneable). Privacy guard returns sufficient_data=false when fewer than 10 rows in the slice (env-tuneable), so a thin (city, role) combination can't reverse-engineer a single business's number./shifter/open-shifts. Every broadcast row gets a "Market $X-Y" line under the offered rate, with a ↓ icon when the offer is below market p25 and a ↑ when it's above market p75. So a shifter can scan the page and see at a glance which offers are competitive. Cached client-side per (city, role) so the page fires unique combos in parallel, not one call per row.Tier-3 strategic moat. Three more shipments on top of the morning's two embed batches. The catalog is now 6 widgets, with dedicated landings for the two biggest no-code platforms.
data-role. First surface of the wage-transparency moat (per-business + opt-in, so it sidesteps the harder cross-business-aggregation question for v1). Drop-in: <script src="/embed/v1/rate.js"></script> + <div data-shiftsee-rate data-slug="acme-bar"></div>./for/wordpress and /for/shopify landing pages. Marketing surfaces targeting partner-platform searches. Each walks the install in 4 steps, covers all the relevant widgets, and lists use cases. Registered before the /for/{slug} catch-all so they take precedence over the data-driven category-page system. Cross-linked from the gallery, and the gallery is cross-linked from each landing.shiftsee.liquid snippet that renders any of the four no-auth widgets via one {% raw %}{% render 'shiftsee', widget: 'profile', slug: '...' %}{% endraw %} include. No app install, no app review queue, works on every Shopify plan including Basic. Downloadable via /embeds/shopify.liquid straight from the gallery or the dedicated /for/shopify landing. Origin overridable via the snippet's origin parameter for staging or self-hosted ShiftSee deploys.[shiftsee_rate] shortcode so WP partners can use the new primitive on day one too. Build pipeline grows from 5 to 6 IIFE bundles in bin/build-embeds.mjs.Tier-3 strategic moat. Three more shipments on top of this morning's profile + shifts + configurator landing. The catalog grows from 4 widgets to 5, with a public-facing gallery and a literal WordPress plugin so a partner can ship in 60 seconds.
dispute_status='upheld' to match the public list endpoint), average rating headline, total count, reviewer name and photo, plus the reviewee's response if posted. Same iframe + auto-resize + light/dark themes as the others. Available at <script src="/embed/v1/reviews.js"></script> plus <div data-shiftsee-reviews data-kind="b" data-slug="acme-bar"></div>./embeds. Marketing surface that walks through all 5 widgets with live preview iframes alongside copy-able snippets. Footer link under "Developers." Default demo slugs configurable via GALLERY_DEMO_BUSINESS_SLUG / GALLERY_DEMO_SHIFTER_SLUG env so staging and prod each show their own demo accounts. The gallery is the front door; the developer portal's configurator is what you reach for after you've decided.resources/wp-plugin/shiftsee/ in the repo. Registers three shortcodes: [shiftsee_profile], [shiftsee_shifts], [shiftsee_reviews]. Each emits a deduped script tag plus the matching mount <div>. Multiple shortcodes of the same type on a page reuse the loaded script (one network request per widget type). Origin overridable via shiftsee_embed_origin filter for staging or self-hosted deploys. Downloadable ZIP via /embeds/wordpress.zip built on demand from the source files using ZipArchive; the gallery has a "Download ZIP" button right next to the install steps.Tier-3 strategic moat. Second AI-adjacent investment of the day. Building on the Partner API + Connect button + Schedule widget that shipped earlier, we now have two simpler embeds that need no auth and a live snippet generator that turns the partner's "ship in an afternoon" loop into "click dropdown, see preview."
<div data-shiftsee-profile data-kind="b" data-slug="acme-bar"></div> and you get the card with logo, name, short bio, "Connect with us on ShiftSee" CTA linking back to the public profile. Iframe boundary keeps the partner's CSS from clobbering our typography. Light + dark themes via data-theme. Auto-resizes to content via postMessage so the iframe never gets an internal scrollbar.is_broadcast=1, request_status=1 rows joined to shiftroles for the role label. Per-row date / time / hourly rate, "View business" CTA at the footer. Same iframe + theme + auto-resize pattern as the profile embed./developers/configurator tab. Pick a widget from the dropdown (Profile, Shifts, Connect button, Schedule widget), fill in the minimum params, see the rendered iframe alongside a copy-able <script> snippet. Profile and Shifts preview live; Connect and Schedule show snippet-only because their preview requires the partner's actual OAuth client.<script> tag plus the matching <div>. Business-mode shows both profile + shifts snippets; shifter-mode shows the profile snippet only.bin/build-embeds.mjs entries list extended to four (connect, schedule, profile, shifts). Each builds as its own self-contained IIFE to public/embed/v1/<name>.js. Same versioning model as before (path-segment /v1/), so v2 ships parallel without breaking pinned partners.Tier-3 strategic moat. First AI feature in the product. A per-business chat assistant that knows your operational state and can suggest one-click actions.
/business/copilot. Dashboard link added next to Labor costs.invite_shifter (fires copilot:invite-shifter event for the existing invite flow to subscribe to next iteration)open_shift_form (links to dashboard with ?openShift=1&date=&in=&out= prefill)open_url (relative paths use spaNavigate; full URLs open in a new tab)ai_chat_messages table persists the full thread per business with token usage (prompt + completion) on each assistant row for cost attribution. Indexed on (business_id, id) so thread render is one keyed range scan./v1/ai/chat/business/{business_id}: GET returns recent thread (50 most recent in chronological order); POST accepts {message} and returns {user_message, assistant_message}.business_colab teammate (status=1) OR admin. So an admin teammate added via Settings → Team has copilot access too.claude-haiku-4-5-20251001 (override via AI_COPILOT_MODEL env). Anthropic call goes through a shared keep-alive httpx client (10 max connections, 30s timeout).ANTHROPIC_API_KEY is missing, endpoints still work and the assistant replies "Copilot is not configured on this environment yet." A faded notice appears at the top of the page until the secret is wired. Lets the UI ship before the key is provisioned.Three quality-of-life items that close out the Tier-2 roadmap before the AI scheduling work begins.
/business/payments next to "Payment methods" emits a flat CSV (invoice_id, shift_request_id, shift_date, shift_in, shift_out, shifter_id, shifter_name, amount, currency, conversion_amount, conversion_currency, status, paid_at) for the current filter (All / Pending / Paid / Failed). One-shot fetch up to 5000 matching rows so a year of history downloads in one click; bookkeeping use case wants the whole year in one file. Filename is shiftsee-payments-YYYY-MM-DD.csv. No new endpoint, just a wider read against the existing /v1/invoices/list.business_colab.user_id in addition to the owner. Connection requests, shift requests, change requests, accept / decline / broadcast claim, clock in, and clock out are all teammate-aware. Card creation and payout notifications stay owner-only by design (financial events live in fewer hands). Owner is included even if the colab table doesn't list them, and recipients are deduped so legacy rows don't double-push. New helper send_notify_to_business(db, owner_user_id, business_id, notify_data) in push_notify_crud.py; if the colab read fails the original owner-only path still fires.templates:auto-generate artisan walks every shift_templates row with is_active=1 and inserts pending shift_requests for the rolling next 4 weeks. Idempotent (matches the manual "Generate next 4 weeks" button's dedup: same business + date + start time, plus shifter when the template targets a default shifter), so re-running daily only inserts the newly-needed tail day. Scheduled in routes/console.php for 03:15 Pacific. --dry-run and --weeks=N flags for ops.business.auto_fill_on_cancel column (bool default false). When on, a shifter-cancelled accepted shift is automatically re-broadcast to the connected crew the moment the cancellation lands, provided there's enough lead time before the start (default 2h, configurable via SHIFT_AUTO_FILL_MIN_LEAD_HOURS).is_broadcast=1 + request_status=1 + parent_id linking back to the cancelled row so the chain is traceable.BusinessSettings + BusinessFullOut schemas grow optional weekly_labor_budget + auto_fill_on_cancel, so the existing update_settings endpoint persists both alongside the others (exclude_unset=True already in place from the clock-in v2 work)./business/labor shows weekly running labor cost with vs-last-week comparison, optional weekly budget + over/under signal, top shifters and roles for the active week, and an 8-week trend sparkline.business.weekly_labor_budget column (decimal, nullable). NULL = no budget; the dashboard skips over/under visuals./v1/business/{id}/: GET /labor_costs?week=YYYY-MM-DD (defaults to current Monday) returns {this_week_total, last_week_total, pct_change_vs_last_week, weekly_budget, over_under_budget, by_shifter[], by_role[], last_8_weeks[]}. PUT /weekly_labor_budget accepts {weekly_labor_budget: number|null}. Auth: business owner or admin only.payment_invoice for the precise figure./shifter/requests next to Accept / Decline. One click opens the existing ProposeChangesModal pre-filled with the suggested rate.is_auto_apply column) for a follow-up./v1/shifter/{id}/counter_rules/*: list, create, update (PATCH-style), delete. Auth: own shifter or admin only. New error code RULE-NOT-FND-ERR.evaluateCounterRules(rules, shift) exported from the panel for any other surface that wants to consume the same logic.shifter_mileage table: free-form log (log_date, miles, rate_per_mile_cents, purpose) deliberately NOT coupled to specific shift_requests so the shifter can record any work-related drive (uniform pickup, multi-gig trip, etc.).rate_per_mile_cents captured at write time so the historical figure persists when the IRS standard rate changes year over year. Default rate read from SHIFT_MILEAGE_RATE_CENTS env var, fallback 67¢ (US IRS 2024 standard)./v1/shifter/{id}/mileage/*: list (with year rollup totals), create, update, delete. Auth: own shifter or admin only (financial data is private)./shifter/earnings between the trend and by-business breakdown. Shows year-total miles + deductible value at the default rate, full table of entries with Edit / Delete actions, "Log miles" button opens an inline modal.monthly_earnings_goal column on shifters. New endpoint GET /v1/shifter/{id}/earnings?year=&month= returns this-month gross + projected end-of-month (linear extrapolation when ≥3 days have elapsed) + typical hourly rate (mean over last 90 days) + typical shift hours + by-business breakdown for the year + 12-month trend + shifts-to-goal calc when the goal is set. Goal-set endpoint at PUT /v1/shifter/{id}/earnings_goal. Both auth-gated to the owning shifter; earnings data is sensitive enough that we don't expose it on public surfaces.ShifterEarningsWidget): self-contained card on the shifter dashboard. Shows current month gross + projected month-end + goal progress bar + "X more shifts at your typical rate to hit it." Hidden when no shift data exists yet so brand-new shifters don't see an empty surface./shifter/earnings page: full breakdown. Four headline tiles (this-month / projected / typical rate / goal), inline-SVG 12-month sparkline trend, by-business table for the selected year (year filter dropdown), CSV export with year-specific filename for tax time, goal edit dialog.shift_in → shift_out) × hourly_rate summed across completed shifts (request_status=2 + shift_end_at + !is_noshow). Numbers are gross before platform fees; CSV export labels this clearly so the shifter knows their bank statement is the source of truth for net.shifter_credentials table: shifters upload food handler cards, bartender licenses, CPR, driver's licenses, anything they want connected businesses to see. Each row has a label, optional issuer, optional issued / expires dates, optional notes, optional uploaded document (PDF or image up to 8 MB), and an is_visible toggle./v1/shifter/{id}/credentials/*: list (visible-only for non-owner reads, full list for owner / admin), create, update (PATCH-style), delete, document upload (multipart). Auth: only the owning shifter can write.ShifterCredentialsPanel)./s/{slug}) lazy-loads the visible credentials and renders them under the work-experience section. Expired credentials get a red "expired" pill so businesses see at a glance which certs need a refresh. Section stays hidden when the shifter has no visible credentials./shifter_credentials/{shifter_id}/. Same upload pattern as the shifter photo + clock-in photo (see business_routes / shifter_routes for the pattern).app/models/shifter_credential.py.POST /v1/shift_request/list_of_broadcast_requests accepts a new sort_by_relevance flag. When set (and the caller provides shifter_id), the server scores every matching row per-(broadcast, shifter) and returns the result ranked best-fit first instead of newest-first.ShiftRequestOut gains an Optional relevance_score (0-100) populated when relevance sort is on; absent otherwise. Mobile clients on the v1 contract ignore unknown fields./shifter/open-shifts page sets the flag automatically; high-relevance rows (score ≥ 70) get a green BEST MATCH badge with a hover-tip explaining the inputs.SendShiftRequestModal (broadcast mode) callout updated to explain "Smart sorting on: shifters see your broadcasts ranked by best fit." So operators know the algorithm is behind the marketing claim.GET /v1/shift_request/conflicts?shifter_id=&date=&shift_in=&shift_out=&exclude_shift_request_id= returns the accepted shifts for a shifter on a single day that overlap a proposed time window. Same overlap rule both server- and client-side: [a_in, a_out) and [b_in, b_out) overlap iff NOT (a_out <= b_in OR a_in >= b_out).SendShiftRequestModal (targeted mode) now debounce-fetches conflicts when shifter + date + shift_in + shift_out are all set. An amber "Possible double-booking on Saturday, May 11" banner lists the conflicting shifts (other business name + role + time) above the form. Doesn't block submit; the operator can still proceed if they've worked it out with the shifter.CalendarView (month) and CalendarTimeAxisView (day / week) compute same-day overlaps client-side from the loaded shifts and render an amber OVERLAP badge + inset glow on every conflicting block. Hover-title says "OVERLAPS another shift on this day." Cross-midnight wrappers (rare) are conservatively treated as overlapping the rest of the day.SendShiftRequestModal (the dashboard's primary "post a shift" surface, used by both targeted and broadcast modes) gets two template-aware controls./v1/shift_templates/* endpoints the standalone Templates page uses; nothing new on the backend./business/bulk (Tier-2 roadmap). Paste a CSV (or TSV: Excel / Google Sheets paste works directly), preview the parsed rows with per-row validation, send the valid ones.date, shift_in, shift_out, role, hourly_rate, shifter, note, is_broadcast. Header row optional. Leave shifter blank for a broadcast./v1/shift_request/send_request per row at concurrency 5 (caps the burst). Per-row status updates as each request resolves; failed rows show the server's error message and stay queued for retry.ShifterConnectionCard.tsx had a Message button permanently disabled with "Messaging is coming soon," but messaging shipped on 2026-05-06. The button is now wired to /messages#new=shifter:<user_id>, the same deep-link convention the Crew tab on /b/{slug} uses. Disabled only when the shifter row has no user_id./business/templates (Tier-2 roadmap). Operators describe a recurring slot once ("Saturday brunch crew · Sat · 9-1 · $25/hr · broadcast") and click Generate 4 weeks to materialize a batch of pending shift_requests. Re-running Generate skips slots that already exist (same business + shifter + date + start time), so it's safe to run weekly.shift_templates table with business_id, label, days_of_week (CSV), shift_in, shift_out, shiftrole_id, hourly_rate, currency_id, is_flexible, is_broadcast, default_shifter_id, note, is_active. Recurrence model is intentionally simple in v1 (weekly + days-of-week); a frequency column is reserved for a future biweekly / monthly expansion.GET /v1/shift_templates/by_business/{id}, POST /v1/shift_templates/create, PUT /v1/shift_templates/{id}, DELETE /v1/shift_templates/{id}, POST /v1/shift_templates/{id}/generate?weeks=4 (clamped 1-12).resources/views/partials/_share_qr_modal.blade.php partial. Floating share button on every public profile (/s/{slug} and /b/{slug}) opens a single modal with the share-options grid + the QR code.navigator.share) when the browser supports it (iOS Safari, Android Chrome, Edge); otherwise a per-platform button row: Copy link, WhatsApp, X, Facebook, LinkedIn, Email. Copy uses navigator.clipboard.writeText with a textarea fallback for older Safari, and shows a tiny "Link copied" toast.simplesoftwareio/simple-qrcode library that the controllers already produce; the partial drops the legacy WhatsApp-only QR popup that lived inline on those pages./qrcodes/. Useful for printing the code (counter posters, check-in signage).GET /v1/business/{business_id}/crew/analytics endpoint. Single GROUP BY query over request_authorizations joined with shift_requests produces the whole crew's per-shifter breakdown in one round trip (no N+1).accept_shift_req flag), 90-day total completed shifts, repeat rate (% of crew with more than one shift in the window), and the avg reliability score across rated crew./business/connected: stat-tile row + Top Performers list. Uses the existing flat-card visual style. Hidden when there are no connected shifters yet, so empty-state surfaces don't get cluttered./business/kiosk. Full-screen, chrome-hidden layout designed for a tablet mounted at a worksite. Polls the active business's today-shifts every 30s.shift_start / shift_end endpoints; passes the shifter's id as the actor (shift_start_by_id)./v1/business/update_settings endpoint, so an operator can flip them on without leaving the page. Replaces the curl-only configuration path that landed earlier today.accept_shift_req flag (shifters.accept_shift_req); a "Paused" badge already shows on the public profile when off, but until now there was no UI to actually toggle the flag. Roadmap cleanup item closed.geofence_radius_m. NULL = no enforcement (legacy default). Shifter's reported lat/lon at clock-in is compared via Haversine to the business's stored lat/lon. Two modes: soft (default; distance is recorded, no block) and hard (SHIFT_CLOCK_GEOFENCE_HARD_FAIL=true; out-of-radius is rejected unless the shifter taps "I'm here" to override).SHFT-PHOTO-REQ-ERR if missing.POST /v1/shift_request/{id}/clock_photo?side=in|out (multipart). Returns {url} the front-end passes back to shift_start / shift_end.getClockGeo / uploadClockPhoto / pickClockPhoto helpers in resources/js/utils/clockIn.ts.BusinessSettings and ShiftStart / ShiftEnd; legacy mobile clients on the v1 contract continue to work unchanged (soft-mode, no enforcement).PUT /v1/business/update_settings; dedicated dashboard control ships in a follow-up.shift_request_id=NULL); new reviews can pin to a specific completed shift via the new shift_request_id column. Schema 4-col unique constraint dropped; app-level dedup in is_review_exist enforces the conditional rule.response_text + response_at columns + PUT /v1/review/respond?review_id=.'pending'; the review stays publicly visible (so disputing isn't a silent mute button). An admin resolves to 'upheld' (review hidden from public lists going forward) or 'rejected' (review stays). Backed by dispute_status / dispute_reason / disputed_at / dispute_resolved_at / dispute_resolved_by_user_id columns + POST /v1/review/dispute?review_id= + PUT /v1/review/dispute/resolve?review_id=&decision= (admin) + GET /v1/review/disputes?status_filter=pending|all (admin queue).GET /v1/review/trend?review_on=&review_on_id=&days= returns bucketed averages (daily for windows < 30d, weekly otherwise). Web UI: a small inline-SVG sparkline appears next to the avg-rating block on /s/{slug} for the last 90 days. No chart library; pure SVG./v1/review/list, get_avg_review, batch averages, rating histogram) exclude dispute_status='upheld' rows so an admin's decision actually hides the review. Pending and rejected stay visible./s/{slug} reviews tab gets the full v2 surface: response display, "Disputed" pill, Respond / Dispute affordances for the reviewee (only the person being reviewed sees them), and the 90-day sparkline. /b/{slug} gets the read-only display additions (response + pill); actionable buttons + sparkline on the business surface ship in a follow-up.ReviewOut schema gains the new fields as Optional. Mobile clients on the v1 contract ignore unknown fields, so this is safely additive on the read side.app/models/review.py./settings/2fa. Reachable from the avatar dropdown ("Two-factor authentication"). Self-service enable / disable / regenerate-backup-codes / trusted-devices.\Auth::login(). After password verifies, if the user has 2FA on, the session parks the FastAPI access/refresh tokens under pending_* keys and bounces to /2fa/challenge. Only a successful TOTP / backup-code verify promotes them. Cancel = logout, no partial state.Crypt::encryptString (AES-256-CBC over APP_KEY). DB compromise alone doesn't leak the second factor.user_2fa_trusted_devices table; the cookie carries <row_id>.<token> and only the SHA-256 hash is stored. HttpOnly, Secure-on-HTTPS, SameSite=Lax. The token rotates on every successful use (and the row's expires_at rolls forward), so a copied cookie loses its second life the moment the legitimate owner signs in next. Settings page lists every active trusted device with last-used + IP + per-device "Sign out" + global "Sign out everywhere". Disabling 2FA or regenerating backup codes auto-revokes every trusted device for the user (and clears the cookie on the current browser) since both signal "I think someone got in" or "starting clean." Daily artisan 2fa:prune-trusted-devices (03:30 Pacific) drops rows older than 7 days past expiration or revocation so storage stays bounded.config/two_factor.php → TWO_FACTOR_SMS_ENABLED=false) until the Telnyx 10DLC 2FA campaign approves. When enabled, codes will route through Paradise (pm.shiftsee.com) like every other transactional message.users.two_fa_enabled / two_fa_method / two_fa_secret / two_fa_phone / two_fa_enabled_at / two_fa_pending_secret, plus a new user_2fa_backup_codes table. SQLAlchemy mirror added in app/models/user.py (read-only on the FastAPI side; the v1 contract is frozen, so the 2FA flow lives entirely in Laravel for now).invoice_payments.requires_action (bool) and requires_action_at (timestamp). Set when Stripe returns a PaymentIntent in requires_action state on an off-session charge. Cleared by the existing payment_intent.succeeded webhook.Customer::charge2 now switches saved-card charges to off_session=true so Stripe throws CardException(authentication_required) instead of silently returning a stuck PI. The exception is caught in ContactBase::initiatePayment and surfaced upstream as status='requires_action' with payment_intent_id and a fresh client_secret.PaymentTransaction::pay() propagates the new status as REQUIRES_ACTION rather than incorrectly flagging the invoice as paid. The invoice stays STATUS_PENDING and transactions.paid stays 0, since the actual money hasn't moved yet.pm.shiftsee.com) on first-touch with a deep link to /finance/web/authenticate/{invoice_id}. Subject: "Action required: confirm your card payment on ShiftSee"./finance/web/authenticate/{invoice_id} page: re-fetches a fresh client_secret from Stripe, runs stripe.confirmCardPayment(client_secret) so the issuer presents 3DS, and bounces the customer back to the dashboard on success. Server-side guards verify the caller owns the invoice and it's still in requires_action./business/dashboard (<RequiresActionBanner />) lists every stuck invoice for the current user with a per-invoice "Confirm payment" link. Renders nothing when there's nothing stuck. Backed by GET /finance/web/requires-action (cookie-authed JSON).callbackPaymentIntentSucceeded now also clears requires_action on the invoice when set, so a webhook delivered slightly before the user's redirect still hits the right state.callbackPaymentIntentFailed clears requires_action (and rolls the invoice back to PENDING). After a 3DS attempt that ultimately failed, keeping the flag would point the user at a stale client_secret. Better to drop them into the standard "retry charge" UX from the dashboard.**MUST READ** in /admin/api-docs#changelog and on the Breaking & action-required page for both items. The "is this invoice paid" predicate should be status == STATUS_PAID, not "PI was created". The latter has always been ambiguous, but is now ambiguous in a more visible way. The 2FA columns are read-only mirrors on the FastAPI side; existing API consumers see new optional fields on user-detail responses (mobile clients ignore unknown fields).Calendar v2 and v3, shipped together. Day and Week now render as vertical time-axis grids with hour and half-hour gridlines. Drag a shift to move it within the day, across days, or resize it. Click any empty time slot to schedule on it. Plus accepted-shift drag, shifter open-shifts date filter, and Month-view cross-day drag.
shift_in; grab the bottom edge to change shift_out. Each handle is independent, so the duration changes by the dragged amount. Snaps to 15 minutes./shifter/open-shifts?date= since shifters don't author shifts.)shift_out is earlier than shift_in) render with a "→ continues into the next day" indicator and aren't drag-mutable in v1; the date math gets gnarlier and the use case is rare.mousedown/mousemove/mouseup at the window level so the drag survives leaving the column.PUT /v1/shift_request/change_request endpoint (no contract change). The original row is marked request_status=4 (superseded) and a new pending child row is created with parent_id linking to it, so the negotiation history reads honestly./shifter/calendar route. Same Day / Week / Month grid as the business side; data source is list_of_requests filtered by shifter_id.Full calendar → link.CalendarView (resources/js/components/ds/CalendarView.tsx); business and shifter wrappers are about ten lines each. Day and Week share a single time-axis component (CalendarTimeAxisView.tsx) parameterized by days: Date[] — same code path for the 1-column day grid and the 7-column week grid.SendShiftRequestModal gained two optional props: initialShiftDate (YYYY-MM-DD) and initialShiftIn (HH:MM). When initialShiftIn is set, end time auto-fills to start + 1 hour. Both fields can be set independently or together./shifter/open-shifts?date=YYYY-MM-DD filtered to that date. The page shows a banner ("Showing open shifts on Tuesday, May 12, 2026") with a one-click "Show all dates" reset. Backed by the existing BroadcastShiftRequestsFind.shift_date filter (no contract change).A week and month calendar at /business/calendar showing every shift on a business at a glance, color-coded by status. Click a shift to open it; click a day to schedule a new one.
/business/calendar page. Renders the active business's shifts on a week or month grid./business/shifts?shift=<id>. Click an empty calendar cell to bounce to the dashboard's send-shift action for that day.Direct messages between any two parties on ShiftSee plus a full help center with searchable articles, tutorials, an AI helper, and seamless human escalation.
/help page. Public, searchable, browseable. Twelve starter articles covering both shifter and business audiences, organized by category.kind='support_ai' threads, so the user has a record. On escalation the thread converts to kind='support' and an admin can join./help and /help/videos; admin uploads via direct DB or the upcoming admin UI./help renders cleanly in-app. Native devs can swap to native UI using /v1/help/* endpoints./messages page. Single thread per pair of parties (a party is a shifter or a business). Three tabs: All, Pinned, Requests./b/<slug>). Lists shifters connected to that business with a Message button on each row.conversations, conversation_participants, messages.help_articles, help_videos, help_questions.php artisan migrate./v1/messaging/*. Inbox, lookup, send, read, pin, archive, accept. New error codes MSG-NOT-FND-ERR, MSG-NOT-PARTICIPANT, MSG-BLOCKED-ERR, MSG-REQ-PENDING-ERR, MSG-BODY-ERR./v1/help/*. Article list/get/feedback/search, video list/get, ask (AI), escalate. New error code HELP-NOT-FND-ERR.ANTHROPIC_API_KEY for the AI helper to function. Without it, the help widget falls back to a hand-written reply that nudges to articles.conversation_participants.is_automated + automation_kind were reserved for the AI-assisted reply layer; the help center is the first user of them.Live search across people and businesses, a usable mobile experience, and several dashboard reliability fixes.
/search with type, connection, and sort filters.InputPhone. Country picker with type-to-search, paste-an-E.164 country detection, ~250 countries, no external library./release-notes and blog at /blog, with RSS feeds and footer links.box-sizing: border-box on the DS Input wrapper.countryTimezone guard was dead code blocked by the global axios interceptor's auto-attached Tz header.A complete partner platform with OAuth, webhooks, embeddable Connect button and Schedule widget, and a self-serve developer portal.
/v1/partner/* API surface for third-party integrations.x-api-key header for server-to-server calls.shift_request.created, broadcast.created, and other events. Stripe-style HMAC-SHA256 signing with a t=<unix>,v1=<hex> header.X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset)./embed/v1/:
<script> includes. Pop-up on desktop, full-page redirect on mobile./developers. Register an app, mint API keys, configure webhook endpoints, read the docs.A workflow-driven admin app, multi-user business accounts, and an SMS provider switch for faster delivery.
/admin. Sidebar of resource groups (People, Activity, Catalog, Geo, Content, Access control, Devices & messaging, Detail tables). Top-bar global search across users, shifters, and businesses.admin_audit_log, admin_notes, business_colab extensions for multi-role support.A persistent identity row above a contextual nav row, with role-aware tier-2 links and primary actions grouped on the right.
My account › <BusinessName or ShifterName> › <Page>.New logo, new palette, updated tokens.
#064E3B. Primary actions, focus rings, and active states across the app pick up the new value via --brand-primary.#F59E0B. Used for Broadcast Shift, the "you" pill on default businesses, and inline highlights via --brand-accent.tokens.css remapped: --brand-primary, --brand-primary-tint, --brand-accent, --brand-accent-tint, --brand-accent-hover, plus existing ink/cream surface tokens.A new login flow that figures out the right credential mode for you, plus a DS conversion of the highest-traffic shift modals.
SendShiftRequestModal and ProposeChangesModal. Every form field uses FormField + Input / Select / Textarea. Standard inline-pill error pattern. DS-grade buttons and chrome.