Compare commits
5 commits
755d43d5c9
...
d197c529ce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d197c529ce | ||
|
|
34e6c08c14 | ||
|
|
aba7237de8 | ||
|
|
ee2d6b0308 | ||
|
|
43dcab2f68 |
12 changed files with 515 additions and 49 deletions
|
|
@ -64,7 +64,7 @@ and stays predictable.
|
|||
|
||||
Whitepaper: `/Users/sttil-solutions/Documents/Obsidian_Vault/STTIL-Vault/Projects/2026-05-18-pilot-readiness-whitepaper.md`
|
||||
|
||||
### Checklist Status (as of 2026-06-07) — 45-50% pilot-ready
|
||||
### Checklist Status (as of 2026-06-09) — 65% pilot-ready
|
||||
|
||||
| Checklist Item | Status |
|
||||
|---|---|
|
||||
|
|
@ -74,9 +74,9 @@ Whitepaper: `/Users/sttil-solutions/Documents/Obsidian_Vault/STTIL-Vault/Project
|
|||
| 25 CSV variants pass ingestion tests | PASS — 50 new files generated (PA + NJ sets), 100% normalizer pass rate 2026-06-07 |
|
||||
| Import flow: mapping, validation, warnings | PARTIAL — CSVImport exists, full mapping review unconfirmed |
|
||||
| Report generation calculates from backend | PASS |
|
||||
| Each status has reason + recommended action | PARTIAL — payer bug fixed; visit date still uses shipment proxy |
|
||||
| Each status has reason + recommended action | PASS — visit date proxy bug fixed 2026-06-09; `_resolve_visit_date()` implements full priority chain (CSV column > Supabase confirmed > shipment-30d estimate) |
|
||||
| Export CSV works and opens cleanly | NOT VERIFIED end to end |
|
||||
| Placeholder content removed | NOT VERIFIED |
|
||||
| Placeholder content removed | PARTIAL — StatCard undefined variables fixed 2026-06-09; Sidebar + sweep in progress |
|
||||
| Browser smoke test passes | NOT RUN |
|
||||
| Data handling rules documented | PASS — privacy-policy.md + data-handling.md written 2026-06-07; minimum necessary fields, CSV disposition policy, retention, and payer_rules.json cadence all documented |
|
||||
| Real PHI blocked | PARTIAL — architecture confirmed; Supabase plan tier (Pro vs Team) not yet verified |
|
||||
|
|
|
|||
247
docs/superpowers/plans/2026-06-09-paste-phase1.md
Normal file
247
docs/superpowers/plans/2026-06-09-paste-phase1.md
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
# Paste MCP Phase 1 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Wire the Content Outbox and Call Prep pinboards into the linkedin-post, linkedin-carousel, and pilot-prep skills so every approved draft and call brief lands in Paste automatically.
|
||||
|
||||
**Architecture:** Two Paste pinboards are created first. Each skill gets one new instruction block at its natural completion point — after copy-review passes (linkedin-post), after final output is rendered (linkedin-carousel), and after the briefing is assembled (pilot-prep). Skills look up pinboard IDs by name via `mcp__paste__list_pinboards` at runtime rather than hardcoding IDs, so the wiring survives pinboard recreation.
|
||||
|
||||
**Tech Stack:** Paste MCP (`mcp__paste__create_pinboard`, `mcp__paste__list_pinboards`, `mcp__paste__create_item`), skill SKILL.md files (plain markdown instruction files, no code).
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Create Content Outbox Pinboard
|
||||
|
||||
**Files:**
|
||||
- No file changes — Paste MCP call only
|
||||
|
||||
- [ ] **Step 1: Create the pinboard**
|
||||
|
||||
Call `mcp__paste__create_pinboard` with name `Content Outbox`.
|
||||
|
||||
Expected response:
|
||||
```json
|
||||
{ "id": "content_outbox_XXXXXXXX-...", "name": "Content Outbox", "itemCount": 0 }
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Record the pinboard ID**
|
||||
|
||||
Note the `id` returned. It will be referenced in verification steps.
|
||||
|
||||
- [ ] **Step 3: Verify via list**
|
||||
|
||||
Call `mcp__paste__list_pinboards`. Confirm `Content Outbox` appears in the list alongside `Useful Links`.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Create Call Prep Pinboard
|
||||
|
||||
**Files:**
|
||||
- No file changes — Paste MCP call only
|
||||
|
||||
- [ ] **Step 1: Create the pinboard**
|
||||
|
||||
Call `mcp__paste__create_pinboard` with name `Call Prep`.
|
||||
|
||||
Expected response:
|
||||
```json
|
||||
{ "id": "call_prep_XXXXXXXX-...", "name": "Call Prep", "itemCount": 0 }
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify via list**
|
||||
|
||||
Call `mcp__paste__list_pinboards`. Confirm both `Content Outbox` and `Call Prep` now appear.
|
||||
|
||||
- [ ] **Step 3: Commit a note**
|
||||
|
||||
```bash
|
||||
cd /Users/sttil-solutions/projects/signal
|
||||
git add docs/superpowers/plans/2026-06-09-paste-phase1.md
|
||||
git commit -m "Add Paste Phase 1 implementation plan"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Update linkedin-post Skill
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/sttil-solutions/.claude/skills/linkedin-post/SKILL.md`
|
||||
|
||||
The current Step 6 handles NocoDB update after Kisa confirms the post is live. A new step goes between Step 5 (Iterate) and Step 6 (Confirm and Update NocoDB) — it fires when the draft is approved, not after posting. The Outbox is a staging layer: Kisa pastes from Paste to LinkedIn, then confirms to Claude, who updates NocoDB.
|
||||
|
||||
- [ ] **Step 1: Add the Paste push step**
|
||||
|
||||
In `/Users/sttil-solutions/.claude/skills/linkedin-post/SKILL.md`, insert the following block **between** the `## Step 5: Iterate` section and `## Step 6: Confirm and Update NocoDB`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
|
||||
## Step 5.5: Push Approved Draft to Paste Content Outbox
|
||||
|
||||
When Kisa says the draft is good (approval signals: "that works", "perfect", "use that", "looks good", or similar) — before waiting for posting confirmation:
|
||||
|
||||
1. Call `mcp__paste__list_pinboards` to get the current pinboard list.
|
||||
2. Find the pinboard named `Content Outbox` and get its `id`.
|
||||
3. Call `mcp__paste__create_item` with:
|
||||
- `content`: the full approved post text (exactly as drafted — no extra formatting)
|
||||
- `list_id`: the Content Outbox id from step 2
|
||||
4. Confirm with one line: "Draft saved to Paste Content Outbox — paste directly to LinkedIn when ready."
|
||||
|
||||
Do not push unreviewed drafts. Only push after Kisa signals approval.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the file change**
|
||||
|
||||
Read the file. Confirm the new Step 5.5 block appears between Step 5 and Step 6. Confirm Step 6 is unchanged.
|
||||
|
||||
- [ ] **Step 3: Smoke test**
|
||||
|
||||
Invoke linkedin-post with a test prompt: "draft a short test post about documentation gaps."
|
||||
After it drafts and copy-review passes, say "that works."
|
||||
Confirm the skill calls `mcp__paste__list_pinboards` and then `mcp__paste__create_item`.
|
||||
Call `mcp__paste__search` with `pinboard_id` set to the Content Outbox id — confirm the test draft appears.
|
||||
Delete the test item via `mcp__paste__delete_item`.
|
||||
|
||||
- [ ] **Step 4: Confirm file saved**
|
||||
|
||||
Read the first 10 lines of `/Users/sttil-solutions/.claude/skills/linkedin-post/SKILL.md` to confirm the edit persisted. No git commit needed — `.claude/skills` is not a git repo.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Update linkedin-carousel Skill
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/sttil-solutions/.claude/skills/linkedin-carousel/SKILL.md`
|
||||
|
||||
The carousel output is a rendered PDF. The Paste push captures the text copy (hook, per-slide headlines and body) — not the PDF file. This gives Kisa a reviewable text record and reusable caption copy.
|
||||
|
||||
- [ ] **Step 1: Find the final output step**
|
||||
|
||||
Read `/Users/sttil-solutions/.claude/skills/linkedin-carousel/SKILL.md` in full. Identify the last step — the one where the rendered PDF path is reported to Kisa.
|
||||
|
||||
- [ ] **Step 2: Add the Paste push block**
|
||||
|
||||
Immediately after the final output/delivery step, append:
|
||||
|
||||
```markdown
|
||||
---
|
||||
|
||||
## Final Step: Push Slide Copy to Paste Content Outbox
|
||||
|
||||
After confirming the PDF has rendered:
|
||||
|
||||
1. Assemble the carousel text copy in this format:
|
||||
```
|
||||
CAROUSEL: [Title]
|
||||
|
||||
HOOK: [Hook text from slide 1]
|
||||
|
||||
SLIDE 2: [Headline]
|
||||
[Body copy if any]
|
||||
|
||||
SLIDE 3: [Headline]
|
||||
[Body copy if any]
|
||||
|
||||
[... continue for all slides ...]
|
||||
|
||||
CTA: [Final slide CTA text]
|
||||
```
|
||||
2. Call `mcp__paste__list_pinboards`. Find `Content Outbox` and get its `id`.
|
||||
3. Call `mcp__paste__create_item` with:
|
||||
- `content`: the assembled text copy above
|
||||
- `list_id`: the Content Outbox id
|
||||
4. Confirm with one line: "Carousel copy saved to Paste Content Outbox. PDF at [path]."
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify the file change**
|
||||
|
||||
Read the file. Confirm the new block appears after the final delivery step and the format template is intact.
|
||||
|
||||
- [ ] **Step 4: Confirm file saved**
|
||||
|
||||
Read the last 20 lines of `/Users/sttil-solutions/.claude/skills/linkedin-carousel/SKILL.md` to confirm the new block is present.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Update pilot-prep Skill
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/sttil-solutions/.claude/skills/pilot-prep/SKILL.md`
|
||||
|
||||
The current Step 5 is "Offer a Practice Question (Optional)." The Paste push is a new Step 6 that fires after the briefing is assembled and the practice question is offered — so the full briefing is in Paste before the call, not just the summary.
|
||||
|
||||
- [ ] **Step 1: Add the Paste push step**
|
||||
|
||||
In `/Users/sttil-solutions/.claude/skills/pilot-prep/SKILL.md`, add the following block **after** `## Step 5: Offer a Practice Question (Optional)`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
|
||||
## Step 6: Push Briefing to Paste Call Prep
|
||||
|
||||
After delivering the briefing and the Step 5 practice question offer:
|
||||
|
||||
1. Call `mcp__paste__list_pinboards`. Find `Call Prep` and get its `id`.
|
||||
2. Call `mcp__paste__create_item` with:
|
||||
- `content`: the full briefing text (all sections from Step 3, exactly as delivered)
|
||||
- `list_id`: the Call Prep id
|
||||
3. Confirm with one line: "Briefing saved to Paste Call Prep — available in your clipboard on any device before the call."
|
||||
|
||||
This step is not optional. Push every briefing. Kisa clears the pinboard after the call.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the file change**
|
||||
|
||||
Read the file. Confirm Step 6 appears after Step 5. Confirm Steps 1-5 are unchanged.
|
||||
|
||||
- [ ] **Step 3: Smoke test**
|
||||
|
||||
Invoke pilot-prep with: "prep for my call with Robert Robinson."
|
||||
After the briefing is delivered, confirm the skill calls `mcp__paste__list_pinboards` and `mcp__paste__create_item`.
|
||||
Call `mcp__paste__search` with `pinboard_id` set to the Call Prep id — confirm the briefing appears.
|
||||
Do not delete the test item — this is a real prep if Robert Robinson's call is upcoming.
|
||||
|
||||
- [ ] **Step 4: Confirm file saved**
|
||||
|
||||
Read the last 20 lines of `/Users/sttil-solutions/.claude/skills/pilot-prep/SKILL.md` to confirm Step 6 is present.
|
||||
|
||||
---
|
||||
|
||||
### Task 6: End-to-End Verification
|
||||
|
||||
- [ ] **Step 1: Verify pinboard state**
|
||||
|
||||
Call `mcp__paste__list_pinboards`. Confirm:
|
||||
- `Content Outbox` exists
|
||||
- `Call Prep` exists
|
||||
- `Useful Links` still exists (default pinboard unchanged)
|
||||
|
||||
- [ ] **Step 2: Verify skill descriptions are updated**
|
||||
|
||||
Check that the `description:` frontmatter in each updated SKILL.md still accurately describes the skill. No change needed unless the description explicitly said "does not save anywhere."
|
||||
|
||||
- [ ] **Step 3: Update the spec to mark Phase 1 complete**
|
||||
|
||||
In `/Users/sttil-solutions/projects/signal/docs/superpowers/specs/2026-06-08-paste-mcp-ambient-layer-design.md`, update the Phase 1 status line to:
|
||||
|
||||
```markdown
|
||||
### Phase 1: Content Outbox + Call Prep
|
||||
**Trigger:** ~~Implement now~~ **COMPLETE — 2026-06-09**
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Final commit**
|
||||
|
||||
```bash
|
||||
cd /Users/sttil-solutions/projects/signal
|
||||
git add docs/superpowers/specs/2026-06-08-paste-mcp-ambient-layer-design.md
|
||||
git commit -m "Mark Paste MCP Phase 1 complete"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Skills are instruction files (markdown), not application code. No unit tests apply. Verification is behavioral: invoke the skill, observe Paste.
|
||||
- The `.claude/skills` directory may not be a git repo. Check with `git -C /Users/sttil-solutions/.claude/skills status` before committing. If not a repo, commits are skipped — file changes are sufficient.
|
||||
- Paste auth must be active for smoke tests to work. If tools are unavailable, re-authenticate via `/mcp` in a separate Claude Code window before running verification steps.
|
||||
- Phase 2 (Brand Vault) does not start until Phase 1 has been used across at least 2-3 real posts and calls and confirmed working.
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
# Paste MCP Ambient Layer — Design Spec
|
||||
**Date:** 2026-06-08
|
||||
**Scope:** STTIL Solutions infrastructure (all projects)
|
||||
**Status:** Approved for Phase 1 implementation
|
||||
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
Every knowledge system in the STTIL stack (TriLane, MemPalace, Obsidian, NocoDB) requires a query. Content produced by Claude lives only in the chat window until manually copied. Call briefs generated by pilot-prep disappear after the session. There is no ambient layer — one that is always live, always accessible in every app, zero friction to use.
|
||||
|
||||
Paste MCP fills that gap. It is the bridge between what AI produces and where it actually gets used.
|
||||
|
||||
---
|
||||
|
||||
## Design Principle
|
||||
|
||||
Paste is always **downstream** of authoritative systems, never upstream.
|
||||
|
||||
- MemPalace KG: authoritative for brand facts
|
||||
- NocoDB: authoritative for leads and content status
|
||||
- TriLane: authoritative for deep research retrieval
|
||||
- Paste: working copy of what is in active use right now
|
||||
|
||||
Paste holds the hot layer. It does not replace any deep storage system.
|
||||
|
||||
---
|
||||
|
||||
## Phased Architecture
|
||||
|
||||
### Phase 1: Content Outbox + Call Prep
|
||||
**Trigger:** ~~Implement now~~ **COMPLETE — 2026-06-09**
|
||||
|
||||
**Pinboards created:**
|
||||
- `Content Outbox` — staging layer for all publishable content
|
||||
- `Call Prep` — pre-call brief for every pilot and investor call
|
||||
|
||||
**Content Outbox — how it works:**
|
||||
1. linkedin-post skill finishes drafting → pushes draft to Content Outbox via `mcp__paste__create_item`
|
||||
2. linkedin-carousel skill finishes drafting → same
|
||||
3. Kisa reviews draft in Paste app, pastes directly to LinkedIn or wherever it's going
|
||||
4. Claude updates NocoDB status to Published in the same move
|
||||
5. Item deleted from Content Outbox — the pinboard stays clean, not an archive
|
||||
|
||||
**Call Prep — how it works:**
|
||||
1. pilot-prep skill finishes → pushes call brief to Call Prep pinboard
|
||||
2. Brief is in clipboard history before the call starts, accessible from any app or device
|
||||
3. After the call, Granola pipeline runs as normal (unchanged)
|
||||
4. Call Prep item cleared after the call
|
||||
|
||||
**Skills updated in Phase 1:**
|
||||
- `linkedin-post`: add final step → `mcp__paste__create_item` to Content Outbox
|
||||
- `linkedin-carousel`: add final step → `mcp__paste__create_item` to Content Outbox
|
||||
- `pilot-prep`: add final step → `mcp__paste__create_item` to Call Prep
|
||||
|
||||
**Granola note:** Granola ingestion (post-call) is unaffected. The pipeline (Obsidian sync → TriLane → NavBot) stays exactly as is. Call Prep is pre-call only.
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Brand Vault
|
||||
**Trigger:** After Phase 1 proves stable across multiple sessions.
|
||||
|
||||
**Pinboard created:** `Brand Vault`
|
||||
|
||||
**Contents (~12-15 items, all KG-verified):**
|
||||
- CERT 2019 framing: "nearly 1 in 3" (32.8%), "over two-thirds from docs" (68.6%)
|
||||
- CGS prepay stat: ~30.86% error rate, Q2 2024
|
||||
- Badge labels: At Risk (SUPPLY_LAPSED/VISIT_REQUIRED/RENEWAL_CRITICAL), Action Needed (RENEWAL_ELEVATED/RENEWAL_SOON/TRANSFER_PENDING), Clear to Ship (RESUPPLY_READY), On Track (ACTIVE)
|
||||
- Supply continuity naming: Supply Lapsed / Renewal Due / Resupply Ready / Active / Outreach Worklist
|
||||
- Renewal Due cadence: 60d patient, 45d prescriber
|
||||
- Pricing tiers: Worklist $149-199, Connected $349-499, Intelligence $699-999+usage
|
||||
- Pitch anchor: "$400-500M/yr spent documenting the problem Signal solves; Worklist costs $1,788/yr"
|
||||
- CMS PA exemption: ≥90% affirmation rate = PA exemption, June 1 2026 first cycle
|
||||
- Pharmacy vs DME adherence (PMC11301113, 2,356 matched adults): 6mo DME 65% vs pharmacy 52%; 9mo DME 61% vs pharmacy 49%; 12mo DME 58.8% vs pharmacy 48% — pitch framing: "nearly half of pharmacy patients have lapsed by month 9"
|
||||
- Medicare DME vs pharmacy (separate study): 78% vs 64% at 12 months; total annual costs 35% lower for DME ($6,967 vs $10,635)
|
||||
- CRITICAL PITCH CONTEXT: Pharmacies carry significantly reduced documentation requirements vs DME (Part D prescription vs Part B CMN/PECOS/qualifying visit/PA). The DME adherence advantage is achieved DESPITE the regulatory handicap. Signal removes that handicap — protecting and extending an advantage the channel already earned. Never cite the adherence stats without this framing in a supplier or VC pitch.
|
||||
- Brand sign-off block: "Signal. / Powered by STTIL Solutions"
|
||||
|
||||
**How it stays current:**
|
||||
The Decision Protocol (CLAUDE.md) already triggers `kg_add` when a brand fact is confirmed. Phase 2 adds one step: after `kg_add`, update or create the corresponding Vault item in Paste. KG remains authoritative — Vault is the working copy.
|
||||
|
||||
**What it replaces:** The need to query me for locked stats and copy blocks during active work.
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Session Handoff
|
||||
**Trigger:** After Paste auth proves reliable across 5+ consecutive sessions.
|
||||
|
||||
**Pinboard created:** `Hot Context`
|
||||
|
||||
**What it contains:** Curated ~200-word summary of what is in active motion — not history, not everything. Current build status, who is being called this week, what is blocked, immediate next actions.
|
||||
|
||||
**navaigate-session-brain update:** Add final step — push Hot Context summary to Paste.
|
||||
|
||||
**Session startup update (CLAUDE.md):** Add Paste Hot Context read at the top of the startup protocol. If Hot Context exists, skip `project_next_steps.md` and `project_vc_traction.md` reads — those become fallbacks for auth failures or empty pinboard. KG queries and NocoDB live pulls stay unchanged.
|
||||
|
||||
**Fallback rule:** If Paste auth fails or Hot Context is empty, the startup protocol falls through to the existing file reads with no change in behavior. Paste is never in the critical path until auth is proven.
|
||||
|
||||
**Token savings:** ~400-500 tokens per focused session. Startup speed improvement on days with a clear working agenda.
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Agent Trays
|
||||
**Trigger:** After AiOS restructure completes and division agents are running.
|
||||
|
||||
**Pinboards created:** `Signal Tray`, `Content Tray`, `STTIL Tray`, `Infrastructure Tray`
|
||||
|
||||
**How it works:** Each AiOS division agent writes its current outputs and status to its tray. The morning skill reads all four trays as part of the briefing — two-sentence status per division, without triggering full agent runs.
|
||||
|
||||
**Scaling:** Med Tracker, UMSIS, and Game Center each get a tray pinboard when they activate. No redesign needed — add pinboard, wire agent, done.
|
||||
|
||||
---
|
||||
|
||||
## Full Pinboard Map
|
||||
|
||||
| Pinboard | Phase | Written by | Read by |
|
||||
|----------|-------|-----------|---------|
|
||||
| Content Outbox | 1 | linkedin-post, linkedin-carousel | Kisa (paste to publish) |
|
||||
| Call Prep | 1 | pilot-prep | Kisa (during calls) |
|
||||
| Brand Vault | 2 | KG update flow | Kisa, copy-review |
|
||||
| Hot Context | 3 | navaigate-session-brain | Session startup protocol |
|
||||
| Signal Tray | 4 | Signal agent | morning skill |
|
||||
| Content Tray | 4 | Content agent | morning skill |
|
||||
| STTIL Tray | 4 | STTIL agent | morning skill |
|
||||
| Infrastructure Tray | 4 | Infrastructure agent | morning skill |
|
||||
|
||||
---
|
||||
|
||||
## What This Does NOT Change
|
||||
|
||||
- MemPalace KG: still authoritative for brand facts
|
||||
- TriLane: still handles deep research retrieval and session logs
|
||||
- NocoDB: still authoritative for leads and content calendar
|
||||
- Granola pipeline: unchanged
|
||||
- Memory .md files: permanent sections (preferences, feedback, paths) stay as-is; rolling sections (next steps, VC status) become fallbacks in Phase 3
|
||||
|
||||
---
|
||||
|
||||
## Pitfalls to Watch
|
||||
|
||||
1. **Auth reliability** — Paste auth does not yet persist reliably across sessions. Phase 3 is blocked until this proves stable. Phase 1 and 2 have no startup dependency and are safe to build now.
|
||||
2. **Dual maintenance** — Brand Vault items must be written FROM KG, never independently. Any fact in Paste must trace back to a KG entry.
|
||||
3. **Pinboard sprawl** — Keep pinboards small and curated. Paste is not a search system. If any pinboard grows past 20-25 items, it needs pruning or a sub-pinboard.
|
||||
4. **Outbox hygiene** — Content Outbox must be cleared after each item is published. Stale drafts in the Outbox create confusion.
|
||||
|
|
@ -3,8 +3,8 @@
|
|||
**Effective Date:** June __, 2026
|
||||
|
||||
**Between:**
|
||||
- **STTIL Solutions LLC** ("STTIL"), a limited liability company, with its principal place of business in [City, State]
|
||||
- **Gaboro DME** ("Gaboro"), with its principal place of business in [City, State]
|
||||
- **STTIL Solutions LLC** ("STTIL"), a limited liability company, with its principal place of business in Jacksonville, Florida
|
||||
- **Gaboro DME** ("Gaboro"), with its principal place of business in Philadelphia, Pennsylvania
|
||||
|
||||
Each a "Party" and together the "Parties."
|
||||
|
||||
|
|
@ -118,4 +118,4 @@ By: ___________________________
|
|||
Name: Robert Robinson
|
||||
Title: Co-Founder and Managing Partner
|
||||
Date: _________________________
|
||||
Email: ___________________________
|
||||
Email: robertr@gaboromed.com
|
||||
|
|
|
|||
|
|
@ -469,6 +469,20 @@ async def confirm_visit(
|
|||
if confirmed > date_type.today():
|
||||
raise HTTPException(status_code=400, detail="Confirmed date cannot be in the future.")
|
||||
|
||||
# Validate: visit must be within 6 months (183 days) before the order/shipment date.
|
||||
# 183 days = ~6 months, consistent with CMS CGM qualifying visit window.
|
||||
from datetime import timedelta as _td
|
||||
six_months_before = shipment - _td(days=183)
|
||||
if confirmed < six_months_before:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=(
|
||||
f"Visit date {confirmed.isoformat()} is more than 6 months before "
|
||||
f"the order date {shipment.isoformat()}. A qualifying visit must fall "
|
||||
f"within the 6-month window before each supply order."
|
||||
),
|
||||
)
|
||||
|
||||
# Get org
|
||||
clerk_org_id = claims.get("o", {}).get("id") if isinstance(claims.get("o"), dict) else None
|
||||
org_id = get_or_create_org(clerk_org_id=clerk_org_id)
|
||||
|
|
|
|||
|
|
@ -29,8 +29,12 @@ function AppInner() {
|
|||
const transferPending = records.filter((r) => r.flag === "TRANSFER_PENDING").length;
|
||||
const refill = records.filter((r) => r.flag === "RESUPPLY_READY").length;
|
||||
const okCount = records.filter((r) => r.flag === "ACTIVE").length;
|
||||
// escalateCount = VISIT_REQUIRED (qualifying visit past due) + RENEWAL_CRITICAL (<=45 days)
|
||||
const escalateCount = visitRequired + renewalCritical;
|
||||
const outreachCount = renewalSoon + renewalElevated;
|
||||
// outreachCount = RENEWAL_ELEVATED (<=60d) + RENEWAL_SOON (<=90d)
|
||||
const outreachCount = renewalElevated + renewalSoon;
|
||||
const atRiskCount = supplyLapsed + visitRequired + renewalCritical;
|
||||
const actionNeededCount = renewalSoon + renewalElevated + transferPending;
|
||||
|
||||
const handleResults = useCallback(async (file) => {
|
||||
const label = new Date().toLocaleDateString("en-US", {
|
||||
|
|
@ -60,7 +64,7 @@ function AppInner() {
|
|||
const rows = parseCSV(e.target.result);
|
||||
const { results, skipped } = processBatch(rows);
|
||||
setRecords(results);
|
||||
setImportLabel(`${file.name} · ${label} · local processing`);
|
||||
setImportLabel(`${file.name} · ${label}`);
|
||||
let msg = `Loaded ${results.length} patient${results.length !== 1 ? "s" : ""} from ${file.name}`;
|
||||
if (skipped.length) msg += ` · ${skipped.length} skipped`;
|
||||
showToast(msg);
|
||||
|
|
@ -72,9 +76,10 @@ function AppInner() {
|
|||
<div className="flex w-full min-h-screen bg-[var(--bg-page)] text-[var(--text-primary)] transition-colors">
|
||||
{/* Sidebar */}
|
||||
<Sidebar
|
||||
supplyLapsedCount={supplyLapsed}
|
||||
escalateCount={escalateCount}
|
||||
atRiskCount={atRiskCount}
|
||||
actionNeededCount={actionNeededCount}
|
||||
clearToShipCount={refill}
|
||||
onTrackCount={okCount}
|
||||
activeFilter={activeFilter}
|
||||
onFilterChange={setActiveFilter}
|
||||
onImportClick={() => csvImportRef.current?.trigger()}
|
||||
|
|
@ -113,8 +118,8 @@ function AppInner() {
|
|||
<StatCard
|
||||
priority
|
||||
label="Needs Attention"
|
||||
value={supplyLapsed + escalateCount + transferPending}
|
||||
sub={`${supplyLapsed} docs required · ${escalateCount} escalate · ${transferPending} new patient`}
|
||||
value={supplyLapsed + visitRequired + renewalCritical + transferPending}
|
||||
sub={`${supplyLapsed} docs required · ${visitRequired + renewalCritical} escalate · ${transferPending} new patient`}
|
||||
/>
|
||||
<StatCard
|
||||
label="Outreach in Progress"
|
||||
|
|
|
|||
|
|
@ -130,10 +130,10 @@ export default function Privacy() {
|
|||
<br />
|
||||
Email:{" "}
|
||||
<a
|
||||
href="mailto:kisasttil@gmail.com"
|
||||
href="mailto:privacy@sttilsolutions.com"
|
||||
className="text-[var(--brand,#147A7A)] underline underline-offset-2"
|
||||
>
|
||||
kisasttil@gmail.com
|
||||
privacy@sttilsolutions.com
|
||||
</a>
|
||||
</p>
|
||||
</section>
|
||||
|
|
@ -143,10 +143,10 @@ export default function Privacy() {
|
|||
<p className="text-[11px] text-[var(--text-muted,#7A9E9E)] leading-[1.6]">
|
||||
Signal is operated by STTIL Solutions LLC.{" "}
|
||||
<a
|
||||
href="https://signal.sttilsolutions.com"
|
||||
href="https://sttilsolutions.com/signal"
|
||||
className="text-[var(--brand,#147A7A)] underline underline-offset-2"
|
||||
>
|
||||
signal.sttilsolutions.com
|
||||
sttilsolutions.com/signal
|
||||
</a>
|
||||
{" "}· We may update this policy from time to time. We will notify active customers of material
|
||||
changes by email. Continued use of Signal following notice constitutes acceptance.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { useState } from "react";
|
||||
import { useAuth } from "@clerk/react";
|
||||
import { confirmVisit } from "../lib/api";
|
||||
|
||||
export default function ConfirmVisitModal({ record, onClose, onConfirmed }) {
|
||||
const { getToken } = useAuth();
|
||||
const [visitDate, setVisitDate] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
|
@ -29,9 +31,20 @@ export default function ConfirmVisitModal({ record, onClose, onConfirmed }) {
|
|||
setError("Visit date cannot be in the future.");
|
||||
return;
|
||||
}
|
||||
// Validate: visit must be within 6 months before the order/shipment date
|
||||
const shipmentDateStr = computeShipmentDate();
|
||||
const shipmentDateObj = new Date(shipmentDateStr + "T00:00:00");
|
||||
const visitDateObj = new Date(visitDate + "T00:00:00");
|
||||
const sixMonthsBeforeShipment = new Date(shipmentDateObj);
|
||||
sixMonthsBeforeShipment.setMonth(sixMonthsBeforeShipment.getMonth() - 6);
|
||||
if (visitDateObj < sixMonthsBeforeShipment) {
|
||||
setError("Visit date must be within 6 months of the order date. A visit older than 6 months does not satisfy the qualifying visit requirement.");
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
const token = await getToken().catch(() => null);
|
||||
const updated = await confirmVisit({
|
||||
patient_id: record.patient_id,
|
||||
confirmed_date: visitDate,
|
||||
|
|
@ -40,7 +53,7 @@ export default function ConfirmVisitModal({ record, onClose, onConfirmed }) {
|
|||
device_type: record.device_type,
|
||||
quantity: 1,
|
||||
component: record.component || "sensor",
|
||||
});
|
||||
}, token);
|
||||
onConfirmed(record.patient_id, updated);
|
||||
onClose();
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,23 @@
|
|||
import { useOrganization } from "@clerk/react";
|
||||
|
||||
export default function Sidebar({
|
||||
supplyLapsedCount,
|
||||
escalateCount,
|
||||
atRiskCount,
|
||||
actionNeededCount,
|
||||
clearToShipCount,
|
||||
onTrackCount,
|
||||
activeFilter,
|
||||
onFilterChange,
|
||||
onImportClick,
|
||||
}) {
|
||||
const { organization } = useOrganization();
|
||||
const orgName = organization?.name ?? "My Organization";
|
||||
const initials = orgName
|
||||
.split(" ")
|
||||
.slice(0, 2)
|
||||
.map((w) => w[0])
|
||||
.join("")
|
||||
.toUpperCase();
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
label: "All Patients",
|
||||
|
|
@ -14,17 +26,17 @@ export default function Sidebar({
|
|||
badge: null,
|
||||
},
|
||||
{
|
||||
label: "Docs Required",
|
||||
key: "SUPPLY_LAPSED",
|
||||
label: "At Risk",
|
||||
key: "at-risk",
|
||||
icon: "!",
|
||||
badge: supplyLapsedCount,
|
||||
badge: atRiskCount,
|
||||
badgeWarn: true,
|
||||
},
|
||||
{
|
||||
label: "Escalate",
|
||||
key: "escalate",
|
||||
label: "Action Needed",
|
||||
key: "action-needed",
|
||||
icon: "!",
|
||||
badge: escalateCount,
|
||||
badge: actionNeededCount,
|
||||
badgeWarn: true,
|
||||
},
|
||||
{
|
||||
|
|
@ -33,6 +45,12 @@ export default function Sidebar({
|
|||
icon: "✓",
|
||||
badge: clearToShipCount,
|
||||
},
|
||||
{
|
||||
label: "On Track",
|
||||
key: "ACTIVE",
|
||||
icon: "✓",
|
||||
badge: onTrackCount,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
@ -116,11 +134,11 @@ export default function Sidebar({
|
|||
<div className="px-5 py-[14px] border-t border-[rgba(15,94,94,0.5)]">
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<div className="w-[30px] h-[30px] bg-[rgba(46,163,163,0.18)] border border-[rgba(46,163,163,0.3)] rounded-full flex items-center justify-center text-[11px] text-teal-400 font-semibold shrink-0">
|
||||
DS
|
||||
{initials}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-medium text-warm-white">
|
||||
Demo Supplier
|
||||
{orgName}
|
||||
</div>
|
||||
<div className="text-[10px] text-teal-300 mt-[1px]">
|
||||
Billing Staff
|
||||
|
|
|
|||
|
|
@ -162,6 +162,11 @@ export default function WorklistTable({ records, activeFilter, onFilterChange })
|
|||
{r.reason}
|
||||
</div>
|
||||
)}
|
||||
{r.flag === "ACTIVE" && r.next_visit_due_date && (
|
||||
<div className="text-[10.5px] text-[var(--text-muted)] mt-[4px]">
|
||||
Next visit due: {new Date(r.next_visit_due_date + "T00:00:00").toLocaleDateString("en-US", { month: "short", year: "numeric" })}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-[22px] py-[13px] align-middle">
|
||||
<span className={`font-mono text-[16px] font-medium ${scoreClass(priority)}`}>
|
||||
|
|
|
|||
|
|
@ -92,8 +92,10 @@ export function apiRecordToLocal(r) {
|
|||
* Confirm a qualifying visit date for a patient.
|
||||
* Returns the updated RecordOut from the backend.
|
||||
*/
|
||||
export async function confirmVisit(payload) {
|
||||
const authHeader = API_KEY ? { "X-API-Key": API_KEY } : {};
|
||||
export async function confirmVisit(payload, token = null) {
|
||||
const authHeader = token
|
||||
? { "Authorization": `Bearer ${token}` }
|
||||
: API_KEY ? { "X-API-Key": API_KEY } : {};
|
||||
try {
|
||||
const res = await fetch(`${BACKEND_URL}/api/confirm-visit`, {
|
||||
method: "POST",
|
||||
|
|
|
|||
|
|
@ -7,24 +7,33 @@
|
|||
*/
|
||||
|
||||
export const FLAG = {
|
||||
OUT_OF_COVERAGE: "OUT_OF_COVERAGE",
|
||||
VISIT_DUE: "VISIT_DUE",
|
||||
REFILL_WINDOW: "REFILL_WINDOW",
|
||||
OK: "OK",
|
||||
SUPPLY_LAPSED: "SUPPLY_LAPSED",
|
||||
VISIT_REQUIRED: "VISIT_REQUIRED",
|
||||
RENEWAL_CRITICAL: "RENEWAL_CRITICAL",
|
||||
RENEWAL_ELEVATED: "RENEWAL_ELEVATED",
|
||||
RENEWAL_SOON: "RENEWAL_SOON",
|
||||
RESUPPLY_READY: "RESUPPLY_READY",
|
||||
ACTIVE: "ACTIVE",
|
||||
};
|
||||
|
||||
const FLAG_LABELS = {
|
||||
[FLAG.OUT_OF_COVERAGE]: "Supply Lapsed",
|
||||
[FLAG.VISIT_DUE]: "Renewal Due",
|
||||
[FLAG.REFILL_WINDOW]: "Resupply Ready",
|
||||
[FLAG.OK]: "Active",
|
||||
[FLAG.SUPPLY_LAPSED]: "Supply Lapsed",
|
||||
[FLAG.VISIT_REQUIRED]: "Visit Required",
|
||||
[FLAG.RENEWAL_CRITICAL]: "Renewal Due",
|
||||
[FLAG.RENEWAL_ELEVATED]: "Renewal Elevated",
|
||||
[FLAG.RENEWAL_SOON]: "Renewal Soon",
|
||||
[FLAG.RESUPPLY_READY]: "Resupply Ready",
|
||||
[FLAG.ACTIVE]: "Active",
|
||||
};
|
||||
|
||||
const FLAG_ACTIONS = {
|
||||
[FLAG.OUT_OF_COVERAGE]: "Contact Prescriber",
|
||||
[FLAG.VISIT_DUE]: "Request Renewal",
|
||||
[FLAG.REFILL_WINDOW]: "Initiate Resupply",
|
||||
[FLAG.OK]: "No action needed",
|
||||
[FLAG.SUPPLY_LAPSED]: "Contact Prescriber",
|
||||
[FLAG.VISIT_REQUIRED]: "Schedule Visit",
|
||||
[FLAG.RENEWAL_CRITICAL]: "Request Renewal",
|
||||
[FLAG.RENEWAL_ELEVATED]: "Request Renewal",
|
||||
[FLAG.RENEWAL_SOON]: "Plan Renewal",
|
||||
[FLAG.RESUPPLY_READY]: "Initiate Resupply",
|
||||
[FLAG.ACTIVE]: "No action needed",
|
||||
};
|
||||
|
||||
/** Wear-day rules — mirrors payer_rules.json */
|
||||
|
|
@ -79,9 +88,12 @@ function getPayerConfig(payer) {
|
|||
}
|
||||
|
||||
function computePriority(flag, daysUntilEnd) {
|
||||
if (flag === FLAG.OUT_OF_COVERAGE) return 1000 + Math.abs(daysUntilEnd);
|
||||
if (flag === FLAG.VISIT_DUE) return 500 + Math.max(0, 90 - daysUntilEnd);
|
||||
if (flag === FLAG.REFILL_WINDOW) return 200 + Math.max(0, 30 - daysUntilEnd);
|
||||
if (flag === FLAG.SUPPLY_LAPSED) return 1000 + Math.abs(daysUntilEnd);
|
||||
if (flag === FLAG.VISIT_REQUIRED) return 1500 + Math.abs(daysUntilEnd);
|
||||
if (flag === FLAG.RENEWAL_CRITICAL) return 500 + Math.max(0, 90 - daysUntilEnd);
|
||||
if (flag === FLAG.RENEWAL_ELEVATED) return 700 + Math.max(0, 90 - daysUntilEnd);
|
||||
if (flag === FLAG.RENEWAL_SOON) return 400 + Math.max(0, 90 - daysUntilEnd);
|
||||
if (flag === FLAG.RESUPPLY_READY) return 200 + Math.max(0, 30 - daysUntilEnd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -109,13 +121,19 @@ export function calculateCoverage(record) {
|
|||
|
||||
let flag;
|
||||
if (daysUntilEnd < 0) {
|
||||
flag = FLAG.OUT_OF_COVERAGE;
|
||||
} else if (daysUntilVisit !== null && daysUntilVisit <= 30) {
|
||||
flag = FLAG.VISIT_DUE;
|
||||
flag = FLAG.SUPPLY_LAPSED;
|
||||
} else if (daysUntilVisit !== null && daysUntilVisit < 0) {
|
||||
flag = FLAG.VISIT_REQUIRED;
|
||||
} else if (daysUntilVisit !== null && daysUntilVisit <= 45) {
|
||||
flag = FLAG.RENEWAL_CRITICAL;
|
||||
} else if (daysUntilVisit !== null && daysUntilVisit <= 60) {
|
||||
flag = FLAG.RENEWAL_ELEVATED;
|
||||
} else if (daysUntilVisit !== null && daysUntilVisit <= 90) {
|
||||
flag = FLAG.RENEWAL_SOON;
|
||||
} else if (daysUntilEnd <= payerCfg.refillWindowDays) {
|
||||
flag = FLAG.REFILL_WINDOW;
|
||||
flag = FLAG.RESUPPLY_READY;
|
||||
} else {
|
||||
flag = FLAG.OK;
|
||||
flag = FLAG.ACTIVE;
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
Loading…
Reference in a new issue