A 30 year old female with history of recurrent sinusitis with elevated igE levels . CT showing b/l turbinate hypertrophy. Give me the correct harrison based approach , investigations and appropriate treatment , MD Medicine Style
"chronic rhinosinusitis" AND "allergic rhinitis" AND management
"chronic rhinosinusitis" AND treatment
| Category | Examples |
|---|---|
| Allergic | Seasonal AR, Perennial AR |
| Inflammatory | NARES (non-allergic rhinitis with eosinophilia), CRS with/without polyps |
| Anatomic | Septal deviation, neoplasm |
| Hormonal | Hypothyroidism, OCP use |
| Vasculitic | Granulomatosis with Polyangiitis (Wegener's) - check for saddle nose, hemoptysis |
| Aspirin-exacerbated | Samter's triad (nasal polyps + asthma + ASA sensitivity) |
| Allergic fungal sinusitis | Elevated IgE + hyperdense sinus opacification on CT + specific IgE to fungi |
Important: Elevated total IgE with specific unilateral expansion on CT should raise suspicion for Allergic Fungal Sinusitis (AFS) - characterized by elevated total IgE, specific IgE to colonizing fungi (Aspergillus, Alternaria), characteristic hyperdense mucin on CT, and eosinophilic mucin. This is a specific subtype requiring surgical management.
| Investigation | Rationale |
|---|---|
| CBC with differential | Eosinophilia (>500/µL supports atopic/eosinophilic CRS) |
| Total serum IgE | Quantify allergic burden; helps dose omalizumab if needed |
| ESR, CRP | Active infection vs. chronic inflammation |
| ANA, ANCA (c-ANCA/PR3) | Exclude GPA (Wegener's) if unusual features |
| Serum IgG, IgA, IgM | Exclude common variable immunodeficiency (CVID) in recurrent sinusitis |
| Sweat chloride / CFTR mutation | Exclude cystic fibrosis if bilateral bronchiectasis, recurrent pulmonary infections |
| Nasal NO (fractional) | Exclude primary ciliary dyskinesia |
| Aspergillus/Alternaria specific IgE | If AFS is suspected on CT |
| Beta-2 transferrin (nasal fluid) | Exclude CSF leak if clear, unilateral discharge |
Antihistamines are most effective for sneezing, rhinorrhea, and pruritus. Their benefit diminishes with continuous allergen exposure (perennial AR). Provide only 20-30% symptom reduction.
| Drug | Dose/actuation | Dosing |
|---|---|---|
| Mometasone furoate | 50 µg | 2 sprays/nostril OD |
| Fluticasone propionate | 50 µg | 2 sprays/nostril OD |
| Fluticasone furoate | 27.5 µg | 2 sprays/nostril OD |
| Budesonide | 32 µg | 2 sprays/nostril BD |
| Triamcinolone acetonide | 55 µg | 2 sprays/nostril OD |
| Ciclesonide | 50 µg | 2 sprays/nostril OD |
| Biologic | Dose | Target | Approval |
|---|---|---|---|
| Dupilumab | 300 mg SC q2 weeks | IL-4Rα (IL-4 + IL-13) | CRSwNP |
| Omalizumab | SC (dose by weight + IgE) | IgE | CRSwNP |
| Mepolizumab | 100 mg SC q4 weeks | IL-5 | CRSwNP |
Recent evidence (Cai S et al., Allergy 2025, PMID 39985317 - meta-analysis): Biologics demonstrate superior efficacy and safety in CRSwNP in real-world settings.
STEP 1: Confirm CRS + Allergic Background
→ Nasal endoscopy + allergy testing (SPT/serum IgE)
→ CBC, IgG/IgA/IgM, ANCA (exclude GPA/CVID)
→ Spirometry (co-existing asthma screen)
STEP 2: Allergen Avoidance + Education
STEP 3: Pharmacotherapy
→ INCS (mometasone/fluticasone) + nasal saline irrigation (daily)
→ Add: montelukast 10mg OD + 2nd gen antihistamine PRN
→ Antibiotic course only during acute bacterial exacerbations
STEP 4: Persist 3 months → reassess
If adequate control → continue + consider SLIT/SCIT
STEP 5: If inadequate control → ENT referral
→ Nasal endoscopy + CT review (Lund-Mackay)
→ Consider: FESS + turbinate reduction
→ Post-surgical INCS maintenance
STEP 6: CRSwNP/Type 2 refractory disease
→ Dupilumab / Omalizumab / Mepolizumab
Create a management flowchart/PDF for this case
mkdir -p /home/daytona/workspace/sinusitis-flowchart && ls /home/daytona/workspace/sinusitis-flowchart
~/sinusitis-flowchart/generate_flowchart.py
"""
Management Flowchart PDF for:
Recurrent Sinusitis + Elevated IgE + Bilateral Turbinate Hypertrophy
MD Medicine Style (Harrison/Goldman-Cecil based)
"""
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm, cm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether
)
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus.flowables import Flowable
import reportlab.lib.colors as rcolors
# ─── Color Palette ────────────────────────────────────────────────────────────
NAVY = colors.HexColor("#1B3A6B")
STEEL = colors.HexColor("#2E6DA4")
TEAL = colors.HexColor("#1A7A7A")
AMBER = colors.HexColor("#D4850A")
RED_DARK = colors.HexColor("#B03030")
GREEN_DARK = colors.HexColor("#2A7A2A")
PURPLE = colors.HexColor("#5B3A8C")
LIGHT_BLUE = colors.HexColor("#D6E8F7")
LIGHT_TEAL = colors.HexColor("#D0EFEF")
LIGHT_AMBER= colors.HexColor("#FEF3DC")
LIGHT_RED = colors.HexColor("#FAE0E0")
LIGHT_GREEN= colors.HexColor("#DCF0DC")
LIGHT_PURP = colors.HexColor("#EDE5F5")
LIGHT_GREY = colors.HexColor("#F4F4F4")
MID_GREY = colors.HexColor("#CCCCCC")
WHITE = colors.white
BLACK = colors.black
W, H = A4 # 595.27 x 841.89
# ─── Document Setup ───────────────────────────────────────────────────────────
doc = SimpleDocTemplate(
"/home/daytona/workspace/sinusitis-flowchart/Sinusitis_Management_Flowchart.pdf",
pagesize=A4,
leftMargin=15*mm, rightMargin=15*mm,
topMargin=12*mm, bottomMargin=12*mm,
title="Sinusitis Management Flowchart",
author="Orris MD Medicine Reference"
)
styles = getSampleStyleSheet()
# Custom styles
def S(name, **kw):
return ParagraphStyle(name, **kw)
title_style = S("Title2",
fontName="Helvetica-Bold", fontSize=15, textColor=WHITE,
alignment=TA_CENTER, spaceAfter=2, leading=18)
subtitle_style = S("Subtitle2",
fontName="Helvetica", fontSize=9, textColor=WHITE,
alignment=TA_CENTER, spaceAfter=0, leading=12)
section_hdr = S("SectionHdr",
fontName="Helvetica-Bold", fontSize=9.5, textColor=WHITE,
alignment=TA_CENTER, leading=12, spaceAfter=0)
body_bold = S("BodyBold",
fontName="Helvetica-Bold", fontSize=8, textColor=BLACK,
leading=10, spaceAfter=1)
body_small = S("BodySmall",
fontName="Helvetica", fontSize=7.5, textColor=BLACK,
leading=9.5, spaceAfter=1)
body_tiny = S("BodyTiny",
fontName="Helvetica", fontSize=7, textColor=BLACK,
leading=9, spaceAfter=0)
drug_style = S("Drug",
fontName="Helvetica-Oblique", fontSize=7.5, textColor=colors.HexColor("#1B3A6B"),
leading=9.5)
warn_style = S("Warn",
fontName="Helvetica-BoldOblique", fontSize=7.5, textColor=RED_DARK,
leading=9.5)
note_style = S("Note",
fontName="Helvetica-Oblique", fontSize=7, textColor=colors.HexColor("#444444"),
leading=9)
# ─── Helper: make a colored header cell ───────────────────────────────────────
def hdr_cell(text, bg=NAVY, fg=WHITE, size=9.5, bold=True):
fn = "Helvetica-Bold" if bold else "Helvetica"
st = S(f"hdr_{text[:8]}", fontName=fn, fontSize=size,
textColor=fg, alignment=TA_CENTER, leading=12)
return Paragraph(text, st)
def body_cell(text, bold=False, color=BLACK, size=7.5, align=TA_LEFT):
fn = "Helvetica-Bold" if bold else "Helvetica"
st = S(f"bc_{text[:8]}", fontName=fn, fontSize=size,
textColor=color, alignment=align, leading=9.5)
return Paragraph(text, st)
def bullet(items, color=BLACK, size=7.5):
lines = []
for item in items:
lines.append(Paragraph(f"• {item}", S("blt",
fontName="Helvetica", fontSize=size, textColor=color,
leading=9.5, leftIndent=6)))
return lines
# ─── Arrow Flowable ───────────────────────────────────────────────────────────
class Arrow(Flowable):
def __init__(self, width=40, height=14, color=STEEL, label=""):
super().__init__()
self.width = width
self.height = height
self._color = color
self.label = label
def draw(self):
c = self.canv
w, h = self.width, self.height
c.setFillColor(self._color)
c.setStrokeColor(self._color)
shaft_w = w * 0.55
shaft_h = h * 0.38
tip_w = w * 0.45
cx = w / 2
# shaft
c.rect(cx - shaft_w/2, h*0.38, shaft_w, shaft_h, fill=1, stroke=0)
# arrowhead
path = c.beginPath()
path.moveTo(cx, 0)
path.lineTo(cx - tip_w/2, h*0.42)
path.lineTo(cx + tip_w/2, h*0.42)
path.close()
c.drawPath(path, fill=1, stroke=0)
if self.label:
c.setFont("Helvetica-Bold", 7)
c.setFillColor(WHITE)
c.drawCentredString(cx, h*0.52, self.label)
def wrap(self, *args):
return self.width, self.height
class CentredArrow(Flowable):
"""Arrow centred in available width"""
def __init__(self, avail_width, color=STEEL, label=""):
super().__init__()
self._avail = avail_width
self._color = color
self.label = label
self.height = 14
def draw(self):
c = self.canv
w, h = 50, self.height
ox = (self._avail - w) / 2
c.saveState()
c.translate(ox, 0)
shaft_w = w * 0.5
shaft_h = h * 0.38
tip_w = w * 0.4
cx = w / 2
c.setFillColor(self._color)
c.setStrokeColor(self._color)
c.rect(cx - shaft_w/2, h*0.38, shaft_w, shaft_h, fill=1, stroke=0)
path = c.beginPath()
path.moveTo(cx, 0)
path.lineTo(cx - tip_w/2, h*0.42)
path.lineTo(cx + tip_w/2, h*0.42)
path.close()
c.drawPath(path, fill=1, stroke=0)
if self.label:
c.setFont("Helvetica-Bold", 7)
c.setFillColor(WHITE)
c.drawCentredString(cx, h*0.52, self.label)
c.restoreState()
def wrap(self, avail_w, avail_h):
self._avail = avail_w
return avail_w, self.height
# ─── Build Content ────────────────────────────────────────────────────────────
story = []
FULL_W = W - 30*mm # 165mm usable
# ══════════════════════════════════════════════════════════════════════════════
# TITLE BANNER
# ══════════════════════════════════════════════════════════════════════════════
title_data = [[
hdr_cell("MANAGEMENT FLOWCHART", bg=NAVY, fg=WHITE, size=14),
hdr_cell("Recurrent Sinusitis + Elevated IgE + Bilateral Turbinate Hypertrophy", bg=NAVY, fg=colors.HexColor("#AADDFF"), size=9, bold=False),
]]
title_tbl = Table(title_data, colWidths=[FULL_W * 0.38, FULL_W * 0.62])
title_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), NAVY),
("ALIGN", (0,0), (-1,-1), "CENTER"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 8),
("BOTTOMPADDING", (0,0), (-1,-1), 8),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 6),
("ROUNDEDCORNERS", [4,4,4,4]),
]))
story.append(title_tbl)
# Subtitle bar
sub_data = [[
hdr_cell("Female, 30 yrs | Hx: Recurrent Sinusitis | ↑ Total IgE | CT: B/L Turbinate Hypertrophy",
bg=STEEL, fg=WHITE, size=8.5, bold=False)
]]
sub_tbl = Table(sub_data, colWidths=[FULL_W])
sub_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), STEEL),
("ALIGN", (0,0), (-1,-1), "CENTER"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
]))
story.append(sub_tbl)
story.append(Spacer(1, 4*mm))
# ══════════════════════════════════════════════════════════════════════════════
# STEP 1: INITIAL ASSESSMENT
# ══════════════════════════════════════════════════════════════════════════════
def make_step_header(text, bg=NAVY, width=FULL_W):
d = [[hdr_cell(text, bg=bg, size=9)]]
t = Table(d, colWidths=[width])
t.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,-1), bg),
("ALIGN",(0,0),(-1,-1),"CENTER"),
("VALIGN",(0,0),(-1,-1),"MIDDLE"),
("TOPPADDING",(0,0),(-1,-1),5),
("BOTTOMPADDING",(0,0),(-1,-1),5),
("LEFTPADDING",(0,0),(-1,-1),6),
("RIGHTPADDING",(0,0),(-1,-1),6),
]))
return t
# STEP 1
story.append(make_step_header("STEP 1 — CLINICAL DIAGNOSIS & DIFFERENTIAL", bg=NAVY))
story.append(Spacer(1, 1.5*mm))
diag_data = [
[
# Col 1: CRS Criteria
[
Paragraph("<b>CRS Diagnostic Criteria (EPOS/Goldman-Cecil)</b>", S("x", fontName="Helvetica-Bold", fontSize=8, textColor=NAVY, leading=10)),
Paragraph("Symptoms >3 months (≥2 of):", body_small),
] + bullet([
"Mucopurulent nasal drainage",
"Nasal obstruction / congestion",
"Facial pressure / fullness",
"Hyposmia / anosmia",
], color=colors.HexColor("#1B3A6B")) + [
Paragraph("<b>PLUS objective finding:</b>", S("x2", fontName="Helvetica-Bold", fontSize=7.5, textColor=NAVY, leading=9.5)),
] + bullet([
"Nasal/middle meatus polyps",
"Edema / purulence in middle meatus",
"CT: sinus opacification",
], color=colors.HexColor("#1B3A6B")),
# Col 2: Differentials
[
Paragraph("<b>Rule Out (Key Differentials)</b>", S("x3", fontName="Helvetica-Bold", fontSize=8, textColor=RED_DARK, leading=10)),
] + bullet([
"GPA (Wegener's): ANCA, saddle nose, hemoptysis",
"NARES: nasal eosinophils, skin-test negative",
"Vasomotor rhinitis: no eosinophils, irritant-driven",
"Allergic Fungal Sinusitis: ↑IgE + hyperdense CT mucin",
"Samter's Triad: polyps + asthma + ASA sensitivity",
"CVID: ↓IgG/IgA/IgM, recurrent infections",
"CSF rhinorrhea: β2-transferrin test",
"Hypothyroidism / OCP-related rhinitis",
], color=RED_DARK),
]
]
diag_tbl = Table(diag_data, colWidths=[FULL_W*0.47, FULL_W*0.53])
diag_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (0,0), LIGHT_BLUE),
("BACKGROUND", (1,0), (1,0), LIGHT_RED),
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("BOX", (0,0), (-1,-1), 0.5, MID_GREY),
("INNERGRID", (0,0), (-1,-1), 0.5, MID_GREY),
]))
story.append(diag_tbl)
story.append(Spacer(1, 2*mm))
story.append(CentredArrow(FULL_W, color=STEEL))
story.append(Spacer(1, 1*mm))
# ══════════════════════════════════════════════════════════════════════════════
# STEP 2: INVESTIGATIONS
# ══════════════════════════════════════════════════════════════════════════════
story.append(make_step_header("STEP 2 — INVESTIGATIONS", bg=TEAL))
story.append(Spacer(1, 1.5*mm))
inv_data = [
# Header row
[
hdr_cell("ALLERGY WORKUP", bg=TEAL, fg=WHITE, size=8),
hdr_cell("NASAL / IMAGING", bg=TEAL, fg=WHITE, size=8),
hdr_cell("LAB PANEL", bg=TEAL, fg=WHITE, size=8),
],
[
# Allergy
[
Paragraph("<b>Skin Prick Test (First Line)</b>", S("al1", fontName="Helvetica-Bold", fontSize=7.5, textColor=TEAL, leading=9.5)),
] + bullet([
"HDM, cockroach, moulds",
"Pollens, animal dander",
"Positive: wheal ≥3mm > control",
"Stop antihistamines 2–7 days prior",
"Sensitivity 80–100%, Spec 70–90%",
], size=7) + [
Paragraph("<b>Serum Specific IgE (ImmunoCAP)</b>", S("al2", fontName="Helvetica-Bold", fontSize=7.5, textColor=TEAL, leading=9.5)),
] + bullet([
"If SPT contraindicated / skin disease",
"Total IgE >100 IU/mL = strongly atopic",
"Aspergillus/Alternaria IgE (if AFS suspected)",
], size=7),
# Nasal/Imaging
[
Paragraph("<b>Nasal Endoscopy</b>", S("ni1", fontName="Helvetica-Bold", fontSize=7.5, textColor=TEAL, leading=9.5)),
] + bullet([
"Turbinate size & mucosal colour",
"Polyps (middle meatus)",
"Purulent discharge",
"Pale/bluish mucosa = allergic",
], size=7) + [
Paragraph("<b>CT PNS (Non-contrast)</b>", S("ni2", fontName="Helvetica-Bold", fontSize=7.5, textColor=TEAL, leading=9.5)),
] + bullet([
"Lund-Mackay score (max 24)",
"OMC obstruction / polyps",
"Hyperdense mucin → AFS",
"Bony erosion → invasive fungal",
], size=7) + [
Paragraph("<b>Nasal Smear</b>", S("ni3", fontName="Helvetica-Bold", fontSize=7.5, textColor=TEAL, leading=9.5)),
] + bullet([
"Eosinophils = allergic / NARES",
"Absent eos = vasomotor",
], size=7),
# Labs
[
Paragraph("<b>Mandatory</b>", S("lb1", fontName="Helvetica-Bold", fontSize=7.5, textColor=TEAL, leading=9.5)),
] + bullet([
"CBC + differential (eosinophilia?)",
"Total serum IgE (quantify, dose biologic)",
"ESR, CRP (inflammatory activity)",
], size=7) + [
Paragraph("<b>If Clinically Indicated</b>", S("lb2", fontName="Helvetica-Bold", fontSize=7.5, textColor=TEAL, leading=9.5)),
] + bullet([
"c-ANCA/p-ANCA, ANA → GPA",
"IgG, IgA, IgM → exclude CVID",
"Sweat chloride / CFTR → CF",
"Nasal NO → primary ciliary dyskinesia",
"β2-transferrin → CSF rhinorrhea",
"Spirometry + FeNO → co-morbid asthma",
], size=7),
]
]
inv_tbl = Table(inv_data, colWidths=[FULL_W*0.33, FULL_W*0.35, FULL_W*0.32])
inv_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), TEAL),
("BACKGROUND", (0,1), (-1,1), LIGHT_TEAL),
("VALIGN", (0,0), (-1,-1), "TOP"),
("ALIGN", (0,0), (-1,0), "CENTER"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 4),
("BOX", (0,0), (-1,-1), 0.5, MID_GREY),
("INNERGRID", (0,0), (-1,-1), 0.5, MID_GREY),
]))
story.append(inv_tbl)
story.append(Spacer(1, 2*mm))
story.append(CentredArrow(FULL_W, color=AMBER))
story.append(Spacer(1, 1*mm))
# ══════════════════════════════════════════════════════════════════════════════
# STEP 3: TREATMENT — ALLERGEN AVOIDANCE
# ══════════════════════════════════════════════════════════════════════════════
story.append(make_step_header("STEP 3 — TREATMENT: PILLAR 1 — ALLERGEN AVOIDANCE (Always First)", bg=AMBER))
story.append(Spacer(1, 1.5*mm))
avoid_data = [
[
[Paragraph("<b>HDM</b>", S("av1", fontName="Helvetica-Bold", fontSize=7.5, textColor=AMBER, leading=9))] +
bullet(["Allergen-impermeable covers (mattress/pillow)", "Humidity <50%", "Hot water wash bedding >130°F", "HEPA vacuum"], size=7),
[Paragraph("<b>Moulds</b>", S("av2", fontName="Helvetica-Bold", fontSize=7.5, textColor=AMBER, leading=9))] +
bullet(["Dehumidify home", "Dilute bleach on windows/shower", "No indoor plants in bedroom"], size=7),
[Paragraph("<b>Pollens</b>", S("av3", fontName="Helvetica-Bold", fontSize=7.5, textColor=AMBER, leading=9))] +
bullet(["AC (filtered) in season", "Close windows during high pollen", "Shower after outdoor exposure"], size=7),
[Paragraph("<b>Pets / Cockroaches</b>", S("av4", fontName="Helvetica-Bold", fontSize=7.5, textColor=AMBER, leading=9))] +
bullet(["Rehome pet ideally; exclude from bedroom", "HEPA air purifier", "Exterminate cockroaches", "Seal food sources"], size=7),
]
]
avoid_tbl = Table(avoid_data, colWidths=[FULL_W*0.27, FULL_W*0.24, FULL_W*0.24, FULL_W*0.25])
avoid_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), LIGHT_AMBER),
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 4),
("BOX", (0,0), (-1,-1), 0.5, MID_GREY),
("INNERGRID", (0,0), (-1,-1), 0.5, MID_GREY),
]))
story.append(avoid_tbl)
story.append(Spacer(1, 2*mm))
story.append(CentredArrow(FULL_W, color=AMBER))
story.append(Spacer(1, 1*mm))
# ══════════════════════════════════════════════════════════════════════════════
# STEP 4: PHARMACOTHERAPY (main table)
# ══════════════════════════════════════════════════════════════════════════════
story.append(make_step_header("STEP 4 — TREATMENT: PILLAR 2 — PHARMACOTHERAPY (Stepwise)", bg=STEEL))
story.append(Spacer(1, 1.5*mm))
pharm_hdr = [
hdr_cell("STEP", bg=STEEL, size=8),
hdr_cell("DISEASE SEVERITY", bg=STEEL, size=8),
hdr_cell("DRUG CLASS", bg=STEEL, size=8),
hdr_cell("AGENTS & DOSES", bg=STEEL, size=8),
hdr_cell("NOTES", bg=STEEL, size=8),
]
def mk(t, bold=False, col=BLACK, size=7.5):
return Paragraph(t, S("c", fontName="Helvetica-Bold" if bold else "Helvetica",
fontSize=size, textColor=col, leading=9.5))
pharm_rows = [
pharm_hdr,
[
mk("1", bold=True, col=STEEL),
mk("Mild / Intermittent"),
mk("2nd-gen Antihistamines\n(oral)", bold=True),
[
mk("• Cetirizine 10 mg OD", size=7),
mk("• Levocetirizine 5 mg OD", size=7),
mk("• Loratadine 10 mg OD", size=7),
mk("• Fexofenadine 180 mg OD", size=7),
mk("• Desloratadine 5 mg OD", size=7),
],
mk("Targets: sneezing, rhinorrhea, itch.\n20–30% symptom reduction.\nDiminished efficacy with continuous exposure.", size=7),
],
[
mk("", bold=True, col=STEEL),
mk(""),
mk("Intranasal Antihistamines", bold=True),
[mk("• Azelastine nasal spray", size=7),
mk("• Olopatadine nasal spray", size=7)],
mk("Faster onset than oral. Also\neffective for non-allergic rhinitis.", size=7),
],
[
mk("2", bold=True, col=GREEN_DARK),
mk("Moderate–Severe /\nPerennial (THIS PATIENT)", bold=True, col=GREEN_DARK),
mk("Intranasal Corticosteroids\n★ GOLD STANDARD", bold=True, col=GREEN_DARK),
[
mk("• Mometasone furoate 50µg — 2 sprays/nostril OD", size=7),
mk("• Fluticasone propionate 50µg — 2 sprays/nostril OD", size=7),
mk("• Fluticasone furoate 27.5µg — 2 sprays/nostril OD", size=7),
mk("• Budesonide 32µg — 2 sprays/nostril BD", size=7),
mk("• Triamcinolone 55µg — 2 sprays/nostril OD", size=7),
],
mk("50–90% symptom reduction.\nAlso reduces turbinate hypertrophy.\nPreferred for perennial AR.\nIf adding antihistamine to INCS,\nuse INTRANASAL antihistamine.", size=7, col=GREEN_DARK),
],
[
mk("", bold=True),
mk("Add-on Therapy"),
mk("Leukotriene Antagonists"),
[mk("• Montelukast 10 mg OD (preferred)", size=7),
mk("• Zileuton 1200 mg BD", size=7),
mk("• Zafirlukast 20 mg BD", size=7)],
mk("Comparable to antihistamines.\nKey role in Samter's triad.\nContinue with SPT (no interference).", size=7),
],
[
mk("", bold=True),
mk("All stages — adjunct"),
mk("Nasal Saline Irrigation"),
[mk("• Isotonic or hypertonic saline", size=7),
mk("• BD–TDS irrigation", size=7)],
mk("Clears allergens & mucus.\nImproves ciliary function.\nEvidence-based in CRS.", size=7),
],
[
mk("", bold=True),
mk("Acute congestion only"),
mk("Oral Decongestants\n(short-term)"),
[mk("• Pseudoephedrine 30–60 mg q4–6h", size=7),
mk(" (max 240 mg/day)", size=7)],
mk("Risk of rebound congestion.\nDo NOT use >3–5 days.", size=7, col=RED_DARK),
],
[
mk("3", bold=True, col=AMBER),
mk("Severe exacerbation /\nPre-surgical"),
mk("Oral Corticosteroids\n(short course)", bold=True),
[mk("• Methylprednisolone 8mg QID×1d", size=7),
mk(" → 4mg QID×2d → taper over 6d", size=7)],
mk("Reserve for acute severe flares.\nBridge to surgery.", size=7, col=AMBER),
],
[
mk("", bold=True),
mk("Acute bacterial\nexacerbation"),
mk("Antibiotics\n(targeted use)", bold=True),
[mk("1st line: Amoxicillin 500mg TDS", size=7),
mk(" OR Amox-clav 875/125mg BD × 7d", size=7),
mk("PCN allergy: Doxycycline 100mg BD", size=7),
mk(" OR Levofloxacin 500mg OD", size=7),
mk("Severe: Amox-clav 2000/125mg BD", size=7)],
mk("Only if: symptoms >7–10d without improvement.\nWatchful waiting acceptable initially (85% resolve).\nAvoid macrolides / 2nd–3rd gen cephalosporins.", size=7),
],
]
pharm_tbl = Table(pharm_rows,
colWidths=[FULL_W*0.05, FULL_W*0.14, FULL_W*0.17, FULL_W*0.38, FULL_W*0.26])
row_bgs = [STEEL, LIGHT_BLUE, LIGHT_BLUE, LIGHT_GREEN, LIGHT_BLUE, LIGHT_BLUE, LIGHT_AMBER, LIGHT_AMBER, LIGHT_RED]
ts = [
("BACKGROUND", (0,0), (-1,0), STEEL),
("VALIGN", (0,0), (-1,-1), "TOP"),
("ALIGN", (0,0), (0,-1), "CENTER"),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
("BOX", (0,0), (-1,-1), 0.5, MID_GREY),
("INNERGRID", (0,0), (-1,-1), 0.5, MID_GREY),
("BACKGROUND", (0,1), (-1,1), LIGHT_BLUE),
("BACKGROUND", (0,2), (-1,2), LIGHT_BLUE),
("BACKGROUND", (0,3), (-1,3), LIGHT_GREEN),
("BACKGROUND", (0,4), (-1,4), LIGHT_BLUE),
("BACKGROUND", (0,5), (-1,5), LIGHT_BLUE),
("BACKGROUND", (0,6), (-1,6), LIGHT_AMBER),
("BACKGROUND", (0,7), (-1,7), LIGHT_AMBER),
("BACKGROUND", (0,8), (-1,8), LIGHT_RED),
("SPAN", (0,1), (0,2)),
("SPAN", (0,3), (0,5)),
("SPAN", (0,6), (0,7)),
]
pharm_tbl.setStyle(TableStyle(ts))
story.append(pharm_tbl)
story.append(Spacer(1, 2*mm))
story.append(CentredArrow(FULL_W, color=PURPLE))
story.append(Spacer(1, 1*mm))
# ══════════════════════════════════════════════════════════════════════════════
# STEP 5: IMMUNOTHERAPY
# ══════════════════════════════════════════════════════════════════════════════
story.append(make_step_header("STEP 5 — TREATMENT: PILLAR 3 — ALLERGEN IMMUNOTHERAPY (Disease-Modifying)", bg=PURPLE))
story.append(Spacer(1, 1.5*mm))
immuno_data = [
[
hdr_cell("SUBCUTANEOUS (SCIT)", bg=PURPLE, size=8),
hdr_cell("SUBLINGUAL (SLIT)", bg=PURPLE, size=8),
hdr_cell("INDICATIONS / CONTRA", bg=PURPLE, size=8),
],
[
[
Paragraph("<b>Build-up phase:</b> Weekly injections, escalating over 3–6 months", S("i1", fontName="Helvetica", fontSize=7.5, textColor=BLACK, leading=9.5)),
] + bullet([
"Maintenance: Monthly injections",
"Total duration: 3–5 years",
"Observe 30 min post-injection",
"Systemic reaction rate: 0.1%",
"Polysensitized: multi-allergen mix",
], size=7),
[
Paragraph("<b>FDA-approved tablets:</b>", S("i2", fontName="Helvetica", fontSize=7.5, textColor=BLACK, leading=9.5)),
] + bullet([
"Grass pollen (Grastek)",
"HDM (Odactra/Acarizax)",
"Better safety profile than SCIT",
"European first-choice for single allergen",
"Daily sublingual dosing at home",
], size=7),
[
Paragraph("<b>Indications:</b>", S("i3", fontName="Helvetica-Bold", fontSize=7.5, textColor=GREEN_DARK, leading=9.5)),
] + bullet([
"Failed pharmacotherapy",
"Desire for durable benefit",
"High-risk for developing asthma",
"New sensitization prevention",
], size=7, color=GREEN_DARK) + [
Paragraph("<b>Contraindications:</b>", S("i4", fontName="Helvetica-Bold", fontSize=7.5, textColor=RED_DARK, leading=9.5)),
] + bullet([
"Poorly controlled asthma",
"Active malignancy / autoimmune",
"Initiating during pregnancy",
"Beta-blockers (relative)",
], size=7, color=RED_DARK),
]
]
immuno_tbl = Table(immuno_data, colWidths=[FULL_W*0.35, FULL_W*0.32, FULL_W*0.33])
immuno_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), PURPLE),
("BACKGROUND", (0,1), (-1,1), LIGHT_PURP),
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("BOX", (0,0), (-1,-1), 0.5, MID_GREY),
("INNERGRID", (0,0), (-1,-1), 0.5, MID_GREY),
]))
story.append(immuno_tbl)
story.append(Spacer(1, 2*mm))
story.append(CentredArrow(FULL_W, color=RED_DARK, label="Refractory?"))
story.append(Spacer(1, 1*mm))
# ══════════════════════════════════════════════════════════════════════════════
# STEP 6: SURGICAL + BIOLOGICS (side by side)
# ══════════════════════════════════════════════════════════════════════════════
story.append(make_step_header("STEP 6 — REFRACTORY DISEASE: SURGERY + BIOLOGICS", bg=RED_DARK))
story.append(Spacer(1, 1.5*mm))
surg_bio_data = [
[
hdr_cell("TURBINATE SURGERY\n(if medical therapy fails)", bg=colors.HexColor("#8B2020"), size=8),
hdr_cell("FUNCTIONAL ENDOSCOPIC\nSINUS SURGERY (FESS)", bg=colors.HexColor("#8B2020"), size=8),
hdr_cell("BIOLOGICS\n(CRSwNP + Type 2 Inflammation)", bg=colors.HexColor("#8B2020"), size=8),
],
[
[
Paragraph("<b>⚠ Do NOT total turbinectomy</b> → Empty nose syndrome", S("s1", fontName="Helvetica-Bold", fontSize=7, textColor=RED_DARK, leading=9)),
Paragraph("<b>Options (preserve mucociliary function):</b>", S("s2", fontName="Helvetica-Bold", fontSize=7.5, textColor=BLACK, leading=9.5)),
] + bullet([
"Radiofrequency volume reduction (RFVTR)",
"Turbinoplasty (modified Mabry technique)",
"Microdebrider-assisted turbinoplasty",
"Laser (Holmium:YAG / KTP)",
"Partial inferior turbinectomy",
"Cryosurgery / infrared coagulation",
], size=7),
bullet([
"Opens ostiomeatal complex (OMC)",
"Removes obstructive tissue",
"Improves mucus drainage",
"Enhances future topical drug delivery",
"Post-op: INCS + saline irrigation mandatory",
"For recurrent acute + CRS refractory to 3+ months medical Rx",
], size=7) + [
Paragraph("<b>Indication:</b> Failure after maximal\nmedical therapy (≥3 months)", S("s3", fontName="Helvetica-Bold", fontSize=7.5, textColor=RED_DARK, leading=9.5)),
],
[
Paragraph("<b>Agents (Goldman-Cecil):</b>", S("bio1", fontName="Helvetica-Bold", fontSize=7.5, textColor=BLACK, leading=9.5)),
] + [
Paragraph("• <b>Dupilumab</b> 300mg SC q2wks (anti-IL-4Rα)", S("bio2", fontName="Helvetica", fontSize=7, textColor=BLACK, leading=9)),
Paragraph("• <b>Omalizumab</b> SC (dose by weight + IgE) (anti-IgE)", S("bio3", fontName="Helvetica", fontSize=7, textColor=BLACK, leading=9)),
Paragraph("• <b>Mepolizumab</b> 100mg SC q4wks (anti-IL-5)", S("bio4", fontName="Helvetica", fontSize=7, textColor=BLACK, leading=9)),
Paragraph("• <b>Tezepelumab</b> (anti-TSLP) — Phase III 2025 [NEJM]", S("bio5", fontName="Helvetica", fontSize=7, textColor=BLACK, leading=9)),
Spacer(1, 2),
Paragraph("<b>Reserve for:</b>", S("bio6", fontName="Helvetica-Bold", fontSize=7.5, textColor=BLACK, leading=9.5)),
] + bullet([
"Recurrent disease post-FESS",
"Comorbidities precluding surgery",
"Severe Type 2 eosinophilic CRS",
], size=7),
]
]
surg_bio_tbl = Table(surg_bio_data, colWidths=[FULL_W*0.31, FULL_W*0.34, FULL_W*0.35])
surg_bio_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), colors.HexColor("#8B2020")),
("BACKGROUND", (0,1), (-1,1), LIGHT_RED),
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("BOX", (0,0), (-1,-1), 0.5, MID_GREY),
("INNERGRID", (0,0), (-1,-1), 0.5, MID_GREY),
]))
story.append(surg_bio_tbl)
story.append(Spacer(1, 2*mm))
# ══════════════════════════════════════════════════════════════════════════════
# WHEN TO REFER BOX
# ══════════════════════════════════════════════════════════════════════════════
refer_data = [
[hdr_cell("⚠ WHEN TO REFER TO ENT IMMEDIATELY (Goldman-Cecil)", bg=colors.HexColor("#7B1515"), size=9)],
[[
Paragraph(
"Temperature >39°C + orbital edema / severe headache / visual disturbance / altered mental status / meningeal signs | "
"Failure to respond to >2 antibiotic courses | Anatomic abnormality / nosocomial infection | "
"Immunocompromised | Unusual / resistant pathogens | Fungal sinusitis / granulomatous disease | "
"Recurrent episodes suggesting CRS (this patient ✓)",
S("ref", fontName="Helvetica", fontSize=7.5, textColor=BLACK, leading=9.5, alignment=TA_JUSTIFY)
)
]]
]
refer_tbl = Table(refer_data, colWidths=[FULL_W])
refer_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), colors.HexColor("#7B1515")),
("BACKGROUND", (0,1), (-1,1), colors.HexColor("#FFF0F0")),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 6),
("BOX", (0,0), (-1,-1), 1, colors.HexColor("#7B1515")),
]))
story.append(refer_tbl)
story.append(Spacer(1, 2*mm))
# ══════════════════════════════════════════════════════════════════════════════
# FOOTER
# ══════════════════════════════════════════════════════════════════════════════
footer_data = [[
Paragraph(
"<b>Sources:</b> Goldman-Cecil Medicine (Intl Ed.) Ch.394 • Cummings Otolaryngology HNS Ch.29,35 • "
"Scott-Brown's ORL Vol.1&2 • EPOS Guidelines • "
"Cai S et al. <i>Allergy</i> 2025 [PMID:39985317] • Lipworth BJ et al. <i>NEJM</i> 2025 [PMID:40106374]",
S("foot", fontName="Helvetica", fontSize=6.5, textColor=colors.HexColor("#555555"), leading=9, alignment=TA_CENTER)
),
Paragraph(
"<b>Orris MD Reference</b> | June 2026",
S("foot2", fontName="Helvetica-Bold", fontSize=6.5, textColor=colors.HexColor("#555555"), leading=9, alignment=TA_CENTER)
),
]]
footer_tbl = Table(footer_data, colWidths=[FULL_W*0.78, FULL_W*0.22])
footer_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), LIGHT_GREY),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 6),
("BOX", (0,0), (-1,-1), 0.5, MID_GREY),
]))
story.append(footer_tbl)
# ── Build ──────────────────────────────────────────────────────────────────────
doc.build(story)
print("PDF generated successfully.")
cd /home/daytona/workspace/sinusitis-flowchart && python generate_flowchart.py 2>&1
ls -lh /home/daytona/workspace/sinusitis-flowchart/
Sinusitis Management Flowchart
PDF Document · PDF
| Section | Content |
|---|---|
| Title Banner | Patient profile: 30F, recurrent sinusitis, ↑IgE, B/L turbinate hypertrophy |
| Step 1 | CRS diagnostic criteria (EPOS) + full differential diagnosis table |
| Step 2 | Investigations - allergy workup, nasal/imaging, and lab panel (side-by-side) |
| Step 3 | Allergen avoidance - HDM, moulds, pollens, pets |
| Step 4 | Pharmacotherapy stepwise table - antihistamines → INCS (gold standard with doses) → LTRAs → saline → decongestants → OCS → antibiotics |
| Step 5 | Immunotherapy - SCIT vs SLIT, indications, contraindications |
| Step 6 | Surgical options (turbinate reduction techniques + FESS) + Biologics table (Dupilumab, Omalizumab, Mepolizumab, Tezepelumab) |
| Emergency Refer Box | ENT referral criteria (red-flag conditions) |
| Footer | Full source citations including 2025 NEJM + Allergy meta-analysis |