this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 263 lines 16 kB view raw
1<!DOCTYPE html> 2<html lang="en"> 3<head> 4<meta charset="UTF-8"> 5<meta name="viewport" content="width=device-width, initial-scale=1.0"> 6<title>Servo — Opex &amp; Staffing Addendum</title> 7<link rel="preconnect" href="https://fonts.googleapis.com"> 8<link href="https://fonts.googleapis.com/css2?family=Pragati+Narrow:wght@400;700&display=swap" rel="stylesheet"> 9<style> 10 @page { size: A4; margin: 0; } 11 * { margin: 0; padding: 0; box-sizing: border-box; } 12 body { 13 font-family: "Pragati Narrow", sans-serif; 14 background: rgb(247, 226, 231); color: rgb(71, 11, 0); 15 width: 210mm; min-height: 297mm; padding: 22px 28px; 16 -webkit-print-color-adjust: exact; print-color-adjust: exact; 17 font-size: 12px; line-height: 1.4; 18 } 19 .header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 10px; padding-bottom: 6px; } 20 h1 { font-size: 24px; font-weight: 700; color: rgb(71, 11, 0); } 21 .byline { font-size: 11px; color: #1d1d1d; margin-top: 2px; } 22 .byline a { color: rgb(71, 11, 0); } 23 .date { font-size: 11px; color: #666; } 24 h2 { font-size: 13px; font-weight: 700; color: rgb(71, 11, 0); text-transform: uppercase; letter-spacing: 0.06em; margin: 12px 0 5px; border-bottom: 1px solid rgba(71, 11, 0, 0.2); padding-bottom: 3px; } 25 p { color: #1d1d1d; margin-bottom: 5px; font-size: 11.5px; line-height: 1.45; } 26 strong { color: rgb(71, 11, 0); } 27 table { width: 100%; border-collapse: collapse; font-size: 10.5px; margin: 4px 0 8px; } 28 th { text-align: left; color: #666; padding: 3px 6px; border-bottom: 1px solid rgba(71, 11, 0, 0.2); font-weight: 700; } 29 td { padding: 3px 6px; border-bottom: 1px solid rgba(71, 11, 0, 0.08); color: #1d1d1d; } 30 .r { text-align: right; } 31 .subtotal td { background: rgba(71, 11, 0, 0.04); font-weight: 600; } 32 .total td { background: rgba(71, 11, 0, 0.1); color: rgb(71, 11, 0); font-weight: 700; font-size: 11.5px; } 33 .two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; } 34 .callout { background: #fff; border: 1px solid rgba(71, 11, 0, 0.2); border-radius: 8px; padding: 10px 14px; margin-bottom: 10px; border-left: 3px solid rgb(71, 11, 0); } 35 .callout p { font-size: 11px; line-height: 1.5; } 36 .pass { color: #1f6e1f; } 37 .fail { color: #a33; } 38 .muted { color: #686868; } 39 a { color: rgb(71, 11, 0); } 40 .footer { font-size: 9px; color: #686868; text-align: center; margin-top: auto; padding-top: 4px; border-top: 1px solid rgba(71, 11, 0, 0.15); } 41 .dial-row { display: flex; gap: 14px; margin-bottom: 10px; flex-wrap: wrap; } 42 .dial-item { flex: 1; min-width: 120px; } 43 .dial-item label { display: block; font-size: 10px; color: #666; margin-bottom: 3px; } 44 .dial-item input[type="range"] { width: 100%; accent-color: rgb(71, 11, 0); } 45 .dial-val { font-size: 14px; font-weight: 700; color: rgb(71, 11, 0); text-align: center; } 46 .note { font-size: 9px; color: #686868; line-height: 1.4; } 47</style> 48</head> 49<body> 50 51<div style="background:#a33;color:#fff;text-align:center;padding:4px;font-size:11px;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;margin-bottom:6px;">DRAFT &mdash; Do not circulate without permission</div> 52 53<div class="header"> 54 <div> 55 <h1>Opex &amp; Staffing Addendum</h1> 56 <div class="byline">Servo Baseline Readiness &mdash; Dietrich Ayala &mdash; <a href="https://webtransitions.org">webtransitions.org</a></div> 57 </div> 58 <span class="date">February 2026</span> 59</div> 60 61<div class="callout"> 62 <p>The main report models <strong>engineering salary only</strong> (&euro;150k avg). This addendum provides the <strong>full loaded cost</strong> to operate an organization that employs those engineers &mdash; leadership, management, HR, legal, accounting, benefits, taxes, on-sites, infrastructure, and CX. Use the dials below to adjust the scenario.</p> 63</div> 64 65<div class="dial-row"> 66 <div class="dial-item"> 67 <label>Engineering FTE (new hires)</label> 68 <input type="range" id="opexFTE" min="5" max="50" value="22"> 69 <div class="dial-val" id="opexFTEVal">22</div> 70 </div> 71 <div class="dial-item"> 72 <label>BWA Target</label> 73 <input type="range" id="opexBWA" min="30" max="100" value="60"> 74 <div class="dial-val" id="opexBWAVal">60%</div> 75 </div> 76 <div class="dial-item"> 77 <label>Avg Eng Salary (&euro;k)</label> 78 <input type="range" id="opexSalary" min="80" max="300" step="5" value="150"> 79 <div class="dial-val" id="opexSalaryVal">&euro;150k</div> 80 </div> 81 <div class="dial-item"> 82 <label>Benefits Loading (%)</label> 83 <input type="range" id="opexBenefits" min="15" max="50" value="30"> 84 <div class="dial-val" id="opexBenefitsVal">30%</div> 85 </div> 86 <div class="dial-item"> 87 <label>Community FTE Offset (%)</label> 88 <input type="range" id="opexCommunity" min="0" max="50" value="0"> 89 <div class="dial-val" id="opexCommunityVal">0%</div> 90 </div> 91</div> 92 93<div id="opexOutput"></div> 94 95<div class="footer"> 96 Servo Baseline Readiness &mdash; Opex Addendum &mdash; <a href="https://webtransitions.org">webtransitions.org</a> 97</div> 98 99<script> 100function runOpex() { 101 const rawFTE = +document.getElementById('opexFTE').value; 102 const bwaPct = +document.getElementById('opexBWA').value; 103 const engSalary = +document.getElementById('opexSalary').value * 1000; 104 const benefitsPct = +document.getElementById('opexBenefits').value / 100; 105 const communityPct = +document.getElementById('opexCommunity').value / 100; 106 107 document.getElementById('opexFTEVal').textContent = rawFTE; 108 document.getElementById('opexBWAVal').textContent = bwaPct + '%'; 109 document.getElementById('opexSalaryVal').innerHTML = '&euro;' + (engSalary / 1000) + 'k'; 110 document.getElementById('opexBenefitsVal').textContent = (benefitsPct * 100) + '%'; 111 document.getElementById('opexCommunityVal').textContent = (communityPct * 100) + '%'; 112 113 // Community offset reduces paid new hires 114 const communityFTE = Math.round(rawFTE * communityPct); 115 const paidNewEng = rawFTE - communityFTE; 116 const existingEng = 13; // current contributors (largely unfunded/volunteer-equivalent) 117 const totalEng = existingEng + paidNewEng; 118 119 // Staffing ratios 120 const mgmtRatio = 7; // 1 eng manager per 7 engineers 121 const engManagers = Math.ceil(paidNewEng / mgmtRatio); 122 const needsVPEng = paidNewEng >= 15; 123 const needsCTO = paidNewEng >= 25; 124 125 // Salaries for non-engineering roles (annual, EUR) 126 const salaries = { 127 engManager: 180000, 128 vpEng: 220000, 129 cto: 250000, 130 devrel: 130000, 131 communityMgr: 120000, 132 hr: 100000, 133 accounting: 90000, 134 legal: 70000, // fractional / outsourced 135 ops: 95000, // office/IT ops 136 }; 137 138 // CX investment: scale with team size 139 const devrelCount = paidNewEng >= 15 ? 2 : 1; 140 const communityMgrCount = paidNewEng >= 10 ? 1 : 0; 141 142 // Support staff: scale with org size 143 const totalHeadcount = paidNewEng + engManagers + (needsVPEng ? 1 : 0) + (needsCTO ? 1 : 0) + devrelCount + communityMgrCount; 144 const hrCount = totalHeadcount >= 20 ? 1 : 0.5; // fractional for small orgs 145 const accountingCount = 0.5; // typically outsourced 146 const legalCount = 0.25; // fractional / outsourced 147 const opsCount = totalHeadcount >= 25 ? 1 : 0.5; 148 149 // Per-person costs 150 const onsitePerPerson = 4000; // per quarter 151 const onsitesPerYear = 4; 152 const infra = 2000; // per eng per month (CI, hosting, tools) 153 const remoteStipend = 3000; // per person per year 154 155 // Build line items 156 const lines = []; 157 const cat = (name, count, salary, note) => { 158 const base = Math.round(count * salary); 159 const loaded = Math.round(base * (1 + benefitsPct)); 160 lines.push({ category: name, count: count, base, loaded, note: note || '' }); 161 return loaded; 162 }; 163 164 // Engineering 165 let total = 0; 166 total += cat('Engineers (new paid)', paidNewEng, engSalary, 'Core engine, layout, DOM, CSS, JS integration'); 167 total += cat('Engineering Managers', engManagers, salaries.engManager, '1 per ~' + mgmtRatio + ' engineers'); 168 if (needsVPEng) total += cat('VP Engineering', 1, salaries.vpEng, 'Technical leadership'); 169 if (needsCTO) total += cat('CTO / Tech Director', 1, salaries.cto, 'Strategic technical direction'); 170 171 // CX 172 total += cat('Developer Relations', devrelCount, salaries.devrel, 'Community engagement, docs, evangelism'); 173 if (communityMgrCount > 0) total += cat('Community Manager', communityMgrCount, salaries.communityMgr, 'Community operations, contributor onboarding'); 174 175 // Operations 176 if (hrCount > 0) total += cat('HR / People Ops', hrCount, salaries.hr, hrCount < 1 ? 'Fractional / shared' : 'Full-time'); 177 total += cat('Accounting / Finance', accountingCount, salaries.accounting, 'Outsourced / fractional'); 178 total += cat('Legal', legalCount, salaries.legal, 'Outsourced / fractional'); 179 if (opsCount > 0) total += cat('IT / Ops', opsCount, salaries.ops, opsCount < 1 ? 'Fractional' : 'Full-time'); 180 181 const totalStaffLoaded = total; 182 183 // Non-staff costs 184 const onsiteCost = Math.round(totalHeadcount * onsitePerPerson * onsitesPerYear); 185 const infraCost = Math.round(paidNewEng * infra * 12); 186 const remoteCost = Math.round(totalHeadcount * remoteStipend); 187 const contingency = Math.round((totalStaffLoaded + onsiteCost + infraCost + remoteCost) * 0.05); 188 const totalAnnual = totalStaffLoaded + onsiteCost + infraCost + remoteCost + contingency; 189 190 // Render 191 const el = document.getElementById('opexOutput'); 192 193 let staffRows = lines.map(l => { 194 const countStr = l.count % 1 === 0 ? l.count : l.count.toFixed(1); 195 return '<tr><td>' + l.category + '</td><td class="r">' + countStr + '</td><td class="r">&euro;' + (l.base / 1000).toFixed(0) + 'k</td><td class="r">&euro;' + (l.loaded / 1000).toFixed(0) + 'k</td><td class="muted">' + l.note + '</td></tr>'; 196 }).join(''); 197 198 const totalHeadFinal = lines.reduce((s, l) => s + l.count, 0); 199 200 el.innerHTML = 201 '<h2>Scenario: ' + bwaPct + '% BWA in 3 Years &mdash; ' + paidNewEng + ' Paid New Engineers' + (communityFTE > 0 ? ' + ' + communityFTE + ' Community FTE' : '') + '</h2>' + 202 '<div class="two-col"><div>' + 203 204 '<h2 style="margin-top:0;">Staff Costs (Loaded)</h2>' + 205 '<table>' + 206 '<tr><th>Role</th><th class="r">Count</th><th class="r">Base/yr</th><th class="r">Loaded/yr</th><th>Notes</th></tr>' + 207 staffRows + 208 '<tr class="subtotal"><td>Staff Subtotal</td><td class="r">' + totalHeadFinal.toFixed(1) + '</td><td></td><td class="r">&euro;' + (totalStaffLoaded / 1e6).toFixed(2) + 'M</td><td class="muted">incl. ' + (benefitsPct * 100) + '% benefits loading</td></tr>' + 209 '</table>' + 210 211 '<h2>Non-Staff Costs</h2>' + 212 '<table>' + 213 '<tr><th>Item</th><th class="r">Annual</th><th>Notes</th></tr>' + 214 '<tr><td>Quarterly On-Sites</td><td class="r">&euro;' + (onsiteCost / 1000).toFixed(0) + 'k</td><td class="muted">' + totalHeadcount + ' people &times; &euro;' + (onsitePerPerson / 1000).toFixed(0) + 'k &times; ' + onsitesPerYear + '/yr</td></tr>' + 215 '<tr><td>Infrastructure &amp; CI</td><td class="r">&euro;' + (infraCost / 1000).toFixed(0) + 'k</td><td class="muted">' + paidNewEng + ' eng &times; &euro;' + (infra / 1000).toFixed(0) + 'k/mo (CI, hosting, tools)</td></tr>' + 216 '<tr><td>Remote Stipend</td><td class="r">&euro;' + (remoteCost / 1000).toFixed(0) + 'k</td><td class="muted">&euro;' + (remoteStipend / 1000).toFixed(0) + 'k/person/yr (equipment, coworking)</td></tr>' + 217 '<tr><td>Contingency (5%)</td><td class="r">&euro;' + (contingency / 1000).toFixed(0) + 'k</td><td class="muted">Buffer for unforeseen costs</td></tr>' + 218 '<tr class="subtotal"><td>Non-Staff Subtotal</td><td class="r">&euro;' + ((onsiteCost + infraCost + remoteCost + contingency) / 1000).toFixed(0) + 'k</td><td></td></tr>' + 219 '</table>' + 220 221 '</div><div>' + 222 223 '<h2 style="margin-top:0;">3-Year Total</h2>' + 224 '<table>' + 225 '<tr><th>Period</th><th class="r">Staff</th><th class="r">Non-Staff</th><th class="r">Total</th></tr>' + 226 '<tr><td>Year 1 (ramp)</td><td class="r">&euro;' + (totalStaffLoaded * 0.6 / 1e6).toFixed(2) + 'M</td><td class="r">&euro;' + ((onsiteCost + infraCost + remoteCost + contingency) * 0.6 / 1e6).toFixed(2) + 'M</td><td class="r">&euro;' + (totalAnnual * 0.6 / 1e6).toFixed(2) + 'M</td></tr>' + 227 '<tr><td>Year 2</td><td class="r">&euro;' + (totalStaffLoaded / 1e6).toFixed(2) + 'M</td><td class="r">&euro;' + ((onsiteCost + infraCost + remoteCost + contingency) / 1e6).toFixed(2) + 'M</td><td class="r">&euro;' + (totalAnnual / 1e6).toFixed(2) + 'M</td></tr>' + 228 '<tr><td>Year 3</td><td class="r">&euro;' + (totalStaffLoaded / 1e6).toFixed(2) + 'M</td><td class="r">&euro;' + ((onsiteCost + infraCost + remoteCost + contingency) / 1e6).toFixed(2) + 'M</td><td class="r">&euro;' + (totalAnnual / 1e6).toFixed(2) + 'M</td></tr>' + 229 '<tr class="total"><td>3-Year Total</td><td class="r">&euro;' + (totalStaffLoaded * 2.6 / 1e6).toFixed(2) + 'M</td><td class="r">&euro;' + ((onsiteCost + infraCost + remoteCost + contingency) * 2.6 / 1e6).toFixed(2) + 'M</td><td class="r">&euro;' + (totalAnnual * 2.6 / 1e6).toFixed(2) + 'M</td></tr>' + 230 '</table>' + 231 232 '<h2>Comparison: Eng-Only vs Full Loaded</h2>' + 233 '<table>' + 234 '<tr><th></th><th class="r">Eng Only</th><th class="r">Full Loaded</th><th class="r">Multiplier</th></tr>' + 235 '<tr><td>Annual</td><td class="r">&euro;' + (paidNewEng * engSalary / 1e6).toFixed(2) + 'M</td><td class="r">&euro;' + (totalAnnual / 1e6).toFixed(2) + 'M</td><td class="r">' + (totalAnnual / (paidNewEng * engSalary || 1)).toFixed(2) + 'x</td></tr>' + 236 '<tr><td>3-Year</td><td class="r">&euro;' + (paidNewEng * engSalary * 3 / 1e6).toFixed(2) + 'M</td><td class="r">&euro;' + (totalAnnual * 2.6 / 1e6).toFixed(2) + 'M</td><td class="r">' + (totalAnnual * 2.6 / (paidNewEng * engSalary * 3 || 1)).toFixed(2) + 'x</td></tr>' + 237 '</table>' + 238 '<p class="note" style="margin-top:6px;">The multiplier shows how much more the full org costs compared to raw engineering salaries. Typical range: 1.8&ndash;2.5x depending on benefits loading, team size, and operational overhead.</p>' + 239 240 '<h2>Org Chart Summary</h2>' + 241 '<p><strong>Total headcount:</strong> ' + totalHeadFinal.toFixed(1) + ' (paid)' + (communityFTE > 0 ? ' + ' + communityFTE + ' community FTE' : '') + '</p>' + 242 '<p><strong>Engineering:</strong> ' + paidNewEng + ' new + 13 existing = ' + totalEng + ' total</p>' + 243 '<p><strong>Management:</strong> ' + engManagers + ' eng mgr' + (needsVPEng ? ' + VP Eng' : '') + (needsCTO ? ' + CTO' : '') + '</p>' + 244 '<p><strong>CX:</strong> ' + devrelCount + ' DevRel' + (communityMgrCount > 0 ? ' + ' + communityMgrCount + ' Community Mgr' : '') + '</p>' + 245 '<p><strong>Ops:</strong> ' + hrCount.toFixed(1) + ' HR, ' + accountingCount.toFixed(1) + ' accounting, ' + legalCount.toFixed(2) + ' legal' + (opsCount > 0 ? ', ' + opsCount.toFixed(1) + ' IT' : '') + '</p>' + 246 247 '</div></div>' + 248 249 '<h2>Assumptions &amp; Notes</h2>' + 250 '<p class="note"><strong>Benefits loading (' + (benefitsPct * 100) + '%):</strong> Employer taxes (social security, pension), health insurance, equipment, professional development. EU-typical range 25&ndash;40%. &bull; <strong>On-sites:</strong> &euro;' + (onsitePerPerson / 1000).toFixed(0) + 'k/person/quarter covers travel, accommodation, venue for distributed team meetups. &bull; <strong>Infrastructure:</strong> &euro;' + (infra / 1000).toFixed(0) + 'k/eng/month covers CI runners, cloud hosting, development tools, WPT infrastructure. &bull; <strong>Ramp:</strong> Year 1 at 60% assumes mid-year average as hiring ramps. &bull; <strong>Existing 13 FTE:</strong> Current contributors are largely volunteer or externally funded; their costs are not included. &bull; <strong>Inflation:</strong> Not modeled; add 2&ndash;4% annually for multi-year planning. &bull; <strong>No office costs:</strong> Assumes remote-first with on-site gatherings.</p>'; 251 252} 253 254document.getElementById('opexFTE').addEventListener('input', runOpex); 255document.getElementById('opexBWA').addEventListener('input', runOpex); 256document.getElementById('opexSalary').addEventListener('input', runOpex); 257document.getElementById('opexBenefits').addEventListener('input', runOpex); 258document.getElementById('opexCommunity').addEventListener('input', runOpex); 259runOpex(); 260</script> 261 262</body> 263</html>