irongit

content: add Feather API (Hygo, Aug 2026 to present, Reno)

- Git-native HTTP client and Postman alternative in Rust with a Slint UI;
  Cloudflare Worker backend in TypeScript for auth and payments; Astro for
  marketing, payment, and sign-in pages
- Memory charts are data-driven: Feather API shows Postman 1.5 GB vs 170 MB,
  FlyingBytes keeps 3 GB vs 50 MB
- New Slint skill; skill group renamed "Web, mobile, and desktop"
huncholanehuncholaneauthored
parent 7174ecbcommit 6ca87abf3229c58339690d9ab3d75b433eb6d02aBrowse files

1 file changed, +44 -31

+44-31src/pages/index.astro
@@ -100,13 +100,27 @@ type Work = {
100100 href?: string;
101101 body: string[];
102102 stack?: string;
103- membar?: boolean;
103+ // Before and after memory chart: [label, value text]; ratio is after / before.
104+ membar?: { caption: string; before: [string, string]; after: [string, string]; ratio: number };
104105 places: PlaceId[];
105106 start?: [number, number]; // [year, month]; dated work draws on the timeline
106107 end?: [number, number]; // omitted means present
107108 };
108109
109110 const work: Work[] = [
111+ {
112+ name: 'Feather API',
113+ era: 'hygo',
114+ href: 'https://featherapi.com',
115+ body: [
116+ 'A git-native HTTP client and Postman alternative, written in Rust with a Slint desktop UI. Where Postman runs 9 processes in 1.5 GB of RAM, Feather API runs in 170 MB.',
117+ 'A Cloudflare Worker backend in TypeScript handles user authentication and payment processing, and Astro powers the marketing, payment, and sign-in pages.',
118+ ],
119+ membar: { caption: 'Memory in use', before: ['Postman', '1.5 GB'], after: ['Feather', '170 MB'], ratio: 170 / 1536 },
120+ stack: 'Rust, Slint, TypeScript, Astro, Cloudflare',
121+ places: ['reno'],
122+ start: [2026, 8],
123+ },
110124 {
111125 name: 'Hygo CRM',
112126 era: 'hygo',
@@ -187,7 +201,7 @@ const work: Work[] = [
187201 'The website and admin panel now run on Astro and TypeScript. It also runs a self-hosted, custom Rust fork of Frankfurter for foreign exchange rates, plus self-hosted Grafana, Loki, and Promtail for logs and dashboards.',
188202 ],
189203 stack: 'Rust, Python, Django, TypeScript, Astro, PostgreSQL, Redis, Docker, NGINX, Certbot, Self-hosted, Observability, CI/CD, AWS',
190- membar: true,
204+ membar: { caption: 'Production memory (RSS)', before: ['Before', '3 GB'], after: ['After', '50 MB'], ratio: 50 / 3072 },
191205 places: ['reno', 'grassvalley', 'knoxville', 'austin'],
192206 start: [2022, 10],
193207 },
@@ -326,8 +340,8 @@ const skillGroups: SkillGroup[] = [
326340 ],
327341 },
328342 {
329- label: 'Web and mobile',
330- short: 'Web and mobile',
343+ label: 'Web, mobile, and desktop',
344+ short: 'Web, mobile, desktop',
331345 color: '#f5d547',
332346 skills: [
333347 { name: 'Next.js', level: 7 },
@@ -335,6 +349,7 @@ const skillGroups: SkillGroup[] = [
335349 { name: 'SEO', level: 7 },
336350 { name: 'Flutter', level: 6 },
337351 { name: 'Web3', level: 6 },
352+ { name: 'Slint', level: 8 },
338353 ],
339354 },
340355 {
@@ -760,16 +775,16 @@ const pad = (n: number) => String(n).padStart(2, '0');
760775 </div>
761776 {w.membar && (
762777 <figure class="mt-6 border border-rule bg-paper/60 p-4" data-membar>
763- <figcaption class={`${mono} text-muted`}>Production memory (RSS)</figcaption>
764- <div class="mt-3 grid grid-cols-[3.75rem_minmax(0,1fr)_3.5rem] items-center gap-x-3 gap-y-2.5 font-mono text-xs tabular-nums">
765- <span class="text-muted">BEFORE</span>
778+ <figcaption class={`${mono} text-muted`}>{w.membar.caption}</figcaption>
779+ <div class="mt-3 grid grid-cols-[4.5rem_minmax(0,1fr)_3.5rem] items-center gap-x-3 gap-y-2.5 font-mono text-xs tabular-nums">
780+ <span class="text-muted uppercase">{w.membar.before[0]}</span>
766781 <span class="block h-3 w-full bg-rock" />
767- <span class="text-right font-bold">3 GB</span>
768- <span class="text-muted">AFTER</span>
782+ <span class="text-right font-bold">{w.membar.before[1]}</span>
783+ <span class="text-muted uppercase">{w.membar.after[0]}</span>
769784 <span class="block h-3">
770- <span class="membar-after block h-full bg-vol" />
785+ <span class="membar-after block h-full bg-vol" style={`--to:${(w.membar.ratio * 100).toFixed(2)}%`} />
771786 </span>
772- <span class="text-right font-bold text-vol">50 MB</span>
787+ <span class="text-right font-bold text-vol">{w.membar.after[1]}</span>
773788 </div>
774789 </figure>
775790 )}
@@ -1059,24 +1074,22 @@ const pad = (n: number) => String(n).padStart(2, '0');
10591074 setTimeout(() => (copyBtn.textContent = 'Copy address'), 2000);
10601075 });
10611076
1062- // Memory bar: the "after" bar starts at full width and shrinks to its
1063- // real size the first time it scrolls into view.
1064- const membar = document.querySelector<HTMLElement>('[data-membar]');
1065- if (membar && !reduce && 'IntersectionObserver' in window) {
1066- const rect = membar.getBoundingClientRect();
1067- if (rect.top > window.innerHeight) {
1068- membar.dataset.armed = '';
1069- const io = new IntersectionObserver(
1070- ([entry]) => {
1071- if (!entry.isIntersecting) return;
1072- io.disconnect();
1073- requestAnimationFrame(() => delete membar.dataset.armed);
1074- },
1075- { threshold: 0.9 },
1076- );
1077- io.observe(membar);
1078- }
1079- }
1077+ // Memory charts: the "after" bar starts at full width and shrinks to its
1078+ // real size the first time each chart scrolls into view.
1079+ document.querySelectorAll<HTMLElement>('[data-membar]').forEach((membar) => {
1080+ if (reduce || !('IntersectionObserver' in window)) return;
1081+ if (membar.getBoundingClientRect().top <= window.innerHeight) return;
1082+ membar.dataset.armed = '';
1083+ const io = new IntersectionObserver(
1084+ ([entry]) => {
1085+ if (!entry.isIntersecting) return;
1086+ io.disconnect();
1087+ requestAnimationFrame(() => delete membar.dataset.armed);
1088+ },
1089+ { threshold: 0.9 },
1090+ );
1091+ io.observe(membar);
1092+ });
10801093 </script>
10811094
10821095 <style is:global>
@@ -1636,9 +1649,9 @@ const pad = (n: number) => String(n).padStart(2, '0');
16361649 background: var(--color-vol);
16371650 }
16381651
1639- /* ---------- Memory bar: 50 MB of 3 GB (3072 MB) ---------- */
1652+ /* ---------- Memory charts: --to is after / before ---------- */
16401653 .membar-after {
1641- width: 1.63%;
1654+ width: var(--to);
16421655 min-width: 3px;
16431656 transition: width 1.6s cubic-bezier(0.22, 0.8, 0.2, 1) 0.15s;
16441657 }