<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Home - Tom Barrasso</title><link>https://barrasso.me/</link><description>Recent content - Home - Tom Barrasso</description><language>en-us</language><managingEditor>blog@barrasso.me (Tom Barrasso)</managingEditor><webMaster>blog@barrasso.me (Tom Barrasso)</webMaster><lastBuildDate>Tue, 21 Oct 2025 15:00:00 GMT</lastBuildDate><image><url>https://barrasso.me///images/portraits.jpg</url><title>Home - Tom Barrasso</title><link>https://barrasso.me/</link></image><atom:link href="https://barrasso.me/index.xml" rel="self" type="application/rss+xml"/><item><title>
Introducing iClaw, a safe, private, on-device AI agent using Apple Intelligence</title><link>https://barrasso.me/posts/2026-04-27-iclaw-ai-agent-using-apple-intelligence/</link><description>
&lt;p&gt;I recently shared &lt;a href="https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/"&gt;Junco&lt;/a&gt;, an on-device AI agent that uses Apple Intelligence to write and edit Swift code. Today I&amp;rsquo;m sharing &lt;a href="https://geticlaw.com"&gt;iClaw&lt;/a&gt;, an &lt;em&gt;experimental&lt;/em&gt; local AI agent for macOS designed around safety and privacy.&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2026-04-27-iclaw-ai-agent-using-apple-intelligence/iClaw-London-Screenrecord_hu_857cb7b07d602876.webp 320w' sizes="100vw" /&gt;
&lt;source type="image/gif" srcset='https://barrasso.me/posts/2026-04-27-iclaw-ai-agent-using-apple-intelligence/iClaw-London-Screenrecord_hu_6311b62e2dce29d4.gif 320w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2026-04-27-iclaw-ai-agent-using-apple-intelligence/iClaw-London-Screenrecord_hu_6311b62e2dce29d4.gif"
alt="Screen recording of iClaw on macOS" class="no-border contain mx-auto"
width="320"
height="369"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Screen recording of iClaw local AI agent on macOS&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;iClaw started as a &lt;a href="https://www.sundai.club/projects/aef4852e-7089-4fa0-8780-d45008f8d791"&gt;SundAI hackathon project&lt;/a&gt; in March. Although our &lt;a href="https://www.youtube.com/live/YM7Oe-I8hIw?t=36204"&gt;demo failed&lt;/a&gt;, we learned many lessons I&amp;rsquo;ll share. But first:&lt;/p&gt;
&lt;h2 id="why-build-iclaw"&gt;
Why Build iClaw?
&lt;a class="heading-anchor" href="#why-build-iclaw" aria-label="Link to Why Build iClaw?"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Technologies like &lt;a href="https://openclaw.ai/"&gt;OpenClaw&lt;/a&gt; are transformative, and while it&amp;rsquo;s among the fastest-growing GitHub repositories, it hasn&amp;rsquo;t &lt;a href="https://en.wikipedia.org/wiki/Crossing_the_Chasm"&gt;crossed the chasm&lt;/a&gt; into mainstream use. I suspect non-technical users lack the time, knowledge, patience, and risk appetite to purchase a Mac Mini and AI credits, configure agents, and give them access to their personal accounts.&lt;/p&gt;
&lt;p&gt;iClaw is fundamentally different. It&amp;rsquo;s designed to use the AI you already have (Apple Intelligence), without buying credits or a subscription. Built inside Apple&amp;rsquo;s &lt;a href="https://developer.apple.com/documentation/security/app-sandbox"&gt;App Sandbox&lt;/a&gt;, iClaw focuses on safety, security, and privacy: it only accesses files, data, and accounts you give it explicit permission to. As a bonus, &lt;a href="https://www.weforum.org/stories/2025/03/on-device-ai-energy-system-chatgpt-grok-deepx/"&gt;on-device AI is green&lt;/a&gt;—using orders of magnitude less energy than frontier data centers.&lt;/p&gt;
&lt;h2 id="what-can-iclaw-do"&gt;
What can iClaw do?
&lt;a class="heading-anchor" href="#what-can-iclaw-do" aria-label="Link to What can iClaw do?"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;At present, &lt;strong&gt;iClaw is basically a bad Siri&lt;/strong&gt;. Ask it for the weather, a stock quote, a Wikipedia summary, or some basic math. It uses Apple&amp;rsquo;s tools to read calendar events, search through email, transcribe a podcast, or translate a document. But iClaw (and general-purpose on-device AI) has some major limits today. Small models excel at executing a single, narrowly defined task, but they fail at task decomposition and &amp;ldquo;reading between the lines.&amp;rdquo; If you don&amp;rsquo;t spell out exactly what you want, on-device AI models are much more likely to make mistakes.&lt;/p&gt;
&lt;p&gt;Some of iClaw&amp;rsquo;s key features include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Just-in-time Permissions&lt;/strong&gt;. iClaw doesn&amp;rsquo;t use blanket permissions or policies. It lives in the App Sandbox and asks permission whenever it wants to read your Calendar or send an iMessage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Browser Bridge&lt;/strong&gt;. iClaw can read and interact with Safari, Chrome, or Firefox through a Web Extension.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dynamic Widgets&lt;/strong&gt;. iClaw creates on-the-fly widgets using it&amp;rsquo;s own DSL, so you don&amp;rsquo;t have to read a wall of text.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What&amp;rsquo;s exciting is what iClaw &lt;em&gt;could&lt;/em&gt; do with an updated model and an improved harness.&lt;/p&gt;
&lt;h3 id="what-could-iclaw-do"&gt;
What &lt;em&gt;could&lt;/em&gt; iClaw do?
&lt;a class="heading-anchor" href="#what-could-iclaw-do" aria-label="Link to What could iClaw do?"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;iClaw suffers because it tries to augment a constrained model (Apple&amp;rsquo;s 3B Foundation Model) with natural language processing (NLP) and machine learning (ML) classifiers. As a task gets more complex, or the number of tools expands, the model gets distracted, makes mistakes, or outright refuses to do anything!&lt;/p&gt;
&lt;p&gt;In building iClaw, I&amp;rsquo;ve explored lots of applications and integration surfaces. Of the many ideas I had for iClaw, a few still hold promise.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Personalized Email Triage&lt;/strong&gt;: use &lt;a href="https://developer.apple.com/documentation/mailkit"&gt;MailKit&lt;/a&gt; and Apple Intelligence to privately and securely triage, tag, filter, forward, and delete incoming emails&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mac-iPhone Continuity&lt;/strong&gt;: use Apple Intelligence and iCloud Sync to search your MacBook&amp;rsquo;s files via &lt;a href="https://developer.apple.com/documentation/foundation/spotlight"&gt;Spotlight&lt;/a&gt; on the go from your iPhone&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automate Everything&lt;/strong&gt;: use Apple Intelligence to author custom &lt;a href="https://developer.apple.com/library/archive/documentation/AppleScript/Conceptual/AppleScriptLangGuide/introduction/ASLR_intro.html"&gt;AppleScript automation&lt;/a&gt; for transcription, conversion, batch image editing, and more&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Background Browsing&lt;/strong&gt;: natively bridge a &lt;a href="https://developer.apple.com/safari/extensions/"&gt;Safari Extension&lt;/a&gt; to Apple Intelligence so AI can browse real estate listings, classifieds, and social media on your behalf&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Learned Skills&lt;/strong&gt;: Skills written in Markdown are simple, but not intuitive for the average user. A better approach would be to &amp;ldquo;learn&amp;rdquo; common tasks and codify them into Skills&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Apple itself may be exploring similar applications, with rumors of &lt;a href="https://www.powerpage.org/rumor-apple-developing-ai-based-siri-chatbot-for-ios-27-ipados-27-and-macos-27/"&gt;&amp;ldquo;Campos&amp;rdquo;&lt;/a&gt;, an AI-powered Siri chatbot with access to Photos, Mail, Messages, and more.&lt;/p&gt;
&lt;h3 id="why-on-device"&gt;
Why On-Device?
&lt;a class="heading-anchor" href="#why-on-device" aria-label="Link to Why On-Device?"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;Local models aren&amp;rsquo;t just about privacy — they&amp;rsquo;re about &lt;a href="https://www.youtube.com/watch?v=RaAFquzj5B8"&gt;business strategy&lt;/a&gt;, unit economics, and &lt;a href="https://www.theregister.com/2026/04/22/anthropic_removes_claude_code_pro/"&gt;experiments&lt;/a&gt; that indicate venture capital subsidies are ending. It will always be more cost-effective in the long run to use the compute you already have, rather than lease it from someone else.&lt;/p&gt;
&lt;h4 id="local-tradeoffs"&gt;
Local Tradeoffs
&lt;a class="heading-anchor" href="#local-tradeoffs" aria-label="Link to Local Tradeoffs"&gt;#&lt;/a&gt;
&lt;/h4&gt;
&lt;p&gt;There&amp;rsquo;s no such thing as a &lt;a href="https://en.wikipedia.org/wiki/No_free_lunch_theorem"&gt;free lunch&lt;/a&gt;. Apple&amp;rsquo;s &lt;a href="https://machinelearning.apple.com/research/apple-foundation-models-2025-updates"&gt;3B Foundation Model&lt;/a&gt; is arguably one of the &lt;em&gt;worst&lt;/em&gt; contenders for an on-device agent. It&amp;rsquo;s terrible at following instructions, has a minuscule 4,096-token context window, doesn&amp;rsquo;t offer a &lt;a href="https://rockyshikoku.medium.com/building-a-thinking-mode-with-apples-foundation-models-5601ff5bd430"&gt;native thinking mode&lt;/a&gt;, has aggressive &lt;a href="https://developer.apple.com/documentation/FoundationModels/improving-the-safety-of-generative-model-output"&gt;safety guardrails&lt;/a&gt;, and can be &lt;a href="https://www.reddit.com/r/swift/comments/1nrv3lo/swiftfoundation_models_framework_missing_single_a/"&gt;extremely flakey&lt;/a&gt;. Then there are concerns related to how AI affects battery life, uses tons of memory, and serves inference sequentially (so your app might have to wait its turn). Despite these shortcomings, Apple Intelligence can still be quite useful for tasks like summarization, redaction, pattern matching, and search augmentation.&lt;/p&gt;
&lt;h4 id="what-about-other-models"&gt;
What about other models?
&lt;a class="heading-anchor" href="#what-about-other-models" aria-label="Link to What about other models?"&gt;#&lt;/a&gt;
&lt;/h4&gt;
&lt;p&gt;My laptop is a &lt;a href="https://amzn.to/3OyFKru"&gt;24GB M4 MacBook Air&lt;/a&gt;, which by most standards is fairly capable. Yet when it comes to local models, 24GB really limits what&amp;rsquo;s possible. iClaw natively supports &lt;a href="https://ollama.com/"&gt;Ollama&lt;/a&gt;, so you can bring-your-own-model (BYOM). I&amp;rsquo;ve tested quantized (Q4_K_M) versions of &lt;a href="https://deepmind.google/models/gemma/gemma-4/"&gt;Gemma 4 E4B&lt;/a&gt; and &lt;a href="https://huggingface.co/Qwen/Qwen3.5-4B"&gt;Qwen3.5-4B&lt;/a&gt;. Both generally performed better than the AFM at routing, judging, and responding in a conversational way. However, both were noticeably slower and had a tendency to ramble. This is of course subjective and likely hardware-dependent, but for most macOS users, the fastest and most efficient on-device AI model is almost certainly Apple&amp;rsquo;s Foundation Models.&lt;/p&gt;
&lt;h2 id="what-i-learned"&gt;
What I Learned
&lt;a class="heading-anchor" href="#what-i-learned" aria-label="Link to What I Learned"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;h3 id="1-just-a-few-tools"&gt;
1. Just a few tools
&lt;a class="heading-anchor" href="#1-just-a-few-tools" aria-label="Link to 1. Just a few tools"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2026-04-27-iclaw-ai-agent-using-apple-intelligence/iClaw-Podcast-Ollama_hu_fa36b8c7e40a09ba.webp 320w, https://barrasso.me/posts/2026-04-27-iclaw-ai-agent-using-apple-intelligence/iClaw-Podcast-Ollama_hu_d95beb2448c94eb0.webp 640w' sizes="100vw" /&gt;
&lt;source type="image/png" srcset='https://barrasso.me/posts/2026-04-27-iclaw-ai-agent-using-apple-intelligence/iClaw-Podcast-Ollama_hu_9ef74d4b8e91b0bf.png 320w, https://barrasso.me/posts/2026-04-27-iclaw-ai-agent-using-apple-intelligence/iClaw-Podcast-Ollama_hu_c7df6d088cd10686.png 640w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2026-04-27-iclaw-ai-agent-using-apple-intelligence/iClaw-Podcast-Ollama_hu_9ef74d4b8e91b0bf.png"
alt="Early screenshot of iClaw listing podcasts using Gemma 4 E2B with Ollama" class="no-border contain mx-auto"
width="320"
height="371"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Early screenshot of iClaw listing podcasts using Gemma 4 E2B with Ollama&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;Arguably it&amp;rsquo;s better to &lt;strong&gt;&lt;a href="https://zenhabits.net/one-thing/"&gt;do one thing well&lt;/a&gt;&lt;/strong&gt;, rather than many things poorly.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://developer.apple.com/documentation/foundationmodels/tool"&gt;Tool calling&lt;/a&gt; is an important capability that allows AI models to access your files, contacts, messages, or the web. Early versions of iClaw actually worked well, but when our tool count ballooned from 4 to 14, we quickly realized that the AFM was basically calling tools at random, or not at all.&lt;/p&gt;
&lt;p&gt;Small models like the Apple Foundation Model (AFM) get &lt;a href="https://tldr.takara.ai/p/2302.00093"&gt;easily distracted&lt;/a&gt;, so if you&amp;rsquo;re building on Apple Intelligence it&amp;rsquo;s best to &lt;strong&gt;limit the model&amp;rsquo;s tool choice&lt;/strong&gt; to just 2 or 3. A major challenge in AI is testability and reproducibility. If your app has fewer tools, fewer settings, and fewer features, there are fewer state combinations to worry about.&lt;/p&gt;
&lt;h3 id="2-no-context-snowballs"&gt;
2. No context snowballs
&lt;a class="heading-anchor" href="#2-no-context-snowballs" aria-label="Link to 2. No context snowballs"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;All AI models, including frontier models, get &lt;a href="https://arxiv.org/abs/2307.03172"&gt;lost in the middle&lt;/a&gt;. On small models, &lt;a href="https://redis.io/blog/context-rot/"&gt;context rot&lt;/a&gt; and bloat will completely derail an agent. OpenClaw has &lt;a href="https://docs.openclaw.ai/concepts/session-pruning"&gt;session pruning&lt;/a&gt;, OpenCode has a &lt;a href="https://opencode.ai/docs/tui/#compact"&gt;&lt;code&gt;/compact&lt;/code&gt; command&lt;/a&gt;, and &lt;a href="https://claudefa.st/blog/guide/mechanics/auto-dream"&gt;Claude can dream&lt;/a&gt;. These are all strategies for pruning context to keep it relevant and stay within context window limits.&lt;/p&gt;
&lt;p&gt;For small models like the AFM, don&amp;rsquo;t let your context snowball. Instead, build scaffolding that keeps you well within the 4,096-token window and avoids the most devastating failure modes. iClaw ships with several &lt;a href="https://developer.apple.com/documentation/createml/creating-a-text-classifier-model"&gt;text classifiers&lt;/a&gt; that act as tool routers.&lt;/p&gt;
&lt;p&gt;Rather than ask the model which of 40+ tools to use, iClaw uses a small classifier that runs in milliseconds. This too can fail, but it will do so deterministically. Sometimes deterministic failure can be an advantage. Consider &lt;a href="https://barrasso.me/posts/2026-04-22-text-to-bash-with-shell-talk/"&gt;ShellTalk&lt;/a&gt;, a text-to-bash utility I built. Changes can be tested against hundreds of cases in minutes. If you can reliably measure accuracy, you can use a technique like &lt;a href="https://arxiv.org/pdf/2603.28052"&gt;Meta-Harness&lt;/a&gt; or &lt;a href="https://github.com/karpathy/autoresearch"&gt;Autoresearch&lt;/a&gt; to autonomously and continuously drive improvements.&lt;/p&gt;
&lt;h3 id="3-lower-your-expectations"&gt;
3. Lower your expectations
&lt;a class="heading-anchor" href="#3-lower-your-expectations" aria-label="Link to 3. Lower your expectations"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2026-04-27-iclaw-ai-agent-using-apple-intelligence/iClaw-Early-Comparison_hu_3422c81336a44682.webp 320w, https://barrasso.me/posts/2026-04-27-iclaw-ai-agent-using-apple-intelligence/iClaw-Early-Comparison_hu_dccbb4eb802d55b7.webp 640w' sizes="100vw" /&gt;
&lt;source type="image/png" srcset='https://barrasso.me/posts/2026-04-27-iclaw-ai-agent-using-apple-intelligence/iClaw-Early-Comparison_hu_ee5d661beda40ca9.png 320w, https://barrasso.me/posts/2026-04-27-iclaw-ai-agent-using-apple-intelligence/iClaw-Early-Comparison_hu_ba99dac5dba5b9ea.png 640w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2026-04-27-iclaw-ai-agent-using-apple-intelligence/iClaw-Early-Comparison_hu_ee5d661beda40ca9.png"
alt="Early screenshot of iClaw dynamically generating a comparison table" class="no-border contain mx-auto"
width="320"
height="488"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Early screenshot of iClaw dynamically generating a comparison table&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;I had dozens of ideas for iClaw. One notable idea took inspiration from &lt;a href="https://a2ui.org/"&gt;Agent-to-UI (A2UI)&lt;/a&gt;: dynamically rendering widgets, rather than a wall of text. Early tests worked! The AFM could output a custom, token-efficient, XML-like domain-specific language (DSL) to render lists, tables, images, and chips. But it wasn&amp;rsquo;t long before the model started spitting out tables with strange layouts: duplicating headers, misaligning columns, or omitting key data.&lt;/p&gt;
&lt;p&gt;My second approach involved &lt;a href="https://developer.apple.com/documentation/foundationmodels/loading-and-using-a-custom-adapter-with-foundation-models"&gt;training a custom adapter&lt;/a&gt; and pre-generating templates for a slot-fill strategy. This worked &lt;em&gt;better&lt;/em&gt; in that it prevented the most egregious failure modes, but it was still far from consistent.&lt;/p&gt;
&lt;p&gt;My advice: lower your expectations. On-device AI &lt;em&gt;will&lt;/em&gt; make more mistakes, so give it fewer tasks to mess up. For now, this is the price you pay when your cost per million tokens is $0.&lt;/p&gt;
&lt;h3 id="4-other-lessons"&gt;
4. Other lessons
&lt;a class="heading-anchor" href="#4-other-lessons" aria-label="Link to 4. Other lessons"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;h4 id="nested-choices"&gt;
Nested choices
&lt;a class="heading-anchor" href="#nested-choices" aria-label="Link to Nested choices"&gt;#&lt;/a&gt;
&lt;/h4&gt;
&lt;p&gt;Replace flat lists–&lt;code&gt;weather&lt;/code&gt;, &lt;code&gt;calendar&lt;/code&gt;, &lt;code&gt;messages&lt;/code&gt;, &lt;code&gt;news&lt;/code&gt;–with clustered domains–&lt;code&gt;media&lt;/code&gt; and &lt;code&gt;communication&lt;/code&gt;. Nested and hierarchical choice enhances user experience by reducing mental load, and improves model performance by reducing tool selection errors. Local models have the benefit of zero network latency, so they are better suited at sequential tasks. Again, these models are hopeless if you give them 40 tools, but use a classifier to pick 1 of 8 domains, then ask the model to select when of the 3 tools in that domain to use.&lt;/p&gt;
&lt;h4 id="arithmetic-and-hallucinations"&gt;
Arithmetic and hallucinations
&lt;a class="heading-anchor" href="#arithmetic-and-hallucinations" aria-label="Link to Arithmetic and hallucinations"&gt;#&lt;/a&gt;
&lt;/h4&gt;
&lt;p&gt;AI models are &lt;em&gt;still&lt;/em&gt; &lt;a href="https://primo.mobi/en/blogs/primo-blog/why-ai-fails-math-en"&gt;bad at math&lt;/a&gt;. Don&amp;rsquo;t ask the AFM to do arithmetic, format dates, or convert units — it will confidently get them &lt;em&gt;really&lt;/em&gt; wrong. Instead, have tools return facts and let the LLM rephrase these into natural language. Emit LaTeX, timezone identifiers, and precisely-formatted numbers then verify the model&amp;rsquo;s output against the formatted inputs.&lt;/p&gt;
&lt;h4 id="dont-pollute-the-context"&gt;
Don&amp;rsquo;t pollute the context
&lt;a class="heading-anchor" href="#dont-pollute-the-context" aria-label="Link to Don’t pollute the context"&gt;#&lt;/a&gt;
&lt;/h4&gt;
&lt;p&gt;One unhandled exception can pollute an entire context chain. If a tool returns an error, and that error gets passed as context into the next turn with the model, prompt outputs can go way off track. Catch errors, and either retry deterministically or ask the LLM to heal certain classes of errors. When it&amp;rsquo;s not possible, ask the LLM to phrase the error in a human-readable way. Error handling is definitely &lt;a href="https://utcc.utoronto.ca/~cks/space/blog/programming/ErrorHandlingNotSolvedProblem"&gt;not a solved problem&lt;/a&gt; and LLMs offer an entirely new class of vulnerabilities and errors to deal with.&lt;/p&gt;
&lt;h4 id="rewind-the-tapes"&gt;
Rewind the tapes
&lt;a class="heading-anchor" href="#rewind-the-tapes" aria-label="Link to Rewind the tapes"&gt;#&lt;/a&gt;
&lt;/h4&gt;
&lt;p&gt;Ruby&amp;rsquo;s &lt;a href="https://github.com/vcr/vcr"&gt;VCR&lt;/a&gt; library is still one of my favorites. Unit tests need to be reliable, repeatable, fast, and thorough in order to be informative. Like HTTP requests, LLM responses are non-deterministic. Consider running, storing, and replaying past responses so your unit tests are consistent.&lt;/p&gt;
&lt;h3 id="tactical-advice"&gt;
Tactical advice
&lt;a class="heading-anchor" href="#tactical-advice" aria-label="Link to Tactical advice"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;The AFM does not have a lot of levers: no public weights to adjust, no thinking or reasoning modes, and no constrained grammar. What it does have, you &lt;em&gt;should&lt;/em&gt; use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Limit response size (and lower response time) with &lt;a href="https://developer.apple.com/documentation/foundationmodels/generationoptions/maximumresponsetokens"&gt;&lt;code&gt;maximumResponseTokens&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Make responses more predictable by adjusting the &lt;a href="https://developer.apple.com/documentation/foundationmodels/generationoptions/temperature"&gt;&lt;code&gt;temperature&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Make diverse but constrained choices by setting the &lt;a href="https://developer.apple.com/documentation/foundationmodels/generationoptions/sampling"&gt;&lt;code&gt;sampling&lt;/code&gt;&lt;/a&gt; mode&lt;/li&gt;
&lt;li&gt;Call &lt;a href="https://developer.apple.com/documentation/foundationmodels/languagemodelsession/prewarm(promptprefix:)"&gt;&lt;code&gt;prewarm&lt;/code&gt;&lt;/a&gt; to reduce latency, cache a prompt prefix, and affect memory residency&lt;/li&gt;
&lt;li&gt;Use &lt;a href="https://developer.apple.com/documentation/foundationmodels/instructionsbuilder"&gt;&lt;code&gt;InstructionsBuilder&lt;/code&gt;&lt;/a&gt; to increase instruction compliance&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The right prompt and appropriate settings can actually produce dramatic improvements!&lt;/p&gt;
&lt;h2 id="whats-next"&gt;
What&amp;rsquo;s Next?
&lt;a class="heading-anchor" href="#whats-next" aria-label="Link to What’s Next?"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://geticlaw.com"&gt;iClaw&lt;/a&gt; is available for free with &lt;a href="https://github.com/LastByteLLC/iClaw"&gt;code on GitHub&lt;/a&gt;. I&amp;rsquo;ll be applying these lessons, and learning many more, as I continue building iClaw and local-first AI products. Stay tuned for more!&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2026-04-27-iclaw-ai-agent-using-apple-intelligence/</guid><pubDate>Mon, 27 Apr 2026 00:00:00 GMT</pubDate></item><item><title>
Deterministic text-to-bash with ShellTalk</title><link>https://barrasso.me/posts/2026-04-22-text-to-bash-with-shell-talk/</link><description>
&lt;p&gt;I recently released &lt;a href="https://github.com/LastByteLLC/ShellTalk"&gt;ShellTalk&lt;/a&gt;, an Apache 2.0 licensed CLI and library that converts natural language English text into the Bash commands.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;shelltalk &lt;span style="color:#e6db74"&gt;&amp;#34;find all images in this folder&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;find . -type f &lt;span style="color:#ae81ff"&gt;\(&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -name &lt;span style="color:#e6db74"&gt;&amp;#39;*.jpg&amp;#39;&lt;/span&gt; -o &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -name &lt;span style="color:#e6db74"&gt;&amp;#39;*.jpeg&amp;#39;&lt;/span&gt; -o &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -name &lt;span style="color:#e6db74"&gt;&amp;#39;*.png&amp;#39;&lt;/span&gt; -o &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -name &lt;span style="color:#e6db74"&gt;&amp;#39;*.gif&amp;#39;&lt;/span&gt; -o &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -name &lt;span style="color:#e6db74"&gt;&amp;#39;*.webp&amp;#39;&lt;/span&gt; -o &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -name &lt;span style="color:#e6db74"&gt;&amp;#39;*.svg&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;\)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;While building &lt;a href="https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/"&gt;Junco&lt;/a&gt;, I discovered &lt;a href="https://github.com/es617/hunch"&gt;Hunch&lt;/a&gt;, an on-device CLI that uses &lt;a href="https://www.apple.com/apple-intelligence/"&gt;Apple Intelligence&lt;/a&gt; and few-shot dynamic retrieval to convert text to bash. The concept was fascinating, and proved that even a small non-coding model like the 3B Apple Foundation Model (AFM), with guardrails, is capable of producing valid Bash scripts from natural language input.&lt;/p&gt;
&lt;p&gt;Using the AFM is ideal because it&amp;rsquo;s incredibly optimized and comes pre-installed on many Macs with macOS 26 Tahoe. However, it&amp;rsquo;s not truly portable since it has a runtime dependency on a limited-availability small language model (SLM). Given that single-command bash is a pretty constrained space, I hypothesized that this might be possible without LLMs at all. My approach for ShellTalk was leveraging &lt;strong&gt;Semantic Template Matching (STM)&lt;/strong&gt; to map &lt;strong&gt;intent → command&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="why-no-llms"&gt;
Why no LLMs?
&lt;a class="heading-anchor" href="#why-no-llms" aria-label="Link to Why no LLMs?"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;On-device AI keeps getting better, cheaper, and more efficient, but it&amp;rsquo;s not a panacea. It&amp;rsquo;s still not easily accessible on many environments like embedded devices, older mobiles, or the web. Beyond LLMs, Natural Language Processing (NLP) has continued to advance with more efficient embedding models that come pre-packaged into many runtimes like Apple&amp;rsquo;s &lt;a href="https://developer.apple.com/documentation/naturallanguage/nlembedding"&gt;&lt;code&gt;NLEmbedding&lt;/code&gt;&lt;/a&gt; introduced in macOS/ iOS 13.&lt;/p&gt;
&lt;p&gt;Moreover, deterministic approaches are typically faster and easier to reproduce, resulting in shorter testing and iteration cycles. Given text-to-bash has relatively constrained inputs and outputs, I figured a tool like &lt;a href="https://claude.ai/referral/1is-TiX1TQ"&gt;Claude Code&lt;/a&gt; could quickly prototype and iterate autonomously towards a working solution similar to &lt;a href="https://arxiv.org/pdf/2603.28052"&gt;Meta-Harness&lt;/a&gt;. Effectively, this technique &lt;strong&gt;builds a system that build systems&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="how-shelltalk-works"&gt;
How ShellTalk works
&lt;a class="heading-anchor" href="#how-shelltalk-works" aria-label="Link to How ShellTalk works"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;ShellTalk has a sequential pipeline:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Entity recognition&lt;/strong&gt; - regular expressions (file paths, URLs), lexicon (installed commands), preposition framing (&amp;ldquo;in X&amp;rdquo; or &amp;ldquo;on Y&amp;rdquo;), and (optionally) &lt;a href="https://developer.apple.com/documentation/naturallanguage/nltagger"&gt;&lt;code&gt;NLTagger&lt;/code&gt;&lt;/a&gt; on macOS for parts of speech (POS) recognition&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Category matching&lt;/strong&gt; - Best Matching 25 (BM25) picks the most probable category (Git, ImageMagick, File I/O)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Template match&lt;/strong&gt; - Score candidates across 167+ templates using BM25, Term Frequency-Inverse Document Frequency (TF-IDF), and &lt;code&gt;NLEmbedding&lt;/code&gt; cosine distance on macOS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Slot extraction&lt;/strong&gt; - entity &amp;amp; regex slot-fill replaces placeholders with actual file names, extension suffixes, URLs, etc&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Path resolution&lt;/strong&gt; - BSD vs GNU adapts commands and flags between macOS (BSD &lt;code&gt;coreutils&lt;/code&gt;) and Linux (GNU)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Validation&lt;/strong&gt; - confirm command existence, &lt;code&gt;bash -n&lt;/code&gt; syntax validation, and safety scoring&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="testing-shelltalk"&gt;
Testing ShellTalk
&lt;a class="heading-anchor" href="#testing-shelltalk" aria-label="Link to Testing ShellTalk"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;figure class="full-bleed"&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2026-04-22-text-to-bash-with-shell-talk/screenshot-shelltalk-wasm_hu_f013f799ab31bd49.webp 320w, https://barrasso.me/posts/2026-04-22-text-to-bash-with-shell-talk/screenshot-shelltalk-wasm_hu_4cbc8174914341fa.webp 640w, https://barrasso.me/posts/2026-04-22-text-to-bash-with-shell-talk/screenshot-shelltalk-wasm_hu_2245fef1cd3f8801.webp 1024w, https://barrasso.me/posts/2026-04-22-text-to-bash-with-shell-talk/screenshot-shelltalk-wasm_hu_28a9f61fd7c51c7f.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/png" srcset='https://barrasso.me/posts/2026-04-22-text-to-bash-with-shell-talk/screenshot-shelltalk-wasm_hu_e893758bcbe998d8.png 320w, https://barrasso.me/posts/2026-04-22-text-to-bash-with-shell-talk/screenshot-shelltalk-wasm_hu_a46d36bed0159e46.png 640w, https://barrasso.me/posts/2026-04-22-text-to-bash-with-shell-talk/screenshot-shelltalk-wasm_hu_5f7d9f8083492ba.png 1024w, https://barrasso.me/posts/2026-04-22-text-to-bash-with-shell-talk/screenshot-shelltalk-wasm_hu_cd36333cc3515c56.png 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2026-04-22-text-to-bash-with-shell-talk/screenshot-shelltalk-wasm_hu_e893758bcbe998d8.png"
alt="Screenshot of ShellTalk in the browser" class="mx-auto no-border contain"
width="320"
height="160"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Screenshot of ShellTalk in the browser&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;You can test ShellTalk yourself on &lt;a href="https://lastbytellc.github.io/ShellTalk/"&gt;GitHub Pages&lt;/a&gt;. This version compiles and optimizes the Swift library into WebAssembly (Wasm) using &lt;a href="https://github.com/WebAssembly/binaryen"&gt;Binaryen&lt;/a&gt;. It works fully offline, although it lacks a few features including command healing–matching output to which commands and versions are installed on your device–and typo correction since &lt;a href="https://developer.apple.com/documentation/appkit/nsspellchecker"&gt;&lt;code&gt;NSSpellChecker&lt;/code&gt;&lt;/a&gt; is only available on macOS. Nonetheless, once the ~45 MB Wasm binary is loaded &amp;amp; cached, you should see near-instant results with safety and confidence scores.&lt;/p&gt;
&lt;p&gt;ShellTalk is not perfect. Although it can detect and leverage installed commands, it works best on pre-trained commands bundled in the tool&amp;rsquo;s corpus. ShellTalk also struggles with ambiguous intent or complex pattern matching for file names, paths, and URLs. But it&amp;rsquo;s failure modes are deterministic, so they are easy to reproduce and debug. This gives ShellTalk the ability to be quickly and autonomously improved by AI agents.&lt;/p&gt;
&lt;h3 id="limitations"&gt;
Limitations
&lt;a class="heading-anchor" href="#limitations" aria-label="Link to Limitations"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;One of the biggest limitations of ShellTalk is that it intentionally doesn&amp;rsquo;t synthesize pipelines from user input. The following inputs won&amp;rsquo;t work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;list swift files, filter by mtime, then wc -l&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;find all png images then convert jpeg and copy to ~/Downloads&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ShellTalk maps &lt;strong&gt;one query to one template&lt;/strong&gt;, so pipes are only used in a stable way for a single intent (counting matches or &lt;code&gt;pbcopy&lt;/code&gt; clipboard sink). Generalizing multi-step decomposition would almost certainly require some kind of domain specific language (DSL) or an LLM planner which risks breaking the, &amp;ldquo;same query, same machine, same result,&amp;rdquo; contract.&lt;/p&gt;
&lt;h2 id="whats-next"&gt;
What&amp;rsquo;s Next?
&lt;a class="heading-anchor" href="#whats-next" aria-label="Link to What’s Next?"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/LastByteLLC/ShellTalk"&gt;ShellTalk&lt;/a&gt; is available for free on GitHub, including &lt;a href="https://github.com/LastByteLLC/ShellTalk/releases/tag/v1.0.0"&gt;pre-built binaries&lt;/a&gt; for macOS (Universal), Linux (x86_64), and Wasm. The same techniques might work for &lt;a href="https://developer.apple.com/library/archive/documentation/AppleScript/Conceptual/AppleScriptLangGuide/introduction/ASLR_intro.html"&gt;AppleScript&lt;/a&gt; or &lt;a href="https://en.wikipedia.org/wiki/PowerShell"&gt;PowerShell&lt;/a&gt;. ShellTalk might prove useful inside of another agentic harness, where it&amp;rsquo;s deterministic output could spare some tokens.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2026-04-22-text-to-bash-with-shell-talk/</guid><pubDate>Wed, 22 Apr 2026 00:00:00 GMT</pubDate></item><item><title>
Building an on-device Swift coding agent using Apple Intelligence</title><link>https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/</link><description>
&lt;p&gt;I built a fully on-device coding agent called &lt;a href="https://github.com/LastByteLLC/junco"&gt;Junco&lt;/a&gt;, using Apple Intelligence, the &lt;a href="https://machinelearning.apple.com/research/introducing-apple-foundation-models"&gt;Apple Foundation Model&lt;/a&gt; (AFM), in &lt;a href="https://www.swift.org/"&gt;Swift&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Why? To learn, but also to build a tool I want to use myself.&lt;/p&gt;
&lt;p&gt;Why call it &amp;ldquo;&lt;a href="https://www.allaboutbirds.org/guide/Dark-eyed_Junco/overview"&gt;Junco&lt;/a&gt;?&amp;rdquo; Because nothing else was named &amp;ldquo;Junco,&amp;rdquo; it fits the Swift-related bird theme, and, to be honest, it&amp;rsquo;s kind of &lt;em&gt;junky&lt;/em&gt; compared to &lt;a href="https://claude.ai/referral/1is-TiX1TQ"&gt;Claude Code&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Does Junco work? &lt;del&gt;Kind of!&lt;/del&gt; Honestly, not well. It has some &lt;em&gt;very&lt;/em&gt; rough edges, but you can try it yourself. Install Junco with a single command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -fsSL https://raw.githubusercontent.com/LastByteLLC/junco/master/install.sh | bash
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="what-is-junco"&gt;
What is Junco?
&lt;a class="heading-anchor" href="#what-is-junco" aria-label="Link to What is Junco?"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/LastByteLLC/junco"&gt;Junco&lt;/a&gt; is a free and open-source (MIT-licensed), fully on-device coding agent built &lt;em&gt;in&lt;/em&gt; Swift &lt;em&gt;for&lt;/em&gt; Swift. It&amp;rsquo;s a single Mach-O binary that includes a terminal user interface (TUI) and leverages Apple Intelligence&amp;rsquo;s &lt;a href="https://developer.apple.com/documentation/foundationmodels/languagemodelsession"&gt;&lt;code&gt;LanguageModelSession&lt;/code&gt;&lt;/a&gt;, or other on-device models via &lt;a href="https://ollama.com/"&gt;Ollama&lt;/a&gt; and &lt;a href="https://github.com/huggingface/AnyLanguageModel"&gt;&lt;code&gt;AnyLanguageModel&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;────────────────────────────────────────────────────────────
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; junco v0.6.0 — on-device AI coding agent
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Domain: Swift / Apple │ Git: branch: master | 14 files changed
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Dir: ~/Documents/GitHub/junco
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Files: 148 │ Reflections: 61
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Model: Apple Foundation Models (Neural Engine)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; /help for commands │ @file to target │ exit to quit
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;────────────────────────────────────────────────────────────
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="why-build-junco"&gt;
Why build Junco?
&lt;a class="heading-anchor" href="#why-build-junco" aria-label="Link to Why build Junco?"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;I was inspired by Ivan Magda&amp;rsquo;s &lt;a href="https://github.com/ivan-magda/swift-claude-code"&gt;&lt;code&gt;swift-claude-code&lt;/code&gt;&lt;/a&gt; and his &lt;a href="https://ivanmagda.dev/"&gt;8-part series&lt;/a&gt; on developing a Claude Code-like command line interface (CLI) tool in Swift that covers agentic loop, tool calls, subagents, and task planning.&lt;/p&gt;
&lt;p&gt;Generally, local models are worse at reasoning, worse at following instructions, more likely to hallucinate, and lack up-to-date world knowledge when compared to state of the art (SOTA) frontier models. Nonetheless, local models like &lt;a href="https://deepmind.google/models/gemma/gemma-4/"&gt;Gemma 4&lt;/a&gt; and &lt;a href="https://huggingface.co/collections/Qwen/qwen35"&gt;Qwen 3.5&lt;/a&gt; continue to get better. Local models have their own benefits like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Work offline&lt;/li&gt;
&lt;li&gt;Better privacy&lt;/li&gt;
&lt;li&gt;Lower energy use&lt;/li&gt;
&lt;li&gt;No subscription fees&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Claude Code launched just over a year ago in February 2025. Now we&amp;rsquo;re in a &lt;a href="https://www.nytimes.com/2026/04/06/technology/ai-code-overload.html"&gt;Code Overload&lt;/a&gt;. I believe local models are on the same, albeit delayed, exponential trajectory.&lt;/p&gt;
&lt;h3 id="why-fully-local"&gt;
Why fully local?
&lt;a class="heading-anchor" href="#why-fully-local" aria-label="Link to Why fully local?"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;Claude Code and Codex are incredibly powerful, but are limited by cost, privacy, and connectivity. A fully local coding agent has several advantages.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Unit economics&lt;/strong&gt;: build once, sell many times without the up-front capital for advanced GPUs or operating expense of inference-as-a-service&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trade secret preservation&lt;/strong&gt;: local inference means intellectual property isn&amp;rsquo;t revealed to providers like Anthropic or OpenAI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data privacy&lt;/strong&gt;: protected health information (PHI), personally-identifiable information (PII), and other sensitive data doesn&amp;rsquo;t get sent to AI providers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In high-stakes industries like Defense, High-Frequency Trading, and Medical Research, sending source code to a third-party API is often a serious offense. Cloud inference always has a &lt;strong&gt;marginal cost&lt;/strong&gt;, either &lt;em&gt;per token&lt;/em&gt; or amortized into a subscription. A local agent could run air-gapped without fear it will train on your trade secrets or &lt;a href="https://bitwarden.com/blog/secure-ai-agent-access-with-secrets-manager/"&gt;leak your &lt;code&gt;.env&lt;/code&gt; secrets&lt;/a&gt;. Imagine agentic coding on an intercontinental flight without WiFi, or automatic agentic code review on a Self-Hosted GitHub Runner (without paying &lt;a href="https://code.claude.com/docs/en/code-review"&gt;$15-25 per pull request&lt;/a&gt;).&lt;/p&gt;
&lt;h3 id="why-not-use-opencode"&gt;
Why not use OpenCode?
&lt;a class="heading-anchor" href="#why-not-use-opencode" aria-label="Link to Why not use OpenCode?"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://opencode.ai/"&gt;OpenCode&lt;/a&gt; is great. I use it to &lt;a href="https://barrasso.me/posts/2026-01-11-android-reverse-engineering-with-opencode/"&gt;reverse-engineer Android APIs&lt;/a&gt;, for &lt;a href="https://barrasso.me/posts/2025-12-22-large-scale-typescript-migration-in-opencode/"&gt;JavaScript-to-TypeScript migrations&lt;/a&gt;, and much more. However, it&amp;rsquo;s not designed to work with the AFM&amp;rsquo;s miniscule &lt;a href="https://developer.apple.com/documentation/technotes/tn3193-managing-the-on-device-foundation-model-s-context-window"&gt;4K context window&lt;/a&gt;. In my limited testing, OpenCode with the Apple Foundation Model almost never succeeded without overflowing the context window, and rarely produced valid code.&lt;/p&gt;
&lt;h2 id="junco-at-a-glance"&gt;
Junco at a glance
&lt;a class="heading-anchor" href="#junco-at-a-glance" aria-label="Link to Junco at a glance"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/junco-ghostty-screenshot_hu_d34e20ceb45c312.webp 320w, https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/junco-ghostty-screenshot_hu_965acd52808b62f4.webp 640w, https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/junco-ghostty-screenshot_hu_befa3dfd724f72e5.webp 1024w, https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/junco-ghostty-screenshot_hu_489e8cc8f2ddbb1f.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/png" srcset='https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/junco-ghostty-screenshot_hu_3c6d526ba76ba25c.png 320w, https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/junco-ghostty-screenshot_hu_8743b00c35e6f1ea.png 640w, https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/junco-ghostty-screenshot_hu_9f8e23d71232ca6d.png 1024w, https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/junco-ghostty-screenshot_hu_7ceaca2e411dfa9e.png 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/junco-ghostty-screenshot_hu_3c6d526ba76ba25c.png"
alt="Screenshot of Junco in Ghostty" class="w-full no-border contain"
width="320"
height="159"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Screenshot of Junco in Ghostty terminal&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;ul&gt;
&lt;li&gt;Completely free and open source under the &lt;a href="https://github.com/LastByteLLC/junco/blob/master/LICENSE"&gt;MIT license&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;A single signed &amp;amp; notarized Mach-O binary (~9MB)&lt;/li&gt;
&lt;li&gt;Written entirely in Swift 6.2+&lt;/li&gt;
&lt;li&gt;Works exclusively on macOS 26+ with Apple Silicon (M1+)&lt;/li&gt;
&lt;li&gt;Trained specifically to edit and generate Swift code&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="lessons--limitations"&gt;
Lessons &amp;amp; Limitations
&lt;a class="heading-anchor" href="#lessons--limitations" aria-label="Link to Lessons &amp;amp; Limitations"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Apple Intelligence &lt;a href="https://www.macworld.com/article/3008896/this-test-shows-how-bad-apple-intelligence-is-and-how-much-better-its-going-to-get.html"&gt;isn&amp;rsquo;t very intelligent&lt;/a&gt;. In my experience, Apple&amp;rsquo;s 3B model performs worse than just about any similarly-sized model.&lt;/p&gt;
&lt;p&gt;So why work with it?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;It comes pre-installed&lt;/strong&gt;. Go to Settings &amp;gt; Apple Intelligence &amp;amp; Siri &amp;gt; Enable, that&amp;rsquo;s it! This is a huge win for user onboarding.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It&amp;rsquo;s highly optimized&lt;/strong&gt;. I get ~40-80+ tokens per second on an M4 Air. The AFM uses less energy and respond faster than most local 2-4B INT4 quantized models in Ollama.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It can be taught&lt;/strong&gt;. You can &lt;a href="https://developer.apple.com/apple-intelligence/foundation-models-adapter/"&gt;train a custom adapter&lt;/a&gt; for the AFM, augmenting its style, format, and (to some extent) knowledge.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So what did I learn?&lt;/p&gt;
&lt;h3 id="ask-not-what-your-model-can-do-for-you"&gt;
Ask not what your model can do for you
&lt;a class="heading-anchor" href="#ask-not-what-your-model-can-do-for-you" aria-label="Link to Ask not what your model can do for you"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/afm-trump-prompt_hu_25d35ab6435697da.webp 320w, https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/afm-trump-prompt_hu_60f7fc4714e5187b.webp 640w, https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/afm-trump-prompt_hu_8ef540730de7edb6.webp 1024w, https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/afm-trump-prompt_hu_87126e15b393447d.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/png" srcset='https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/afm-trump-prompt_hu_1bbeac529c22c57.png 320w, https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/afm-trump-prompt_hu_3462af8bd06438dc.png 640w, https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/afm-trump-prompt_hu_e2b285ab5d4cfbbd.png 1024w, https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/afm-trump-prompt_hu_af8afe865f9e5c34.png 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/afm-trump-prompt_hu_1bbeac529c22c57.png"
alt="Apple Foundation Model summarizing Donald Trump" class="w-full no-border contain"
width="320"
height="130"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Apple Foundation Model summarizing Donald Trump&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;Ask what you can do for your model. Help local models help you: train a text classifier, use named-entity recognition (NER), parse prompts with regular expressions, and search the web. Don&amp;rsquo;t ask a tiny model to do everything because they&amp;rsquo;re slow, they hallucinate, and they lack world knowledge.&lt;/p&gt;
&lt;p&gt;This screenshot is from &lt;a href="https://github.com/scouzi1966/maclocal-api"&gt;&lt;code&gt;maclocal-api&lt;/code&gt;&lt;/a&gt;, using the &lt;a href="https://github.com/ggml-org/llama.cpp"&gt;Llama.cpp&lt;/a&gt; Web UI. Clearly the AFM has a knowledge cutoff before the 2024 US Presidential Election.&lt;/p&gt;
&lt;p&gt;Set the model up for success by giving it concise, recent, and relevant information, validate its output, and ask it to try again. For Junco, one of the single most important components was the &lt;strong&gt;Compile-Verify-Fix (CVF) Loop&lt;/strong&gt;. This applies &lt;code&gt;swift-format&lt;/code&gt; styles, runs a linter, then executes &lt;code&gt;swiftc -typecheck&lt;/code&gt; on all LLM-generated code.&lt;/p&gt;
&lt;h3 id="one-thing-at-a-time"&gt;
One thing at a time
&lt;a class="heading-anchor" href="#one-thing-at-a-time" aria-label="Link to One thing at a time"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;Language models, especially small language models (SLMs), get &lt;a href="https://arxiv.org/abs/2307.03172"&gt;&amp;ldquo;Lost in the Middle&amp;rdquo;&lt;/a&gt;. The best thing you can do is to have &lt;strong&gt;one instruction per prompt&lt;/strong&gt;, and to put the critical task instruction at the &lt;em&gt;very end&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Claude can handle, &amp;ldquo;go fix this bug and also check out these logs and then update @README.md,&amp;rdquo; but SLMs can&amp;rsquo;t reliably decompose multiple instructions. So give them exactly &lt;strong&gt;one task at a time&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id="happy-little-trees"&gt;
Happy little trees
&lt;a class="heading-anchor" href="#happy-little-trees" aria-label="Link to Happy little trees"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;Both SLMs and Bob Ross love, &amp;ldquo;Happy Little Trees.&amp;rdquo; Specially, &lt;strong&gt;abstract syntax trees (ASTs) and file trees&lt;/strong&gt;. These represent token-dense model context that supports cross-file references, symbol extraction, and context compression.&lt;/p&gt;
&lt;p&gt;With 1M tokens you might include an entire file plus its call sites. But with 4K tokens, you can only request edits on focused 30 +/- lines of code around the deepest named declaration (i.e. function, class, module) that correspond to a specific error.&lt;/p&gt;
&lt;h3 id="teach-a-model-how-to-fish"&gt;
Teach a model how to fish
&lt;a class="heading-anchor" href="#teach-a-model-how-to-fish" aria-label="Link to Teach a model how to fish"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Give a model a fish and you feed it for one prompt.&lt;/p&gt;
&lt;p&gt;Teach a model how to fish and you feed it for a lifetime.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Prompt engineering can only take you so far. For Junco, to generate somewhat-consistently valid and up-to-date Swift code I needed to train a &lt;a href="https://developer.apple.com/apple-intelligence/foundation-models-adapter/"&gt;custom model adapter&lt;/a&gt; using Low-Rank Adaption (LoRA).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Caution: macOS 26 Apple Intelligence Bug&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;macOS 26.3.1 (likely earlier versions) has a system service bug in &lt;code&gt;TGOnDeviceInferenceProviderService&lt;/code&gt;, where an AFM LoRA adapter gets copied into a &lt;a href="https://developer.apple.com/documentation/security/disabling-and-enabling-system-integrity-protection"&gt;System Integrity Protection (SIP)&lt;/a&gt;-protected location, &lt;code&gt;/private/var/db/AppleIntelligencePlatform/AppModelAssets/&lt;/code&gt;, with &lt;em&gt;every instantiation&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;This means every time you load an adapter using &lt;code&gt;LanguageModelSession(model: customAdapterModel)&lt;/code&gt;, you&amp;rsquo;re copying ~150MB of data that can only be deleted in &lt;a href="https://support.apple.com/en-us/102518"&gt;Recovery Mode&lt;/a&gt;. Until Apple fixes this bug, &lt;strong&gt;exercise caution when using custom AFM adapters&lt;/strong&gt;. See &lt;a href="https://developer.apple.com/forums/thread/823001"&gt;Apple Forum #823001&lt;/a&gt; for status and details.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;LoRAs are a form of fine tuning where you train a tiny ~60M parameter model. In the case of AFM, you generate a ~160MB &lt;code&gt;.fmadapter&lt;/code&gt; file (itself a folder with model weights &amp;amp; metadata). I trained several &lt;a href="https://github.com/LastByteLLC/junco/releases/tag/v0.6.0-lora"&gt;LoRA iterations for Junco&lt;/a&gt; using permissively-licensed code samples, synthetic code, and API signatures. Although initially challenging, the &lt;strong&gt;LoRA was the most effective AFM enhancement&lt;/strong&gt;. It&amp;rsquo;s difficult to quantize because it&amp;rsquo;s inherently stochastic, but before the LoRA I almost never got the AFM to generate valid, modern Swift code.&lt;/p&gt;
&lt;p&gt;Keep in mind, Apple says you need, &amp;ldquo;at least 32GB RAM,&amp;rdquo; to train an adapter. You also need to submit a &lt;a href="https://developer.apple.com/contact/request/foundation-models-framework-adapter-entitlement/"&gt;request form&lt;/a&gt; for access, to accept a separate agreement, and to include the &lt;a href="https://developer.apple.com/documentation/bundleresources/entitlements/com.apple.developer.foundation-model-adapter"&gt;&lt;code&gt;com.apple.developer.foundation-model-adapter&lt;/code&gt;&lt;/a&gt; entitlement in your application. Because this is a &lt;code&gt;com.apple.*&lt;/code&gt; restricted namespaced entitlement, you&amp;rsquo;ll also need to set up a &lt;a href="https://developer.apple.com/help/account/provisioning-profiles/edit-download-or-delete-profiles/"&gt;Provisioning Profile&lt;/a&gt;, otherwise users will see, &amp;ldquo;The application “&amp;hellip;” can’t be opened.&amp;rdquo; Apple Mobile File Integrity (AMFI) will log an error like, &amp;ldquo;Unsatisfied Entitlements: Code has restricted entitlements.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;I managed to train my first adapter on a MacBook Air with 24GB RAM. However, it took ~18 hours on just a single epoch across ~1,000 samples. I&amp;rsquo;ve since moved to &lt;a href="https://runpod.io?ref=o0vjyqx6"&gt;RunPod&lt;/a&gt; where I rent an NVIDIA L40S for ~$0.89/hour, and can train several epochs across ~10,000 samples in under an hour! CUDA, Flash Attention, and 48GB VRAM really make a difference.&lt;/p&gt;
&lt;h3 id="fewer-guardrails"&gt;
Fewer guardrails
&lt;a class="heading-anchor" href="#fewer-guardrails" aria-label="Link to Fewer guardrails"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;Apple wants you to talk nicely to its AI. Don&amp;rsquo;t swear or get political, otherwise you&amp;rsquo;ll trigger a &lt;a href="https://developer.apple.com/documentation/FoundationModels/improving-the-safety-of-generative-model-output"&gt;guardrail violation&lt;/a&gt;! Apple says, &amp;ldquo;Guardrails aim to block harmful or sensitive content, such as self-harm, violence, and adult materials, from both model input and output.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;In practice, Apple&amp;rsquo;s guardrails feel &lt;em&gt;too safe&lt;/em&gt;. Violations get triggered by aggressive prompts, but also by code, and sometimes mixed-language input. Fortunately, you can turn these guardrails off with &lt;a href="https://developer.apple.com/documentation/foundationmodels/systemlanguagemodel/guardrails/permissivecontenttransformations"&gt;&lt;code&gt;SystemLanguageModel(guardrails: .permissiveContentTransformations)&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Behind the scenes, Apple uses the &lt;code&gt;MobileAsset&lt;/code&gt; framework to load a model with the identifier &lt;code&gt;com.apple.fm.language.instruct_300m.safety&lt;/code&gt;. Guardrails uses a &lt;a href="https://arxiv.org/pdf/2507.08284"&gt;300M-parameter model&lt;/a&gt; trained using RL-guidance and human seed data with synthetic expansion to classify intent. &lt;code&gt;permissiveContentTransformations&lt;/code&gt; appears to disable these calls and the upstream &lt;code&gt;GenerationError.guardrailViolation&lt;/code&gt; error.&lt;/p&gt;
&lt;p&gt;Fun fact: Apple&amp;rsquo;s original codename for the AFM was &lt;a href="https://www.linkedin.com/pulse/what-apples-project-graymatter-taught-me-management-bhatnagar-pmp--x67sc/"&gt;&amp;ldquo;Project Greymatter&amp;rdquo;&lt;/a&gt;, which you&amp;rsquo;ll see signs of in macOS app logs like &lt;code&gt;AFIsDeviceGreymatterEligible&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="count-your-tokens"&gt;
Count your tokens
&lt;a class="heading-anchor" href="#count-your-tokens" aria-label="Link to Count your tokens"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;4,096 tokens do not go far when you&amp;rsquo;re generating code! macOS 26.4+ finally introduces a &lt;a href="https://developer.apple.com/documentation/foundationmodels/systemlanguagemodel/tokencount(for:)"&gt;&lt;code&gt;tokenCount&lt;/code&gt;&lt;/a&gt; method and &lt;a href="https://developer.apple.com/documentation/foundationmodels/systemlanguagemodel/contextsize"&gt;&lt;code&gt;contextSize&lt;/code&gt;&lt;/a&gt; property to avoid context overflows. Presumably, this is in preparation for future model releases where context window size might vary based on device hardware and available memory.&lt;/p&gt;
&lt;p&gt;Hopefully future versions of macOS support more features like grammar-constrained decoding. Currently the only output options are &lt;code&gt;maximumResponseTokens&lt;/code&gt; and &lt;code&gt;temperature&lt;/code&gt;, and the use of &lt;code&gt;@Generable&lt;/code&gt; for structured JSON output. These constrain the schema (JSON) but not the syntax.&lt;/p&gt;
&lt;p&gt;If you want to go deeper into the technology, check out the &lt;a href="https://arxiv.org/abs/2507.13575"&gt;Apple Foundation Model Tech Report&lt;/a&gt; or poke around &lt;code&gt;/System/Library/PrivateFrameworks/GenerativeModels.framework/&lt;/code&gt; on any Apple Silicon Mac running macOS 26+, although it&amp;rsquo;s saved in Apple Encrypted Archives (AEA) format.&lt;/p&gt;
&lt;h4 id="stochastic-parroting"&gt;
Stochastic Parroting
&lt;a class="heading-anchor" href="#stochastic-parroting" aria-label="Link to Stochastic Parroting"&gt;#&lt;/a&gt;
&lt;/h4&gt;
&lt;p&gt;Another weakness of the AFM is &lt;a href="https://arxiv.org/html/2505.05298v1"&gt;stochastic parroting&lt;/a&gt;. If you provide exemplar context, you&amp;rsquo;ll almost certainly get it back &lt;em&gt;verbatim&lt;/em&gt;. The best mitigation is to set &lt;a href="https://developer.apple.com/documentation/foundationmodels/generationoptions/temperature"&gt;&lt;code&gt;temperature&lt;/code&gt;&lt;/a&gt; to &lt;code&gt;1.0&lt;/code&gt; and do NOT provide example data. If you do not explicitly set the temperature, &amp;ldquo;the system chooses a reasonable default on your behalf.&amp;rdquo;&lt;/p&gt;
&lt;h3 id="model-of-the-week"&gt;
Model of the week
&lt;a class="heading-anchor" href="#model-of-the-week" aria-label="Link to Model of the week"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;The past few weeks saw unbelievable development in local models.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Qwen &lt;a href="https://huggingface.co/Qwen/Qwen3.6-35B-A3B"&gt;3.6-35B-A3B&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Google&amp;rsquo;s &lt;a href="https://deepmind.google/models/gemma/gemma-4/"&gt;Gemma 4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;PrismML&amp;rsquo;s &lt;a href="https://prismml.com/news/bonsai-8b"&gt;1-Bit Bonsai&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Liquid AI&amp;rsquo;s &lt;a href="https://www.liquid.ai/blog/lfm2-5-350m-no-size-left-behind"&gt;LFM2.5-350M&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Rta AI Lab&amp;rsquo;s &lt;a href="https://huggingface.co/Rta-AILabs/Nandi-Mini-150M"&gt;Nandi-Mini-150M&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of these are small enough to run on a MacBook Air. Every week, a new model is released. Technologies like ternary quantization and techniques like TurboQuant compression enable smaller, more capable local models with larger context windows and lower memory usage.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Local models keep getting better&lt;/strong&gt;. The technology isn&amp;rsquo;t perfect today, but projects like &lt;a href="https://github.com/LastByteLLC/junco"&gt;Junco&lt;/a&gt;, &lt;a href="https://github.com/macOS26/Agent"&gt;Agent!&lt;/a&gt;, and &lt;a href="https://github.com/LastByteLLC/iClaw"&gt;iClaw&lt;/a&gt;, along with companies like &lt;a href="https://rig.ai/?rc=nlf46l"&gt;Rig&lt;/a&gt; demonstrate that &lt;strong&gt;fully-local agents are possible&lt;/strong&gt;. With the right deterministic harness, proper safeguards, quality training data, and enough investment, local coding agents will get good enough for regular use. It&amp;rsquo;s only a matter of time.&lt;/p&gt;
&lt;h2 id="whats-next"&gt;
What&amp;rsquo;s Next?
&lt;a class="heading-anchor" href="#whats-next" aria-label="Link to What’s Next?"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/iClaw-preview-screenshot_hu_c8e1925f5b4c398d.webp 320w, https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/iClaw-preview-screenshot_hu_35d868e74604d52e.webp 640w, https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/iClaw-preview-screenshot_hu_545a87b6a17a79a1.webp 1024w' sizes="100vw" /&gt;
&lt;source type="image/png" srcset='https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/iClaw-preview-screenshot_hu_214c91a0a76e42f8.png 320w, https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/iClaw-preview-screenshot_hu_ac501493cabd236b.png 640w, https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/iClaw-preview-screenshot_hu_fe55b9bc211e4950.png 1024w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/iClaw-preview-screenshot_hu_214c91a0a76e42f8.png"
alt="Screenshot of iClaw AI agent" class="mx-auto no-border contain"
width="320"
height="320"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Screenshot of iClaw, an on-device AI agent for macOS&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;I&amp;rsquo;m continuing to explore on-device agents with a soon-to-be-released project called &lt;strong&gt;&lt;a href="https://github.com/LastByteLLC/iClaw"&gt;iClaw&lt;/a&gt;&lt;/strong&gt;. Many of the same techniques: auto-compacted context, custom LoRA adapter, and MaxEnt tool routing classifier all apply. My goal is to build a safe, helpful, local AI agent. More on iClaw coming soon at &lt;a href="https://geticlaw.com/"&gt;geticlaw.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Affiliate Disclosure&lt;/strong&gt;: this article contains affiliate links to RunPod and Claude Code. If you click them and make a purchase, I may receive a small commission at no extra cost to you.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2026-04-09-on-device-coding-with-apple-intelligence/</guid><pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate></item><item><title>
Why Dumbphone Apps Are So Hard to Build</title><link>https://barrasso.me/talks/2026-02-20-why-dumbphone-apps-are-hard-to-build/</link><description>
&lt;p&gt;On Jose Briones&amp;rsquo; &lt;a href="https://josebriones.substack.com/"&gt;Moving Offline&lt;/a&gt; Podcast, I talk about &lt;a href="https://podlp.com"&gt;PodLP&lt;/a&gt;, and how the podcast app for dumbphones was made. We dive into why it’s tough to find your favorite apps on these minimalist devices, and what inspired me to build PodLP during the pandemic. We cover the challenges developers face like small screens, keypad navigation, and platform fragmentation, and discuss why big companies aren&amp;rsquo;t rushing into the dumbphone movement.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/talks/2026-02-20-why-dumbphone-apps-are-hard-to-build/</guid><pubDate>Fri, 20 Feb 2026 00:00:00 GMT</pubDate></item><item><title>
6 lessons from 6 years building software for flip phones</title><link>https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/</link><description>
&lt;p&gt;I started &lt;a href="https://podlp.com"&gt;PodLP&lt;/a&gt; in 2020 with no clue it would lead me give talks with 20,000+ aspiring Indian developers and take calls from a New Zealand campground with Bangladesh&amp;rsquo;s largest telecom company. It&amp;rsquo;s now 2026, I just launched &lt;a href="https://www.podcastvideos.com/articles/dumbphones-podlp-podcast-app/"&gt;PodLP for Android&lt;/a&gt;, and I still don&amp;rsquo;t know where it all ends.&lt;/p&gt;
&lt;p&gt;Like many, the COVID-19 pandemic gave me sudden free time. Curious to try a &amp;ldquo;digital detox,&amp;rdquo; I wanted a flip phone for camping trips. However, the phones then on the market (like the &lt;a href="https://amzn.to/3MbIom8"&gt;Go Flip 3&lt;/a&gt;) didn&amp;rsquo;t have a podcast app, so I built one.&lt;/p&gt;
&lt;p&gt;Fast forward to 2026, I&amp;rsquo;ve built (and rebuilt) PodLP for three very different platforms: &lt;a href="https://www.kaiostech.com/"&gt;KaiOS&lt;/a&gt;, &lt;a href="https://developer.cloudfone.com/"&gt;Cloud Phone&lt;/a&gt;, and &lt;a href="https://developer.android.com/"&gt;Android&lt;/a&gt;. It&amp;rsquo;s been downloaded over 10 million times across 175+ countries. For many, it was their introduction to podcasts. While its growth has ebbed and flowed, PodLP has proved to be both a successful side hustle and technical playground.&lt;/p&gt;
&lt;p&gt;These lessons might &lt;em&gt;not&lt;/em&gt; apply to starting a one-person billion-dollar company, but they are relevant for bootstrapping an impactful consumer-facing application without quitting your day job.&lt;/p&gt;
&lt;h2 id="1-leave-the-crowds"&gt;
1. Leave the crowds
&lt;a class="heading-anchor" href="#1-leave-the-crowds" aria-label="Link to 1. Leave the crowds"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Podcast apps are a popular genre for burgeoning programmers; building one requires learning networking, parsing, UX, data storage, and media playback. While it&amp;rsquo;s a valid way to grow as an engineer, most podcast apps never leave GitHub. If you do publish on Google Play or the Apple App Store, yours is often lost in a sea of apps.&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/podlp-installs-2020_hu_a86185dd0b854d4f.webp 320w, https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/podlp-installs-2020_hu_f2005e9515ad8428.webp 640w' sizes="100vw" /&gt;
&lt;source type="image/png" srcset='https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/podlp-installs-2020_hu_de6a50ae566652ec.png 320w, https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/podlp-installs-2020_hu_6e01ed470e7f9e39.png 640w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/podlp-installs-2020_hu_de6a50ae566652ec.png"
alt="Google Analytics for PodLP" class="no-border contain mx-auto"
width="320"
height="470"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;PodLP launches on the JioStore&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;Skip the crowds and launch somewhere you can get organic installs. When I launched PodLP on the JioStore for the JioPhone in India, I saw 100,000+ installs on the first day. Within days there were &lt;a href="https://www.youtube.com/results?search_query=PodLP"&gt;dozens of YouTube videos&lt;/a&gt; featuring PodLP. All with $0 in ad spend.&lt;/p&gt;
&lt;p&gt;The challenge is picking the &lt;em&gt;right&lt;/em&gt; app store when there are so many. For Android there&amp;rsquo;s &lt;a href="https://f-droid.org/en/"&gt;F-Droid&lt;/a&gt; (if your app is open source), &lt;a href="https://consumer.huawei.com/en/mobileservices/appgallery/"&gt;Huawei AppGallery&lt;/a&gt; in China, &lt;a href="https://appstore.tencent.com/"&gt;Tencent Appstore&lt;/a&gt;, and &lt;a href="https://dev.dlightek.com/services/app"&gt;Palm Store&lt;/a&gt; from Transsion. For browser extensions there&amp;rsquo;s the &lt;a href="https://store.whale.naver.com"&gt;Naver Whale Store&lt;/a&gt; in Korea and &lt;a href="https://addons.opera.com/en/"&gt;Opera add-ons&lt;/a&gt;. For TVs and set-top boxes (STBs) there&amp;rsquo;s the &lt;a href="https://developer.jio.com/"&gt;Jio Developer Program&lt;/a&gt;, &lt;a href="https://us.lgappstv.com/main/tvapp"&gt;LG&amp;rsquo;s Content Store&lt;/a&gt; for webOS, and the &lt;a href="https://foxxum.com/"&gt;Foxxum App Store&lt;/a&gt;. For games there&amp;rsquo;s &lt;a href="https://developer.jiogames.com/"&gt;JioGames&lt;/a&gt;. For web apps there&amp;rsquo;s &lt;a href="https://developer.kaiostech.com/"&gt;KaiOS&lt;/a&gt; and &lt;a href="https://developer.cloudfone.com/"&gt;Cloud Phone&lt;/a&gt;. Even the &lt;a href="https://yoto.dev/"&gt;Yoto kids speaker&lt;/a&gt; has an API! This list isn&amp;rsquo;t remotely exhaustive.&lt;/p&gt;
&lt;p&gt;The lessons you&amp;rsquo;ll learn measuring engagement, debugging crashes, localizing for diverse markets, generating marketing material, and responding to real user feedback are irreplaceable.&lt;/p&gt;
&lt;h2 id="2-the-riches-are-still-in-the-niches"&gt;
2. The riches are (still) in the niches
&lt;a class="heading-anchor" href="#2-the-riches-are-still-in-the-niches" aria-label="Link to 2. The riches are (still) in the niches"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Ask AI to write a KaiOS app, then try to run it on a &lt;a href="https://amzn.to/4tzfpcx"&gt;TCL Flip 4 5G&lt;/a&gt;. There&amp;rsquo;s a good chance it will hallucinate non-existent APIs, and it won&amp;rsquo;t natively handle directional pad or soft-key navigation unless prompted. You still can&amp;rsquo;t effectively &amp;ldquo;vibe code&amp;rdquo; a decent flip phone app.&lt;/p&gt;
&lt;p&gt;In the process of building apps for niche platforms, you&amp;rsquo;ll learn that &lt;strong&gt;documentation lies&lt;/strong&gt;. Maybe the docs are out of date. Maybe that feature was never built. Maybe its behind an undocumented permission or feature flag. Maybe it only works on one model. You might have to &lt;a href="https://barrasso.me/posts/2026-01-11-android-reverse-engineering-with-opencode/"&gt;reverse engineer&lt;/a&gt; hidden APIs and while you&amp;rsquo;re looking, stumble upon an &amp;ldquo;undocumented API&amp;rdquo; that accidentally grants you &lt;a href="https://kaios.dev/2023/05/cve-2023-33294-kaios-3.0-root-command-line-in-browser-via-tct-web-server/"&gt;root shell access from the browser&lt;/a&gt;. These things happen.&lt;/p&gt;
&lt;p&gt;This mess is infuriating, but it is exactly why AI has little hope of displacing human labor here. &lt;strong&gt;The real world is messy&lt;/strong&gt;. It requires forging relationships, compliance with arcane rules, and access to hard-to-obtain physical devices.&lt;/p&gt;
&lt;p&gt;Despite selling ~150M units, good luck buying a &lt;a href="https://kaios.dev/2023/08/whats-next-for-the-jiophone/"&gt;JioPhone&lt;/a&gt; in the United States. Yet somehow I built a JioPhone collection including the JioPhone 1, 2 &amp;amp; Next thanks to serendipity, relationships, and perseverance. One from a &lt;a href="https://www.reddit.com/r/KaiOS/"&gt;r/KaiOS Discord&lt;/a&gt; member, one via an eBay alert, and one shipped on trust from a translator in India.&lt;/p&gt;
&lt;p&gt;Knowing this gives me hope. The future remains full of niches that AI agents can&amp;rsquo;t or won&amp;rsquo;t touch because the opportunities are too small, too fragmented, too messy, and too physical.&lt;/p&gt;
&lt;h2 id="3-stick-with-it"&gt;
3. Stick with it
&lt;a class="heading-anchor" href="#3-stick-with-it" aria-label="Link to 3. Stick with it"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;The full picture requires a focus to ignore &amp;ldquo;the next thing,&amp;rdquo; a time horizon in years not minutes, and a psychological &amp;ldquo;context window&amp;rdquo; well beyond one million tokens.&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/podlp-white-board_hu_ed4a6d83c2ec0278.webp 320w, https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/podlp-white-board_hu_93a8c86a6056c6e3.webp 640w, https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/podlp-white-board_hu_4527c76bf1c3522b.webp 1024w, https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/podlp-white-board_hu_d996933daf964537.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/podlp-white-board_hu_7b88abf27b8ae173.jpeg 320w, https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/podlp-white-board_hu_c9594a5fe3f02098.jpeg 640w, https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/podlp-white-board_hu_88e31332a4443f83.jpeg 1024w, https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/podlp-white-board_hu_643397422cf64563.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/podlp-white-board_hu_7b88abf27b8ae173.jpeg"
alt="PodLP Whiteboard" class="w-full no-border contain"
width="320"
height="240"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Outlining PodLP.com on a whiteboard&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;It&amp;rsquo;s easy to get &lt;strong&gt;&lt;a href="https://en.wikipedia.org/wiki/Shiny_object_syndrome"&gt;shiny object syndrome&lt;/a&gt;&lt;/strong&gt;. Trends come and go. One minute everyone is &amp;ldquo;cloud native&amp;rdquo;, then suddenly everyone is moving back on-prem. IoT, Big Data, and Edge Computing all had their 15 minutes. Now the talk of the Gas Town is AI. Surprisingly few people stick with someone long enough to see it through before getting caught in the next hype cycle.&lt;/p&gt;
&lt;p&gt;Because of PodLP and my blog (&lt;a href="https://kaios.dev"&gt;KaiOS.dev&lt;/a&gt;), I was contacted to build &lt;a href="https://barrasso.me/projects/vipaso/"&gt;Vipaso&lt;/a&gt;, a Bluetooth payment app, and support Developer Relations for &lt;a href="https://barrasso.me/projects/cloud-phone/"&gt;CloudMosa&lt;/a&gt;. I recognize it sounds egotistical, but in some ways, these opportunities were created &lt;em&gt;for me&lt;/em&gt;. Had I (or someone like me) not turned up in a search result, these opportunities likely would&amp;rsquo;ve never appeared on a job board.&lt;/p&gt;
&lt;p&gt;You can&amp;rsquo;t know where it will go unless you stick with it.&lt;/p&gt;
&lt;h2 id="4-the-power-of-wow"&gt;
4. The power of &amp;ldquo;Wow!&amp;rdquo;
&lt;a class="heading-anchor" href="#4-the-power-of-wow" aria-label="Link to 4. The power of “Wow!”"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;It has never been easier to bake the same cookies everyone else is baking. Using AI, you can generated a polished set of Tailwind landing pages in seconds, but with results that look nearly identical to everyone else.&lt;/p&gt;
&lt;p&gt;Not every application needs a custom component library, but don&amp;rsquo;t overlook the power of &amp;ldquo;wow!&amp;rdquo; People remember something distinct, something they never thought was possible. As one Reddit user &lt;a href="https://www.reddit.com/r/dumbphones/comments/1r2zkmf/comment/o58myp6/"&gt;commented&lt;/a&gt;, I&amp;rsquo;m &amp;ldquo;surprised you got the native menu row to work on the Kyocera&amp;hellip; impressive, it actually feels almost native.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Spend the extra minute: &lt;strong&gt;prompt something novel into existence&lt;/strong&gt;, preferably not in &lt;a href="https://dev.to/jaainil/ai-purple-problem-make-your-ui-unmistakable-3ono"&gt;AI purple&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="5-technical-lesson-memory-matters"&gt;
5. Technical lesson: memory matters
&lt;a class="heading-anchor" href="#5-technical-lesson-memory-matters" aria-label="Link to 5. Technical lesson: memory matters"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/lg-oom_hu_c58a8c21c6fc0485.webp 240w' sizes="100vw" /&gt;
&lt;source type="image/png" srcset='https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/lg-oom.png 240w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/lg-oom.png"
alt="PodLP Crash Screenshot" class="no-border contain mx-auto"
width="240"
height="320"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;OutOfMemoryError from PodLP on the LG Wine 2&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;Now for some nerd talk. Most of what I&amp;rsquo;ve built is I/O bound. For flip phones, a major hurdle is out-of-memory (OOM) errors. I soon discovered how big podcasts get. Take Blue Wire&amp;rsquo;s &lt;a href="https://podnews.net/podcast/i4u97"&gt;Pack-a-Day Podcast&lt;/a&gt; as an example.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The RSS feed on Simplecast is 12.97MB of XML!&lt;/li&gt;
&lt;li&gt;The artwork is a 3000x3000 PNG totalling 251KB&lt;/li&gt;
&lt;li&gt;There are 4,275+ episodes averaging ~56MB&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now consider the &lt;code&gt;build.prop&lt;/code&gt; from my &lt;a href="https://amzn.to/4bXyO0D"&gt;LG Wine 2&lt;/a&gt; (2020), a flip phone with 1GB RAM running Android Go.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[dalvik.vm.heapgrowthlimit]: [128m]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[dalvik.vm.heapsize]: [36m]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With a 36MB heap limit, every byte counts. I probably should have ignored the LG Wine 2, but I wanted PodLP to work well on low-end phones. I implemented:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Chunking &amp;amp; streaming&lt;/strong&gt;. I moved away from &lt;code&gt;JSON.parse&lt;/code&gt; or &lt;code&gt;responseXML&lt;/code&gt;, which loads the whole document into memory. Instead, I used stream parsing and tools like &lt;a href="https://developer.android.com/kotlin/flow"&gt;Kotlin Flows&lt;/a&gt;. For audio I used &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests"&gt;&lt;code&gt;Range&lt;/code&gt; requests&lt;/a&gt;, chunking, or streaming (based on what&amp;rsquo;s available).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don&amp;rsquo;t be transparent&lt;/strong&gt;. Images are collections of pixels. Android&amp;rsquo;s default &lt;code&gt;ARGB_8888&lt;/code&gt; format uses 32 bits per pixel, but, flip phones don&amp;rsquo;t exactly have Retina displays. Switching to &lt;a href="https://developer.android.com/reference/android/graphics/Bitmap.Config#RGB_565"&gt;&lt;code&gt;RGB_565&lt;/code&gt;&lt;/a&gt; uses only 16 bits, doubling the number of images I can display.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Take control&lt;/strong&gt;. On native platforms like Android, I switched from the default to &lt;a href="https://developer.android.com/media/media3/exoplayer/network-stacks"&gt;OkHttpDataSource&lt;/a&gt; to share the same connect and thread pools, add interceptors for Brotli and GZIP compression, support HTTP/2 multiplexing, and have more consistent buffer size control.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the end, getting PodLP to work on low-end phones like the LG Wine 2 meant it ran faster on newer devices with better hardware like the Sonim XP3+ and Kyocera DuraXV.&lt;/p&gt;
&lt;h2 id="6-it-only-gets-easier"&gt;
6. It only gets easier
&lt;a class="heading-anchor" href="#6-it-only-gets-easier" aria-label="Link to 6. It only gets easier"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;When I started PodLP, podcast APIs were paid. I built my own directory, and now there&amp;rsquo;s a free and open source &lt;a href="https://podcastindex.org/"&gt;Podcast Index&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;When I started, low-cost distributed databases didn&amp;rsquo;t exist. I made my own SQLite on Elastic File Service (EFS) that supported millions of users for pennies per month. Now services like Amazon Aurora Serverless and Cloudflare D1 are easy to use and scale to zero.&lt;/p&gt;
&lt;p&gt;My first Cloud Phone was a Nokia 110 4G with a QQVGA (128x160) screen using a 4G SIM card from Red Pocket. I would debug remotely using tools like &lt;a href="https://remotejs.com/"&gt;RemoteJS&lt;/a&gt;. Now there&amp;rsquo;s a &lt;a href="https://developer.cloudfone.com/docs/guides/developer-tools/#cloud-phone-debugger"&gt;Cloud Phone Simulator&lt;/a&gt; that runs a full developer console in your browser.&lt;/p&gt;
&lt;p&gt;In 2024, I wrote the first–and only–guide to &lt;a href="https://kaios.dev/2024/11/the-definitive-guide-to-publishing-kaios-apps-on-the-jiostore/"&gt;publishing on the JioStore&lt;/a&gt; for JioPhone. After having nearly a dozen builds rejected were the complete set of rules and requirements shared with me. Things like using a four digit version code, supporting IPv6-only networks (without NAT64), and displaying an exit confirmation dialog.&lt;/p&gt;
&lt;p&gt;As time moves on, eventually someone else will share your problem, and eventually someone else fix it. Each problem could itself become a new product or pivot. Either way, you can make it easier for someone else, or someone else can make it easier for you. But it only gets easier.&lt;/p&gt;
&lt;h3 id="closing-thoughts"&gt;
Closing thoughts
&lt;a class="heading-anchor" href="#closing-thoughts" aria-label="Link to Closing thoughts"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/marked-access-ends_hu_ceeb126b3eb2c2a0.webp 320w, https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/marked-access-ends_hu_49e114b64c155693.webp 640w, https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/marked-access-ends_hu_208b50dd35f857be.webp 1024w, https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/marked-access-ends_hu_3e64cee0083dae46.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/marked-access-ends_hu_3304b0c01b342aee.jpeg 320w, https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/marked-access-ends_hu_3399f844a46fee51.jpeg 640w, https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/marked-access-ends_hu_6d50f30e2af32f2e.jpeg 1024w, https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/marked-access-ends_hu_7cf5bd36b47c4c19.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/marked-access-ends_hu_3304b0c01b342aee.jpeg"
alt="Marked Access Ends Here" class="w-full no-border contain"
width="320"
height="108"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Marked Access Ends Here&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;I find &lt;a href="https://www.psypost.org/neuroscience-explains-why-writing-creates-mental-clarity/"&gt;writing &lt;em&gt;creates&lt;/em&gt; clarity&lt;/a&gt;. This article scratches the surface of a journey that&amp;rsquo;s taken me–and continues to take me–around the world. It&amp;rsquo;s taught me the immense value of the small, the fragmented, and the persistent.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2026-02-16-6-lessons-from-6-years-building-for-flip-phones/</guid><pubDate>Mon, 16 Feb 2026 00:00:00 GMT</pubDate></item><item><title>
Quickly reverse engineer hidden Android APIs with Gemini and OpenCode</title><link>https://barrasso.me/posts/2026-01-11-android-reverse-engineering-with-opencode/</link><description>
&lt;h2 id="why-reverse-engineer-an-apk"&gt;
Why Reverse Engineer an APK?
&lt;a class="heading-anchor" href="#why-reverse-engineer-an-apk" aria-label="Link to Why Reverse Engineer an APK?"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;In the early days of Android more than a decade ago, I built UI customization apps like &lt;a href="https://barrasso.me/projects/statusbarplus/"&gt;StatusBar+&lt;/a&gt;, &lt;a href="https://barrasso.me/projects/chargebar/"&gt;ChargeBar&lt;/a&gt;, and &lt;a href="https://barrasso.me/projects/noyze/"&gt;Noyze&lt;/a&gt;. &lt;a href="https://andrewzuo.com/has-android-fragmentation-finally-been-fixed-adf363c887c0"&gt;Fragmentation&lt;/a&gt; was a big problem. This was long before &lt;a href="https://en.wikipedia.org/wiki/Material_Design"&gt;Material UI&lt;/a&gt; was released in 2014, and well before &lt;a href="https://www.notebookcheck.net/Google-launches-Certified-Android-device-program.244974.0.html"&gt;Google certified&lt;/a&gt; original equipment manufacturer (OEMs) through &lt;a href="https://www.android.com/certified/"&gt;Play Protect&lt;/a&gt; in 2017.&lt;/p&gt;
&lt;p&gt;To build apps that looked and worked well on Samsung&amp;rsquo;s &lt;a href="https://en.wikipedia.org/wiki/TouchWiz"&gt;TouchWiz&lt;/a&gt;, Xiaomi&amp;rsquo;s &lt;a href="https://en.wikipedia.org/wiki/MIUI"&gt;MIUI&lt;/a&gt;, and HTC&amp;rsquo;s &lt;a href="https://en.wikipedia.org/wiki/HTC_Sense"&gt;Sense&lt;/a&gt;, you needed to know how to reverse engineer preinstalled APKs and the SystemUI to understand what modifications had been made on each device. At runtime, this looked like a lot of &lt;a href="https://www.baeldung.com/java-reflection"&gt;Java reflection&lt;/a&gt; checking for the availability of hidden APIs. Android Interface Definition Language (AIDL) files were reconstructed and imported from disassembled APKs to call into device-specific APIs and offer better features.&lt;/p&gt;
&lt;p&gt;This is an example of code I wrote in 2014 for &lt;a href="https://barrasso.me/projects/noyze/"&gt;Noyze&lt;/a&gt; used to call into &lt;a href="https://source.android.com/docs/core/audio/terminology#androidSpecificTerms"&gt;AudioFlinger&lt;/a&gt; to determine the available volume streams and the associated number of steps. Some devices let you adjust in steps as few as 5, while others offered more gradations as high as 15. Many of these variations weren&amp;rsquo;t yet standardized into higher-level APIs that developers could query.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;package&lt;/span&gt; me.barrasso.android.volume.media;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;/**
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; * Proxy to safely access android.media.IAudioFlinger methods.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;AudioFlingerProxy&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Values from IAudioFlinger.cpp&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; CREATE_TRACK &lt;span style="color:#f92672"&gt;=&lt;/span&gt; IBinder.&lt;span style="color:#a6e22e"&gt;FIRST_CALL_TRANSACTION&lt;/span&gt;; &lt;span style="color:#75715e"&gt;// 1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; OPEN_RECORD &lt;span style="color:#f92672"&gt;=&lt;/span&gt; CREATE_TRACK &lt;span style="color:#f92672"&gt;+&lt;/span&gt; 1; &lt;span style="color:#75715e"&gt;// 2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; SAMPLE_RATE &lt;span style="color:#f92672"&gt;=&lt;/span&gt; OPEN_RECORD &lt;span style="color:#f92672"&gt;+&lt;/span&gt; 1; &lt;span style="color:#75715e"&gt;// 3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; CHANNEL_COUNT &lt;span style="color:#f92672"&gt;=&lt;/span&gt; SAMPLE_RATE &lt;span style="color:#f92672"&gt;+&lt;/span&gt; 1; &lt;span style="color:#75715e"&gt;// 4&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; FORMAT &lt;span style="color:#f92672"&gt;=&lt;/span&gt; CHANNEL_COUNT &lt;span style="color:#f92672"&gt;+&lt;/span&gt; 1; &lt;span style="color:#75715e"&gt;// 5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; FRAME_COUNT &lt;span style="color:#f92672"&gt;=&lt;/span&gt; FORMAT &lt;span style="color:#f92672"&gt;+&lt;/span&gt; 1; &lt;span style="color:#75715e"&gt;// 6&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; LATENCY &lt;span style="color:#f92672"&gt;=&lt;/span&gt; FRAME_COUNT &lt;span style="color:#f92672"&gt;+&lt;/span&gt; 1; &lt;span style="color:#75715e"&gt;// 7&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; SET_MASTER_VOLUME &lt;span style="color:#f92672"&gt;=&lt;/span&gt; LATENCY &lt;span style="color:#f92672"&gt;+&lt;/span&gt; 1; &lt;span style="color:#75715e"&gt;// 8&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; SET_MASTER_MUTE &lt;span style="color:#f92672"&gt;=&lt;/span&gt; SET_MASTER_VOLUME &lt;span style="color:#f92672"&gt;+&lt;/span&gt; 1; &lt;span style="color:#75715e"&gt;// 9&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; MASTER_VOLUME &lt;span style="color:#f92672"&gt;=&lt;/span&gt; SET_MASTER_MUTE &lt;span style="color:#f92672"&gt;+&lt;/span&gt; 1; &lt;span style="color:#75715e"&gt;// 10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; MASTER_MUTE &lt;span style="color:#f92672"&gt;=&lt;/span&gt; MASTER_VOLUME &lt;span style="color:#f92672"&gt;+&lt;/span&gt; 1; &lt;span style="color:#75715e"&gt;// 11&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; SET_STREAM_VOLUME &lt;span style="color:#f92672"&gt;=&lt;/span&gt; MASTER_MUTE &lt;span style="color:#f92672"&gt;+&lt;/span&gt; 1; &lt;span style="color:#75715e"&gt;// 12&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; SET_STREAM_MUTE &lt;span style="color:#f92672"&gt;=&lt;/span&gt; SET_STREAM_VOLUME &lt;span style="color:#f92672"&gt;+&lt;/span&gt; 1; &lt;span style="color:#75715e"&gt;// 13&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; STREAM_VOLUME &lt;span style="color:#f92672"&gt;=&lt;/span&gt; SET_STREAM_MUTE &lt;span style="color:#f92672"&gt;+&lt;/span&gt; 1; &lt;span style="color:#75715e"&gt;// 14&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; STREAM_MUTE &lt;span style="color:#f92672"&gt;=&lt;/span&gt; STREAM_VOLUME &lt;span style="color:#f92672"&gt;+&lt;/span&gt; 1; &lt;span style="color:#75715e"&gt;// 15&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; SET_MODE &lt;span style="color:#f92672"&gt;=&lt;/span&gt; STREAM_MUTE &lt;span style="color:#f92672"&gt;+&lt;/span&gt; 1; &lt;span style="color:#75715e"&gt;// 16&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; DEFAULT &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;1;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; VOICE_CALL &lt;span style="color:#f92672"&gt;=&lt;/span&gt; 0;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; SYSTEM &lt;span style="color:#f92672"&gt;=&lt;/span&gt; 1;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; RING &lt;span style="color:#f92672"&gt;=&lt;/span&gt; 2;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; MUSIC &lt;span style="color:#f92672"&gt;=&lt;/span&gt; 3;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; ALARM &lt;span style="color:#f92672"&gt;=&lt;/span&gt; 4;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; NOTIFICATION &lt;span style="color:#f92672"&gt;=&lt;/span&gt; 5;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; BLUETOOTH_SCO &lt;span style="color:#f92672"&gt;=&lt;/span&gt; 6;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; ENFORCED_AUDIBLE &lt;span style="color:#f92672"&gt;=&lt;/span&gt; 7;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; DTMF &lt;span style="color:#f92672"&gt;=&lt;/span&gt; 8;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; TTS &lt;span style="color:#f92672"&gt;=&lt;/span&gt; 9;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Values from errno.h&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; NO_ERROR &lt;span style="color:#f92672"&gt;=&lt;/span&gt; 0;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; UNKNOWN_TRANSACTION &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;74; &lt;span style="color:#75715e"&gt;// -EBADMSG;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; BAD_VALUE &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;22; &lt;span style="color:#75715e"&gt;// -EINVAL;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; PERMISSION_DENIED &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;1; &lt;span style="color:#75715e"&gt;// -EPERM;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; CALIBRATION_ERROR &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;64;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; String FLINGER_SERVICE &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;media.audio_flinger&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; String mInterfaceDescriptor;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; IBinder mAudioFlinger;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; SparseArray&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;SparseArray&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Float&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&lt;/span&gt; mStreamStepMap &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; SparseArray&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;SparseArray&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Float&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;AudioFlingerProxy&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mAudioFlinger &lt;span style="color:#f92672"&gt;=&lt;/span&gt; ReflectionUtils.&lt;span style="color:#a6e22e"&gt;getServiceManager&lt;/span&gt;(FLINGER_SERVICE);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; String mID &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mID &lt;span style="color:#f92672"&gt;=&lt;/span&gt; mAudioFlinger.&lt;span style="color:#a6e22e"&gt;getInterfaceDescriptor&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;catch&lt;/span&gt; (RemoteException e) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mID &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mInterfaceDescriptor &lt;span style="color:#f92672"&gt;=&lt;/span&gt; mID;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;boolean&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;isCalibrated&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; stream) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; SparseArray&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Float&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; map &lt;span style="color:#f92672"&gt;=&lt;/span&gt; mStreamStepMap.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(stream);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; (map &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; map.&lt;span style="color:#a6e22e"&gt;size&lt;/span&gt;() &lt;span style="color:#f92672"&gt;&amp;gt;=&lt;/span&gt; 2);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;mapStreamIndex&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; stream, &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; index) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; SparseArray&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Float&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; map &lt;span style="color:#f92672"&gt;=&lt;/span&gt; mStreamStepMap.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(stream);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;null&lt;/span&gt; &lt;span style="color:#f92672"&gt;==&lt;/span&gt; map) map &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; SparseArray&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Float&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;float&lt;/span&gt; value &lt;span style="color:#f92672"&gt;=&lt;/span&gt; getStreamVolume(stream);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; map.&lt;span style="color:#a6e22e"&gt;put&lt;/span&gt;(index, value);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;catch&lt;/span&gt; (RemoteException re) { }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mStreamStepMap.&lt;span style="color:#a6e22e"&gt;put&lt;/span&gt;(stream, map);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;/**
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; * Set the volume_3 of a calibrated stream.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; * @see {@link #setStreamVolume(int, float)}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; * @throws RemoteException
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;adjustStreamVolume&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; stream, &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; direction, &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; index, &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; max) &lt;span style="color:#66d9ef"&gt;throws&lt;/span&gt; RemoteException {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;null&lt;/span&gt; &lt;span style="color:#f92672"&gt;==&lt;/span&gt; mAudioFlinger &lt;span style="color:#f92672"&gt;||&lt;/span&gt; TextUtils.&lt;span style="color:#a6e22e"&gt;isEmpty&lt;/span&gt;(mInterfaceDescriptor) &lt;span style="color:#f92672"&gt;||&lt;/span&gt; &lt;span style="color:#f92672"&gt;!&lt;/span&gt;isCalibrated(stream)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; BAD_VALUE;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;float&lt;/span&gt; value &lt;span style="color:#f92672"&gt;=&lt;/span&gt; getStreamVolume(stream);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;float&lt;/span&gt; increment &lt;span style="color:#f92672"&gt;=&lt;/span&gt; getStreamIncrement(stream);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;float&lt;/span&gt; newValue &lt;span style="color:#f92672"&gt;=&lt;/span&gt; value;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;switch&lt;/span&gt; (direction) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;case&lt;/span&gt; AudioManager.&lt;span style="color:#a6e22e"&gt;ADJUST_LOWER&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; newValue &lt;span style="color:#f92672"&gt;-=&lt;/span&gt; increment;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;case&lt;/span&gt; AudioManager.&lt;span style="color:#a6e22e"&gt;ADJUST_RAISE&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; newValue &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; increment;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; newValue &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Math.&lt;span style="color:#a6e22e"&gt;max&lt;/span&gt;(0, Math.&lt;span style="color:#a6e22e"&gt;min&lt;/span&gt;(newValue, max &lt;span style="color:#f92672"&gt;*&lt;/span&gt; increment));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; setStreamVolume(stream, newValue);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// change this value to change volume_3 scaling&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;protected&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;float&lt;/span&gt; dBPerStep &lt;span style="color:#f92672"&gt;=&lt;/span&gt; 0.&lt;span style="color:#a6e22e"&gt;5f&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// shouldn&amp;#39;t need to touch these&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;protected&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;float&lt;/span&gt; dBConvert &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;dBPerStep &lt;span style="color:#f92672"&gt;*&lt;/span&gt; 2.&lt;span style="color:#a6e22e"&gt;302585093f&lt;/span&gt; &lt;span style="color:#f92672"&gt;/&lt;/span&gt; 20.&lt;span style="color:#a6e22e"&gt;0f&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;protected&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;float&lt;/span&gt; dBConvertInverse &lt;span style="color:#f92672"&gt;=&lt;/span&gt; 1.&lt;span style="color:#a6e22e"&gt;0f&lt;/span&gt; &lt;span style="color:#f92672"&gt;/&lt;/span&gt; dBConvert;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;protected&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;float&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;linearToLog&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; volume) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (volume &lt;span style="color:#f92672"&gt;==&lt;/span&gt; 0) &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; 0.&lt;span style="color:#a6e22e"&gt;0f&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;float&lt;/span&gt;) Math.&lt;span style="color:#a6e22e"&gt;exp&lt;/span&gt;(100 &lt;span style="color:#f92672"&gt;-&lt;/span&gt; volume &lt;span style="color:#f92672"&gt;*&lt;/span&gt; dBConvert);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;protected&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;logToLinear&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;float&lt;/span&gt; volume) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (volume &lt;span style="color:#f92672"&gt;==&lt;/span&gt; 0.&lt;span style="color:#a6e22e"&gt;0f&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; 0;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt;) (100 &lt;span style="color:#f92672"&gt;-&lt;/span&gt; (dBConvertInverse &lt;span style="color:#f92672"&gt;*&lt;/span&gt; Math.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(volume) &lt;span style="color:#f92672"&gt;+&lt;/span&gt; 0.&lt;span style="color:#a6e22e"&gt;5&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;protected&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;float&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;computeVolume&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; index, &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; max) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; mIndexMin &lt;span style="color:#f92672"&gt;=&lt;/span&gt; 0;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; volInt &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (100 &lt;span style="color:#f92672"&gt;*&lt;/span&gt; (index &lt;span style="color:#f92672"&gt;-&lt;/span&gt; mIndexMin)) &lt;span style="color:#f92672"&gt;/&lt;/span&gt; (max &lt;span style="color:#f92672"&gt;-&lt;/span&gt; mIndexMin);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; linearToLog(volInt);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;protected&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;float&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getStreamIncrement&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; stream) &lt;span style="color:#66d9ef"&gt;throws&lt;/span&gt; RemoteException {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Figure out what the increment is.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; SparseArray&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Float&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; map &lt;span style="color:#f92672"&gt;=&lt;/span&gt; mStreamStepMap.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(stream);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; indexOne &lt;span style="color:#f92672"&gt;=&lt;/span&gt; map.&lt;span style="color:#a6e22e"&gt;keyAt&lt;/span&gt;(0);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; indexTwo &lt;span style="color:#f92672"&gt;=&lt;/span&gt; map.&lt;span style="color:#a6e22e"&gt;keyAt&lt;/span&gt;(1);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;float&lt;/span&gt; valueOne &lt;span style="color:#f92672"&gt;=&lt;/span&gt; map.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(indexOne);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;float&lt;/span&gt; valueTwo &lt;span style="color:#f92672"&gt;=&lt;/span&gt; map.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(indexTwo);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; Math.&lt;span style="color:#a6e22e"&gt;abs&lt;/span&gt;(valueTwo &lt;span style="color:#f92672"&gt;-&lt;/span&gt; valueOne) &lt;span style="color:#f92672"&gt;/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Math.&lt;span style="color:#a6e22e"&gt;abs&lt;/span&gt;(indexTwo &lt;span style="color:#f92672"&gt;-&lt;/span&gt; indexOne);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getStreamIndex&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; stream) &lt;span style="color:#66d9ef"&gt;throws&lt;/span&gt; RemoteException {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;null&lt;/span&gt; &lt;span style="color:#f92672"&gt;==&lt;/span&gt; mAudioFlinger &lt;span style="color:#f92672"&gt;||&lt;/span&gt; TextUtils.&lt;span style="color:#a6e22e"&gt;isEmpty&lt;/span&gt;(mInterfaceDescriptor) &lt;span style="color:#f92672"&gt;||&lt;/span&gt; &lt;span style="color:#f92672"&gt;!&lt;/span&gt;isCalibrated(stream)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; BAD_VALUE;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;float&lt;/span&gt; value &lt;span style="color:#f92672"&gt;=&lt;/span&gt; getStreamVolume(stream);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;float&lt;/span&gt; increment &lt;span style="color:#f92672"&gt;=&lt;/span&gt; getStreamIncrement(stream);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; Math.&lt;span style="color:#a6e22e"&gt;round&lt;/span&gt;(value &lt;span style="color:#f92672"&gt;/&lt;/span&gt; increment);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;setStreamVolume&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; stream, &lt;span style="color:#66d9ef"&gt;float&lt;/span&gt; value) &lt;span style="color:#66d9ef"&gt;throws&lt;/span&gt; RemoteException {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;null&lt;/span&gt; &lt;span style="color:#f92672"&gt;==&lt;/span&gt; mAudioFlinger &lt;span style="color:#f92672"&gt;||&lt;/span&gt; TextUtils.&lt;span style="color:#a6e22e"&gt;isEmpty&lt;/span&gt;(mInterfaceDescriptor)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; BAD_VALUE;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Parcel data &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Parcel.&lt;span style="color:#a6e22e"&gt;obtain&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Parcel reply &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Parcel.&lt;span style="color:#a6e22e"&gt;obtain&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; data.&lt;span style="color:#a6e22e"&gt;writeInterfaceToken&lt;/span&gt;(mInterfaceDescriptor);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; data.&lt;span style="color:#a6e22e"&gt;writeInt&lt;/span&gt;(stream);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; data.&lt;span style="color:#a6e22e"&gt;writeFloat&lt;/span&gt;(value);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mAudioFlinger.&lt;span style="color:#a6e22e"&gt;transact&lt;/span&gt;(SET_STREAM_VOLUME, data, reply, 0);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; reply.&lt;span style="color:#a6e22e"&gt;readInt&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;float&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getStreamVolume&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; stream) &lt;span style="color:#66d9ef"&gt;throws&lt;/span&gt; RemoteException {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;null&lt;/span&gt; &lt;span style="color:#f92672"&gt;==&lt;/span&gt; mAudioFlinger &lt;span style="color:#f92672"&gt;||&lt;/span&gt; TextUtils.&lt;span style="color:#a6e22e"&gt;isEmpty&lt;/span&gt;(mInterfaceDescriptor)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; BAD_VALUE;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Parcel data &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Parcel.&lt;span style="color:#a6e22e"&gt;obtain&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Parcel reply &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Parcel.&lt;span style="color:#a6e22e"&gt;obtain&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; data.&lt;span style="color:#a6e22e"&gt;writeInterfaceToken&lt;/span&gt;(mInterfaceDescriptor);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; data.&lt;span style="color:#a6e22e"&gt;writeInt&lt;/span&gt;(stream);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mAudioFlinger.&lt;span style="color:#a6e22e"&gt;transact&lt;/span&gt;(STREAM_VOLUME, data, reply, 0);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;float&lt;/span&gt; ret &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Float.&lt;span style="color:#a6e22e"&gt;intBitsToFloat&lt;/span&gt;(reply.&lt;span style="color:#a6e22e"&gt;readInt&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; ret;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="reverse-engineering-tools"&gt;
Reverse Engineering Tools
&lt;a class="heading-anchor" href="#reverse-engineering-tools" aria-label="Link to Reverse Engineering Tools"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;There are tons of tools for reverse engineering Android apps. Back then, I primarily used &lt;a href="https://java-decompiler.github.io/"&gt;JD-GUI&lt;/a&gt;, &lt;a href="https://github.com/skylot/jadx"&gt;JADX&lt;/a&gt;, &lt;a href="https://apktool.org/"&gt;Apktool&lt;/a&gt;. Apktool is a powerful Android disassembler that rips apart an Android Package Kit (APK) file, itself just a ZIP archive, into its constituent parts. Java/ Kotlin code built for the Android Runtime (ART) is compiled into DEX bytecode, which can be disassembled into &lt;a href="https://github.com/google/smali"&gt;Smali&lt;/a&gt;, a human-readable text format that represents the complete structure of a Dex Virtual Machine (VM) class including all of its instructions.&lt;/p&gt;
&lt;p&gt;In 2014, whenever these tools failed to disassemble certain classes I would manually reconstruct them through some informal deduction. I&amp;rsquo;d look at the various call sites, interface definitions, and available resources to mentally re-assemble API signatures. Then I&amp;rsquo;d run some reflection inside of an app to test what was and wasn&amp;rsquo;t available. Some APIs were restricted to certain packages, permissions, or required proprietary features.&lt;/p&gt;
&lt;h2 id="why-now"&gt;
Why Now?
&lt;a class="heading-anchor" href="#why-now" aria-label="Link to Why Now?"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;In 2026, I find myself in the same situation as I was in 2014. I&amp;rsquo;m building my podcast app, &lt;strong&gt;&lt;a href="https://barrasso.me/projects/podlp/"&gt;PodLP&lt;/a&gt;, for Android flip phones&lt;/strong&gt; and once again I am running into inconsistent behaviors. Some devices (i.e. Kyocera, Sonim, and Ketai) have custom APIs to control soft keys, some have special features like Push-to-Talk (PTT) and SOS hot keys, and some offer an Eco mode that interferes with foreground services and network access when the clamshell is closed or screen dims.&lt;/p&gt;
&lt;p&gt;Because I&amp;rsquo;m targeting a niche set of highly-fragmented devices, once again I need to reverse engineer non-standard APIs in order to offer a high-quality, consistent experience across a large number of non-certified devices.&lt;/p&gt;
&lt;h2 id="reverse-engineering-in-opencode"&gt;
Reverse Engineering in OpenCode
&lt;a class="heading-anchor" href="#reverse-engineering-in-opencode" aria-label="Link to Reverse Engineering in OpenCode"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m pleasantly surprised that in order to reconstruct an APK (or at least several relevant APIs), there&amp;rsquo;s virtually no prompt engineering needed.&lt;/p&gt;
&lt;p&gt;First I &lt;strong&gt;enable developer options&lt;/strong&gt; via Settings &amp;gt; About &amp;gt;, then click Build 7 times. Next, I query the installed packages&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;adb shell cmd package list packages
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On my &lt;a href="https://amzn.to/49qb9nX"&gt;Kyocera DuraXV Extreme E4810&lt;/a&gt; this returned:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.hiddendebug
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.kyocerahome
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.cts.priv.ctsshim
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.vzw.apnlib
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.internal.display.cutout.emulation.corner
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.internal.display.cutout.emulation.double
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.providers.telephony
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.kyocera.diagnostic
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.verizon.mips.services
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.providers.calendar
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.providers.media
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.wallpapercropper
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.quicinc.cne.CNEService
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.filemanager.launcher
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.kyocera.flashlight
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.documentsui
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.externalstorage
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.frameworks.overlay
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.htmlviewer
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.companiondevicemanager
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.verizon.obdm_permissions
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.mms.service
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.kyocera.calculator2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.providers.downloads
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.gallery.launcher
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.mobisystems.office.kyocera
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.qualcomm.qti.telephonyservice
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.kyocera.labtest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.browser
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.vcast.mediamanager
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.defcontainer
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.oemsetupwizard
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.providers.downloads.ui
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.pacprocessor
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.simappdialog
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.datafolder
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.internal.display.cutout.emulation.tall
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.certinstaller
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.corp.manager
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.carrierconfig
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.google.android.marvin.talkback
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.kchandlenv
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.qti.qualcomm.datastatusnotification
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.LocationConsentApp
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;android
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.contacts
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.egg
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.mms
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.mtp
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.stk
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.backupconfirm
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.ecomode
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.kyocera.telephony
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.settings.nfp
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.provision
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;org.codeaurora.ims
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.statementservice
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.verizon.bootstrapclient
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.settings.intelligence
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.calendar
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.systemui.theme.dark
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.kyocera.kcPhoneService
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.WebViewer
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.kc_soundrecorder
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.qualcomm.qti.dynamicddsservice
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.qualcomm.qcrilmsgtunnel
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.providers.settings
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.kdfs
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.kerr
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.memo
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.sharedstoragebackup
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.printspooler
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.dreams.basic
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.webview
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.kyocera.kcTelecommService
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.inputdevices
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.selfprovisioning
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.customizekey
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.bips
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.providers.memo
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.musicfx
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.kyocera.worldclock
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.ffm.kyocera
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.kyocera.voicerecognition
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.poweronactivation
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.cellbroadcastreceiver
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;android.ext.shared
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.onetimeinitializer
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.skyhookwireless.provider
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.kyocera.telephonyextra
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.server.telecom
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.keychain
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.vzwextension.service
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.kyocera.stopwatch
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.printservice.recommendation
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.dialer
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;android.ext.services
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.calllogbackup
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.packageinstaller
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.carrierdefaultapp
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.svox.pico
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.proxyhandler
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.kitting
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.managedprovisioning
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.kyocera.wifiautoactivate
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.kc_fmradio
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.batterycare
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.kyocera.mediumkey
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.server.telecom.overlay
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.kyocera.PcoStatusReceiver
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.kcPhoneSettings
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.storagemanager
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.bookmarkprovider
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.settings
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.kyocera.calendar.localaccount
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.verizon.obdm
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.qualcomm.location
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.cts.ctsshim
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.kyocera.alarm
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.kyocera.omadm
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.kyocera.timer
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.vpndialogs
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.kyocera.musicplayer
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.email
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.phone
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.shell
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.wallpaperbackup
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.providers.blockednumber
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.providers.userdictionary
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.emergency
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.providers.settings
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.location.fused
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.systemui
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.devicecontrol
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.exchange
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.bluetoothmidiservice
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.qualcomm.qti.poweroffalarm
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.kclightsservice
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.kyocera.apnsetting
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.traceur
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.thermalmonitor
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.bluetooth
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.qualcomm.timeservice
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.qualcomm.atfwd
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.providers.contacts
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.android.captiveportallogin
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.verizon.mdm.xvlte
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;com.verizon.pushtotalkplus
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jp.kyocera.sub.kitting
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;At this point, I can use my intuition or defer to an LLM which packages to search. I&amp;rsquo;ll pull those packages to my computer and run Apktool to disassemble.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#!/usr/bin/env bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$#&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; -eq &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#f92672"&gt;]&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34;Usage: &lt;/span&gt;$0&lt;span style="color:#e6db74"&gt; &amp;lt;package.name&amp;gt; [package.name ...]&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; exit &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;OUT_DIR&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;decompiled&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;APK_DIR&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;apks&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mkdir -p &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$OUT_DIR&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$APK_DIR&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; pkg in &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$@&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Query APK paths from device&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; paths&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;adb shell pm path &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$pkg&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; | sed &lt;span style="color:#e6db74"&gt;&amp;#39;s/^package://&amp;#39;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt; -z &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$paths&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;]&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34; ! Package not found: &lt;/span&gt;$pkg&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;continue&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; pkg_apk_dir&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$APK_DIR&lt;span style="color:#e6db74"&gt;/&lt;/span&gt;$pkg&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mkdir -p &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$pkg_apk_dir&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Pull base + split APKs&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; path in $paths; &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; apk_name&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;basename &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$path&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34; Pulling &lt;/span&gt;$apk_name&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; adb pull &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$path&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$pkg_apk_dir&lt;span style="color:#e6db74"&gt;/&lt;/span&gt;$apk_name&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Prefer base.apk for apktool&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; base_apk&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;ls &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$pkg_apk_dir&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;/base*.apk 2&amp;gt;/dev/null | head -n 1&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Fallback for legacy single-apk installs&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt; -z &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$base_apk&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;]&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; base_apk&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;ls &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$pkg_apk_dir&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;/*.apk 2&amp;gt;/dev/null | head -n 1&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt; -n &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$base_apk&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;]&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34; Decompiling &lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;basename &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$base_apk&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; apktool d -f &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$base_apk&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; -o &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$OUT_DIR&lt;span style="color:#e6db74"&gt;/&lt;/span&gt;$pkg&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34; ! No APK found to decompile for &lt;/span&gt;$pkg&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;From this point, I&amp;rsquo;ll grep the resulting Smali for interfaces I&amp;rsquo;m interested in. Here&amp;rsquo;s an snippet of an Activity in the Voice Recognition app that calls into &lt;code&gt;jp/kyocera/kcfp/util/KCfpSoftkeyGuide&lt;/code&gt; which is a strong candidate.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-smali" data-lang="smali"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;.class&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;com/kyocera/voicerecognition/&lt;span style="color:#a6e22e"&gt;TopHelpActivity&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;.super&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;android/app/&lt;span style="color:#a6e22e"&gt;Activity&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;.source&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;TopHelpActivity.java&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# static fields
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;.field&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; LOGTAG:&lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;java/lang/&lt;span style="color:#a6e22e"&gt;String&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# instance fields
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;.field&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; closeAppTask:&lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;java/lang/&lt;span style="color:#a6e22e"&gt;Runnable&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;.field&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; handler:&lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;android/os/&lt;span style="color:#a6e22e"&gt;Handler&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;.field&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; mEngine:&lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;com/kyocera/voicerecognition/&lt;span style="color:#a6e22e"&gt;ReadoutText&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;.method&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;protected&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;onCreate&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;android/os/&lt;span style="color:#a6e22e"&gt;Bundle&lt;/span&gt;;)&lt;span style="color:#66d9ef"&gt;V&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;.locals&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;.line&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;32&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; invoke-super {p0, p1}, &lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;android/app/&lt;span style="color:#a6e22e"&gt;Activity&lt;/span&gt;;-&amp;gt;&lt;span style="color:#a6e22e"&gt;onCreate&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;android/os/&lt;span style="color:#a6e22e"&gt;Bundle&lt;/span&gt;;)&lt;span style="color:#66d9ef"&gt;V&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;.line&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;33&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sget-object p1, &lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;com/kyocera/voicerecognition/&lt;span style="color:#a6e22e"&gt;TopHelpActivity&lt;/span&gt;;-&amp;gt;LOGTAG:&lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;java/lang/&lt;span style="color:#a6e22e"&gt;String&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; const-string v0, &lt;span style="color:#e6db74"&gt;&amp;#34;onCreate[in]&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;//&lt;/span&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;.line&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;38&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; invoke-static {}, &lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;jp/kyocera/kcfp/util/&lt;span style="color:#a6e22e"&gt;KCfpWidgetUtil&lt;/span&gt;;-&amp;gt;&lt;span style="color:#a6e22e"&gt;getInstance&lt;/span&gt;()&lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;jp/kyocera/kcfp/util/&lt;span style="color:#a6e22e"&gt;KCfpWidgetUtil&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; move-result-object v0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; invoke-virtual {p0}, &lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;com/kyocera/voicerecognition/&lt;span style="color:#a6e22e"&gt;TopHelpActivity&lt;/span&gt;;-&amp;gt;&lt;span style="color:#a6e22e"&gt;getActionBar&lt;/span&gt;()&lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;android/app/&lt;span style="color:#a6e22e"&gt;ActionBar&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; move-result-object v1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; const/4 v2, &lt;span style="color:#ae81ff"&gt;0x0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; invoke-virtual {v0, v1, v2}, &lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;jp/kyocera/kcfp/util/&lt;span style="color:#a6e22e"&gt;KCfpWidgetUtil&lt;/span&gt;;-&amp;gt;&lt;span style="color:#a6e22e"&gt;setDisplayShowOverflowMenuButtonEnabled&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;android/app/&lt;span style="color:#a6e22e"&gt;ActionBar&lt;/span&gt;;&lt;span style="color:#66d9ef"&gt;Z&lt;/span&gt;)&lt;span style="color:#66d9ef"&gt;V&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; const v0, &lt;span style="color:#ae81ff"&gt;0x7f0d009f&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;.line&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;39&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; invoke-virtual {p0, v0}, &lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;com/kyocera/voicerecognition/&lt;span style="color:#a6e22e"&gt;TopHelpActivity&lt;/span&gt;;-&amp;gt;&lt;span style="color:#a6e22e"&gt;getString&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;I&lt;/span&gt;)&lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;java/lang/&lt;span style="color:#a6e22e"&gt;String&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; move-result-object v0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;.line&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;40&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; invoke-virtual {p0}, &lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;com/kyocera/voicerecognition/&lt;span style="color:#a6e22e"&gt;TopHelpActivity&lt;/span&gt;;-&amp;gt;&lt;span style="color:#a6e22e"&gt;getWindow&lt;/span&gt;()&lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;android/view/&lt;span style="color:#a6e22e"&gt;Window&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; move-result-object v1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; invoke-static {v1}, &lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;jp/kyocera/kcfp/util/&lt;span style="color:#a6e22e"&gt;KCfpSoftkeyGuide&lt;/span&gt;;-&amp;gt;&lt;span style="color:#a6e22e"&gt;getSoftkeyGuide&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;android/view/&lt;span style="color:#a6e22e"&gt;Window&lt;/span&gt;;)&lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;jp/kyocera/kcfp/util/&lt;span style="color:#a6e22e"&gt;KCfpSoftkeyGuide&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; move-result-object v1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; if-eqz v1, :cond_0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;.line&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;43&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; invoke-virtual {v1, v2, v0}, &lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;jp/kyocera/kcfp/util/&lt;span style="color:#a6e22e"&gt;KCfpSoftkeyGuide&lt;/span&gt;;-&amp;gt;&lt;span style="color:#a6e22e"&gt;setText&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;IL&lt;/span&gt;java/lang/&lt;span style="color:#a6e22e"&gt;CharSequence&lt;/span&gt;;)&lt;span style="color:#66d9ef"&gt;V&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;.line&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;44&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; invoke-virtual {v1}, &lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;jp/kyocera/kcfp/util/&lt;span style="color:#a6e22e"&gt;KCfpSoftkeyGuide&lt;/span&gt;;-&amp;gt;&lt;span style="color:#a6e22e"&gt;invalidate&lt;/span&gt;()&lt;span style="color:#66d9ef"&gt;V&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; :cond_0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; const v0, &lt;span style="color:#ae81ff"&gt;0x7f080054&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;.line&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;47&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; invoke-virtual {p0, v0}, &lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;com/kyocera/voicerecognition/&lt;span style="color:#a6e22e"&gt;TopHelpActivity&lt;/span&gt;;-&amp;gt;&lt;span style="color:#a6e22e"&gt;findViewById&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;I&lt;/span&gt;)&lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;android/view/&lt;span style="color:#a6e22e"&gt;View&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; move-result-object v0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; check-cast v0, &lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;android/widget/&lt;span style="color:#a6e22e"&gt;TextView&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;.line&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;48&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; invoke-virtual {v0, p1}, &lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;android/widget/&lt;span style="color:#a6e22e"&gt;TextView&lt;/span&gt;;-&amp;gt;&lt;span style="color:#a6e22e"&gt;setText&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;I&lt;/span&gt;)&lt;span style="color:#66d9ef"&gt;V&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; const p1, &lt;span style="color:#ae81ff"&gt;0x7f080053&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;.line&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;49&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; invoke-virtual {p0, p1}, &lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;com/kyocera/voicerecognition/&lt;span style="color:#a6e22e"&gt;TopHelpActivity&lt;/span&gt;;-&amp;gt;&lt;span style="color:#a6e22e"&gt;findViewById&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;I&lt;/span&gt;)&lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;android/view/&lt;span style="color:#a6e22e"&gt;View&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; move-result-object p1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; check-cast p1, &lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;android/widget/&lt;span style="color:#a6e22e"&gt;TextView&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; const/16 v0, &lt;span style="color:#ae81ff"&gt;0x8&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;.line&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;50&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; invoke-virtual {p1, v0}, &lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;android/widget/&lt;span style="color:#a6e22e"&gt;TextView&lt;/span&gt;;-&amp;gt;&lt;span style="color:#a6e22e"&gt;setVisibility&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;I&lt;/span&gt;)&lt;span style="color:#66d9ef"&gt;V&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;.line&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;51&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sget-object p1, &lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;com/kyocera/voicerecognition/&lt;span style="color:#a6e22e"&gt;TopHelpActivity&lt;/span&gt;;-&amp;gt;LOGTAG:&lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;java/lang/&lt;span style="color:#a6e22e"&gt;String&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; const-string v0, &lt;span style="color:#e6db74"&gt;&amp;#34;onCreate[out]&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; invoke-static {p1, v0}, &lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;com/kyocera/voicerecognition/util/&lt;span style="color:#a6e22e"&gt;LogUtils&lt;/span&gt;;-&amp;gt;&lt;span style="color:#a6e22e"&gt;d&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;java/lang/&lt;span style="color:#a6e22e"&gt;String&lt;/span&gt;;&lt;span style="color:#66d9ef"&gt;L&lt;/span&gt;java/lang/&lt;span style="color:#a6e22e"&gt;String&lt;/span&gt;;)&lt;span style="color:#66d9ef"&gt;V&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; return-void
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;.end&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;method&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then with a single prompt in &lt;a href="https://opencode.ai/"&gt;OpenCode&lt;/a&gt;, I have a Java class! I&amp;rsquo;m using &lt;a href="https://blog.google/products-and-platforms/products/gemini/gemini-3/"&gt;Gemini 3 Flash&lt;/a&gt;, which I&amp;rsquo;ve generally found to be the most effective model when working with Android. Plus Anthropic is sadly &lt;a href="https://github.com/anomalyco/opencode/issues/7410"&gt;blocking Claude subscriptions&lt;/a&gt; inside OpenCode.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; █▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; █░░█ █░░█ █▀▀▀ █░░█ █░░░ █░░█ █░░█ █▀▀▀
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ┃
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ┃ Reassemble the @smali/com/kyocera/voicerecognition/TopHelpActivity.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ┃ smali Smali into its original Java class, with a focus on
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ┃ reconstructing APIs it calls into from the &amp;#39;jp.kyocera.kcfp` package
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ┃
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ┃ Build Gemini 3 Flash Preview Google
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ╹▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tab switch agent ctrl+p commands
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ~/Kyocera/decompiled/VoiceRecognition 1.1.13
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After a few seconds and 47,573 tokens (~$0.05), OpenCode wrote the full Java class and specifically re-constructed the interfaces to the APIs that I&amp;rsquo;m interested in.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Locating Dependent Smali Files&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m now focusing on locating the necessary smali files for the &lt;code&gt;jp.kyocera.kcfp classes&lt;/code&gt;. Specifically, I need the files for the package structure to ensure proper reconstruction of the dependent classes. I plan to begin by searching for the jp/kyocera/kcfp directory. This should provide the information needed for accurate reassembly.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve analyzed the &lt;code&gt;TopHelpActivity.smali&lt;/code&gt; file. To accurately reassemble it and reconstruct the &lt;code&gt;jp.kyocera.kcfp APIs&lt;/code&gt;, I&amp;rsquo;ll first locate and examine the relevant smali files for the external libraries and inner classes.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;tool_call&lt;/code&gt;: glob for pattern &lt;code&gt;'*/jp/kyocera/kcfp/util/.smali'&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;tool_call&lt;/code&gt;: glob for pattern &lt;code&gt;'*/com/kyocera/voicerecognition/TopHelpActivity$.smali'&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;tool_call&lt;/code&gt;: glob for pattern &lt;code&gt;'**/com/kyocera/voicerecognition/ReadoutText.smali'&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;tool_call&lt;/code&gt;: glob for pattern &lt;code&gt;'**/com/kyocera/voicerecognition/VoiceRecognition.smali'&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;tool_call&lt;/code&gt;: glob for pattern &lt;code&gt;'**/com/kyocera/voicerecognition/util/LogUtils.smali'&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;✱ Glob &amp;ldquo;**/jp/kyocera/kcfp/util/*.smali&amp;rdquo;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;✱ Glob &amp;ldquo;**/com/kyocera/voicerecognition/TopHelpActivity$*.smali&amp;rdquo; (3 matches)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;✱ Glob &amp;ldquo;**/com/kyocera/voicerecognition/ReadoutText.smali&amp;rdquo; (1 matches)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;✱ Glob &amp;ldquo;**/com/kyocera/voicerecognition/VoiceRecognition.smali&amp;rdquo; (1 matches)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;✱ Glob &amp;ldquo;**/com/kyocera/voicerecognition/util/LogUtils.smali&amp;rdquo; (1 matches)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here&amp;rsquo;s a snippet from the resulting Java file.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;package&lt;/span&gt; com.kyocera.voicerecognition;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; jp.kyocera.kcfp.util.KCfpSoftkeyGuide;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; jp.kyocera.kcfp.util.KCfpWidgetUtil;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;/**
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; * Activity for displaying top-level help information with voice readout.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;TopHelpActivity&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;extends&lt;/span&gt; Activity {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Override&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;protected&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;onCreate&lt;/span&gt;(Bundle savedInstanceState) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;super&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;onCreate&lt;/span&gt;(savedInstanceState);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; setContentView(0x7f0a0023); &lt;span style="color:#75715e"&gt;// R.layout.top_help_activity&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; setTitle(0x7f0d006e); &lt;span style="color:#75715e"&gt;// R.string.help_title&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mEngine &lt;span style="color:#f92672"&gt;=&lt;/span&gt; ReadoutText.&lt;span style="color:#a6e22e"&gt;getInstance&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Initial voice readout: &amp;#34;Help Title, Help Description&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; String speakText &lt;span style="color:#f92672"&gt;=&lt;/span&gt; getString(0x7f0d006e) &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;,&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; getString(0x7f0d006d);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mEngine.&lt;span style="color:#a6e22e"&gt;speak&lt;/span&gt;(speakText, &lt;span style="color:#e6db74"&gt;&amp;#34;INIT&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Kyocera Framework: Disable overflow menu button in ActionBar&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; KCfpWidgetUtil.&lt;span style="color:#a6e22e"&gt;getInstance&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;setDisplayShowOverflowMenuButtonEnabled&lt;/span&gt;(getActionBar(), &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Kyocera Framework: Set softkey guide text&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; String softkeyText &lt;span style="color:#f92672"&gt;=&lt;/span&gt; getString(0x7f0d009f); &lt;span style="color:#75715e"&gt;// R.string.softkey_close&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; KCfpSoftkeyGuide guide &lt;span style="color:#f92672"&gt;=&lt;/span&gt; KCfpSoftkeyGuide.&lt;span style="color:#a6e22e"&gt;getSoftkeyGuide&lt;/span&gt;(getWindow());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (guide &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; guide.&lt;span style="color:#a6e22e"&gt;setText&lt;/span&gt;(0, softkeyText);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; guide.&lt;span style="color:#a6e22e"&gt;invalidate&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Set UI text&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; TextView titleView &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (TextView) findViewById(0x7f080054); &lt;span style="color:#75715e"&gt;// R.id.help_title&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; titleView.&lt;span style="color:#a6e22e"&gt;setText&lt;/span&gt;(0x7f0d006e);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; TextView subtitleView &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (TextView) findViewById(0x7f080053); &lt;span style="color:#75715e"&gt;// R.id.help_subtitle&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; subtitleView.&lt;span style="color:#a6e22e"&gt;setVisibility&lt;/span&gt;(8); &lt;span style="color:#75715e"&gt;// GONE&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As well as the soft key API that I care about:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;package&lt;/span&gt; jp.kyocera.kcfp.util;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; android.view.Window;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;/**
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; * Reconstructed utility for managing softkey guides on Kyocera devices.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;KCfpSoftkeyGuide&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; KCfpSoftkeyGuide &lt;span style="color:#a6e22e"&gt;getSoftkeyGuide&lt;/span&gt;(Window window) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Implementation provided by Kyocera KCFP framework&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;/**
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; * Sets the text for a specific softkey.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; * @param index The index of the softkey (e.g., 0 for left/center softkey).
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; * @param text The text to display.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;setText&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; index, CharSequence text) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Implementation provided by Kyocera KCFP framework&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;/**
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; * Invalidates the guide view to trigger a redraw.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;invalidate&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Implementation provided by Kyocera KCFP framework&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It turns out this API was already documented on &lt;a href="https://www.reddit.com/r/flipphones/comments/1ncaub7/i_managed_to_sniff_out_the_hidden_api_to_show/"&gt;r/flipphones&lt;/a&gt; as well as the &lt;a href="https://habr.com/ru/companies/timeweb/articles/844936/"&gt;timeweb blog&lt;/a&gt; (in Russian). Nonetheless, it illustrates just how quickly an LLM can reconstruct Java code from Smali. With a more thorough prompt, this could be applied to an entire APK, properly map resource IDs, and even apply reasonable variable names to code minified using Proguard!&lt;/p&gt;
&lt;p&gt;After a bit more searching, I was able to reconsruct the entire API including the &lt;code&gt;setSoftkeyEventListener&lt;/code&gt; method and corresponding listener. Before integrating these APIs into PodLP, clicking the physical volume keys would trigger the soft key bar to show and not hide with the system dialog. After integration, the app feels more native and no longer has the previous quirks.&lt;/p&gt;
&lt;h2 id="part-2-sonim-and-beyond"&gt;
Part 2. Sonim and Beyond
&lt;a class="heading-anchor" href="#part-2-sonim-and-beyond" aria-label="Link to Part 2. Sonim and Beyond"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2026-01-11-android-reverse-engineering-with-opencode/kyocera-and-sonim-podlp_hu_75c44f1527f801b7.webp 320w, https://barrasso.me/posts/2026-01-11-android-reverse-engineering-with-opencode/kyocera-and-sonim-podlp_hu_b8587fbc4c1ad988.webp 640w, https://barrasso.me/posts/2026-01-11-android-reverse-engineering-with-opencode/kyocera-and-sonim-podlp_hu_8ed0acb3284f7348.webp 1024w, https://barrasso.me/posts/2026-01-11-android-reverse-engineering-with-opencode/kyocera-and-sonim-podlp_hu_e2b286328654fbc.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2026-01-11-android-reverse-engineering-with-opencode/kyocera-and-sonim-podlp_hu_438c474749a5b2b.jpeg 320w, https://barrasso.me/posts/2026-01-11-android-reverse-engineering-with-opencode/kyocera-and-sonim-podlp_hu_6caf1b250d23a298.jpeg 640w, https://barrasso.me/posts/2026-01-11-android-reverse-engineering-with-opencode/kyocera-and-sonim-podlp_hu_c35e58e056c709b3.jpeg 1024w, https://barrasso.me/posts/2026-01-11-android-reverse-engineering-with-opencode/kyocera-and-sonim-podlp_hu_2a33d1564448e0f6.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2026-01-11-android-reverse-engineering-with-opencode/kyocera-and-sonim-podlp_hu_438c474749a5b2b.jpeg"
alt="PodLP on Android Flip Phones" class="w-full no-border contain"
width="320"
height="240"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;PodLP on the Sonim XP3+ (left) and Kyocera DuraXV (right)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;Thanks to the folks on the &lt;a href="https://forums.jtechforums.org/"&gt;JTech Forum&lt;/a&gt; and for several newly-acquired devices on eBay, I&amp;rsquo;ve been able to investigate the firmware on more models. For instance, on the &lt;a href="https://amzn.to/3ZmSW4u"&gt;Sonim XP3+ XP3900&lt;/a&gt; soft keys are handled in a completely different way.&lt;/p&gt;
&lt;p&gt;Unlike Kyocera, there are three soft keys (left, middle, and right). Soft middle is a separate key from &lt;code&gt;Enter&lt;/code&gt; or &lt;code&gt;DPAD_CENTER&lt;/code&gt;. The soft key UI cannot be hidden, and menu items can only be &lt;code&gt;Strings&lt;/code&gt; (no &lt;code&gt;Drawables&lt;/code&gt;). Setting soft keys is easy:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-kotlin" data-lang="kotlin"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;val&lt;/span&gt; softBarIntent = Intent(&lt;span style="color:#e6db74"&gt;&amp;#34;android.intent.action.CHANGE_NAV_BAR&amp;#34;&lt;/span&gt;).apply {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; putExtra(&lt;span style="color:#e6db74"&gt;&amp;#34;left&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;Menu&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; putExtra(&lt;span style="color:#e6db74"&gt;&amp;#34;center&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;Select&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; putExtra(&lt;span style="color:#e6db74"&gt;&amp;#34;right&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;Back&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; putExtra(&lt;span style="color:#e6db74"&gt;&amp;#34;from_package&amp;#34;&lt;/span&gt;, context.packageName)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;context.sendBroadcast(softBarIntent)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Unfortunately, it&amp;rsquo;s not possible to detect this &lt;code&gt;Intent&lt;/code&gt;&amp;rsquo;s receiver with &lt;code&gt;packageManager.queryBroadcastReceivers&lt;/code&gt; because it&amp;rsquo;s registered dynamically inside SystemUI using the centralized &lt;code&gt;BroadcastDispatcher&lt;/code&gt;. There is no way to know whether the &lt;code&gt;Intent&lt;/code&gt; was received and the UI updated.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;01-21 17:29:32.806 7704 7713 W dex2oat32: Accessing hidden method Landroid/content/Context;-&amp;gt;isUiContext()Z (blacklist, linking, denied)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;ve also been experimenting with other hidden APIs. In this instance, access via Android X or some compatability library was blocked from calling &lt;code&gt;Context.isUiContext&lt;/code&gt;. Because the Sonim XP3+ runs Android 11 (the Kyocera DuraXV runs the much older Android 8), I&amp;rsquo;ve had to use &lt;a href="https://github.com/LSPosed/AndroidHiddenApiBypass"&gt;HiddenApiBypass&lt;/a&gt; to circumvent the Android 9+ &lt;a href="https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces"&gt;restrictions on non-SDK interfaces&lt;/a&gt;. Fortunately, LSPass works well on the Sonim XP3+ so I&amp;rsquo;ve been able to continue experimenting.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;
Conclusion
&lt;a class="heading-anchor" href="#conclusion" aria-label="Link to Conclusion"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Using Gemini via OpenCode for reverse engineering hidden Android APIs is much more efficient than doing it by hand. As I acquire more phone models, I&amp;rsquo;ll expand this analysis to better integrate into these fragmented feature phones. I&amp;rsquo;m also exploring other uses of Gemini &amp;amp; OpenCode for Android development including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Constructing utility classes leveraging reflection to call hidden APIs&lt;/li&gt;
&lt;li&gt;Integrating hidden API utilities into my application&amp;rsquo;s lifecycle&lt;/li&gt;
&lt;li&gt;Generating Vector Drawables and adaptive icons from Scalable Vector Graphics (SVG) files&lt;/li&gt;
&lt;li&gt;Writing Proguard rules to optimize release builds&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Testing on physical devices is still critical, but I wish I had tools like Gemini and OpenCode back in 2012-2015 when I was actively reverse engineering many flagship Android phones for fun and profit.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2026-01-11-android-reverse-engineering-with-opencode/</guid><pubDate>Sun, 11 Jan 2026 00:00:00 GMT</pubDate></item><item><title>
Migrating PodLP from JavaScript to TypeScript using OpenCode</title><link>https://barrasso.me/posts/2025-12-22-large-scale-typescript-migration-in-opencode/</link><description>
&lt;h2 id="at-a-glance"&gt;
At a Glance
&lt;a class="heading-anchor" href="#at-a-glance" aria-label="Link to At a Glance"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;I migrated &lt;a href="https://barrasso.me/projects/podlp/"&gt;PodLP&lt;/a&gt;, my podcast app for &lt;a href="https://kaiostech.com"&gt;KaiOS&lt;/a&gt;, from JavaScript to TypeScript.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scale&lt;/strong&gt;. ~35,000 lines of code (LOC) across 160 JavaScript files, 67 Svelte components, and 28 JSON translation dictionaries&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost&lt;/strong&gt;. $43 using &lt;a href="https://cloud.google.com/free"&gt;$300 free credit&lt;/a&gt; for new Google Cloud customers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Time&lt;/strong&gt;. Two half days of background tasks, primarily baking cookies and working on other projects&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Impact&lt;/strong&gt;. A codebase that&amp;rsquo;s &lt;em&gt;completely&lt;/em&gt; migrated to TypeScript&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What didn&amp;rsquo;t work?&lt;/strong&gt; Attempting a one-shot conversion&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What did work?&lt;/strong&gt; Multiple well-scoped subagents and some patience&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="background-and-motivation"&gt;
Background and Motivation
&lt;a class="heading-anchor" href="#background-and-motivation" aria-label="Link to Background and Motivation"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve maintained PodLP for 5+ years. As it grew, it became more complex and honestly a &lt;em&gt;bit messy&lt;/em&gt;. It eventually morphed into a monorepo with several front-end versions: the original for KaiOS, one for the &lt;a href="https://www.jio.com/general/jiophone/"&gt;JioPhone&lt;/a&gt; (India), and one for &lt;a href="https://barrasso.me/projects/cloud-phone/"&gt;Cloud Phone&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Moreover, when I started PodLP, I knew nothing about podcatchers or podcast indexing. I also had no experience with KaiOS or it&amp;rsquo;s many quirks and undocumented APIs. Iteratively over several years, the app grew in size and complexity. Yet I still generated builds (ZIP files) locally on my &lt;a href="https://amzn.to/4p0N4s5"&gt;M1 MacBook Air&lt;/a&gt; and uploaded them manually to the &lt;a href="https://developer.kaiostech.com/docs/distribution/submission-portal/"&gt;KaiStore&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I tried to keep up-to-date with processes and dependencies. I upgraded PodLP from Svelte 3 to Svelte 4, I rewrote &lt;a href="https://kaios.dev/2023/02/the-download-on-kaios-downloads/"&gt;offline downloads&lt;/a&gt; to used chunked streams (&lt;code&gt;moz-chunked-arraybuffer&lt;/code&gt;) written not to &lt;code&gt;IndexedDB&lt;/code&gt; but instead to &lt;a href="https://developer.kaiostech.com/docs/api/web-apis/deviceStorage/device-storage/"&gt;&lt;code&gt;DeviceStorage&lt;/code&gt;&lt;/a&gt;, I added support for &lt;a href="https://blog.podlp.com/posts/podlp-kaios-3/"&gt;KaiOS 3.0&lt;/a&gt; and I migrated PodLP&amp;rsquo;s back-end to the &lt;a href="https://blog.podlp.com/posts/podlp-podcast-index/"&gt;Podcast Index&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Although there are tools like &lt;a href="https://github.com/facebook/jscodeshift"&gt;jscodeshift&lt;/a&gt; and &lt;a href="https://svelte.dev/docs/cli/sv-migrate"&gt;&lt;code&gt;sv migrate&lt;/code&gt;&lt;/a&gt;, large-scale refactoring of 200+ files and 30,000+ lines without ready-made scripts was too much time and effort to justify. Inspired by Simon Willison’s port of &lt;a href="https://simonwillison.net/2025/Dec/15/porting-justhtml/"&gt;JustHTML from Python to JavaScript&lt;/a&gt;, I decided to try a similar agentic approach.&lt;/p&gt;
&lt;h2 id="but-first-a-types-library"&gt;
But First, A Types Library
&lt;a class="heading-anchor" href="#but-first-a-types-library" aria-label="Link to But First, A Types Library"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-22-large-scale-typescript-migration-in-opencode/paths-in-snow_hu_1e0f294f736f74ab.webp 320w, https://barrasso.me/posts/2025-12-22-large-scale-typescript-migration-in-opencode/paths-in-snow_hu_2eb6dcec81a55d29.webp 640w, https://barrasso.me/posts/2025-12-22-large-scale-typescript-migration-in-opencode/paths-in-snow_hu_8259275fed0f68f5.webp 1024w, https://barrasso.me/posts/2025-12-22-large-scale-typescript-migration-in-opencode/paths-in-snow_hu_3f37c4f8464c2c8f.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-22-large-scale-typescript-migration-in-opencode/paths-in-snow_hu_c80ae31480417134.jpeg 320w, https://barrasso.me/posts/2025-12-22-large-scale-typescript-migration-in-opencode/paths-in-snow_hu_6cdae42948340302.jpeg 640w, https://barrasso.me/posts/2025-12-22-large-scale-typescript-migration-in-opencode/paths-in-snow_hu_1e59b56afc46a127.jpeg 1024w, https://barrasso.me/posts/2025-12-22-large-scale-typescript-migration-in-opencode/paths-in-snow_hu_8044e40236444c8b.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-22-large-scale-typescript-migration-in-opencode/paths-in-snow_hu_c80ae31480417134.jpeg"
alt="Footpaths in snow on Mt. Watatic" class="w-full no-border contain"
width="320"
height="143"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Many footpaths to choose on Mt. Watatic&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;Before beginning this migration, I knew the biggest challenge would be the lack of type definitions for the many non-standard KaiOS APIs. Fortunately, large parts of KaiOS are built on top of &lt;a href="https://en.wikipedia.org/wiki/Firefox_OS"&gt;Firefox OS&lt;/a&gt; (aka Boot2Gecko or B2G) and are &lt;a href="https://github.com/kaiostech/gecko-b2g"&gt;open source&lt;/a&gt;. This informed many of my articles on &lt;a href="https://kaios.dev"&gt;KaiOS.dev&lt;/a&gt;, since I often needed to &lt;code&gt;grep&lt;/code&gt; the codebase to uncover implementation details or edge cases.&lt;/p&gt;
&lt;p&gt;Many KaiOS APIs are written in &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/WebIDL"&gt;WebIDL&lt;/a&gt; (Web Interface Description Language), which is very similar to TypeScript type declarations (&lt;code&gt;.d.ts&lt;/code&gt; files).&lt;/p&gt;
&lt;p&gt;Consider &lt;a href="https://github.com/kaiostech/gecko-b2g/blob/b2g48_v2_6/dom/webidl/MozWakeLock.webidl"&gt;&lt;code&gt;MozWakeLock.webidl&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#a6e22e"&gt;Pref&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;dom.wakelock.enabled&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Func&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;Navigator::HasWakeLockSupport&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;interface&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;MozWakeLock&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;readonly&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;attribute&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;DOMString&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;topic&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;/**
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; * Release the wake lock.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; * @throw NS_ERROR_DOM_INVALID_STATE_ERR if already unlocked.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [&lt;span style="color:#a6e22e"&gt;Throws&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;unlock&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In TypeScript (see &lt;a href="https://github.com/Tombarr/kaios-types/blob/main/system/moz-wake-lock.d.ts"&gt;&lt;code&gt;moz-wake-lock.d.ts&lt;/code&gt;&lt;/a&gt;), the interface looks like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;/**
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; * @preference dom.wakelock.enabled
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; * Represents a lock that prevents the device from entering sleep mode.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;export&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;interface&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;MozWakeLock&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;readonly&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;topic&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;/**
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; * Releases the wake lock.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; * @throws
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;unlock&lt;/span&gt;()&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For clients like &lt;a href="https://barrasso.me/projects/vipaso/"&gt;Vipaso&lt;/a&gt; I&amp;rsquo;ve done this conversion by hand. It&amp;rsquo;s straightforward but tedious. So I decided to see if I could do this at scale using &lt;a href="https://opencode.ai/"&gt;OpenCode&lt;/a&gt;. From the onset, my approach looked like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prompt through a few examples and summarize conversion steps&lt;/li&gt;
&lt;li&gt;Define a purpose-limited &lt;a href="https://opencode.ai/docs/agents/"&gt;subagent&lt;/a&gt; for conversion&lt;/li&gt;
&lt;li&gt;Draft a set of quality &amp;amp; validation steps (i.e. files validates with &lt;code&gt;tsc&lt;/code&gt;, formatted using &lt;code&gt;prettier&lt;/code&gt;, etc)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I then had Gemini explore the codebase and decompose the process into a task list and begin conversion with the following agents:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project Manager&lt;/strong&gt;: oversaw the process and provided status reports with checklists of tasks pending and completed; handled retries when issues were uncovered&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Converter&lt;/strong&gt;: a narrowly-scoped subagent for converting WebIDL to TypeScript type declarations with clear instructions on converting types and handling annotations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Documenter&lt;/strong&gt;: a subagent to copy WebIDL comments and annotations into consistently-formatted &lt;a href="https://tsdoc.org/"&gt;TSDoc&lt;/a&gt; comments with tags like &lt;code&gt;@see&lt;/code&gt; for documentation links and &lt;code&gt;@throws&lt;/code&gt; for errors&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The entire process was remarkably smooth. In a few hours, I had dozens of type definitions spanning hundreds of interfaces. I then had the default &lt;code&gt;Build&lt;/code&gt; agent write a set of tests and a GitHub Action to deploy to NPM, and my new &lt;a href="https://github.com/Tombarr/kaios-types"&gt;&lt;code&gt;kaios-types&lt;/code&gt;&lt;/a&gt; library for KaiOS 2.5 was ready to use! I considered publishing on &lt;a href="https://github.com/DefinitelyTyped/DefinitelyTyped"&gt;DefinitelyTypes&lt;/a&gt;, which runs the &lt;code&gt;@types/&lt;/code&gt; NPM namespace, but figured as a first pass there would be many changes needed. Once it&amp;rsquo;s more complete (esp. including KaiOS 3.0+ APIs) and tested in the real-world, it may make sense to bring to DefinitelyTyped.&lt;/p&gt;
&lt;h3 id="aside-webidl2ts"&gt;
Aside: &lt;code&gt;webidl2ts&lt;/code&gt;
&lt;a class="heading-anchor" href="#aside-webidl2ts" aria-label="Link to Aside: webidl2ts"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;m aware that there are several libraries like &lt;a href="https://github.com/giniedp/webidl2ts"&gt;&lt;code&gt;webidl2ts&lt;/code&gt;&lt;/a&gt; that convert WebIDL to TypeScript like &lt;a href="https://www.npmjs.com/package/webidl2"&gt;&lt;code&gt;webidl2&lt;/code&gt;&lt;/a&gt;. In the past, I&amp;rsquo;ve tried this approach but ran into several issues.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Not all KaiOS APIs are defined in WebIDL files&lt;/li&gt;
&lt;li&gt;Some WebIDL files that don&amp;rsquo;t originate from Boot2Gecko are not open source&lt;/li&gt;
&lt;li&gt;Parsers operate file-by-file without an understanding of module resolution&lt;/li&gt;
&lt;li&gt;Parsers often fail for complex WebIDL files with inheritance or incomplete but similar types&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;KaiOS can be a mess. For example, asynchronous programming includes &lt;code&gt;DOMRequest&lt;/code&gt; callbacks (KaiOS 2.5 only), &lt;code&gt;Promises&lt;/code&gt; (partially supported on KaiOS 2.5 without the &lt;code&gt;finally&lt;/code&gt; method), and the &lt;code&gt;async&lt;/code&gt; keyword on KaiOS 3.0+. I probably could have taken a hybrid approach allowing an agent to make a tool call to &lt;code&gt;webidl2&lt;/code&gt;, but I didn&amp;rsquo;t find it necessary. Plus, it would have roughly doubled input token usage to send both a &lt;code&gt;.webidl&lt;/code&gt; and &lt;code&gt;.d.ts&lt;/code&gt; file in the context for each request. Perhaps a future attempt would use AI to write a custom parser using &lt;code&gt;webidl2&lt;/code&gt; tested specifically against Firefox and KaiOS.&lt;/p&gt;
&lt;h2 id="now-with-more-logic"&gt;
Now With More Logic
&lt;a class="heading-anchor" href="#now-with-more-logic" aria-label="Link to Now With More Logic"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Given how easy it was to create &lt;code&gt;kaios-types&lt;/code&gt;, I decided to replicate the success with a JS to TS conversion for PodLP. This time, I asked Claude to come up with a plan. Here is an excerpt:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-md" data-lang="md"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# TypeScript Migration Plan - Fast Track
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;## Overview
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Migrate JavaScript files to TypeScript using pragmatic typing with liberal &lt;span style="color:#e6db74"&gt;`any`&lt;/span&gt; usage for speed. Testing on both KaiOS 2.5 and 3.0 devices available.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;## Strategy
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; **Speed First**: Use &lt;span style="color:#e6db74"&gt;`any`&lt;/span&gt; liberally where KaiOS APIs lack complete type definitions
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; **Follow Patterns**: Match existing migrated files&amp;#39; typing approach
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; **Build Validation**: Verify TypeScript compiles and Babel transpiles correctly
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; **No Babel Changes**: Preserve existing transpilation configuration
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;## Migration Workflow Per File
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;1.&lt;/span&gt; &lt;span style="font-weight:bold"&gt;**Rename**&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;`mv file.js file.ts`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;2.&lt;/span&gt; &lt;span style="font-weight:bold"&gt;**Add Types**&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Function parameters: &lt;span style="color:#e6db74"&gt;`function foo(param: type): returnType`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Use &lt;span style="color:#e6db74"&gt;`any`&lt;/span&gt; for complex KaiOS APIs
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Use &lt;span style="color:#e6db74"&gt;`any`&lt;/span&gt; for Svelte stores unless trivial
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;3.&lt;/span&gt; &lt;span style="font-weight:bold"&gt;**Import Types**&lt;/span&gt;: Add &lt;span style="color:#e6db74"&gt;`import type`&lt;/span&gt; for type-only imports
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;4.&lt;/span&gt; &lt;span style="font-weight:bold"&gt;**Build**&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;`npm run build`&lt;/span&gt; (fixes Babel issues)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;5.&lt;/span&gt; &lt;span style="font-weight:bold"&gt;**Type Check**&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;`npx tsc --noEmit`&lt;/span&gt; (optional, for catching errors)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;6.&lt;/span&gt; &lt;span style="font-weight:bold"&gt;**Commit**&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;`git add file.ts &amp;amp;&amp;amp; git commit -m &amp;#34;Migrate file.js to TypeScript&amp;#34;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;## Validation Checklist
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;After completing all phases:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;- [ ]&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`npx tsc --noEmit`&lt;/span&gt; passes (zero TypeScript errors)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;- [ ]&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`npm run build`&lt;/span&gt; succeeds
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;- [ ]&lt;/span&gt; All &lt;span style="color:#e6db74"&gt;`.js`&lt;/span&gt; files in &lt;span style="color:#e6db74"&gt;`src/`&lt;/span&gt; migrated (except rollup config)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;- [ ]&lt;/span&gt; App loads without console errors
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;- [ ]&lt;/span&gt; Audio playback works
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;- [ ]&lt;/span&gt; Downloads work
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;- [ ]&lt;/span&gt; Settings persist
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;- [ ]&lt;/span&gt; Navigation works
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;- [ ]&lt;/span&gt; Test on KaiOS 2.5 device
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;- [ ]&lt;/span&gt; Test on KaiOS 3.0 device
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;## Estimated Timeline
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Phase 1: 30 min (type infrastructure)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Phase 2: 1 hour (foundation)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Phase 3: 1.5 hours (core infrastructure)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Phase 4: 1 hour (KaiOS shims)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Phase 5: 2 hours (audio system)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Phase 6: 2.5 hours (downloads)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Phase 7: 1.5 hours (push notifications)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Phase 8: 45 min (transcripts)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Phase 9: 1 hour (workers &amp;amp; entry)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; **Total: ~12 hours** (can be split across multiple sessions)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;## Risk Mitigation
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; **Babel Issues**: If Babel fails, check for &lt;span style="color:#e6db74"&gt;`const enum`&lt;/span&gt; or &lt;span style="color:#e6db74"&gt;`namespace`&lt;/span&gt; usage (not supported)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; **Circular Dependencies**: Use &lt;span style="color:#e6db74"&gt;`import type`&lt;/span&gt; to break cycles
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; **Runtime Errors**: Keep all existing runtime type checks (don&amp;#39;t rely on TypeScript)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; **Type Errors**: When stuck, use &lt;span style="color:#e6db74"&gt;`any`&lt;/span&gt; and add &lt;span style="color:#e6db74"&gt;`// TODO: improve typing`&lt;/span&gt; comment
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; **Build Size**: Verify bundle sizes don&amp;#39;t increase significantly
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;## Success Criteria
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Migration complete when:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;1.&lt;/span&gt; Zero &lt;span style="color:#e6db74"&gt;`.js`&lt;/span&gt; files remain in &lt;span style="color:#e6db74"&gt;`src/`&lt;/span&gt; (excluding config)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;2.&lt;/span&gt; TypeScript compilation succeeds
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;3.&lt;/span&gt; Production build works
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;4.&lt;/span&gt; All features tested on both KaiOS versions
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;At first glance, the plan seemed fine although I would prefer to not &amp;ldquo;use &lt;code&gt;any&lt;/code&gt; liberally.&amp;rdquo; For small files with few dependencies, this was sufficient. However, I exhausted my Claude Pro daily usage &lt;em&gt;very quickly&lt;/em&gt; and found it got stuck far too often. Claude reinvented the wheel (didn&amp;rsquo;t use &lt;code&gt;kaios-types&lt;/code&gt;) and was &lt;strong&gt;too liberal&lt;/strong&gt; with type assertions to &lt;code&gt;any&lt;/code&gt;. For better results, I needed to be more specific about the changes I expected, more methodical in iterating across files, and narrower in scope with each prompt.&lt;/p&gt;
&lt;p&gt;I noticed most failures came when converting highly-interconnected components (itself a code smell for refactor, but that&amp;rsquo;s a problem for another day). My approach was similar to the &lt;a href="https://martinfowler.com/bliki/StranglerFigApplication.html"&gt;Strangler Fig&lt;/a&gt;: don&amp;rsquo;t arbitrarily walk file-by-file, but instead focus on the &amp;ldquo;outermost&amp;rdquo; independent components with the least imports. As Gemini put it, &amp;ldquo;start with leaf utilities (e.g., &lt;code&gt;src/strings/&lt;/code&gt;, &lt;code&gt;src/utils/&lt;/code&gt;) and move up to components.&amp;rdquo; For PodLP, this meant the parts of the code that handle transcripts, translations, push notifications, and downloads.&lt;/p&gt;
&lt;p&gt;Moving back to Gemini, it ended up defining three agents:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Architect&lt;/strong&gt;: responsible for configuring the build pipeline (&lt;code&gt;tsconfig.json&lt;/code&gt;, &lt;code&gt;rollup.config.js&lt;/code&gt;, etc) to support TypeScript while maintaining KaiOS compatibility.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gardener&lt;/strong&gt;: responsible for managing this project and it&amp;rsquo;s configuration, dependencies, and code formatting standards&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Migrator&lt;/strong&gt;: responsible for converting the codebase from JavaScript to TypeScript and porting application to ensure type safety&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&amp;rsquo;s an exercept of the revamped migration plan:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-md" data-lang="md"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# Upgrade Plan: Modernization &amp;amp; TypeScript Migration
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;## Overview
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;This document outlines the strategy to modernize the PodLP codebase by migrating from JavaScript to TypeScript. The goal is to improve developer experience and code safety while maintaining compatibility with KaiOS (Firefox 48 target).
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;## Phase 1: TypeScript Infrastructure
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="font-weight:bold"&gt;**Goal:**&lt;/span&gt; Enable TypeScript support in the build pipeline while &lt;span style="font-weight:bold"&gt;**keeping Babel**&lt;/span&gt; for critical KaiOS down-leveling.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;1.&lt;/span&gt; &lt;span style="font-weight:bold"&gt;**Install Dependencies:**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`typescript`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`tslib`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`svelte-preprocess`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`@tsconfig/svelte`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`svelte-check`&lt;/span&gt; (for type checking)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`@rollup/plugin-typescript`&lt;/span&gt; (to handle .ts files in Rollup)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`kaios-types`&lt;/span&gt; (already installed or ensure it is added)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;2.&lt;/span&gt; &lt;span style="font-weight:bold"&gt;**Configuration:**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Create &lt;span style="color:#e6db74"&gt;`tsconfig.json`&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Extend &lt;span style="color:#e6db74"&gt;`@tsconfig/svelte/tsconfig.json`&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Set &lt;span style="color:#e6db74"&gt;`target`&lt;/span&gt; to &lt;span style="color:#e6db74"&gt;`ESNext`&lt;/span&gt; (let Babel handle the heavy lifting to ES5/ES6 for Firefox 48).
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Include &lt;span style="color:#e6db74"&gt;`kaios-types`&lt;/span&gt; in &lt;span style="color:#e6db74"&gt;`compilerOptions.types`&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Ensure strict mode is enabled (can be loose initially).
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Create &lt;span style="color:#e6db74"&gt;`svelte.config.js`&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Configure &lt;span style="color:#e6db74"&gt;`svelte-preprocess`&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;3.&lt;/span&gt; &lt;span style="font-weight:bold"&gt;**Update Build System (`rollup.config.js`):**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; **CRITICAL:** Do NOT remove Babel. The pipeline should be TS -&amp;gt; Babel -&amp;gt; Output.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Import and register &lt;span style="color:#e6db74"&gt;`svelte-preprocess`&lt;/span&gt; in the &lt;span style="color:#e6db74"&gt;`svelte`&lt;/span&gt; plugin options.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Add &lt;span style="color:#e6db74"&gt;`@rollup/plugin-typescript`&lt;/span&gt; to the plugin list &lt;span style="font-style:italic"&gt;_before_&lt;/span&gt; Babel.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Ensure source maps are configured correctly for debugging.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;## Phase 2: Migration Execution
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="font-weight:bold"&gt;**Goal:**&lt;/span&gt; Convert codebase to TypeScript.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;1.&lt;/span&gt; &lt;span style="font-weight:bold"&gt;**Iterative Conversion:**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Rename &lt;span style="color:#e6db74"&gt;`.js`&lt;/span&gt; files to &lt;span style="color:#e6db74"&gt;`.ts`&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Update &lt;span style="color:#e6db74"&gt;`.svelte`&lt;/span&gt; files: &lt;span style="color:#e6db74"&gt;`&amp;lt;script&amp;gt;`&lt;/span&gt; -&amp;gt; &lt;span style="color:#e6db74"&gt;`&amp;lt;script lang=&amp;#34;ts&amp;#34;&amp;gt;`&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Fix immediate type errors.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Use &lt;span style="color:#e6db74"&gt;`kaios-types`&lt;/span&gt; for &lt;span style="color:#e6db74"&gt;`navigator.mozApps`&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;`navigator.volumeManager`&lt;/span&gt;, etc.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Start with leaf utilities (e.g., &lt;span style="color:#e6db74"&gt;`src/strings/`&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;`src/utils/`&lt;/span&gt;) and move up to components.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;2.&lt;/span&gt; &lt;span style="font-weight:bold"&gt;**Type Safety:**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Run &lt;span style="color:#e6db74"&gt;`npm run check`&lt;/span&gt; (using &lt;span style="color:#e6db74"&gt;`svelte-check`&lt;/span&gt;) to identify issues.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Define interfaces for API responses and core data structures (Podcast, Episode).
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Replace &lt;span style="color:#e6db74"&gt;`any`&lt;/span&gt; with specific types where possible.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;3.&lt;/span&gt; &lt;span style="font-weight:bold"&gt;**Verification:**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Build the app (&lt;span style="color:#e6db74"&gt;`npm run build`&lt;/span&gt;).
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;-&lt;/span&gt; Verify the app runs in the KaiOS simulator or device (manual verification required by user).
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;## Phase 3: Cleanup &amp;amp; Documentation
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;1.&lt;/span&gt; Update &lt;span style="color:#e6db74"&gt;`README.md`&lt;/span&gt; with new build instructions and TS guidelines.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;2.&lt;/span&gt; Verify &lt;span style="color:#e6db74"&gt;`package.json`&lt;/span&gt; scripts.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This process continued for several hours split across two days driven by rate limits and resource quotas. I began with Gemini 3 Pro to draft and vet the plan, then to Gemini 3 Flash and 2.5 Flash for the file-by-file conversion. Only a few highly-interconnected files caused issues, and even these were able to be converted with some manual intervention.&lt;/p&gt;
&lt;h2 id="testing"&gt;
Testing
&lt;a class="heading-anchor" href="#testing" aria-label="Link to Testing"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;When &lt;code&gt;npm run build&lt;/code&gt; worked, I expected disaster: a bunch of mangled transpiled JavaScript that was not KaiOS compatible, littered with runtime errors, that did not resembled my app. What I got was a TypeScript codebase with just a few errors that were easy to fix.&lt;/p&gt;
&lt;p&gt;Ironically, the very first issue I found after this TypeScript migration was a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError"&gt;&lt;code&gt;TypeError&lt;/code&gt;&lt;/a&gt;!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;onAppError&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;TypeError&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;_ref11&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;detail&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;is&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;undefined&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Stack&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;trace&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;onOptionClick&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;@&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;&lt;span style="color:#75715e"&gt;//podlp.com/build/podlp.js:40504:10
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;handleOptionClickEvent&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;@&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;&lt;span style="color:#75715e"&gt;//podlp.com/build/podlp.js:60395:25
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;createEventDispatcher&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;podlp&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&amp;lt;&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;@&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;&lt;span style="color:#75715e"&gt;//podlp.com/build/podlp.js:24887:10
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;createEventDispatcher&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;podlp&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;@&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;&lt;span style="color:#75715e"&gt;//podlp.com/build/podlp.js:24886:8
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;onOptionClick&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;@&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;&lt;span style="color:#75715e"&gt;//podlp.com/build/podlp.js:30579:10
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;instance$G&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;click_handler_1&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;@&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;&lt;span style="color:#75715e"&gt;//podlp.com/build/podlp.js:30623:41
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;click_handler_1&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;@&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;&lt;span style="color:#75715e"&gt;//podlp.com/build/podlp.js:30128:59
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;prevent_default&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;podlp&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;@&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;&lt;span style="color:#75715e"&gt;//podlp.com/build/podlp.js:24422:13
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;During the conversion process, liberal use of &lt;code&gt;any&lt;/code&gt; or generic types like &lt;code&gt;Event&lt;/code&gt; resulted in some event handlers being called with trusted user-generated events (lacking the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail"&gt;&lt;code&gt;detail&lt;/code&gt;&lt;/a&gt; property). I fixed a few function signatures to use more specific types like &lt;code&gt;CustomEvent&amp;lt;{ action: string }&amp;gt;&lt;/code&gt;, changed the caller, and that was that.&lt;/p&gt;
&lt;p&gt;The second issue was with a single instance where the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax"&gt;&lt;code&gt;...&lt;/code&gt; spread operator&lt;/a&gt; was dropped, causing Svelte&amp;rsquo;s reactivity statement to not trigger and a component to not render. This was incredibly easy to spot since it caused the entire page to be blank. One more fix, and it was resolved.&lt;/p&gt;
&lt;p&gt;The only other issues I ran into were duplication of KaiOS API types (redefining APIs already present in &lt;code&gt;kaios-types&lt;/code&gt;). Given how little training data is available for KaiOS, it&amp;rsquo;s likely the model wasn&amp;rsquo;t able to &lt;code&gt;grep&lt;/code&gt; for the correct API and choose instead to redefine it. I addressed most of this with a single prompt listing where to find each duplicated type definition. In the end, I even spotted a few APIs &lt;em&gt;missing&lt;/em&gt; from &lt;code&gt;kaios-types&lt;/code&gt;!&lt;/p&gt;
&lt;h2 id="results"&gt;
Results
&lt;a class="heading-anchor" href="#results" aria-label="Link to Results"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-22-large-scale-typescript-migration-in-opencode/mt-watatic-summit_hu_4c13471f98581422.webp 320w, https://barrasso.me/posts/2025-12-22-large-scale-typescript-migration-in-opencode/mt-watatic-summit_hu_ac02b411008ca7a3.webp 640w, https://barrasso.me/posts/2025-12-22-large-scale-typescript-migration-in-opencode/mt-watatic-summit_hu_a3d060aa5ae23f47.webp 1024w, https://barrasso.me/posts/2025-12-22-large-scale-typescript-migration-in-opencode/mt-watatic-summit_hu_61b39708c2e7b940.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-22-large-scale-typescript-migration-in-opencode/mt-watatic-summit_hu_663a3e85d9d7cb69.jpeg 320w, https://barrasso.me/posts/2025-12-22-large-scale-typescript-migration-in-opencode/mt-watatic-summit_hu_628cd0ade0d9cf98.jpeg 640w, https://barrasso.me/posts/2025-12-22-large-scale-typescript-migration-in-opencode/mt-watatic-summit_hu_b2c8ae9575d70946.jpeg 1024w, https://barrasso.me/posts/2025-12-22-large-scale-typescript-migration-in-opencode/mt-watatic-summit_hu_cc38ed303aa3f66a.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-22-large-scale-typescript-migration-in-opencode/mt-watatic-summit_hu_663a3e85d9d7cb69.jpeg"
alt="Summit sign for Mt. Watatic" class="w-full no-border contain"
width="320"
height="134"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Sign at the summit of Mt. Watatic&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;After spending $43 (in Gemini credits) and an afternoon, PodLP is now fully written in TypeScript. Build stats show that the main artifact grew marginally, from 842.76 kB to 852.59 kB uncompressed (~1.2% increase). This definitely answered the question, &amp;ldquo;can AI agents migrate a non-trivial codebase from JavaScript to TypeScript?&amp;rdquo; However, I&amp;rsquo;m still reevaluating whether it &lt;strong&gt;should&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://world.hey.com/dhh/turbo-8-is-dropping-typescript-70165c01"&gt;Unlike DHH&lt;/a&gt;, I still see great value in TypeScript primarily in long-term maintainability and productivity. Yes, it takes more type to write type definitions (or have AI generate them from an existing codebase). Examples like &lt;a href="https://github.com/codemix/ts-sql"&gt;&lt;code&gt;ts-sql&lt;/code&gt;&lt;/a&gt; show just how complex TypeScript types can get.&lt;/p&gt;
&lt;p&gt;For PodLP, I see the primary value being able to iterate more quickly on new features, and to have AI implement new features with greater confidence. However, TypeScript&amp;rsquo;s value is somewhat stunted by the fact PodLP runs on a variety of KaiOS devices spanning 3 major OS versions (KaiOS 2.5, 3.0, and 4.0).&lt;/p&gt;
&lt;p&gt;There are no KaiOS virtual machines to test against, and the &lt;a href="https://developer.kaiostech.com/docs/getting-started/env-setup/simulator/"&gt;KaiOS Simulator&lt;/a&gt; is virtually useless since it lacks support for many features and has substantially different configurations than commercial devices. Base Gecko versions like Firefox 48 aren&amp;rsquo;t good proxies either, since they have no &lt;a href="https://developer.kaiostech.com/docs/getting-started/main-concepts/permissions/"&gt;permission model&lt;/a&gt;, don&amp;rsquo;t ship with system APIs, and lack newer features like &lt;code&gt;display: grid&lt;/code&gt; cherry-picked from Firefox 52 into the corresponding KaiOS 2.5 version.&lt;/p&gt;
&lt;p&gt;TypeScript won&amp;rsquo;t solve OS fragmentation or reliance on undocumented or under-documented proprietary APIs. But for me this was a worthwhile exercise in leveraging AI to modernize a substantial, real-world codebase that helps me maintain and improve &lt;a href="https://podlp.com"&gt;PodLP&lt;/a&gt; for years to come.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2025-12-22-large-scale-typescript-migration-in-opencode/</guid><pubDate>Mon, 22 Dec 2025 00:00:00 GMT</pubDate></item><item><title>
Reflections on a year abroad</title><link>https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/</link><description>
&lt;p&gt;For a year I experienced &lt;strong&gt;truly &amp;ldquo;remote&amp;rdquo; work.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;From Summer 2024 to Fall 2025, I spent a year traveling across Peru, Australia, Vietnam, Thailand, India, the United Kingdom, the Netherlands, Switzerland, Italy, Romania, Turkey, Cyprus, and &lt;em&gt;almost&lt;/em&gt; half a year living in a van in New Zealand.&lt;/p&gt;
&lt;p&gt;During this time, I worked remotely at &lt;a href="https://puffin.com"&gt;CloudMosa&lt;/a&gt; as the Head of Developer Relations for &lt;a href="https://www.cloudphone.tech"&gt;Cloud Phone&lt;/a&gt;. Although my interest in flip phones began in 2020 when I launched &lt;a href="https://barrasso.me/projects/podlp/"&gt;PodLP&lt;/a&gt;, this was quite the departure from the past 5+ years in Big Tech living in Seattle. This writing is part of the long journey to unpack and make meaning from such a profound experience.&lt;/p&gt;
&lt;h2 id="aotearoa-new-zealand-"&gt;
Aotearoa New Zealand 🇳🇿
&lt;a class="heading-anchor" href="#aotearoa-new-zealand-" aria-label="Link to Aotearoa New Zealand 🇳🇿"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Isolation, postcard-quality sunsets, striking mountains, flightless birds, solid 4G signal, and the kindest people I&amp;rsquo;ve ever met&lt;/em&gt;&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/tom-van-life_hu_3dad8385aa994cbf.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/tom-van-life_hu_43221712727b1a55.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/tom-van-life_hu_1370f7b7accd0f59.webp 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/tom-van-life_hu_947a09a2f3d0ff4b.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/tom-van-life_hu_8ed5fbc71f3066c3.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/tom-van-life_hu_69d59962bd40204b.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/tom-van-life_hu_6b26753b60f0cab8.jpeg 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/tom-van-life_hu_c58a9b10a57b6f03.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/tom-van-life_hu_8ed5fbc71f3066c3.jpeg"
alt="Tom in a 2000 Toyota Hiace" class="w-full no-border contain"
width="320"
height="266"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Tom in Hugo, a 2000 Toyota Hiace&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;I&amp;rsquo;ve worked with &lt;a href="https://gohugo.io/"&gt;Hugo&lt;/a&gt; (the static site generator) for years. This blog is built with Hugo, and so is &lt;a href="https://barrasso.me/projects/cloud-phone/"&gt;Cloud Phone for Developers&lt;/a&gt;. Coincidentally, our van also came with the name &amp;ldquo;Hugo!&amp;rdquo; Fortunately, Hugo survived another 10,000km around the North &amp;amp; South Islands, and thanks to an &lt;a href="https://amzn.to/3MygIHE"&gt;Asus 4G-AX56&lt;/a&gt; (powered by rooftop solar), I had WiFi wherever I went using the &lt;a href="https://www.spark.co.nz/"&gt;Spark&lt;/a&gt; network.&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/arthurs-pass-bridge_hu_4e661550ae8b42a8.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/arthurs-pass-bridge_hu_2df3b21e9e365feb.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/arthurs-pass-bridge_hu_80c6214cf9692a80.webp 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/arthurs-pass-bridge_hu_59f6c12dab849545.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/arthurs-pass-bridge_hu_91b441207ab5ba40.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/arthurs-pass-bridge_hu_d690040ee97854b6.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/arthurs-pass-bridge_hu_d9b66e0dba4a6d8f.jpeg 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/arthurs-pass-bridge_hu_dd4384ce1df8de4d.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/arthurs-pass-bridge_hu_91b441207ab5ba40.jpeg"
alt="Arthur&amp;#39;s Pass, South Island" class="w-full no-border contain"
width="320"
height="188"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Arthur&amp;rsquo;s Pass, South Island&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;A 12-hour flight from San Francisco: New Zealand has no dangerous animals, no land borders, and it&amp;rsquo;s often &lt;a href="https://www.reddit.com/r/MapsWithoutNZ/"&gt;omitted from maps&lt;/a&gt;. I&amp;rsquo;ve been to dark-sky preserves, but New Zealand is on another level. This is just a several second exposure on an &lt;a href="https://amzn.to/4oTQL3k"&gt;iPhone 13 Mini&lt;/a&gt;.&lt;/p&gt;
&lt;figure class="full-bleed"&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/southern-sky_hu_f71917ef555628f9.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/southern-sky_hu_29921509ef33262e.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/southern-sky_hu_1050a58929ba326b.webp 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/southern-sky_hu_3d3aa910718175c1.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/southern-sky_hu_d265e7b374a167d8.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/southern-sky_hu_cdffa6ae3c94a43.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/southern-sky_hu_f4a2f9ec6b6fe41f.jpeg 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/southern-sky_hu_5a6f20ed6c7a95b6.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/southern-sky_hu_d265e7b374a167d8.jpeg"
alt="Night sky in the Southern Hemisphere" class="w-full no-border contain"
width="320"
height="189"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;~3 second exposure of the night sky in the Southern Hemisphere&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;Run by the Department of Conservation (DOC), New Zealand&amp;rsquo;s &lt;a href="https://www.doc.govt.nz/parks-and-recreation/things-to-do/walking-and-tramping/great-walks/"&gt;Great Walks&lt;/a&gt; were one of the initial draws to visit the islands. Of the 11 Great Walks, I completed 4 and hiked partway across another 5. In between tramping trips I dropped by the &lt;a href="https://www.meetup.com/nelson-dev-group/"&gt;Nelson Dev Group&lt;/a&gt; and gave a talk at &lt;a href="https://www.linkedin.com/posts/juan-luis-ramirez_rubynights-aucklandtech-activity-7320402927104274432-LQs0"&gt;Ruby Nights Auckland&lt;/a&gt;. I launched the &lt;a href="https://developer.cloudphone.tech/"&gt;Cloud Phone for Developers&lt;/a&gt; website, was a guest on the &lt;a href="https://www.youtube.com/watch?v=XFXJpbJMFr4"&gt;WasmAssembly Podcast&lt;/a&gt;, and helped launch the MyGP app for &lt;a href="https://www.grameenphone.com/"&gt;Grameenphone&lt;/a&gt;, Bangladesh&amp;rsquo;s largest telecommunication company.&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kiwi-crossing_hu_f8a9e14cdab00445.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kiwi-crossing_hu_ad8611fb2cc481c9.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kiwi-crossing_hu_b7a49d3ab6080ceb.webp 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kiwi-crossing_hu_6652f6694012e48c.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kiwi-crossing_hu_92bee2132e045617.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kiwi-crossing_hu_79e31a2992bdda29.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kiwi-crossing_hu_ef5838558c000353.jpeg 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kiwi-crossing_hu_97d68148241b8c7a.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kiwi-crossing_hu_92bee2132e045617.jpeg"
alt="Kiwi crossing sign in Rakiura (Stewart Island)" class="w-full no-border contain"
width="320"
height="175"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Kiwi crossing sign in Rakiura (Stewart Island)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;No doubt, New Zealand is weird. It&amp;rsquo;s home to hobbits, alpine parrots, spherical &lt;a href="https://en.wikipedia.org/wiki/Moeraki_Boulders"&gt;concretions&lt;/a&gt;, and &lt;a href="https://www.steampunkoamaru.co.nz/"&gt;Steampunk HQ&lt;/a&gt;. I saw the world&amp;rsquo;s smallest penguins and walked up the world&amp;rsquo;s &lt;a href="https://www.treehugger.com/worlds-steepest-streets-4869323#list-sc__content_1-0"&gt;steepest street&lt;/a&gt;. I tried &lt;a href="https://en.wikipedia.org/wiki/Amazake"&gt;Amazake&lt;/a&gt;, witnessed &lt;a href="https://www.newzealand.com/us/glow-worms/"&gt;glowworms&lt;/a&gt;, got into &lt;a href="https://f45training.com/"&gt;F45&lt;/a&gt;, and picked up a few Māori phrases.&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kea-alpine-parrot_hu_92a93d0b0ed30b61.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kea-alpine-parrot_hu_3965c35a05a836d9.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kea-alpine-parrot_hu_d089d7c4c13e11c2.webp 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kea-alpine-parrot_hu_438830c93ff550f2.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kea-alpine-parrot_hu_29df44bfb41698f1.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kea-alpine-parrot_hu_cb1cedfb8276c0cf.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kea-alpine-parrot_hu_6bf14efebd258d63.jpeg 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kea-alpine-parrot_hu_e1faf7b61c4fb536.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kea-alpine-parrot_hu_29df44bfb41698f1.jpeg"
alt="Kea parrot on the Milford Track" class="w-full no-border contain"
width="320"
height="162"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Kea, an endangered alpine parrot, on the Milford Track&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;I drove on the left, lived in other people&amp;rsquo;s homes (house sitting), and had my first run-in with rites of passage where this poems came to life.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The Breeze At Dawn&lt;/strong&gt;&lt;br /&gt;
&lt;br /&gt;
The breeze at dawn has secrets to tell you.&lt;br /&gt;
Don&amp;rsquo;t go back to sleep!&lt;br /&gt;
&lt;br /&gt;
You must ask for what you really want.&lt;br /&gt;
Don&amp;rsquo;t go back to sleep!&lt;br /&gt;
&lt;br /&gt;
People are going back and forth&lt;br /&gt;
across the threshold&lt;br /&gt;
where the two worlds meet.&lt;br /&gt;
&lt;br /&gt;
The door is round and open.&lt;br /&gt;
Don&amp;rsquo;t go back to sleep!&lt;br /&gt;
&lt;br /&gt;
&lt;a href="https://onbeing.org/poetry/a-great-wagon/"&gt;A Great Wagon&lt;/a&gt;, by Rumi&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This was definitely a departure from riding the corporate shuttle, responding to hundreds of &lt;a href="https://www.workplace.com/"&gt;Workplace&lt;/a&gt; messages, and providing status updates at the semi-annual All Hands meeting. In many ways, I was now confronted with the &lt;a href="https://thedecisionlab.com/reference-guide/economics/the-paradox-of-choice"&gt;paradox of choice&lt;/a&gt;.&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/castle-hill_hu_c69233dd5480222e.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/castle-hill_hu_25dc6fcc42e1d533.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/castle-hill_hu_e1dc42f840fdafaf.webp 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/castle-hill_hu_83924ade52555eee.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/castle-hill_hu_44060ccb2f9b01b3.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/castle-hill_hu_753f97de994496e8.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/castle-hill_hu_f9a4320f2fcd065b.jpeg 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/castle-hill_hu_e9a7b067d9117ddd.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/castle-hill_hu_44060ccb2f9b01b3.jpeg"
alt="Castle Hill in the South Island, New Zealand" class="w-full no-border contain"
width="320"
height="213"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Kura Tāwhiti (Castle Hill) in the South Island, New Zealand&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;Maya Angelou wrote that, &amp;ldquo;people will never forget how you made them feel.&amp;rdquo; I will never forget the feeling of being as far as humanly possible from the &lt;a href="https://hbr.org/2023/03/beware-a-culture-of-busyness"&gt;culture of busyness&lt;/a&gt;. The feeling when ducks and &lt;a href="https://www.doc.govt.nz/nature/native-animals/birds/birds-a-z/weka/"&gt;weka&lt;/a&gt; were my only coworkers. The feeling when projects were punctuated by a swim in a brisk alpine lake.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s more than just the question: &amp;ldquo;live to work or work to live?&amp;rdquo; The balance between under and overworked isn&amp;rsquo;t a fixed point. In Safi Bahcall&amp;rsquo;s book &lt;em&gt;&lt;a href="https://amzn.to/4a2EYLV"&gt;Loonshots&lt;/a&gt;&lt;/em&gt;, he attempts to quantify the conditions under which return-on-politics exceeds return-on-work. At larger companies, I&amp;rsquo;ve observed the primary motivation was compensation, and the primary form of communication was meetings. Being truly &amp;ldquo;remote&amp;rdquo; at CloudMosa, meetings were rare and the driving force I saw was &lt;strong&gt;the satisfaction of seeing your work help real people.&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;OMGOSH !&lt;br /&gt;
OMGOSH !&lt;br /&gt;
OMGOSH !&lt;br /&gt;
THANK YOU FOR THE LINK TO PATRICK MADRID!&lt;br /&gt;
YOU ARE AMAZING, TOTALLY MADE MY LIFE (ESP BC I AM HOMELESS RIGHT NOW) SO MUCH MORE OF A HAPPY PLACE TO BE. . .&lt;br /&gt;
I HOPE SOMEONE DOES THE SAME FOR YOU,IN WHICHEVER WAY YOU WISH. . . .&lt;br /&gt;
THANK YOU FOR YOUR TIME&lt;br /&gt;
P.S. CANT WAIT TO GET TO DOWNLOAD THEM IN 2 WEEKS, MUUUWAH.&lt;br /&gt;
&lt;br /&gt;
– Anonymous &lt;a href="https://barrasso.me/projects/podlp/"&gt;PodLP&lt;/a&gt; User&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This was an email a user sent me. Five years ago, I built PodLP because it was something I wanted myself. I continued building it for the same reason that CloudMosa builds Cloud Phone. PodLP turned into an (admittedly small) business that continues to this day.&lt;/p&gt;
&lt;p&gt;Although sponsorship revenue keeps PodLP profitable, no doubt I won&amp;rsquo;t look back and smile at every invoice. But &lt;strong&gt;I&amp;rsquo;ll never forget unsolicited feedback like this.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="the-next-chapter-rest-of-world"&gt;
The next chapter: rest of world
&lt;a class="heading-anchor" href="#the-next-chapter-rest-of-world" aria-label="Link to The next chapter: rest of world"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Rest of world (ROW) is a pejorative commonly used at technology companies to mean everywhere outside of the US and Europe. I spent several weeks in each country, experiencing them more deeply than a typical American vacation but not to the same extent as New Zealand. Each place shaped my understanding of the world in ways I hadn&amp;rsquo;t expected.&lt;/p&gt;
&lt;h2 id="australia-"&gt;
Australia 🇦🇺
&lt;a class="heading-anchor" href="#australia-" aria-label="Link to Australia 🇦🇺"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Marsupials, meetups, speed cameras, and a sausage on election day&lt;/em&gt;&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kangaroo_hu_99f88f6374170d5.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kangaroo_hu_ffe60a97324d31b4.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kangaroo_hu_f800d21d71476aa5.webp 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kangaroo_hu_7ce674c0fddd2b7b.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kangaroo_hu_5fd18cadc596e5b2.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kangaroo_hu_f9c4187146fb8ccc.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kangaroo_hu_f8a0ce83215796ac.jpeg 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kangaroo_hu_28315bbcc6a37b86.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kangaroo_hu_5fd18cadc596e5b2.jpeg"
alt="Kangaroo near Brisbane, Australia" class="w-full no-border contain"
width="320"
height="190"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Kangaroo near Brisbane, Australia&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;Growing up, I loved &lt;a href="https://en.wikipedia.org/wiki/Zoboomafoo"&gt;Zoboomafoo&lt;/a&gt; so naturally I was excited to visit the country full of strange animals. Yet it was &lt;em&gt;I&lt;/em&gt; who felt watched by the &lt;a href="https://www.reddit.com/r/CarsAustralia/comments/14npkny/is_speed_enforcement_in_australia_absurd_by/"&gt;insane speed enforcement&lt;/a&gt; cameras. After spending 6 months in small towns, it was jarring to return to large cities, many more populous than the entire country of New Zealand.&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/sydney-harbor_hu_5934b9f819d1d0e5.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/sydney-harbor_hu_7c3fa4bad10d2e97.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/sydney-harbor_hu_548f4726b458d60a.webp 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/sydney-harbor_hu_644438adc52c005a.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/sydney-harbor_hu_1b17aaffebcbba4.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/sydney-harbor_hu_30d4c94d8a4cf123.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/sydney-harbor_hu_c01c8e7f0ef086d9.jpeg 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/sydney-harbor_hu_5bd41d2d1803d4f2.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/sydney-harbor_hu_1b17aaffebcbba4.jpeg"
alt="The Opera House in Sydney, Australia" class="w-full no-border contain"
width="320"
height="142"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;The Opera House in Sydney, Australia&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;I&amp;rsquo;m grateful to &lt;a href="https://www.meetup.com/vue-sydney/events/306581679/"&gt;Frontenders Sydney&lt;/a&gt; for offering me the opportunity to present to 100+ developers, and for our hosts in Brisbane who prepared a classic Aussie Barbie (and so much more). Plus Australia is where I discovered &lt;a href="https://open.spotify.com/artist/1a6a0hVxotITvRTjVXaT9n"&gt;Jack Davies and The Bush Chooks&lt;/a&gt; at a last minute concert.&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/frontenders-sydney_hu_24d04ba81bdb7332.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/frontenders-sydney_hu_74c916a9eafb7831.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/frontenders-sydney_hu_19a942cba8857470.webp 1024w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/frontenders-sydney_hu_50151612f3cf232d.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/frontenders-sydney_hu_59d9863581095d86.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/frontenders-sydney_hu_57931a76e0db0e4e.jpeg 1024w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/frontenders-sydney_hu_50151612f3cf232d.jpeg"
alt="Tom giving a talk at Frontenders Sydney in May 2025" class="w-full no-border contain"
width="320"
height="136"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Tom giving a talk at &lt;a href="https://www.linkedin.com/posts/frontenders-au_frontendersau-frontendcommunity-devevents-activity-7325651785300201472-BHQQ"&gt;Frontenders Sydney&lt;/a&gt; in May 2025&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;I&amp;rsquo;d love to see America adopt the &lt;a href="https://en.wikipedia.org/wiki/Democracy_sausage"&gt;democracy sausage&lt;/a&gt;, and aspire to the return of politics summed up as &lt;a href="https://www.bbc.com/news/videos/cdxgw9x44x8o"&gt;&amp;ldquo;boring&amp;rdquo; and &amp;ldquo;weird&amp;rdquo;&lt;/a&gt;. Yet I&amp;rsquo;m not called to return to Australia. Maybe it&amp;rsquo;s the lack of prominent mountains, or abundance of deadly creatures. Maybe it&amp;rsquo;s because I don&amp;rsquo;t surf, or because I&amp;rsquo;m not interested in their &lt;a href="https://www.news.com.au/lifestyle/food/drink/western-sydney-cafe-offers-1500-cup-of-coffee-that-has-brought-people-to-tears/news-story/f8bb288402e3bae2fe75f8e46c83ff0e"&gt;$1500 coffee&lt;/a&gt;. Or perhaps it felt a bit too much like the parts of home that I&amp;rsquo;d rather avoid.&lt;/p&gt;
&lt;h2 id="vietnam-"&gt;
Vietnam 🇻🇳
&lt;a class="heading-anchor" href="#vietnam-" aria-label="Link to Vietnam 🇻🇳"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Gastronomic excellence and highly caffeinated hustle culture&lt;/em&gt;&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-rice_hu_f4868e16624be28c.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-rice_hu_2216b51e1ce8f47e.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-rice_hu_652b50749e4d3791.webp 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-rice_hu_9fae586d38b22cf0.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-rice_hu_d7be0e1a9de2961f.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-rice_hu_a3ec171744ce927e.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-rice_hu_81b72f117a529aca.jpeg 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-rice_hu_9db2a2e932fe4934.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-rice_hu_d7be0e1a9de2961f.jpeg"
alt="Rice terraces in Pù Luông, Vietnam" class="w-full no-border contain"
width="320"
height="183"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Rice terraces in Pù Luông, Vietnam&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;Vietnam is &lt;a href="https://asiatimes.com/2025/12/vietnams-8-growth-making-a-mockery-of-trumps-tariffs/"&gt;growing fast&lt;/a&gt;, and it&amp;rsquo;s abundantly clear why. It&amp;rsquo;s a place of hardcore hustle culture: operating an AirBnB, restaurant, nail salon, farming durian, brewing kombucha, and driving for Grab. Although the small demographic I encountered was biased towards tourism, it seems as if everyone run a business and has 3 jobs. After all, this is a place where &lt;a href="https://www.youtube.com/watch?v=EmDntqkisi8"&gt;you can&amp;rsquo;t put too much on a moped&lt;/a&gt;.&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-flip-phones_hu_59742784b338aa0c.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-flip-phones_hu_c6b04acdd51f7354.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-flip-phones_hu_7ca38c90de23e7d8.webp 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-flip-phones_hu_d4c6377352895bee.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-flip-phones_hu_2fb90ffd5f17cb50.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-flip-phones_hu_957125b312475131.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-flip-phones_hu_bf10f0468841791.jpeg 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-flip-phones_hu_2382118d9bde55aa.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-flip-phones_hu_2fb90ffd5f17cb50.jpeg"
alt="Old Vietnamese poster featuring flip phones" class="w-full no-border contain"
width="320"
height="220"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Old Vietnamese poster featuring flip phones from a Cửa Hàng ĐTDĐ (mobile phone shop)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;Vietnam is where I bought my first Cloud Phone in person, a &lt;a href="https://developer.cloudfone.com/device/viettel_v_1_s/"&gt;Viettel V1S 4G&lt;/a&gt; for ₫390,000 (~$15 USD). I also saw real people using feature phones for practical purposes, not just as part of a &amp;ldquo;digital detox.&amp;rdquo; CloudMosa also &lt;a href="https://www.linkedin.com/posts/cloudmosa_cloudphone-cloudup-csr-activity-7385986268461064192-thI2/"&gt;donated 100 4G Cloud Phones&lt;/a&gt; to provide connectivity to residents in remote areas of Vinh Long province.&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-boats_hu_d5ae49ef6a5eb6bf.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-boats_hu_9dfbb6b496c3cc99.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-boats_hu_40fe5c6f0002409b.webp 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-boats_hu_82f7b354c4f17567.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-boats_hu_f1a8365f399344d6.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-boats_hu_cd0fcfbef07e1595.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-boats_hu_ace46037c9218380.jpeg 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-boats_hu_ae3ec66a3ad99b90.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/vietnam-boats_hu_f1a8365f399344d6.jpeg"
alt="Boats in Tràng An, a UNESCO World Heritage Site" class="w-full no-border contain"
width="320"
height="165"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Boats in Tràng An, a UNESCO World Heritage Site&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;If America runs on Dunkin, Vietnam runs on &lt;a href="https://www.highlandscoffee.com.vn/en/coffee.html"&gt;Highland&lt;/a&gt;. You can&amp;rsquo;t escape their strong, sugary drinks. Nor should you. Vietnamese coffee is delicious and its how everyone keeps going day and night.&lt;/p&gt;
&lt;p&gt;Outside of the cities, Vietnam&amp;rsquo;s karst landscapes are stunning. Limestone towers jut out of nowhere, and rice terraces sprawl as far as the eye can see. Yet the price of progress goes beyond caffeine addiction. Like anywhere growing quickly, smog (especially in Hanoi) and serious litter pollution is common. Venture slightly off the beaten path and the ocean hardly looks safe to swim.&lt;/p&gt;
&lt;h2 id="thailand-"&gt;
Thailand 🇹🇭
&lt;a class="heading-anchor" href="#thailand-" aria-label="Link to Thailand 🇹🇭"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Mangosteen, meditation, and mud houses&lt;/em&gt;&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/lotus-flower_hu_442315300a57b094.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/lotus-flower_hu_e52950105d484e07.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/lotus-flower_hu_1cd69d051132139e.webp 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/lotus-flower_hu_635ba4413a46130e.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/lotus-flower_hu_423b1f0542d25ab4.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/lotus-flower_hu_64ed2c735be52a62.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/lotus-flower_hu_4947ede2d6a53a17.jpeg 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/lotus-flower_hu_8d6e603352062758.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/lotus-flower_hu_423b1f0542d25ab4.jpeg"
alt="Lotus flower near Chiang Mai, Thailand" class="w-full no-border contain"
width="320"
height="200"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Lotus flower near Chiang Mai, Thailand&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;My time in Thailand was intentionally slow. I enjoyed &lt;a href="https://faerieimps.blogspot.com/2017/09/icedea.html"&gt;mangosteen-shaped ice cream&lt;/a&gt; (my favorite fruit), joined a yoga retreat, and took impromptu lessons on how to build houses out of clay bricks. As Lao Tzu put it, &amp;ldquo;Nature does not hurry, yet everything is accomplished.&amp;rdquo; While I raced from rice paddy to museum to coffee stand to hawker market in Vietnam, I reconnected and recalibrated in Chiang Mai and Bangkok.&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/clay-house_hu_df79fcfb4e7425f8.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/clay-house_hu_62195c149dea771a.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/clay-house_hu_15638fd914ccc4c7.webp 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/clay-house_hu_a807831ff61335ad.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/clay-house_hu_9b296a230079495a.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/clay-house_hu_1344fbb88ea75c45.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/clay-house_hu_fb9b2495b8b6ae7b.jpeg 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/clay-house_hu_7e3cbd22f090af3e.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/clay-house_hu_9b296a230079495a.jpeg"
alt="Clay brick building in Thailand" class="w-full no-border contain"
width="320"
height="161"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Clay brick building in Thailand&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;h2 id="india-"&gt;
India 🇮🇳
&lt;a class="heading-anchor" href="#india-" aria-label="Link to India 🇮🇳"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Rick Steves&amp;rsquo; favorite and the &amp;ldquo;keypad mobile&amp;rdquo; capital&lt;/em&gt;&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kochi-monsoon_hu_464dcd5eef56b862.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kochi-monsoon_hu_cfe6fedf7389f62b.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kochi-monsoon_hu_f03ecdadc82ef697.webp 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kochi-monsoon_hu_aa88760471069657.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kochi-monsoon_hu_8acd2739749e3b44.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kochi-monsoon_hu_c80d425492ba2b15.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kochi-monsoon_hu_1fc03d46ba9d0ad9.jpeg 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kochi-monsoon_hu_edf8c2141adde495.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kochi-monsoon_hu_8acd2739749e3b44.jpeg"
alt="Monsoon season in Kochi, India" class="w-full no-border contain"
width="320"
height="195"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Monsoon season in Kochi, India&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;Although my time was spent in Kerala (often billed as &lt;a href="https://www.fathomaway.com/colorful-india-photo-tour/"&gt;&amp;ldquo;India Lite&amp;rdquo;&lt;/a&gt;), it was still quite intense. But this was one of those times when I thrived on the chaos. I roamed the streets of Kochi by rickshaw and public transport: buses, trains, and even ferries. I even found plenty of my &lt;a href="https://www.civilsocietyonline.com/agriculture/mangosteen-bonanza-in-kerala-village/"&gt;beloved mangosteen&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When asked my favorite country, I say, &amp;ldquo;India.&amp;rdquo; While I enjoy the shock value of saying that, that’s not why I say it. I say India because it’s true. India wallops anyone’s self-assuredness. India rearranged my cultural furniture. My confidence that I understood such basics to life as time, joy, love, pain, and music was shaken. I was humbled in a way that made me a better person.&lt;br /&gt;
– &lt;a href="https://blog.ricksteves.com/blog/tag/india/"&gt;Rick Steves&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Much to the chagrin of many a mobile store salesperson, I had no interest in the latest Huawei Mate or iPhone. Eventually I bought what you can&amp;rsquo;t find on eBay: feature phones (mostly called &amp;ldquo;keypad mobiles&amp;rdquo; in India) like a &lt;a href="https://developer.cloudfone.com/device/snexian_guru_4_g/"&gt;Snexian Guru 4G&lt;/a&gt; and one of the very first Cloud Phone models, an &lt;a href="https://developer.cloudfone.com/device/itel_it9020/"&gt;Itel Super Guru 4G&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="europe"&gt;
Europe
&lt;a class="heading-anchor" href="#europe" aria-label="Link to Europe"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;A wedding in the United Kingdom kicked off the next phase of my trip, including visits with family and in-person meetings with developers in Germany and the Netherlands. Then it was back to the unfamiliar.&lt;/p&gt;
&lt;h2 id="switzerland-"&gt;
Switzerland 🇨🇭
&lt;a class="heading-anchor" href="#switzerland-" aria-label="Link to Switzerland 🇨🇭"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Charming and alarmingly expensive&lt;/em&gt;&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/swiss-cow_hu_a1f108d04a1d6d13.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/swiss-cow_hu_5ae87ec06209f542.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/swiss-cow_hu_b125c79acf76098a.webp 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/swiss-cow_hu_ce12ab594a71e995.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/swiss-cow_hu_776b189332cf9c6e.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/swiss-cow_hu_108ea8035a9da748.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/swiss-cow_hu_63420194203535c8.jpeg 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/swiss-cow_hu_4e04fc55842a06ec.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/swiss-cow_hu_776b189332cf9c6e.jpeg"
alt="Cows in Switzerland" class="w-full no-border contain"
width="320"
height="157"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Cows in Switzerland&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;Switzerland is quaint, peaceful, and anything but flat. It&amp;rsquo;s a place where you&amp;rsquo;re not wanting for views, where trains arrive to the minute, and a $10 lunch no longer exists. Once again I followed Rick Steves&amp;rsquo; advice and visited Gimmelwald. While the cozy vibes were welcome during a very rainy summer, what made it special was meeting locals on the train who invited us for tea.&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/chilchbalm_hu_863bc21a7dc2db03.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/chilchbalm_hu_d7a3b39e1b465d99.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/chilchbalm_hu_5fafddf163e7c6.webp 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/chilchbalm_hu_3501914b32f52fe7.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/chilchbalm_hu_21f7a8ad6e1cb106.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/chilchbalm_hu_28f71e442e2a2d17.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/chilchbalm_hu_e6b3036c371c7ce0.jpeg 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/chilchbalm_hu_28278c5369101c36.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/chilchbalm_hu_21f7a8ad6e1cb106.jpeg"
alt="Chilchbalm marker" class="w-full no-border contain"
width="320"
height="157"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Chilchbalm marker at 1631m&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;During a downpour, Heinz and Annelies recounted their own travels decades earlier during an era when visa applications were made in paper and travelers cheques were still in fashion. By comparison, my own journey was far less harrowing. Agonizing or not, my journey was my own. The world has clearly changed in half a century, and I&amp;rsquo;m fortunate to be able to be put to productive use anywhere I can secure a somewhat stable internet connection.&lt;/p&gt;
&lt;figure class="full-bleed"&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/chilchbalm-mountains_hu_f9b2ee41d76c7eb5.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/chilchbalm-mountains_hu_7a17db10d7173300.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/chilchbalm-mountains_hu_7c01da43f092ec2c.webp 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/chilchbalm-mountains_hu_497c841feca11ce.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/chilchbalm-mountains_hu_42d97104730d338c.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/chilchbalm-mountains_hu_87bed3987c111d20.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/chilchbalm-mountains_hu_9c6a0064db592cb2.jpeg 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/chilchbalm-mountains_hu_d4b1ecdfa26813cb.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/chilchbalm-mountains_hu_42d97104730d338c.jpeg"
alt="Chilchbalm marker" class="w-full no-border contain"
width="320"
height="165"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Chilchbalm marker at 1631m&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;The hikes throughout Lauterbrunnen and the Alps were incredible, but taking a series of gondolas and trains up several thousand meters feels like cheating. Hard work that leads to &lt;strong&gt;a sense of accomplishment is motivating&lt;/strong&gt;. It&amp;rsquo;s more than just the &lt;a href="https://en.wikipedia.org/wiki/IKEA_effect"&gt;IKEA effect&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Surprisingly, a last minute hike to Chilchbalm proved more memorable. It&amp;rsquo;s an awesome glacial basin, and one of the view places outside of National Parks like Glacier and the Grand Tetons that I truly felt small.&lt;/p&gt;
&lt;h2 id="italy-"&gt;
Italy 🇮🇹
&lt;a class="heading-anchor" href="#italy-" aria-label="Link to Italy 🇮🇹"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;La vita è bella&lt;/em&gt;&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/la-madonna-bianca_hu_74f03cfcf0b41cb7.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/la-madonna-bianca_hu_fbc70be867947757.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/la-madonna-bianca_hu_8adcd92ffc79263e.webp 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/la-madonna-bianca_hu_f55a9f06ad8ae682.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/la-madonna-bianca_hu_1a224fea6c6acead.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/la-madonna-bianca_hu_7de799aeeb37e1de.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/la-madonna-bianca_hu_32496a2e46f30ac6.jpeg 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/la-madonna-bianca_hu_dc0e35b7ea525535.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/la-madonna-bianca_hu_1a224fea6c6acead.jpeg"
alt="La festa della Madonna Bianca in Portovenere, Italy" class="w-full no-border contain"
width="320"
height="154"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;La festa della Madonna Bianca in Portovenere, Italy&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;Some say, &amp;ldquo;if you fail to plan, you plan to fail.&amp;rdquo; Others believe that, &amp;ldquo;life happens while you&amp;rsquo;re busy making other plans.&amp;rdquo; As a Technical Program Manager (TPM), you&amp;rsquo;re expected to have Plans A through E for when the inevitable goes wrong.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://discoverportovenere.com/portovenere-white-madonna-procession/"&gt;White Madonna Festival&lt;/a&gt; was one of those unplanned highlights that I only learned about day of thanks to flyers on the street. Hiking the Cinque Terre was incredible. So was swimming in the Ligurian Sea and eating lemon gelato in Corniglia. But neither rival the unplanned nighttime visit to the Grotto of Lord Byron celebrating a 14th century plague miracle by lighting more than 2,000 roman candles.&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/whatsbar_hu_2639ed147fa25254.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/whatsbar_hu_1e719044602eb9da.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/whatsbar_hu_ee1d3a82444c6b4a.webp 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/whatsbar_hu_85c937694768dad1.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/whatsbar_hu_c2348cffe32a9fc0.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/whatsbar_hu_3a68f6db49d20177.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/whatsbar_hu_f640c052939de620.jpeg 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/whatsbar_hu_ed2b029e3704bb4a.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/whatsbar_hu_c2348cffe32a9fc0.jpeg"
alt="WhatsBar in Ravenna, Italy" class="w-full no-border contain"
width="320"
height="163"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;WhatsBar in Ravenna, Italy&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;The rest of Italy (namely Rome) was impressive, but I had high expectations that couldn&amp;rsquo;t be exceeded by the dense crowds, sweltering heat, and fear of pickpockets. I was also perplexed by &lt;a href="https://restaurantguru.it/WhatsBar-24h-Ravenna"&gt;WhatsBar&lt;/a&gt;, a 24-hour snack vending machine and ATM in Ravenna that appears to combine the names and logos of WhatsApp and the Java programming language.&lt;/p&gt;
&lt;h2 id="romania-"&gt;
Romania 🇷🇴
&lt;a class="heading-anchor" href="#romania-" aria-label="Link to Romania 🇷🇴"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;A more rugged and affordable Switzerland&lt;/em&gt;&lt;/p&gt;
&lt;figure class="full-bleed"&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/carpathians_hu_67efe73def036b6c.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/carpathians_hu_ada5fc521598b2fa.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/carpathians_hu_aecab015dbcdd7a9.webp 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/carpathians_hu_6c479e1416fc9193.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/carpathians_hu_b27b92062b8ad67f.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/carpathians_hu_d7698acdc6c1f629.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/carpathians_hu_83be847fdc9db4b5.jpeg 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/carpathians_hu_fd8a7de52195b418.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/carpathians_hu_b27b92062b8ad67f.jpeg"
alt="The Carpathian Mountains near Cheia, Romania" class="w-full no-border contain"
width="320"
height="125"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;The Carpathian Mountains near Cheia, Romania&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;Romania was a sleeper hit and another last minute decision. It came on my radar because I met a Romanian at a tech meetup in Auckland. That&amp;rsquo;s all the planning, and it turned out to be incredible.&lt;/p&gt;
&lt;p&gt;After reconnecting with my Romanian acquaintance on WhatsApp, I learned it&amp;rsquo;s best to hike in groups because of the prevalence of brown bears. Although no bears were on this hike, it proved to be sound advice for making friends. Plus, I did eventually see bears.&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/romania-bear_hu_be71e82717813d01.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/romania-bear_hu_25a4ee3c7f4a89dc.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/romania-bear_hu_c99816034bf78062.webp 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/romania-bear_hu_c88a2f51bc7e549c.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/romania-bear_hu_147d97241798fe5f.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/romania-bear_hu_18e6bae2a00fbab2.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/romania-bear_hu_cb74cf1abbff28ab.jpeg 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/romania-bear_hu_98bcaf022044f739.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/romania-bear_hu_147d97241798fe5f.jpeg"
alt="Brown bear near the Transfăgărășan in Romania" class="w-full no-border contain"
width="320"
height="191"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Brown bear near the Transfăgărășan in Romania&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;h2 id="cyprus-"&gt;
Cyprus 🇨🇾
&lt;a class="heading-anchor" href="#cyprus-" aria-label="Link to Cyprus 🇨🇾"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Adorable cats and a return to building&lt;/em&gt;&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/cyprus-cat_hu_3b53661cb199619e.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/cyprus-cat_hu_1010bb5208a70b9f.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/cyprus-cat_hu_b89f6f22ca4e128b.webp 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/cyprus-cat_hu_43bdf293e29de0a9.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/cyprus-cat_hu_47cfe63157aaa3a2.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/cyprus-cat_hu_e565c3aacef7b297.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/cyprus-cat_hu_91b491e6a100d3f1.jpeg 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/cyprus-cat_hu_5e968e23d9e9b4a5.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/cyprus-cat_hu_47cfe63157aaa3a2.jpeg"
alt="Calico Cyprus Cat" class="w-full no-border contain"
width="320"
height="181"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Calico Cyprus cat sleeping at a cafe&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;Cyprus was a place I had no connection to. I&amp;rsquo;m still not quite sure what drew me there. But the intense daytime heat let me feel less guilty spending time inside building my hackathon project, &lt;a href="https://barrasso.me/projects/backyard-birds/"&gt;Backyard Birds&lt;/a&gt;, in between sampling wines and visits to ancient ruins.&lt;/p&gt;
&lt;p&gt;It turns out &lt;a href="https://en.wikipedia.org/wiki/Cyprus_cat"&gt;Cyprus cats&lt;/a&gt; have ruled the island for over 10,000 years since they were apparently brought from Egypt to eradicate snakes. They&amp;rsquo;re are so cute, I made a &lt;a href="https://www.youtube.com/shorts/wj0_vcsJ9AA"&gt;montage video&lt;/a&gt; of the many cats I encountered.&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kaios-poster_hu_f112cbf8f4142c14.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kaios-poster_hu_5b204fd1489b7d6d.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kaios-poster_hu_ab098c8b2bcc59e5.webp 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kaios-poster_hu_65f478dfe5f767fa.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kaios-poster_hu_c8e31ee18f69520c.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kaios-poster_hu_1f5c1704fcb95841.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kaios-poster_hu_416169bd54ca0f5.jpeg 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kaios-poster_hu_6a8de4f6fc865a2c.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/kaios-poster_hu_c8e31ee18f69520c.jpeg"
alt="KaiOS poster in Nicosia, Cyprus" class="w-full no-border contain"
width="320"
height="170"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;KaiOS poster in Nicosia, Cyprus&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;As a software engineer, there&amp;rsquo;s a sense of excitement and pride seeing your work in the real world. I am often asked by folks in Africa and South Asia why I work on flip phones when they&amp;rsquo;re mostly for seniors and hipsters in the west. This flyer for the iPRO K2 running &lt;a href="https://www.kaiostech.com/"&gt;KaiOS&lt;/a&gt; was probably the first time I encountered an advertisement for KaiOS in real life.&lt;/p&gt;
&lt;p&gt;From the data, I knew my work reached far flung places. Five and a half years after launch, PodLP has active listeners in over 170+ countries including Cyprus! Despite a $0 marketing budget, PodLP still gets organically mentions and linked on podcasts in places where it&amp;rsquo;s most popular like India.&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/dont-make-drama_hu_eeb1b5d3b4988ef6.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/dont-make-drama_hu_39eb6f4da6e5a46d.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/dont-make-drama_hu_e225cccb56ba3b1f.webp 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/dont-make-drama_hu_54f7c82c84f887c9.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/dont-make-drama_hu_65720efa93a43ae4.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/dont-make-drama_hu_42be5173710150c4.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/dont-make-drama_hu_ae0fabdc17fe5b73.jpeg 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/dont-make-drama_hu_3a6ddcc7bebff1a2.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/dont-make-drama_hu_65720efa93a43ae4.jpeg"
alt="Stop making drama, you&amp;#39;re not Shakespeare" class="w-full no-border contain"
width="320"
height="260"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;&amp;lsquo;Stop making drama, you&amp;rsquo;re not Shakespeare&amp;rsquo; in Nicosia, Cyprus&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;I have no personal connection to this poster, it&amp;rsquo;s just sage advice.&lt;/p&gt;
&lt;h2 id="türkiye-"&gt;
Türkiye 🇹🇷
&lt;a class="heading-anchor" href="#t%c3%bcrkiye-" aria-label="Link to Türkiye 🇹🇷"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Cats, cats, and more cats&lt;/em&gt;&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/cat-market-istanbul_hu_9e447621b18c1c89.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/cat-market-istanbul_hu_d2827c74c5af1795.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/cat-market-istanbul_hu_a6586afcffb1f460.webp 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/cat-market-istanbul_hu_c0cc6f1df30b9027.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/jpeg" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/cat-market-istanbul_hu_35f8df4f44ed3dfb.jpeg 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/cat-market-istanbul_hu_f3b4859c357c7515.jpeg 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/cat-market-istanbul_hu_1294f16ca0809afb.jpeg 1024w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/cat-market-istanbul_hu_82de520de3a8a3ce.jpeg 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/cat-market-istanbul_hu_35f8df4f44ed3dfb.jpeg"
alt="Cat at a bazaar in Istanbul" class="w-full no-border contain"
width="320"
height="235"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Cat at a bazaar in Istanbul&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;The final stop on my year-long adventure was Turkey, where I experienced life in a different set of rhythms like waking up to the morning adhan (call to prayer) and greeting the local cats. Following their successful invasion of Cyprus, &lt;strong&gt;&lt;a href="https://www.bbc.com/travel/article/20251118-istanbul-turkey-inside-the-city-where-cats-rule-the-streets"&gt;cats conquered Istanbul&lt;/a&gt;&lt;/strong&gt; where they remain in charge to this day.&lt;/p&gt;
&lt;figure&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/fez-the-cat_hu_38fb0cd9e29bb42.webp 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/fez-the-cat_hu_a543e85da877d03d.webp 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/fez-the-cat_hu_3028ea3c670b98a1.webp 1024w' sizes="100vw" /&gt;
&lt;source type="image/png" srcset='https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/fez-the-cat_hu_1674c396e1ec1d19.png 320w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/fez-the-cat_hu_7dae6c64a4430884.png 640w, https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/fez-the-cat_hu_744f98bc054d968.png 1024w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/fez-the-cat_hu_1674c396e1ec1d19.png"
alt="Fez the Cat" class="w-full no-border contain"
width="320"
height="213"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;figcaption class="text-sm text-ht-light italic mt-2 text-center"&gt;Fez the Cat&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;With some spare time and an extended deadline, I decided to work on another submission for the Yoto Hackathon: &lt;a href="https://yoto.space/developers/post/fez-the-cat-7XbFLVY7sXNlNVA"&gt;Fez the Cat&lt;/a&gt;. Clearly inspired by my time in Istanbul, Fez tells 26 stories, one for each letter of the alphabet. Each story is almost a pangram, a sequence that containers every letter of the alphabet from A-Z. Except each story is missing one letter which the listener has to guess.&lt;/p&gt;
&lt;p&gt;Fez gave me more time to &lt;a href="https://barrasso.me/posts/2025-09-25-cloudfront-function-midnight-local-time/"&gt;explore vibe coding&lt;/a&gt; and tinker with text-to-speech (TTS) voices in ElevenLabs. Backyard Birds and Fez the Cat were the start of a return to building that continues by hacking at &lt;a href="https://www.sundai.club/"&gt;Sundai Club&lt;/a&gt; and open sourcing many of my projects over the past few years.&lt;/p&gt;
&lt;h2 id="returning-home"&gt;
Returning Home
&lt;a class="heading-anchor" href="#returning-home" aria-label="Link to Returning Home"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;It&amp;rsquo;s a strange and disorienting feeling returning home after being away for so long. This post is just one part of processing the journey. I still have much to unfold on the nature of work, the call to wander, and the meaning of &amp;ldquo;home.&amp;rdquo; But for now I&amp;rsquo;m intent on building something new that I can feel proud of.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2025-12-10-reflections-on-a-year-abroad/</guid><pubDate>Wed, 10 Dec 2025 00:00:00 GMT</pubDate></item><item><title>
Rotating content daily with Cloudfront Functions</title><link>https://barrasso.me/posts/2025-09-25-cloudfront-function-midnight-local-time/</link><description>
&lt;p&gt;How to use Cloudfront Functions to rotate content daily at &lt;em&gt;roughly&lt;/em&gt; midnight local time.&lt;/p&gt;
&lt;h2 id="content-rotation"&gt;
Content Rotation
&lt;a class="heading-anchor" href="#content-rotation" aria-label="Link to Content Rotation"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;I built two projects for the &lt;a href="https://yoto.space/news/post/build-a-yoto-app-and-you-could-win-up-to-5-000-hQVNmKqCsfLNoj0"&gt;Yoto Hackathon&lt;/a&gt;: &lt;a href="https://barrasso.me/projects/backyard-birds/"&gt;Backyard Birds&lt;/a&gt; and &lt;a href="https://yoto.space/developers/post/fez-the-cat-7XbFLVY7sXNlNVA"&gt;Fez the Cat&lt;/a&gt;. Fez tells a new story each each that contains every letter of the alphabet, except for one letter that listeners need to guess.&lt;/p&gt;
&lt;p&gt;Fez&amp;rsquo;s stories are &lt;em&gt;almost&lt;/em&gt; &lt;strong&gt;&lt;a href="https://en.wikipedia.org/wiki/Pangram"&gt;pangrams&lt;/a&gt;&lt;/strong&gt;, the most famous pangram being:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The quick brown fox jumps over the lazy dog&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After writing and editing 26 scripts (one for each letter A-Z), I used &lt;a href="https://elevenlabs.io/"&gt;ElevenLabs&lt;/a&gt; to generate audio using a custom voice. Now with 26 MP3 files, I needed to automatically change the story of the day. Yoto allows playlists to include &lt;a href="https://yoto.dev/myo/streaming-tracks/"&gt;streaming tracks&lt;/a&gt; where the device makes an HTTP requests to a static URL each time the user inserts the card.&lt;/p&gt;
&lt;p&gt;To rotate content daily I considered:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Serving fully static using Cloudfront with an S3 origin, then using a Lambda function on a daily &lt;a href="https://docs.aws.amazon.com/scheduler/latest/UserGuide/what-is-scheduler.html"&gt;EventBridge scheduler&lt;/a&gt; to copy one of the 26 stories as &amp;ldquo;today&amp;rsquo;s story&amp;rdquo;&lt;/li&gt;
&lt;li&gt;Serving static content using either a &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-functions.html"&gt;Cloudfront Function&lt;/a&gt; or Lambda@Edge function to select an S3 object based on the current day in the user&amp;rsquo;s local time using the &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/adding-cloudfront-headers.html"&gt;&lt;code&gt;CloudFront-Viewer-Time-Zone&lt;/code&gt; header&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I choose the second option because it&amp;rsquo;s cumbersome to explain that, &amp;ldquo;the story updates every day at Midnight UTC.&amp;rdquo; I opted instead for a Cloufront Function because they are very low latency and &lt;em&gt;much cheaper&lt;/em&gt; than Lambda@Edge. Cloudfront Functions cost a flat $0.10 per 1 million invocations vs. Lambda@Edge which costs approximately $0.60 per 1 million invocations (price depends on region) plus execution time and memory measured in GB-seconds.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s nothing wrong with Lambda@Edge, I was just feeling frugal plus &lt;strong&gt;What Could Go Wrong?™&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="limited-runtime"&gt;
Limited Runtime
&lt;a class="heading-anchor" href="#limited-runtime" aria-label="Link to Limited Runtime"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;Cloudfront Functions has a &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/functions-javascript-runtime-features.html"&gt;very limited runtime&lt;/a&gt;. The more recent &lt;code&gt;cloudfront-js-2.0&lt;/code&gt; runtime supports ECMAScript 5.1 and &lt;em&gt;some&lt;/em&gt; ES6 features. Notably, it lacks networking (no &lt;code&gt;fetch&lt;/code&gt;), file system access (no &lt;code&gt;fs&lt;/code&gt;), environment variables, or timers.&lt;/p&gt;
&lt;p&gt;Important for me, the &lt;a href="https://repost.aws/questions/QUjbND1VmvQp2LLe1K211Fwg/is-date-tolocalestring-supported-in-cloudfrontfunctions"&gt;&lt;code&gt;Date.toLocaleString&lt;/code&gt; implementation&lt;/a&gt; supported in Cloudfront Functions follows the ECMAScript 5.1 standard where it accepts no arguments. What could have just been a single line with &lt;code&gt;now.toLocaleString(&amp;quot;de-DE&amp;quot;, { timeZone: 'Europe/Berlin'})&lt;/code&gt; got a lot more complicated.&lt;/p&gt;
&lt;h3 id="ask-chatgpt"&gt;
Ask ChatGPT
&lt;a class="heading-anchor" href="#ask-chatgpt" aria-label="Link to Ask ChatGPT"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;TLDR - Don&amp;rsquo;t ask AI when there is lot of nuance in a specific runtime environment.&lt;/p&gt;
&lt;p&gt;I made the mistake of asking ChatGPT, Claude, and Gemini for help. Each gave me a convincing option. One response suggested I use the &lt;strong&gt;non-existent &lt;code&gt;CloudFront-Viewer-Time-Offset&lt;/code&gt; header&lt;/strong&gt; to calculate time in UTC and offset it according to the user&amp;rsquo;s local time. This would have been great, except a &lt;a href="https://github.com/search?q=%22CloudFront-Viewer-Time-Offset%22&amp;amp;type=code"&gt;GitHub search&lt;/a&gt; yielded exactly 0 results.&lt;/p&gt;
&lt;p&gt;Another response indicated that I could use the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString"&gt;&lt;code&gt;Date.toLocaleDateString&lt;/code&gt;&lt;/a&gt; function which, &amp;ldquo;returns a string with a language-sensitive representation of the date portion of this date in the local timezone.&amp;rdquo; &lt;em&gt;Spoiler alert&lt;/em&gt;: The Cloudfront Functions runtime doesn&amp;rsquo;t support the &lt;code&gt;Intl&lt;/code&gt; API, and &lt;strong&gt;&amp;ldquo;local time&amp;rdquo; is function start time&lt;/strong&gt;, always in UTC. This makes sense since all Cloudfront Functions have a &lt;strong&gt;strict 1ms execution limit&lt;/strong&gt;!&lt;/p&gt;
&lt;h3 id="time-to-pivot"&gt;
Time to Pivot?
&lt;a class="heading-anchor" href="#time-to-pivot" aria-label="Link to Time to Pivot?"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;At this point, it would have been much simpler to just migrate over to Lambda@Edge which supports the &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-at-edge-function-restrictions.html#lambda-at-edge-restrictions-runtime"&gt;&lt;code&gt;nodejs22.x&lt;/code&gt; runtime&lt;/a&gt;, but I was determined to shoehorn a solution using Cloudfront Functions! I could calculate Midnight UTC, and I knew the IANA name of the user&amp;rsquo;s timezone, so I just needed to map IANA names to UTC offsets to calculate midnight local time.&lt;/p&gt;
&lt;p&gt;To map IANA timezones to UTC offsets I considered:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Storing timezone name to UTC offsets in &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/kvs-with-functions.html"&gt;Cloudfront KeyValueStore&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Using an NPM package that contained the mappings&lt;/li&gt;
&lt;li&gt;Inlining the mappings within my application code&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Another important limitation of Cloudfront Functions is that the &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html"&gt;maximum function size&lt;/a&gt; is just 10 KB! Unfortunately, I could not find any NPM packages that would comfortably fit within that limit. Cloudfront KeyValueStore isn&amp;rsquo;t too expensive at &lt;a href="https://aws.amazon.com/cloudfront/pricing/"&gt;$0.03 per 1 million reads&lt;/a&gt;, and probably would have been a convenient long-term solution, especially if I had multiple functions rotating content and reading from the same KV store.&lt;/p&gt;
&lt;h3 id="time-zone-mappings"&gt;
Time Zone Mappings
&lt;a class="heading-anchor" href="#time-zone-mappings" aria-label="Link to Time Zone Mappings"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;I opted to have Gemini create a map from IANA time zone name to UTC offset. To keep the mapping well within the 10 KB limit, I adjusted the mapping structure for the smallest file size. The original mapping was a single map like &lt;code&gt;{ &amp;quot;Africa/Kigali&amp;quot;: 120 }&lt;/code&gt;, but it was well above 10 KB. Smaller was a nested variant representing IANA time zone names like &lt;code&gt;Africa/Kigali&lt;/code&gt; mapping to &lt;code&gt;Africa.Kigali&lt;/code&gt; with the timezone offset at index &lt;code&gt;17&lt;/code&gt; in the &lt;code&gt;UTC_OFFSETS_IN_MINUTES&lt;/code&gt; array.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UTC_OFFSETS_IN_MINUTES&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;720&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;660&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;600&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;570&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;540&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;480&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;420&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;360&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;300&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;240&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;210&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;180&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;150&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;120&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;60&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;60&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;120&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;180&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;210&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;240&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;270&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;300&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;330&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;345&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;360&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;390&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;420&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;480&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;525&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;540&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;570&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;600&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;630&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;660&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;720&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;765&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;780&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;840&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;TIMEZONES&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Africa&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Abidjan&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Accra&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Addis_Ababa&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;18&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Algiers&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Asmara&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;18&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Asmera&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;18&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Bamako&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Bangui&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Banjul&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Bissau&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Blantyre&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Brazzaville&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Bujumbura&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Cairo&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Casablanca&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Ceuta&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Conakry&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Dakar&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Dar_es_Salaam&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;18&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Djibouti&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;18&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Douala&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;El_Aaiun&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Freetown&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Gaborone&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Harare&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Johannesburg&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Juba&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Kampala&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;18&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Khartoum&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Kigali&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Kinshasa&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Lagos&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Libreville&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Lome&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Luanda&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Lubumbashi&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Lusaka&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Malabo&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Maputo&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Maseru&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Mbabane&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Mogadishu&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;18&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Monrovia&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Nairobi&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;18&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Ndjamena&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Niamey&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Nouakchott&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Ouagadougou&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;Porto-Novo&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Sao_Tome&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Timbuktu&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Tripoli&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Tunis&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Windhoek&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Still fairly clean, this version was 9.98 KB (7.1 KB minified), and I still needed space for my function code. After a few more attempts, I landed on:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;offsets&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;720&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;660&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;600&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;570&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;540&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;480&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;420&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;360&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;300&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;240&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;210&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;180&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;150&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;120&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;60&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;60&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;120&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;180&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;210&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;240&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;270&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;300&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;330&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;345&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;360&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;390&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;420&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;480&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;525&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;540&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;570&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;600&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;630&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;660&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;720&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;765&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;780&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;840&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;zones&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Africa&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Abidjan Accra Bamako Banjul Bissau Conakry Dakar Freetown Lome Monrovia Nouakchott Ouagadougou Sao_Tome Timbuktu&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Algiers Bangui Brazzaville Casablanca Ceuta Douala El_Aaiun Kinshasa Lagos Libreville Luanda Malabo Ndjamena Niamey Porto-Novo Tunis&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Blantyre Bujumbura Cairo Gaborone Harare Johannesburg Juba Khartoum Kigali Lubumbashi Lusaka Maputo Maseru Mbabane Tripoli Windhoek&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;18&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Addis_Ababa Asmara Asmera Dar_es_Salaam Djibouti Kampala Mogadishu Nairobi&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I estimated that I needed about ~3.5 KB for my function code unminified, and this representation got down to 6.4 KB minified so it fit! The full mapping is available below as a &lt;a href="https://gist.github.com/Tombarr/376c057b7adc2e3df74aaee031b4c637"&gt;GitHub Gist&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="what-about-daylight-savings"&gt;
What about Daylight Savings?
&lt;a class="heading-anchor" href="#what-about-daylight-savings" aria-label="Link to What about Daylight Savings?"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;This static timezone mapping has several obvious &lt;del&gt;flaws&lt;/del&gt; limitations, most notably:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It&amp;rsquo;s static: sometimes countries or territories change timezones&lt;/li&gt;
&lt;li&gt;It doesn&amp;rsquo;t account for Daylight Savings Time (DST)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That said, countries changing timezones isn&amp;rsquo;t a frequent occurrence. It&amp;rsquo;s even less likely that I&amp;rsquo;ll have many users in such countries. Plus, there&amp;rsquo;s growing evidence that &lt;a href="https://www.health.harvard.edu/staying-healthy/the-dark-side-of-daylight-saving-time"&gt;daylight savings is unhealthy&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So this approach doesn&amp;rsquo;t really rotate content at mightnight local time. Instead, it adjusts at approximately &lt;strong&gt;midnight ±1 hour local time&lt;/strong&gt;, which I&amp;rsquo;d say is Good Enough™.&lt;/p&gt;
&lt;h2 id="complete-timezone-mapping"&gt;
Complete Timezone Mapping
&lt;a class="heading-anchor" href="#complete-timezone-mapping" aria-label="Link to Complete Timezone Mapping"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;This function is intended to map timezone names to UTC offsets (in minutes), and fit within the constraints of the Cloudfront Function runtime environment without any external dependencies.&lt;/p&gt;
&lt;div class="gist-embed"&gt;
&lt;div class="gist-header"&gt;
&lt;a
href="https://gist.github.com/Tombarr/376c057b7adc2e3df74aaee031b4c637"
target="_blank"
rel="noopener noreferrer"
class="gist-filename"&gt;
&lt;svg
class="inline-svg"
aria-hidden="true"
height="16"
viewBox="0 0 16 16"
version="1.1"
width="16"&gt;
&lt;path
fill="currentColor"
d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0113.25 16h-9.5A1.75 1.75 0 012 14.25V1.75zm1.75-.25a.25.25 0 00-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 00.25-.25V6h-2.75A1.75 1.75 0 019 4.25V1.5H3.75a.25.25 0 00-.25.25z"&gt;&lt;/path&gt;
&lt;/svg&gt;
timezone-to-offset.js
&lt;/a&gt;
&lt;/div&gt;
&lt;div class="gist-code-container"&gt;
&lt;div&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;offsets&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;720&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;660&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;600&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;570&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;540&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;480&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;420&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;360&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;300&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;240&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;210&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;180&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;150&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;120&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;60&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;60&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;120&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;180&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;210&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;240&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;270&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;300&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;330&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;345&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;360&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;390&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;420&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;480&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;525&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;540&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;570&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;600&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;630&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;660&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;720&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;765&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;780&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;840&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;zones&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Africa&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Abidjan Accra Bamako Banjul Bissau Conakry Dakar Freetown Lome Monrovia Nouakchott Ouagadougou Sao_Tome Timbuktu&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Algiers Bangui Brazzaville Casablanca Ceuta Douala El_Aaiun Kinshasa Lagos Libreville Luanda Malabo Ndjamena Niamey Porto-Novo Tunis&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Blantyre Bujumbura Cairo Gaborone Harare Johannesburg Juba Khartoum Kigali Lubumbashi Lusaka Maputo Maseru Mbabane Tripoli Windhoek&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;18&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Addis_Ababa Asmara Asmera Dar_es_Salaam Djibouti Kampala Mogadishu Nairobi&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;America&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Adak Atka&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Anchorage Juneau Metlakatla Nome Sitka Yakutat&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Ensenada Los_Angeles Santa_Isabel Tijuana Vancouver&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;6&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Boise Cambridge_Bay Creston Dawson Dawson_Creek Denver Edmonton Fort_Nelson Hermosillo Inuvik Mazatlan Ojinaga Phoenix Shiprock Whitehorse Yellowknife&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Bahia_Banderas Belize Chicago Costa_Rica El_Salvador Guatemala Indiana/Knox Indiana/Tell_City Knox_IN Managua Matamoros Menominee Merida Mexico_City Monterrey North_Dakota/Beulah North_Dakota/Center North_Dakota/New_Salem Rainy_River Rankin_Inlet Regina Resolute Swift_Current Tegucigalpa Winnipeg&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;8&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Atikokan Bogota Cancun Cayman Coral_Harbour Detroit Eirunepe Fort_Wayne Grand_Turk Havana Indiana/Indianapolis Indiana/Marengo Indiana/Petersburg Indiana/Vevay Indiana/Vincennes Indiana/Winamac Indianapolis Iqaluit Jamaica Kentucky/Louisville Kentucky/Monticello Lima Louisville Montreal Nassau New_York Nipigon Panama Pangnirtung Port-au-Prince Porto_Acre Rio_Branco Thunder_Bay Toronto&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;9&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Anguilla Antigua Aruba Asuncion Barbados Blanc-Sablon Boa_Vista Campo_Grande Caracas Cuiaba Curacao Dominica Glace_Bay Goose_Bay Grenada Guadeloupe Guyana Halifax Kralendijk La_Paz Lower_Princes Manaus Marigot Martinique Moncton Montserrat Port_of_Spain Porto_Velho Puerto_Rico Santiago Santo_Domingo St_Barthelemy St_Kitts St_Lucia St_Thomas St_Vincent Thule Tortola Virgin&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;St_Johns&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Araguaina Bahia Belem Buenos_Aires Catamarca Cordoba Cayenne Fortaleza Godthab Jujuy Maceio Mendoza Miquelon Montevideo Nuuk Paramaribo Punta_Arenas Recife Rosario Santarem Sao_Paulo Stanley&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;13&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Noronha&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;14&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Scoresbysund&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Danmarkshavn&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Argentina&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; { &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Buenos_Aires Catamarca ComodRivadavia Cordoba Jujuy La_Rioja Mendoza Rio_Gallegos Salta San_Juan San_Luis Tucuman Ushuaia&amp;#39;&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Antarctica&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Palmer Rothera&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Troll&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;18&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Syowa&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;22&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Mawson&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;25&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Vostok&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;27&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Davis&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;32&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;DumontDUrville Macquarie&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;34&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Casey&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;35&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;McMurdo South_Pole GMT+12&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Asia&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Beirut Famagusta Gaza Hebron Jerusalem Nicosia Tel_Aviv&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;18&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Aden Amman Baghdad Bahrain Damascus Djibouti Istanbul Kuwait Qatar Riyadh&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;19&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Tehran&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Baku Dubai Muscat Tbilisi Yerevan&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;21&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Kabul&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;22&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Aqtau Aqtobe Ashgabat Ashkhabad Atyrau Dushanbe Karachi Oral Qyzylorda Samarkand Tashkent Yekaterinburg&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;23&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Calcutta Colombo Kolkata&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;24&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Kathmandu Katmandu&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;25&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Almaty Bishkek Dacca Dhaka Kashgar Omsk Qostanay Thimbu Thimphu Urumqi&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;26&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Rangoon Yangon&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;27&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Bangkok Barnaul Ho_Chi_Minh Hovd Jakarta Krasnoyarsk Novokuznetsk Novosibirsk Phnom_Penh Pontianak Saigon Tomsk Vientiane&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;28&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Brunei Choibalsan Chongqing Chungking Hong_Kong Irkutsk Kuala_Lumpur Kuching Macao Macau Makassar Manila Shanghai Singapore Taipei Ulaanbaatar Ulan_Bator&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;30&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Dili Jayapura Khandyga Pyongyang Seoul Tokyo Yakutsk&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;32&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Chita Ust-Nera Vladivostok&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;34&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Magadan Sakhalin Srednekolymsk&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;35&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Anadyr Kamchatka&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Atlantic&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; { &lt;span style="color:#ae81ff"&gt;9&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Bermuda&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Stanley&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;13&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;South_Georgia&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;14&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Azores Cape_Verde&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Canary Faeroe Faroe Madeira Reykjavik St_Helena&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Jan_Mayen&amp;#39;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Australia&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; { &lt;span style="color:#ae81ff"&gt;28&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Perth West&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;29&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Eucla&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;31&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Adelaide Broken_Hill Darwin North South Yancowinna&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;32&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;ACT Brisbane Canberra Currie Hobart Lindeman Melbourne NSW Queensland Sydney Tasmania Victoria&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;33&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;LHI Lord_Howe&amp;#39;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Brazil&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; { &lt;span style="color:#ae81ff"&gt;8&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Acre&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;9&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;West&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;East&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;13&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;DeNoronha&amp;#39;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Canada&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; { &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Pacific&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;6&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Mountain Yukon&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Central Saskatchewan&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;8&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Eastern&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;9&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Atlantic&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Newfoundland&amp;#39;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Chile&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; { &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;EasterIsland&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;9&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Continental&amp;#39;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Etc&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT+11&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT+10&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;6&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT+9&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT+8&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;8&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT+7&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;9&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT+6&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT+5&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT+4&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT+3&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;13&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT+2&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;14&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT+1&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT GMT+0 GMT-0 GMT0 Greenwich UCT UTC Universal Zulu&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT-1&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT-2&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;18&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT-3&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT-4&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;22&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT-5&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;25&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT-6&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;27&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT-7&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;28&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT-8&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;30&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT-9&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;32&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT-10&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;34&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT-11&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;35&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT-12 GMT+12&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;37&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT-13&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;38&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT-14&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Europe&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Belfast Guernsey Isle_of_Man Jersey Lisbon London&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Amsterdam Andorra Belgrade Berlin Bratislava Brussels Budapest Busingen Copenhagen Dublin Gibraltar Ljubljana Luxembourg Madrid Malta Monaco Oslo Paris Podgorica Prague Rome San_Marino Sarajevo Skopje Stockholm Tirane Vaduz Vatican Vienna Warsaw Zagreb Zurich&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Athens Bucharest Chisinau Famagusta Helsinki Kaliningrad Kiev Mariehamn Nicosia Riga Sofia Tallinn Tiraspol Uzhgorod Vilnius Zaporozhye&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;18&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Istanbul Kirov Minsk Moscow Simferopol Volgograd&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Astrakhan Samara Saratov Ulyanovsk&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Indian&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; { &lt;span style="color:#ae81ff"&gt;18&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Antananarivo Comoro Mayotte&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Mahe Mauritius Reunion&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;22&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Kerguelen Maldives&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;25&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Chagos&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;26&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Cocos&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;27&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Christmas&amp;#39;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Mexico&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; { &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;BajaNorte&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;6&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;BajaSur&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;General&amp;#39;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Pacific&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;-1&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GMT+12&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Midway Niue Pago_Pago Samoa&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Honolulu Johnston Rarotonga Tahiti&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Marquesas&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Gambier&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Pitcairn&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Easter Galapagos&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;30&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Palau&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;32&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Chuuk Guam Port_Moresby Saipan Truk Yap&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;34&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Bougainville Efate Guadalcanal Kosrae Norfolk Noumea Pohnpei Ponape&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;35&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Auckland Fiji Funafuti Kwajalein Majuro Nauru Tarawa Wake Wallis&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;36&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Chatham&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;37&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Apia Enderbury Fakaofo Kanton Tongatapu&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;38&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Kiritimati&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;US&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; { &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Aleutian Hawaii&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Alaska&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Pacific&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;6&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Arizona Mountain&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Central Indiana-Starke&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;8&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;East-Indiana Eastern Michigan&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Samoa&amp;#39;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;HST&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;PST8PDT&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;6&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;MST MST7MDT Navajo&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;CST6CDT&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;8&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Cuba EST EST5EDT Jamaica&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;GB GB-Eire GMT GMT+0 GMT-0 GMT0 Greenwich Iceland Portugal UCT UTC Universal WET Zulu&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Arctic/Longyearbyen Atlantic/Jan_Mayen CET Eire MET Poland&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;EET Egypt Israel Libya&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;18&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Asia/Istanbul Europe/Istanbul Turkey W-SU&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;19&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Iran&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;28&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Hongkong PRC ROC Singapore&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;30&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Japan ROK&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;35&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Kwajalein NZ&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;36&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;NZ-CHAT&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getUtcOffset&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;tz&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;parts&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;tz&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;split&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;city&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;parts&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;pop&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;node&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;zones&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;i&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;e&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;parts&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;length&lt;/span&gt;; &lt;span style="color:#a6e22e"&gt;i&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;e&lt;/span&gt;; &lt;span style="color:#a6e22e"&gt;i&lt;/span&gt;&lt;span style="color:#f92672"&gt;++&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;node&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;node&lt;/span&gt;[&lt;span style="color:#a6e22e"&gt;parts&lt;/span&gt;[&lt;span style="color:#a6e22e"&gt;i&lt;/span&gt;]];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#f92672"&gt;!&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;node&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;key&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;node&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;val&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;node&lt;/span&gt;[&lt;span style="color:#a6e22e"&gt;key&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;typeof&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;val&lt;/span&gt; &lt;span style="color:#f92672"&gt;===&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;string&amp;#39;&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; (&lt;span style="color:#e6db74"&gt;&amp;#39; &amp;#39;&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;val&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39; &amp;#39;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;indexOf&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39; &amp;#39;&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;city&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39; &amp;#39;&lt;/span&gt;) &lt;span style="color:#f92672"&gt;!==&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;offsets&lt;/span&gt;[&lt;span style="color:#a6e22e"&gt;key&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="gist-footer"&gt;
&lt;div class="gist-footer-left"&gt;
timezone-to-offset.js
&lt;/div&gt;
&lt;div class="gist-footer-right"&gt;
&lt;a
href="https://gist.githubusercontent.com/Tombarr/376c057b7adc2e3df74aaee031b4c637/raw/c883b1c72cf590d70b9ce5f88295bd302899779d/timezone-to-offset.js"
target="_blank"
rel="noopener noreferrer"
class="gist-raw-link"&gt;
view raw
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2025-09-25-cloudfront-function-midnight-local-time/</guid><pubDate>Thu, 25 Sep 2025 00:00:00 GMT</pubDate></item><item><title>
Deploy an Elixir + Phoenix web app on AWS EC2</title><link>https://barrasso.me/posts/2025-09-09-deploy-an-elixir-phoenix-web-app-aws-ec2/</link><description>
&lt;p&gt;Using GitHub Actions to automatically build and deploy an Elixir + Phoenix web app on AWS EC2 running Amazon Linux 2023&lt;/p&gt;
&lt;h2 id="at-a-glance"&gt;
At a glance
&lt;a class="heading-anchor" href="#at-a-glance" aria-label="Link to At a glance"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Below is a GitHub Action with two steps that builds an Elixir + Phoenix release binary using &lt;a href="https://www.docker.com/"&gt;Docker&lt;/a&gt; and deploys the binary onto an AWS EC2 instance running Amazon Linux 2023. This approach worked fairly well for my hackathon project, but be warned that single-instance deployments like this are not horizontally scalable or highly available.&lt;/p&gt;
&lt;h2 id="why-elixir"&gt;
Why Elixir?
&lt;a class="heading-anchor" href="#why-elixir" aria-label="Link to Why Elixir?"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;I chose to build &lt;a href="https://barrasso.me/projects/backyard-birds/"&gt;Backyard Birds&lt;/a&gt; in &lt;a href="https://elixir-lang.org/"&gt;Elixir&lt;/a&gt; because Hackathons are a good opportunity to explore new technology. I &lt;a href="https://itnext.io/hello-elixir-the-first-month-d24255860b"&gt;first learned&lt;/a&gt; about Elixir back in 2019 and generally liked the functional syntax and &amp;ldquo;let it crash&amp;rdquo; ethos, centralizing error and retry logic inside a supervisor process.&lt;/p&gt;
&lt;p&gt;Given 6 years had passed since I first tried Elixir, I assumed it would be smooth sailing. Initially setting up a &lt;a href="https://phoenixframework.org/"&gt;Phoenix&lt;/a&gt; web app and running it locally was very easy. Installing dependencies using the Elixir build tool, &lt;code&gt;mix&lt;/code&gt;, was just as simple as &lt;code&gt;npm&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The first obstacle I faced was the &lt;strong&gt;lack of official &lt;a href="https://hex.pm/"&gt;Hex&lt;/a&gt; packages&lt;/strong&gt;. Most notably, there was no AWS SDK. I choose to use &lt;a href="https://hexdocs.pm/aws/readme.html"&gt;&lt;code&gt;aws-elixir&lt;/code&gt;&lt;/a&gt; which worked well enough, but it was poorly documented, didn&amp;rsquo;t feel very idiomatic, and annoying didn&amp;rsquo;t have separate packages for each service so I had to compile hundreds of modules.&lt;/p&gt;
&lt;p&gt;Second, coming from TypeScript, &lt;strong&gt;I missed static types&lt;/strong&gt;. &lt;a href="https://hexdocs.pm/jason/Jason.html"&gt;&lt;code&gt;jason&lt;/code&gt;&lt;/a&gt; made it easy to parse JSON responses, but I just got back tuples &lt;code&gt;{:ok, %{}}&lt;/code&gt; with a &lt;code&gt;Map&lt;/code&gt; as the I had to manually extract values from. Folks on the &lt;a href="https://discord.com/invite/elixir"&gt;Elixir Discord&lt;/a&gt; were incredibly helpful and suggested I consider making structs to represent API responses. This was a bit cumbersome but gave me a bit more structural safety when handling various JSON payloads.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-elixir" data-lang="elixir"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;defmodule&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Yoto.Models.DisplayIcon&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@moduledoc&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Represents a display icon uploaded for a Card.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@derive&lt;/span&gt; {&lt;span style="color:#a6e22e"&gt;Jason.Encoder&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;only&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;:mediaId&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;:userId&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;:displayIconId&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;:url&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;:new&lt;/span&gt;]}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;defstruct&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;:mediaId&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;:userId&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;:displayIconId&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;:url&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;:new&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I had mixed luck using LLMs like ChatGPT and agentic coding tools like Claude Code with Elixir. As a beginner, it robbed me of the experience (sometimes painful) that comes when you learn a new language. Worse, it often gaslit me by making up non-existent functions in the Elixir standard library or suggesting the language had features that it did not. I often &lt;strong&gt;got syntactically invalid &amp;ldquo;Elixir&amp;rdquo; from ChatGPT or Claude&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="deploying-to-aws"&gt;
Deploying to AWS
&lt;a class="heading-anchor" href="#deploying-to-aws" aria-label="Link to Deploying to AWS"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;After much struggle, I finally had an Elixir app running locally that I was happy with. This is when things got surprisingly difficult. I develop on an M-series MacBook (arm64), and I wanted to build and deploy on AWS since I already had an account and domain name in Route53. Yet there&amp;rsquo;s surprisingly little prior art showing how to deploy Elixir on AWS EC2, and even less showing how to do automated, continuous deployments using GitHub Actions.&lt;/p&gt;
&lt;p&gt;My preference was to automatically build my Elixir app an AWS EC2 instance with a Gravitron3 (arm64) CPU running Amazon Linux 2023 because this was the cheapest, and likely most secure, default configuration. With great help from the folks on Discord, I tried:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;⛔ GitHub Action + arm64 = Dead End&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Building on an &lt;code&gt;arm64&lt;/code&gt; runner (in &lt;a href="https://github.blog/news-insights/product-news/arm64-on-github-actions-powering-faster-more-efficient-build-systems/"&gt;Public Beta&lt;/a&gt;) using &lt;code&gt;ubuntu-24.04-arm&lt;/code&gt; in the hopes of deploying to an EC2 instance with a Gravitron CPU. My Actions sat idle for a day before hitting a timeout. &lt;code&gt;arm64&lt;/code&gt; runners were never available to pick up my Action.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;⛔ GitHub Action + Ubuntu (default) = Dead End&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;GitHub generously provides free compute for Actions which defaults to Ubuntu 22 running on an x86_64 CPU. Building directly within GitHub Actions and running on Amazon Linux 2023 resulted in a discrepancy between the &lt;code&gt;glibc&lt;/code&gt; version used to compile the service and the server running it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;🏆 Build and deploy on the same environment&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The best solution was to ensure the build and runtime environments matched as closely as possible. In order to do that, I used Docker to build from a &lt;a href="https://hub.docker.com/_/amazonlinux"&gt;&lt;code&gt;amazonlinux:2023&lt;/code&gt;&lt;/a&gt; base image, then download and install Elixir, Erlang, and OTP. Amazon Linux is based on Fedora, but Elixir and Erlang aren&amp;rsquo;t available via the &lt;code&gt;dnf&lt;/code&gt; package manager using it&amp;rsquo;s default repositories so they had to be build from source and installed manually.&lt;/p&gt;
&lt;p&gt;The resulting binary is then copied using Secure File Copy (SCP) onto my EC2 instance using a private key stored in GitHub Secrets. Then a script is run to create a &lt;code&gt;systemd&lt;/code&gt; unit file, and the service is started.&lt;/p&gt;
&lt;p&gt;This approach actually worked quite well for a single, small EC2 instance. Presumably there&amp;rsquo;s some downtime during deployments when the running service is stopped and the new service is started. This was too short to actually perceive, and more than acceptable for a hackathon project.&lt;/p&gt;
&lt;h3 id="tmpfs-missteps"&gt;
&lt;code&gt;tmpfs&lt;/code&gt; Missteps
&lt;a class="heading-anchor" href="#tmpfs-missteps" aria-label="Link to tmpfs Missteps"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;I picked the smallest EC2 instance size because I didn&amp;rsquo;t expect significant traffic, and I reviewed my app as primarily I/O bound. However, I forgot that there was a single step in a workflow I wrote where &lt;a href="https://imagemagick.org/"&gt;ImageMagick&lt;/a&gt; is used to scale down and pixelate a 1024x1024 image from ChatGPT into a 16x16 pixel art icon for Yoto. All &lt;strong&gt;data in Elixir is immutable&lt;/strong&gt; and passed via value. I was probably overly cautious, so to avoid keeping the PNG file in memory throughout the workflow, I instead wrote it to a temporary file and passed the file name instead.&lt;/p&gt;
&lt;p&gt;A problem arose when temporary files appeared to not get deleted, and the partition ran out of disk space. Calls to the &lt;a href="https://github.com/elixir-mogrify/mogrify"&gt;&lt;code&gt;mogrify&lt;/code&gt;&lt;/a&gt; library (which in turn just runs system commands to &lt;code&gt;System.cmd&lt;/code&gt;) failed due to insufficient space. After some research, I realized the default &lt;code&gt;tmpfs&lt;/code&gt; size is half of physical RAM without swap. That didn&amp;rsquo;t leave a lot of space on my &lt;code&gt;t3.nano&lt;/code&gt; instance with just 0.5GB of memory.&lt;/p&gt;
&lt;p&gt;I ended up using &lt;code&gt;PrivateTmp&lt;/code&gt;, a &lt;code&gt;systemd&lt;/code&gt; setting for sandboxing, to create a new namespace for &lt;code&gt;/tmp&lt;/code&gt; and &lt;code&gt;/var/tmp&lt;/code&gt;. This seems like best practice, and make sure no other process was contributing to this issue. I also used the &lt;a href="https://github.com/CargoSense/briefly"&gt;&lt;code&gt;briefly&lt;/code&gt;&lt;/a&gt; library to ensure temporary files were deleted shortly after the workflow execution completed.&lt;/p&gt;
&lt;h3 id="cloudfront-vpc-origin-doesnt-play-nice-with-websockets"&gt;
Cloudfront VPC Origin doesn&amp;rsquo;t play nice with WebSockets
&lt;a class="heading-anchor" href="#cloudfront-vpc-origin-doesnt-play-nice-with-websockets" aria-label="Link to Cloudfront VPC Origin doesn’t play nice with WebSockets"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;Perhaps the biggest dead end I faced was with AWS Cloudfront&amp;rsquo;s &lt;a href="https://aws.amazon.com/blogs/networking-and-content-delivery/introducing-cloudfront-virtual-private-cloud-vpc-origins-shield-your-web-applications-from-public-internet/"&gt;VPC Origin&lt;/a&gt; configurations. This let me set my EC2 instance, inside of a private subnet in a Virtual Private Cloud (VPC), as an origin server for a Cloudfront distribution. It seemed like a great solution: I could use ACM to auto-renew my SSL certificates, Route53 to manage the DNS entries, and I had a Content Delivery Network (CDN) in front of my small EC2 instance to reduce load via caching.&lt;/p&gt;
&lt;p&gt;However, I quickly realized that my app didn&amp;rsquo;t work as expected because the Phoenix client couldn&amp;rsquo;t establish a WebSockets connection to my server. I enabled Cloudfront logging and saw the HTTP 502 Bad Gateway errors showed log entries for &lt;code&gt;OriginDnsError&lt;/code&gt; in CloudWatch. I found several &lt;a href="https://repost.aws/questions/QU9RNe5fD_SsK7UIGGG26yOA/origindnserror-from-cloudfront-vpc-origin-when-websocket-is-used"&gt;similar issues&lt;/a&gt;, and after some consultation on the &lt;a href="https://discord.com/invite/aws"&gt;AWS Discord&lt;/a&gt;, realized this was a fundamental limitation within Cloudfront that I could not overcome. In the end, I swapped WebSockets for HTTP polling during wait states when a Yoto card was being generated.&lt;/p&gt;
&lt;p&gt;Lesson learned: &lt;strong&gt;CloudFront does not support WebSockets for VPC Origins pointing to an EC2 instance&lt;/strong&gt;. It &lt;em&gt;may&lt;/em&gt; work if you&amp;rsquo;re using a load balancer.&lt;/p&gt;
&lt;h2 id="github-action-to-build--deploy-an-elixir-service-on-aws-ec2-running-al2023"&gt;
GitHub Action to Build + Deploy an Elixir service on AWS EC2 running AL2023
&lt;a class="heading-anchor" href="#github-action-to-build--deploy-an-elixir-service-on-aws-ec2-running-al2023" aria-label="Link to GitHub Action to Build &amp;#43; Deploy an Elixir service on AWS EC2 running AL2023"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;This GitHub Action uses Docker to build a release binary of an Elixir + Phoenix web app on Amazon Linux 2023 (AL2023). GitHub Container Registry (GHCR) is used to cache intermediate build steps to reduce subsequent build times. The second step uses SCP to copy a tarball of the binary onto an EC2 host, then uses Secure Shell (SSH) to start a &lt;code&gt;systemd&lt;/code&gt; service of the Phoenix web app.&lt;/p&gt;
&lt;h3 id="secrets"&gt;
Secrets
&lt;a class="heading-anchor" href="#secrets" aria-label="Link to Secrets"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;The following values need to be added to your GitHub repository under Settings &amp;gt; Secrets and variables &amp;gt; Actions.&lt;/p&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/posts/2025-09-09-deploy-an-elixir-phoenix-web-app-aws-ec2/github-action-secrets_hu_fd57074c81f14cde.webp 320w, https://barrasso.me/posts/2025-09-09-deploy-an-elixir-phoenix-web-app-aws-ec2/github-action-secrets_hu_d8ef35708a1bf6d0.webp 640w, https://barrasso.me/posts/2025-09-09-deploy-an-elixir-phoenix-web-app-aws-ec2/github-action-secrets_hu_4a842f5f345feedd.webp 1024w' sizes="100vw" /&gt;
&lt;source type="image/png" srcset='https://barrasso.me/posts/2025-09-09-deploy-an-elixir-phoenix-web-app-aws-ec2/github-action-secrets_hu_da8ec4d38d9aceb2.png 320w, https://barrasso.me/posts/2025-09-09-deploy-an-elixir-phoenix-web-app-aws-ec2/github-action-secrets_hu_ddecfc5ae608c9d5.png 640w, https://barrasso.me/posts/2025-09-09-deploy-an-elixir-phoenix-web-app-aws-ec2/github-action-secrets_hu_24d253507a5b70cc.png 1024w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/posts/2025-09-09-deploy-an-elixir-phoenix-web-app-aws-ec2/github-action-secrets_hu_da8ec4d38d9aceb2.png"
alt=""
width="320"
height="154"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret Name&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Example Values&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EC2_HOST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The SSH host with port&lt;/td&gt;
&lt;td&gt;&lt;code&gt;172.32.18.129&lt;/code&gt; or &lt;code&gt;ec2-172-32-18-129.us-east-2.compute.amazonaws.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EC2_SSH_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The private SSH key, in &lt;code&gt;.pem&lt;/code&gt; file format&lt;/td&gt;
&lt;td&gt;Contents of your &lt;code&gt;.pem&lt;/code&gt; key file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EC2_USER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The SSH user&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ec2-user&lt;/code&gt; (default for Amazon Linux)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ENV_VARS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Environment variables in &lt;code&gt;.env&lt;/code&gt; format&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DATABASE_URL=postgres://...&lt;/code&gt;&lt;br /&gt;&lt;code&gt;API_KEY=abc123&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RELEASE_COOKIE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The release cookie for distributed Erlang&lt;/td&gt;
&lt;td&gt;Value from &lt;code&gt;releases/COOKIE&lt;/code&gt; or generate with &lt;code&gt;mix release.init&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SECRET_KEY_BASE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Phoenix secret key base&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;mix phx.gen.secret&lt;/code&gt; to generate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class="gist-embed"&gt;
&lt;div class="gist-header"&gt;
&lt;a
href="https://gist.github.com/Tombarr/17b94f79ccfc698792a2abafb8955409"
target="_blank"
rel="noopener noreferrer"
class="gist-filename"&gt;
&lt;svg
class="inline-svg"
aria-hidden="true"
height="16"
viewBox="0 0 16 16"
version="1.1"
width="16"&gt;
&lt;path
fill="currentColor"
d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0113.25 16h-9.5A1.75 1.75 0 012 14.25V1.75zm1.75-.25a.25.25 0 00-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 00.25-.25V6h-2.75A1.75 1.75 0 019 4.25V1.5H3.75a.25.25 0 00-.25.25z"&gt;&lt;/path&gt;
&lt;/svg&gt;
deploy-elixir-ec2.yml
&lt;/a&gt;
&lt;/div&gt;
&lt;div class="gist-code-container"&gt;
&lt;div&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Build &amp;amp; Deploy Phoenix (linux/x86) to EC2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;on&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;push&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;branches&lt;/span&gt;: [ &lt;span style="color:#e6db74"&gt;&amp;#34;main&amp;#34;&lt;/span&gt; ]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;workflow_dispatch&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;env&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;MIX_ENV&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;prod&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ELIXIR_VERSION&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;1.18.4&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;# adjust as needed&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;OTP_VERSION&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;27.3.4.2&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;# match what you have in prod&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;BASE_OTP_VERSION&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;27&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;# match what you have in prod&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;APP_NAME&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;myapp&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;# lower_snake_case as created by mix new&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;PHX_PORT&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;4000&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;PHX_HOST&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;example.com&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;jobs&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;build&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;permissions&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;contents&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;read&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;packages&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;write&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Build linux/x86 release&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;runs-on&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ubuntu-latest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;environment&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;prod&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;steps&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Checkout&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/checkout@v4&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Set up Docker Buildx&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;docker/setup-buildx-action@v3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;buildkitd-flags&lt;/span&gt;: --&lt;span style="color:#ae81ff"&gt;debug&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;install&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;driver-opts&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; image=moby/buildkit:buildx-stable-1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; network=host&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Create optimized Dockerfile&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; cat &amp;lt;&amp;lt;EOF &amp;gt; Dockerfile.build
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; FROM amazonlinux:2023 AS base
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; # Install build tools and dependencies (cached layer)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; RUN dnf update -y &amp;amp;&amp;amp; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; dnf install -y \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; git tar gzip gcc gcc-c++ make wget \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; openssl-devel ncurses-devel libxslt libxml2 \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; openssl-static ncurses-static zlib-static \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; unzip libtool \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --skip-broken &amp;amp;&amp;amp; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; dnf clean all
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; # Download stage - separate for early failure and caching
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; FROM base AS downloader
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; ARG OTP_VERSION
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; ARG ELIXIR_VERSION
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; ARG BASE_OTP_VERSION
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; # Download files in parallel where possible
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; RUN echo &amp;#34;Downloading OTP \${OTP_VERSION} and Elixir \${ELIXIR_VERSION}...&amp;#34; &amp;amp;&amp;amp; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; wget -q --spider &amp;#34;https://github.com/erlang/otp/releases/download/OTP-\${OTP_VERSION}/otp_src_\${OTP_VERSION}.tar.gz&amp;#34; &amp;amp;&amp;amp; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; wget -q --spider &amp;#34;https://github.com/elixir-lang/elixir/releases/download/v\${ELIXIR_VERSION}/elixir-otp-\${BASE_OTP_VERSION}.zip&amp;#34; &amp;amp;&amp;amp; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; wget -q &amp;#34;https://github.com/erlang/otp/releases/download/OTP-\${OTP_VERSION}/otp_src_\${OTP_VERSION}.tar.gz&amp;#34; -O /tmp/otp_src.tar.gz &amp;amp; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; wget -q &amp;#34;https://github.com/elixir-lang/elixir/releases/download/v\${ELIXIR_VERSION}/elixir-otp-\${BASE_OTP_VERSION}.zip&amp;#34; -O /tmp/elixir-precompiled.zip &amp;amp; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; wait
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; # OTP build stage - heavily optimized
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; FROM downloader AS otp-builder
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; COPY --from=downloader /tmp/otp_src.tar.gz /tmp/
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; RUN echo &amp;#34;Building OTP with optimizations...&amp;#34; &amp;amp;&amp;amp; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; tar -xzf /tmp/otp_src.tar.gz -C /tmp &amp;amp;&amp;amp; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; cd /tmp/otp_src_* &amp;amp;&amp;amp; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; ./configure \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --disable-jit \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --disable-hipe \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --disable-sctp \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --disable-silent-rules \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --enable-shared-zlib \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --enable-threads \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --with-ssl \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --without-javac \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --without-odbc \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --without-wx &amp;amp;&amp;amp; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; make -j\$(nproc) &amp;amp;&amp;amp; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; make install &amp;amp;&amp;amp; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; rm -rf /tmp/*
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; # Final runtime stage
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; FROM base AS runtime
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; # Copy OTP from builder stage
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; COPY --from=otp-builder /usr/local /usr/local
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; # Install Elixir from precompiled binary
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; COPY --from=downloader /tmp/elixir-precompiled.zip /tmp/
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; RUN unzip -q /tmp/elixir-precompiled.zip -d /usr/local/elixir &amp;amp;&amp;amp; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; ln -sf /usr/local/elixir/bin/elixir /usr/local/bin/elixir &amp;amp;&amp;amp; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; ln -sf /usr/local/elixir/bin/mix /usr/local/bin/mix &amp;amp;&amp;amp; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; ln -sf /usr/local/elixir/bin/iex /usr/local/bin/iex &amp;amp;&amp;amp; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; rm -f /tmp/elixir-precompiled.zip
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; # Verify installations and pre-install common tools
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; RUN erl -version &amp;amp;&amp;amp; elixir --version &amp;amp;&amp;amp; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; mix local.hex --force &amp;amp;&amp;amp; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; mix local.rebar --force
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; # Set up working directory
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; WORKDIR /app
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; EOF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Log in to GitHub Container Registry&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;docker/login-action@v3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;registry&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ghcr.io&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;username&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;${{ github.actor }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;password&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;logout&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Build optimized container with caching&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; REPO_LC=$(echo &amp;#39;${{ github.repository }}&amp;#39; | tr &amp;#39;[:upper:]&amp;#39; &amp;#39;[:lower:]&amp;#39;) &amp;amp;&amp;amp; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; docker buildx build \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --build-arg OTP_VERSION=${{ env.OTP_VERSION }} \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --build-arg ELIXIR_VERSION=${{ env.ELIXIR_VERSION }} \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --build-arg BASE_OTP_VERSION=${{ env.BASE_OTP_VERSION }} \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --cache-from=&amp;#34;type=registry,ref=ghcr.io/${REPO_LC}/builder-cache:latest&amp;#34; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --cache-to=&amp;#34;type=registry,ref=ghcr.io/${REPO_LC}/builder-cache:latest,mode=max&amp;#34; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --load \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; -t amazon-linux-builder \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; -f Dockerfile.build .&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Create artifact directory&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;mkdir -p release_out&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Build Phoenix Release with optimizations&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; docker run --rm \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; -v ${{ github.workspace }}:/app \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; -v $PWD/release_out:/app/release_out \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; -e MIX_ENV=${{ env.MIX_ENV }} \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; -e SECRET_KEY_BASE=${{ secrets.SECRET_KEY_BASE }} \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; -e RELEASE_COOKIE=${{ secrets.RELEASE_COOKIE }} \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; -e ELIXIR_ERL_OPTIONS=&amp;#34;+fnu&amp;#34; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --workdir /app \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; amazon-linux-builder \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; /bin/bash -c &amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; set -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; # Ensure release_out directory exists inside container
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; mkdir -p /app/release_out
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; echo &amp;#39;Getting dependencies...&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; mix deps.get --only \${MIX_ENV}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; echo &amp;#39;Compiling with optimizations...&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; # Compile with multiple jobs and optimizations
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; MIX_ENV=\${MIX_ENV} mix compile --force --all-warnings
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; echo &amp;#39;Building release...&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; MIX_ENV=\${MIX_ENV} mix release --overwrite
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; echo &amp;#39;Packaging artifact...&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; tar -C _build/\${MIX_ENV}/rel/${{ env.APP_NAME }} -czf /app/release_out/${{ env.APP_NAME }}.tar.gz .
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; echo &amp;#39;Build complete!&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Upload artifact&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/upload-artifact@v4&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;${{ env.APP_NAME }}-x86-release&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;path&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;release_out/${{ env.APP_NAME }}.tar.gz&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;retention-days&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;outputs&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;app_name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;${{ env.APP_NAME }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;deploy&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Deploy to EC2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;runs-on&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ubuntu-latest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;needs&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;build&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;environment&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;prod&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;if&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;github.ref == &amp;#39;refs/heads/main&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;env&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;APP_NAME&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;${{ needs.build.outputs.app_name }}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;REMOTE_DIR&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;/opt/${{ needs.build.outputs.app_name }}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;SERVICE_NAME&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;${{ needs.build.outputs.app_name }}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ENV_FILE&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;/etc/${{ needs.build.outputs.app_name }}.env&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;steps&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Download artifact&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/download-artifact@v4&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;${{ env.APP_NAME }}-x86-release&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;path&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Copy release to EC2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;appleboy/scp-action@v0.1.7&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;host&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;${{ secrets.EC2_HOST }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;username&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;${{ secrets.EC2_USER }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;key&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;${{ secrets.EC2_SSH_KEY }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;source&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;${{ env.APP_NAME }}.tar.gz&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;target&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;/tmp/&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;debug&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Configure &amp;amp; restart service on EC2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;appleboy/ssh-action@v1.0.3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;host&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;${{ secrets.EC2_HOST }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;username&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;${{ secrets.EC2_USER }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;key&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;${{ secrets.EC2_SSH_KEY }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;script&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; set -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; APP_NAME=&amp;#34;${{ env.APP_NAME }}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; REMOTE_DIR=&amp;#34;${{ env.REMOTE_DIR }}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; SERVICE_NAME=&amp;#34;${{ env.SERVICE_NAME }}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; ENV_FILE=&amp;#34;${{ env.ENV_FILE }}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; TARBALL=&amp;#34;/tmp/${APP_NAME}.tar.gz&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; # Create app directory and user if needed
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; if ! id -u &amp;#34;$APP_NAME&amp;#34; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; sudo useradd -r -s /bin/false -M &amp;#34;$APP_NAME&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; fi
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; sudo mkdir -p &amp;#34;$REMOTE_DIR&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; sudo chown -R &amp;#34;$APP_NAME&amp;#34;:&amp;#34;$APP_NAME&amp;#34; &amp;#34;$REMOTE_DIR&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; # Write/refresh environment file (runtime config)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; sudo bash -c &amp;#34;cat &amp;gt; $ENV_FILE&amp;#34; &amp;lt;&amp;lt; &amp;#39;EOF&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; MIX_ENV=prod
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; PHX_SERVER=true
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; PORT=${{ env.PHX_PORT }}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; PHX_HOST=${{ env.PHX_HOST }}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; SECRET_KEY_BASE=${SECRET_KEY_BASE}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; RELEASE_COOKIE=${RELEASE_COOKIE}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; ${{ secrets.ENV_VARS }}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; EOF
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; # Substitute secrets into env file
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; sudo sed -i &amp;#34;s|\${SECRET_KEY_BASE}|${{ secrets.SECRET_KEY_BASE }}|g&amp;#34; &amp;#34;$ENV_FILE&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; sudo sed -i &amp;#34;s|\${RELEASE_COOKIE}|${{ secrets.RELEASE_COOKIE }}|g&amp;#34; &amp;#34;$ENV_FILE&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; sudo sed -i &amp;#34;s|\${PHX_HOST}|${{ env.PHX_HOST || &amp;#39;localhost&amp;#39; }}|g&amp;#34; &amp;#34;$ENV_FILE&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; sudo sed -i &amp;#34;s|\${PORT}|${{ env.PHX_PORT || &amp;#39;80&amp;#39; }}|g&amp;#34; &amp;#34;$ENV_FILE&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; # Lock down environment file
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; sudo chown root:root &amp;#34;$ENV_FILE&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; sudo chmod 600 &amp;#34;$ENV_FILE&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; # Install or refresh systemd service
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; SERVICE_FILE=&amp;#34;/etc/systemd/system/${SERVICE_NAME}.service&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; sudo bash -c &amp;#34;cat &amp;gt; $SERVICE_FILE&amp;#34; &amp;lt;&amp;lt; EOF
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; [Unit]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; Description=${APP_NAME} Phoenix app
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; After=network-online.target
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; Wants=network-online.target
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; [Service]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; Type=simple
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; User=${APP_NAME}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; Group=${APP_NAME}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; EnvironmentFile=${ENV_FILE}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; WorkingDirectory=${REMOTE_DIR}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; ExecStart=${REMOTE_DIR}/bin/${APP_NAME} start
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; ExecStop=${REMOTE_DIR}/bin/${APP_NAME} stop
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; Restart=always
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; RestartSec=5
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; PrivateTmp=true
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; LimitNOFILE=65536
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; [Install]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; WantedBy=multi-user.target
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; EOF
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; sudo systemctl daemon-reload
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; sudo systemctl enable &amp;#34;${SERVICE_NAME}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; # Deploy new release
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; TMP_DIR=&amp;#34;$(mktemp -d)&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; sudo tar -C &amp;#34;$TMP_DIR&amp;#34; -xzf &amp;#34;$TARBALL&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; if [ -d &amp;#34;${REMOTE_DIR}/var&amp;#34; ]; then
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; sudo rsync -a &amp;#34;${REMOTE_DIR}/var/&amp;#34; &amp;#34;${TMP_DIR}/var/&amp;#34; || true
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; fi
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; sudo rsync -a --delete &amp;#34;${TMP_DIR}/&amp;#34; &amp;#34;${REMOTE_DIR}/&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; sudo chown -R ${APP_NAME}:${APP_NAME} &amp;#34;${REMOTE_DIR}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; # Clean old logs for a fresh journal view
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; sudo systemctl stop &amp;#34;${SERVICE_NAME}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; sudo journalctl --rotate
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; sudo journalctl --vacuum-time=1s
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; # Restart service with clean logs
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; sudo systemctl start &amp;#34;${SERVICE_NAME}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; sudo systemctl status &amp;#34;${SERVICE_NAME}&amp;#34; --no-pager -l || true
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; echo &amp;#34;Deployment complete.&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; # Log service status directly
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; sudo journalctl -u &amp;#34;${SERVICE_NAME}&amp;#34; -n 20 --since &amp;#34;10 second ago&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="gist-footer"&gt;
&lt;div class="gist-footer-left"&gt;
deploy-elixir-ec2.yml
&lt;/div&gt;
&lt;div class="gist-footer-right"&gt;
&lt;a
href="https://gist.githubusercontent.com/Tombarr/17b94f79ccfc698792a2abafb8955409/raw/093cf7f84d02ec3e0841f291bf42e7be5080e562/deploy-elixir-ec2.yml"
target="_blank"
rel="noopener noreferrer"
class="gist-raw-link"&gt;
view raw
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2025-09-09-deploy-an-elixir-phoenix-web-app-aws-ec2/</guid><pubDate>Tue, 09 Sep 2025 00:00:00 GMT</pubDate></item><item><title>
Backyard Birds</title><link>https://barrasso.me/projects/backyard-birds/</link><description>
&lt;p&gt;Backyard Birds is an educational app created for the &lt;a href="https://us.yotoplay.com/yoto-player"&gt;Yoto Player&lt;/a&gt;, a screen-free audio platform for kids. Backyard Birds brings the magic of birdwatching to children and families through personalized audio cards. The app uses AI (using OpenAI &amp;amp; Gemini) to write and narrate (using ElevanLabs) stories featuring birds recently sighted nearby, complete with authentic bird calls, interesting facts, and vibrant icons.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;
Backyard Birds was voted a &lt;strong&gt;&lt;a href="https://yoto.space/news/post/the-developer-challenge-2025-winners-bbCk0Y8q8fK6JNY"&gt;Community Choice Winner&lt;/a&gt;&lt;/strong&gt; in the &lt;a href="https://yoto.space/news/post/build-a-yoto-app-and-you-could-win-up-to-5-000-hQVNmKqCsfLNoj0"&gt;Yoto 2025 Developer Challenge&lt;/a&gt;. Alongside Backyard Birds, I created another project, &lt;a href="https://yoto.space/developers/post/fez-the-cat-7XbFLVY7sXNlNVA"&gt;Fez the Cat&lt;/a&gt;, that delighted kids and parents alike with daily stories and letter-guessing games that test listening comprehension and spelling skills.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/projects/backyard-birds/</guid><pubDate>Mon, 01 Sep 2025 00:00:00 GMT</pubDate></item><item><title>
Tiny Screens, Big Impact: The Forgotten Art Of Developing Web Apps For Feature Phones</title><link>https://barrasso.me/posts/2025-07-16-tiny-screens-big-impact-the-forgotten-art-of-developing-web-apps-for-feature-phones/</link><description>
&lt;p&gt;Learn why flip phones still matter in 2025, and how you can build and launch web apps for these tiny devices.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2025-07-16-tiny-screens-big-impact-the-forgotten-art-of-developing-web-apps-for-feature-phones/</guid><pubDate>Wed, 16 Jul 2025 00:00:00 GMT</pubDate></item><item><title>
How PodLP Unlocks 3B+ Listeners Beyond Smartphones</title><link>https://barrasso.me/talks/2025-05-27-how-podlp-unlocks-3b-listeners-beyond-smartphones/</link><description>
&lt;p&gt;PodcastingTech Show host Mathew Passy chats with Tom Barrasso, founder and developer of &lt;a href="https://podlp.com"&gt;PodLP&lt;/a&gt;, the innovative app that brings podcasts to feature phones (&amp;ldquo;dumb phones&amp;rdquo;) around the world. If you’ve ever assumed your listeners all have iPhones or Androids, this episode will seriously open your eyes!&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/talks/2025-05-27-how-podlp-unlocks-3b-listeners-beyond-smartphones/</guid><pubDate>Tue, 27 May 2025 00:00:00 GMT</pubDate></item><item><title>
Remote Browsers and the Feature Phone Market with Tom Barrasso</title><link>https://barrasso.me/talks/2025-05-01-why-flip-phones-still-matter-in-2025-w-tom-barrasso/</link><description>
&lt;p&gt;Matt sits down with Tom Barrasso from &lt;a href="https://developer.cloudphone.tech"&gt;Cloud Phone&lt;/a&gt; to explore why flip phones are still relevant in 2025. They discuss the rise of digital detox, who’s using feature phones today, and how developers can still build apps for low-spec devices. From KaiOS and Cloud Phone to nostalgia and screen-time reduction—this is a deep dive into the flip phone revival and the tech powering it.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/talks/2025-05-01-why-flip-phones-still-matter-in-2025-w-tom-barrasso/</guid><pubDate>Thu, 01 May 2025 00:00:00 GMT</pubDate></item><item><title>
Feature phones, remote browsers and connecting the next billion users</title><link>https://barrasso.me/talks/2025-04-23-feature-phones-remote-browsers-and-connecting-the-next-billion-users-tom-barrasso/</link><description>
&lt;p&gt;Explore the opportunities and challenges of building web applications for feature phones powered by remote Chromium browsers. Dive into what makes feature phones appealing in certain regions, how cloud-rendered browsers change the developer experience, and the practical aspects of working with these platforms from API support and navigation patterns to PWAs and future-facing capabilities.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/talks/2025-04-23-feature-phones-remote-browsers-and-connecting-the-next-billion-users-tom-barrasso/</guid><pubDate>Wed, 23 Apr 2025 00:00:00 GMT</pubDate></item><item><title>
Remote Browsers and the Feature Phone Market with Tom Barrasso</title><link>https://barrasso.me/talks/2025-04-09-remote-browsers-and-the-feature-phone-market-with-tom-barrasso/</link><description>
&lt;p&gt;Explore the world of feature phones, how remote browsers make it possible to run modern apps on these low-end devices, and the unique challenges and opportunities for developers in this space.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/talks/2025-04-09-remote-browsers-and-the-feature-phone-market-with-tom-barrasso/</guid><pubDate>Wed, 09 Apr 2025 00:00:00 GMT</pubDate></item><item><title>
Millions Could Be Using Your App on Flip Phones</title><link>https://barrasso.me/posts/2025-03-25-millions-could-be-using-your-app-on-flip-phones/</link><description>
&lt;p&gt;In markets like India, sales of 4G feature phones are growing quickly as network operators transition from 2G to 4G &amp;amp; 5G. With just a few changes, your web app can be in the hands of millions on T9 feature phones.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2025-03-25-millions-could-be-using-your-app-on-flip-phones/</guid><pubDate>Tue, 25 Mar 2025 00:00:00 GMT</pubDate></item><item><title>
Wasm on feature phones with Cloud Phone's Thomas Barrasso</title><link>https://barrasso.me/talks/2025-03-24-wasm-on-feature-phones-with-cloud-phones-thomas-barrasso/</link><description>
&lt;p&gt;Feature phones? Yes, they still make them. And they run Wasm! In this WasmAssembly podcast, Thomas Steiner hosts Thomas Barrasso from &lt;a href="https://puffin.com"&gt;CloudMosa&lt;/a&gt; to talk about the Cloud Phone platform and what it takes to run WebAssembly on tiny feature phones by streaming Web apps from a remote server that runs Chromium.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/talks/2025-03-24-wasm-on-feature-phones-with-cloud-phones-thomas-barrasso/</guid><pubDate>Mon, 24 Mar 2025 00:00:00 GMT</pubDate></item><item><title>
Cloud Phone for Developers</title><link>https://barrasso.me/projects/cloud-phone/</link><description>
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
&lt;iframe src="https://www.youtube-nocookie.com/embed/u_OwAl8r6nY?rel=0}&amp;start=0&amp;end=0"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;"
allowfullscreen=""&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;
Cloud Phone is a Chromium-based remote browser from CloudMosa, the company behind the &lt;a href="https://puffin.com"&gt;Puffin&lt;/a&gt; browser. Cloud Phone lets users watch YouTube and TikTok on budget features phones with as little as 16MB RAM! As the &lt;strong&gt;Head of Developer Relations&lt;/strong&gt;, I designed, authored, and built the &lt;a href="https://developer.cloudfone.com"&gt;Cloud Phone for Developers&lt;/a&gt; website, made &lt;a href="https://barrasso.me/talks/"&gt;guest appearances&lt;/a&gt; on podcasts, filmed and &lt;a href="https://www.youtube.com/@CloudPhone-CloudMosa/shorts"&gt;published content&lt;/a&gt;, developed &lt;a href="https://github.com/search?q=topic%3Acloudphone+org%3Acloudmosa+fork%3Atrue&amp;amp;type=repositories"&gt;reference applications&lt;/a&gt;, published &lt;a href="https://github.com/cloudmosa/cloudphone-types"&gt;TypeScript type declarations&lt;/a&gt;, and supported many Cloud Phone developers.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;
Cloud Phone for Developers is a static website generated using &lt;a href="https://gohugo.io/"&gt;Hugo&lt;/a&gt; using a custom &lt;a href="https://getdoks.org/"&gt;Doks&lt;/a&gt;-based theme and &lt;a href="https://docsearch.algolia.com/"&gt;Algolia DocSearch&lt;/a&gt;. It&amp;rsquo;s continuously checked for &lt;a href="https://crates.io/crates/typos"&gt;typos&lt;/a&gt; and updated via &lt;a href="https://github.com/features/actions"&gt;GitHub Actions&lt;/a&gt;. Screenshots, code snippets, and resources are based on the feedback of developers from around the world including India, Nepal, Bangladesh, Pakistan, Vietnam, South Africa, and the United States.&lt;/p&gt;
&lt;br /&gt;
&lt;h2 id="reference-applications"&gt;
&lt;strong&gt;Reference Applications&lt;/strong&gt;
&lt;a class="heading-anchor" href="#reference-applications" aria-label="Link to Reference Applications"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;br /&gt;
&lt;h3 id="currency-converter"&gt;
Currency Converter
&lt;a class="heading-anchor" href="#currency-converter" aria-label="Link to Currency Converter"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;br /&gt;
&lt;p&gt;I built open-source reference applications like a &lt;a href="https://github.com/cloudmosa/cloudphone-currency"&gt;Currency Converter&lt;/a&gt; and &lt;a href="https://github.com/cloudmosa/authenticator"&gt;Authenticator&lt;/a&gt; for time-based one-time passwords (OTP).&lt;/p&gt;
&lt;br /&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/projects/cloud-phone/currency-converter_hu_822da6449a11cec0.webp 320w, https://barrasso.me/projects/cloud-phone/currency-converter_hu_416652e8e33212d5.webp 640w, https://barrasso.me/projects/cloud-phone/currency-converter_hu_503154b6cf16350f.webp 1024w' sizes="100vw" /&gt;
&lt;source type="image/png" srcset='https://barrasso.me/projects/cloud-phone/currency-converter_hu_3697d7b9b5838212.png 320w, https://barrasso.me/projects/cloud-phone/currency-converter_hu_b8e793c4b2b89af8.png 640w, https://barrasso.me/projects/cloud-phone/currency-converter_hu_2e74c1bf3f2c8049.png 1024w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/projects/cloud-phone/currency-converter_hu_3697d7b9b5838212.png"
alt="Cloud Phone Currency Converter" class="w-full contain"
width="320"
height="379"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;br /&gt;
&lt;h3 id="authenticator"&gt;
Authenticator
&lt;a class="heading-anchor" href="#authenticator" aria-label="Link to Authenticator"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;br /&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/projects/cloud-phone/mfa-sync_hu_b3d1ba63dcfc7138.webp 320w, https://barrasso.me/projects/cloud-phone/mfa-sync_hu_f3f90cf33d70f3e4.webp 640w, https://barrasso.me/projects/cloud-phone/mfa-sync_hu_e5182a3e0e998be3.webp 1024w, https://barrasso.me/projects/cloud-phone/mfa-sync_hu_1d7488539e5e7ecf.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/png" srcset='https://barrasso.me/projects/cloud-phone/mfa-sync_hu_8e9ca1ab310aeec0.png 320w, https://barrasso.me/projects/cloud-phone/mfa-sync_hu_463dd4ea13be124f.png 640w, https://barrasso.me/projects/cloud-phone/mfa-sync_hu_4436fcf98c332d07.png 1024w, https://barrasso.me/projects/cloud-phone/mfa-sync_hu_ae6da6feab309593.png 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/projects/cloud-phone/mfa-sync_hu_8e9ca1ab310aeec0.png"
alt="Cloud Phone Multi-Factor Authenticator App" class="w-full contain"
width="320"
height="280"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;br /&gt;
&lt;p&gt;Each reference application expands the app catalog with valuable utilities, while providing real-world codebases and &lt;a href="https://developer.cloudfone.com/docs/walkthroughs/currency-converter/"&gt;tutorials&lt;/a&gt; to showcase a platform-specific user experience for developers.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/projects/cloud-phone/</guid><pubDate>Fri, 01 Nov 2024 00:00:00 GMT</pubDate></item><item><title>
Policy Zones</title><link>https://barrasso.me/projects/policy-zones/</link><description>
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
&lt;iframe src="https://www.youtube-nocookie.com/embed/LzxfxL_lE80?rel=0}&amp;start=0&amp;end=0"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;"
allowfullscreen=""&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;
Get ready for the acronyms! As a Staff Technical Program Manager (TPM), I led the adoption of Privacy Aware Infrastructure (PAI) for privacy regulations like GDPR, DMA, CCPA, and ePD. I met with dozens of teams across Meta to pilot and scale information flow control (IFC) systems like Policy Zones to &lt;strong&gt;enforce flow restrictions at runtime&lt;/strong&gt;. Programs I led ensured robust data protections across dozens of databases and processors at &lt;strong&gt;exabyte scale&lt;/strong&gt;, including my principal domain focused on Artificial Intelligence (AI) data collection, model training, and inference serving systems.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;
Basically, I was a digital janitor making sure data was appropriately stored, processed, and deleted.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/projects/policy-zones/</guid><pubDate>Sun, 01 Sep 2024 00:00:00 GMT</pubDate></item><item><title>
Vipaso</title><link>https://barrasso.me/projects/vipaso/</link><description>
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
&lt;iframe src="https://www.youtube-nocookie.com/embed/eZrcJxmX8uY?rel=0}&amp;start=0&amp;end=0"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;"
allowfullscreen=""&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;
Vipaso provides secure mobile payments using Bluetooth Low Energy (BLE) to connect merchants and buyers without the need for expensive terminals or specialized hardware. I was contacted via &lt;a href="https://kaios.dev"&gt;KaiOS.dev&lt;/a&gt; to research and develop the Vipaso app for &lt;a href="https://kaiostech.com"&gt;KaiOS&lt;/a&gt;. The app uses the &lt;a href="https://wiki.mozilla.org/B2G/Bluetooth"&gt;Bluetooth Web API&lt;/a&gt;, a Firefox OS-specific predecessor to &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API"&gt;WebBluetooth API&lt;/a&gt;, to expose Generic Attribute Profile (GATT) services to discover payment intents.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;
Vipaso is a cross-platform payment app that supports payments on Bluetooth-enabled mobile devices running iOS, Android, or KaiOS.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/projects/vipaso/</guid><pubDate>Wed, 01 May 2024 00:00:00 GMT</pubDate></item><item><title>
CVE-2023-33293</title><link>https://barrasso.me/posts/2023-05-22-cve-2023-33293/</link><description>
&lt;p&gt;A vulnerability (CVSS 5.3) in KaiOS 3.0 and 3.1 where the /system/kaios/api-daemon binary exposes a local web server on *.localhost with subdomains for each installed application, allowing attackers to determine which apps are installed and access their manifest.webmanifest contents.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2023-05-22-cve-2023-33293/</guid><pubDate>Mon, 22 May 2023 00:00:00 GMT</pubDate></item><item><title>
CVE-2023-33294</title><link>https://barrasso.me/posts/2023-05-22-cve-2023-33294/</link><description>
&lt;p&gt;A critical vulnerability (CVSS 9.8) in KaiOS 3.0 before 3.1 where the /system/bin/tctweb_server binary exposes an unprotected local web server on port 2929 that accepts and executes arbitrary Bash commands as root, accessible to all websites via the browser.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2023-05-22-cve-2023-33294/</guid><pubDate>Mon, 22 May 2023 00:00:00 GMT</pubDate></item><item><title>
CVE-2023-27108</title><link>https://barrasso.me/posts/2023-02-23-cve-2023-27108/</link><description>
&lt;p&gt;A vulnerability in KaiOS 3.0 where the pre-installed Communications application exposes a Web Activity that returns the user&amp;rsquo;s call log without origin or permission checks, enabling attackers to inject JavaScript code and transmit call logs to remote servers.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2023-02-23-cve-2023-27108/</guid><pubDate>Thu, 23 Feb 2023 00:00:00 GMT</pubDate></item><item><title>
Amazon Pharmacy</title><link>https://barrasso.me/projects/amazon-pharmacy/</link><description>
&lt;p&gt;As a Software Development Engineer (SDE), I built the real-time engine to calculate, collect, reconcile, and audit sales tax at &lt;a href="https://www.pillpack.com/"&gt;PillPack&lt;/a&gt; and Amazon Pharmacy, part of the HIPAA covered entities for Amazon Health Services. As a Technical Program Manager (TPM), I led application security certification across many healthcare services. I led threat modeling, penetration testing (red teamimg), vulnerability triaging, incident response, and remediation.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/projects/amazon-pharmacy/</guid><pubDate>Tue, 17 Nov 2020 00:00:00 GMT</pubDate></item><item><title>
Is podcasting ready for IPv6?</title><link>https://barrasso.me/posts/2020-09-10-is-podcasting-ready-for-ipv6/</link><description>
&lt;p&gt;The world has been running out of IPv4 addresses for some time. Podcasting may not be prepared for the inevitable IPv6 future&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2020-09-10-is-podcasting-ready-for-ipv6/</guid><pubDate>Thu, 10 Sep 2020 00:00:00 GMT</pubDate></item><item><title>
Podcasts for the Next Billion</title><link>https://barrasso.me/posts/2020-06-24-podcasts-for-the-next-billion/</link><description>
&lt;p&gt;Designing a podcast client for &lt;a href="https://kaiostech.com"&gt;KaiOS&lt;/a&gt; smart feature phones&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2020-06-24-podcasts-for-the-next-billion/</guid><pubDate>Wed, 24 Jun 2020 00:00:00 GMT</pubDate></item><item><title>
PodLP</title><link>https://barrasso.me/projects/podlp/</link><description>
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
&lt;iframe src="https://www.youtube-nocookie.com/embed/jjFP9yMVAM4?rel=0}&amp;start=0&amp;end=0"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;"
allowfullscreen=""&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;
PodLP is the first and most popular podcast app for feature phones, available on &lt;a href="https://www.kaiostech.com/store/apps/?bundle_id=com.podlp.podlp"&gt;KaiOS&lt;/a&gt;, the JioPhone, and Cloud Phone. With &lt;strong&gt;10+ million installs&lt;/strong&gt; across more than &lt;strong&gt;175 countries&lt;/strong&gt;, PodLP brings the podcast listening experience to the next billion listeners on affordable devices.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;
PodLP was a &lt;a href="https://www.kaiostech.com/discover-podlp-the-leading-podcast-app-for-kaios-smart-feature-phones/"&gt;2020 KaiOS Staff Pick&lt;/a&gt;, featured on &lt;a href="https://podnews.net/update/kaios-podcast-app"&gt;Podnews&lt;/a&gt;, &lt;a href="https://www.wbur.org/citrus/2020/12/15/public-radio-future-digital-divide"&gt;WBUR&lt;/a&gt;, and &lt;a href="https://soundsprofitable.com/the-download/improving-accessibility/"&gt;Sounds Profitable&lt;/a&gt;. Users find PodLP &lt;a href="https://www.reddit.com/r/dumbphones/comments/1mdq996/edc_of_a_22f_in_nyc_3_months_with_the_nokia_2780/"&gt;easy to use&lt;/a&gt; and one of &lt;a href="https://www.reddit.com/r/dumbphones/comments/1oe18e2/most_useful_kaios_apps/"&gt;most useful apps&lt;/a&gt;. You can also listen to podcasts discussing PodLP including &lt;a href="https://www.buzzsprout.com/231452/episodes/4419371-india-s-newest-podcast-app-magic-mastering-update"&gt;Buzzcast&lt;/a&gt;, &lt;a href="https://sveltesociety.dev"&gt;Svelte Society&lt;/a&gt;, and &lt;a href="https://www.podcastingtech.com/tom-barrasso-reveals-how-podlp-unlocks-3b-listeners-beyond-smartphones/"&gt;Podcasting Tech&lt;/a&gt;.&lt;/p&gt;
&lt;br /&gt;
&lt;h2 id="labs--libraries"&gt;
&lt;strong&gt;Labs &amp;amp; Libraries&lt;/strong&gt;
&lt;a class="heading-anchor" href="#labs--libraries" aria-label="Link to Labs &amp;amp; Libraries"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;br /&gt;
PodLP has branched out into experiments like &lt;a href="https://podcal.app/"&gt;PodCal&lt;/a&gt;, a tool to subscribe to podcasts in your calendar app, an NPM package to parse RSS feeds called &lt;a href="https://www.npmjs.com/package/podparse"&gt;&lt;code&gt;podparse&lt;/code&gt;&lt;/a&gt;, and feature suggestions for the &lt;a href="https://github.com/Podcastindex-org/podcast-namespace/issues/174"&gt;Podcast Index&lt;/a&gt; XML namespace.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/projects/podlp/podcal-promo_hu_60970af1b1bdce3f.webp 320w, https://barrasso.me/projects/podlp/podcal-promo_hu_92c5dce3ca1f8e66.webp 640w, https://barrasso.me/projects/podlp/podcal-promo_hu_b312a6236d1aadb2.webp 1024w, https://barrasso.me/projects/podlp/podcal-promo_hu_c3339d346468a264.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/png" srcset='https://barrasso.me/projects/podlp/podcal-promo_hu_7d8211add862dfe.png 320w, https://barrasso.me/projects/podlp/podcal-promo_hu_a91654ddd3f9c719.png 640w, https://barrasso.me/projects/podlp/podcal-promo_hu_b0754e0cfaf5eb53.png 1024w, https://barrasso.me/projects/podlp/podcal-promo_hu_702e2d2f458bc021.png 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/projects/podlp/podcal-promo_hu_7d8211add862dfe.png"
alt="PodCal" class="w-full contain"
width="320"
height="233"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;
Alongside PodLP, I authored &lt;a href="https://kaios.dev"&gt;KaiOS.dev&lt;/a&gt; to share lessons from 5+ years of experience building, launching, and scaling apps on feature phones. I also built &lt;a href="https://kaios.app"&gt;KaiOS.app&lt;/a&gt; providing public landing pages with screenshots, and on KaiOS, each page launches the app details page on the KaiStore or JioStore using &lt;a href="https://kaios.dev/2023/02/web-activities-on-kaios/"&gt;Web Activities&lt;/a&gt;. Both websites are open source on GitHub (&lt;a href="https://github.com/Tombarr/kaios-dev-blog"&gt;&lt;code&gt;kaios-dev-blog&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/Tombarr/kaios-app/"&gt;&lt;code&gt;kaios-app&lt;/code&gt;&lt;/a&gt;), alongside my NPM package &lt;a href="https://www.npmjs.com/package/kaios-types"&gt;&lt;code&gt;kaios-types&lt;/code&gt;&lt;/a&gt; offering TypeScript type declarations for dozens of non-standard APIs.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/projects/podlp/</guid><pubDate>Wed, 01 Jan 2020 00:00:00 GMT</pubDate></item><item><title>
Why does JavaScript have -0?</title><link>https://barrasso.me/posts/2019-12-03-why-does-javascript-have-negative-zero/</link><description>
&lt;p&gt;Why so negative zero? JavaScript has two zeroes and they&amp;rsquo;re both the same and different&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2019-12-03-why-does-javascript-have-negative-zero/</guid><pubDate>Tue, 03 Dec 2019 00:00:00 GMT</pubDate></item><item><title>
Scale Securely with Transit Gateway</title><link>https://barrasso.me/posts/2019-12-02-scale-securely-with-transit-gateway/</link><description>
&lt;p&gt;AWS Transit Gateway provides secure and scalable cloud connectivity to on-premise networks&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2019-12-02-scale-securely-with-transit-gateway/</guid><pubDate>Mon, 02 Dec 2019 00:00:00 GMT</pubDate></item><item><title>
Totally legit software methodologies</title><link>https://barrasso.me/posts/2019-10-14-totally-legit-software-methodologies/</link><description>
&lt;p&gt;Instagram Driven Development, and other commonly employed strategies for crafting digital experiences&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2019-10-14-totally-legit-software-methodologies/</guid><pubDate>Mon, 14 Oct 2019 00:00:00 GMT</pubDate></item><item><title>
Why RSpec Tests Fail (and How To Fix Them)</title><link>https://barrasso.me/posts/2019-10-07-why-rspec-tests-fail/</link><description>
&lt;p&gt;Knowing how to troubleshoot tests is an essential software skill&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2019-10-07-why-rspec-tests-fail/</guid><pubDate>Mon, 07 Oct 2019 00:00:00 GMT</pubDate></item><item><title>
A practical guide to ES6 Proxy</title><link>https://barrasso.me/posts/2019-05-16-practical-guide-to-es6-proxy/</link><description>
&lt;p&gt;Proxies on proxies for clean and composable Javascript metaprogramming&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2019-05-16-practical-guide-to-es6-proxy/</guid><pubDate>Thu, 16 May 2019 00:00:00 GMT</pubDate></item><item><title>
A practical guide to ES6 Symbol</title><link>https://barrasso.me/posts/2019-05-13-practical-guide-to-es6-symbol/</link><description>
&lt;p&gt;Practical Javascript metaprogramming with Symbols&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2019-05-13-practical-guide-to-es6-symbol/</guid><pubDate>Mon, 13 May 2019 00:00:00 GMT</pubDate></item><item><title>
thank u, [Symbol.iterator].next</title><link>https://barrasso.me/posts/2019-04-10-thank-u-symbol-iterator-next/</link><description>
&lt;p&gt;I&amp;rsquo;m so f****in&amp;rsquo; grateful for this spec. Modular iteration without cloning or modifying in-place&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2019-04-10-thank-u-symbol-iterator-next/</guid><pubDate>Wed, 10 Apr 2019 00:00:00 GMT</pubDate></item><item><title>
The first shall be last with JavaScript arrays</title><link>https://barrasso.me/posts/2019-03-28-first-shall-be-last-with-javascript-arrays/</link><description>
&lt;p&gt;So the last shall be &lt;code&gt;[0]&lt;/code&gt;, and the first &lt;code&gt;[length - 1]&lt;/code&gt;&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2019-03-28-first-shall-be-last-with-javascript-arrays/</guid><pubDate>Thu, 28 Mar 2019 00:00:00 GMT</pubDate></item><item><title>
Dear Javascript,</title><link>https://barrasso.me/posts/2019-02-08-dear-javascript/</link><description>
&lt;p&gt;this isn&amp;rsquo;t working.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2019-02-08-dear-javascript/</guid><pubDate>Fri, 08 Feb 2019 00:00:00 GMT</pubDate></item><item><title>
How to Learn Elixir for Hipster Javascript Developers</title><link>https://barrasso.me/posts/2019-02-05-learn-elixir-for-javascript-developers/</link><description>
&lt;p&gt;Everyone knows Javascript. Your grandma, father-in-law, old college roommate… even your cat knows Javascript!&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2019-02-05-learn-elixir-for-javascript-developers/</guid><pubDate>Tue, 05 Feb 2019 00:00:00 GMT</pubDate></item><item><title>
Hello, Elixir — The First Month</title><link>https://barrasso.me/posts/2019-01-10-hello-elixir-the-first-month/</link><description>
&lt;p&gt;I&amp;rsquo;ve used Elixir for a month and mostly like it&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2019-01-10-hello-elixir-the-first-month/</guid><pubDate>Thu, 10 Jan 2019 00:00:00 GMT</pubDate></item><item><title>
John Hancock Investments</title><link>https://barrasso.me/projects/jhinvestments/</link><description>
&lt;p&gt;I led the front-end team launching the redesigned &lt;a href="https://jhinvestments.com"&gt;John Hancock Investments&lt;/a&gt; website, including the interactive &lt;a href="https://www.jhinvestments.com/viewpoints/portfolio-construction/comparing-funds-in-three-easy-steps"&gt;Fund Compare&lt;/a&gt; tool. Complex financial data was rendered precisely using custom &lt;strong&gt;&lt;a href="https://plotly.com/javascript/"&gt;Plotly&lt;/a&gt;&lt;/strong&gt; charts and dynamic SVGs. Downloadable Fact Sheets were rendered using Puppeteer to month and quarter-end PDFs. I collaborated with our backend team, often landing backend changes in &lt;strong&gt;Adobe Experience Manager&lt;/strong&gt; (AEM) to ensure a timely and pixel-perfect delivery for our client.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/projects/jhinvestments/</guid><pubDate>Mon, 10 Sep 2018 00:00:00 GMT</pubDate></item><item><title>
Animated React Metaballs</title><link>https://barrasso.me/posts/2018-08-01-animated-react-metaballs/</link><description>
&lt;p&gt;React component for animated SVG metaballs&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2018-08-01-animated-react-metaballs/</guid><pubDate>Wed, 01 Aug 2018 00:00:00 GMT</pubDate></item><item><title>
Basics of Text Analysis &amp; Visualization</title><link>https://barrasso.me/posts/2018-07-24-basics-of-text-analysis-visualization/</link><description>
&lt;p&gt;Bigram cloud and sentiment analysis applied to GitHub READMEs&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2018-07-24-basics-of-text-analysis-visualization/</guid><pubDate>Tue, 24 Jul 2018 00:00:00 GMT</pubDate></item><item><title>
Open Source Words — Part 2</title><link>https://barrasso.me/posts/2018-07-16-open-source-words-part-2/</link><description>
&lt;p&gt;There are many words used to describe software: framework, progressive, modular, reusable, extensible, scalable, simple, ad infinitum…&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2018-07-16-open-source-words-part-2/</guid><pubDate>Mon, 16 Jul 2018 00:00:00 GMT</pubDate></item><item><title>
Open Source Words — Part 1</title><link>https://barrasso.me/posts/2018-07-15-open-source-words-part-1/</link><description>
&lt;p&gt;I set out to understand words used to describe software, and in particular, open source software. What words form the collective developer…&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2018-07-15-open-source-words-part-1/</guid><pubDate>Sun, 15 Jul 2018 00:00:00 GMT</pubDate></item><item><title>
HTML Overlays in Plotly.js</title><link>https://barrasso.me/posts/2018-07-03-html-overlays-in-plotly/</link><description>
&lt;p&gt;I&amp;rsquo;ve talked about several features and limitations of Plotly.js before. It&amp;rsquo;s a very capable framework but I&amp;rsquo;ve found a number of important…&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2018-07-03-html-overlays-in-plotly/</guid><pubDate>Tue, 03 Jul 2018 00:00:00 GMT</pubDate></item><item><title>
Resumes in HTML, CSS, and JS</title><link>https://barrasso.me/posts/2018-06-29-resumes-in-html-css-js/</link><description>
&lt;p&gt;There&amp;rsquo;s lots of guides on how to write a resume as a developer, but very few resources for actually creating one. Google for &amp;ldquo;resume…&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2018-06-29-resumes-in-html-css-js/</guid><pubDate>Fri, 29 Jun 2018 00:00:00 GMT</pubDate></item><item><title>
Quick Start with HTML Template Language on Adobe Experience Manager</title><link>https://barrasso.me/posts/2018-03-07-quick-start-html-template-language-aem/</link><description>
&lt;p&gt;Introduction&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2018-03-07-quick-start-html-template-language-aem/</guid><pubDate>Wed, 07 Mar 2018 00:00:00 GMT</pubDate></item><item><title>
Plotly Tip #6: positioning axis titles in horizontal bar chart</title><link>https://barrasso.me/posts/2018-02-26-plotly-tip-6-positioning-axis-titles/</link><description>
&lt;p&gt;About Plot.ly&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2018-02-26-plotly-tip-6-positioning-axis-titles/</guid><pubDate>Mon, 26 Feb 2018 00:00:00 GMT</pubDate></item><item><title>
Plotly Tip #5: sand charts</title><link>https://barrasso.me/posts/2018-02-06-plotly-tip-5-sand-charts/</link><description>
&lt;p&gt;About Plot.ly&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2018-02-06-plotly-tip-5-sand-charts/</guid><pubDate>Tue, 06 Feb 2018 00:00:00 GMT</pubDate></item><item><title>
Plotly Tip #4: text outlines</title><link>https://barrasso.me/posts/2018-02-02-plotly-tip-4-text-outlines/</link><description>
&lt;p&gt;About Plot.ly&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2018-02-02-plotly-tip-4-text-outlines/</guid><pubDate>Fri, 02 Feb 2018 00:00:00 GMT</pubDate></item><item><title>
Plotly Tip #3: Custom Legends</title><link>https://barrasso.me/posts/2018-01-24-plotly-tip-3-custom-legends/</link><description>
&lt;p&gt;About Plot.ly&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2018-01-24-plotly-tip-3-custom-legends/</guid><pubDate>Wed, 24 Jan 2018 00:00:00 GMT</pubDate></item><item><title>
Plotly Tip #2: SVG &amp; z-index</title><link>https://barrasso.me/posts/2018-01-23-plotly-tip-2-svg-z-index/</link><description>
&lt;p&gt;About Plot.ly&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2018-01-23-plotly-tip-2-svg-z-index/</guid><pubDate>Tue, 23 Jan 2018 00:00:00 GMT</pubDate></item><item><title>
Plotly Tip #1: dtick</title><link>https://barrasso.me/posts/2018-01-22-plotly-tip-1-dtick/</link><description>
&lt;p&gt;About Plot.ly&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2018-01-22-plotly-tip-1-dtick/</guid><pubDate>Mon, 22 Jan 2018 00:00:00 GMT</pubDate></item><item><title>
How Many Ways To Say, Unsubscribe</title><link>https://barrasso.me/posts/2018-01-12-how-many-ways-to-say-unsubscribe/</link><description>
&lt;p&gt;I&amp;rsquo;ve recently been purging spam in my inbox.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2018-01-12-how-many-ways-to-say-unsubscribe/</guid><pubDate>Fri, 12 Jan 2018 00:00:00 GMT</pubDate></item><item><title>
Drag &amp; Drop using CSS Transforms</title><link>https://barrasso.me/posts/2018-01-08-drag-drop-using-css-transforms/</link><description>
&lt;p&gt;Perhaps one of the most common features of modern web applications is the ability to drag (and sometimes drop or sort) elements. So common…&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2018-01-08-drag-drop-using-css-transforms/</guid><pubDate>Mon, 08 Jan 2018 00:00:00 GMT</pubDate></item><item><title>
On The "Broken Image Icon"</title><link>https://barrasso.me/posts/2018-01-03-on-the-broken-image-icon/</link><description>
&lt;p&gt;A while ago I got to wondering if it is possible to style the broken image icon. As it turns out, aside from Firefox&amp;rsquo;s…&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2018-01-03-on-the-broken-image-icon/</guid><pubDate>Wed, 03 Jan 2018 00:00:00 GMT</pubDate></item><item><title>
Keeping current with CSS currentColor</title><link>https://barrasso.me/posts/2018-01-01-keeping-current-with-css-currentcolor/</link><description>
&lt;p&gt;If you&amp;rsquo;re familiar with Sass or LESS then you&amp;rsquo;re apt to use preprocessor variables to parameterize the dimensions and colors of a site…&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2018-01-01-keeping-current-with-css-currentcolor/</guid><pubDate>Mon, 01 Jan 2018 00:00:00 GMT</pubDate></item><item><title>
How I wish new password fields worked</title><link>https://barrasso.me/posts/2017-12-30-how-i-wish-new-password-fields-worked/</link><description>
&lt;p&gt;For a recent project I was tasked with implementing client-side login &amp;amp; registration forms. The flow was quite complex because it involved…&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2017-12-30-how-i-wish-new-password-fields-worked/</guid><pubDate>Sat, 30 Dec 2017 00:00:00 GMT</pubDate></item><item><title>
Advanced meta-Metaballs</title><link>https://barrasso.me/posts/2017-12-29-advanced-meta-metaballs/</link><description>
&lt;p&gt;Building on my last article, Pretty Metaballs, I have seen a number of metaball examples that connect two circles, or that do so in a…&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2017-12-29-advanced-meta-metaballs/</guid><pubDate>Fri, 29 Dec 2017 00:00:00 GMT</pubDate></item><item><title>
Pretty SVG Metaballs</title><link>https://barrasso.me/posts/2017-12-20-pretty-svg-metaballs/</link><description>
&lt;p&gt;Metaballs (not meatballs), like the ones below, are circles with overlapping areas proportional to their size and proximity.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/posts/2017-12-20-pretty-svg-metaballs/</guid><pubDate>Wed, 20 Dec 2017 00:00:00 GMT</pubDate></item><item><title>
Noyze</title><link>https://barrasso.me/projects/noyze/</link><description>
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
&lt;iframe src="https://www.youtube-nocookie.com/embed/B_eZyYcv6Cs?rel=0}&amp;start=0&amp;end=0"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;"
allowfullscreen=""&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;
Noyze was the first volume panel replacement app for Android. Leveraging the new &lt;a href="https://developer.android.com/reference/android/accessibilityservice/AccessibilityService"&gt;&lt;code&gt;AccessibilityService&lt;/code&gt;&lt;/a&gt; API, Noyze could override how the system responds to volume buttons, providing a themed extensive with endless customization options. Noyze added playback and multi-channel controls, along with the ability to launch apps by long pressing volume up or down.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;
With more than &lt;strong&gt;500,000+ installs&lt;/strong&gt;, Noyze was featured on &lt;a href="https://samsung.gadgethacks.com/how-to/customize-androids-volume-panel-no-root-required-0157381/"&gt;Gadget Hacks&lt;/a&gt;, &lt;a href="https://www.androidpolice.com/2014/09/14/noyze-is-a-sleek-customizable-volume-panel-replacement-that-doesnt-require-root/"&gt;Android Police&lt;/a&gt;, &lt;a href="https://www.phonearena.com/news/Noyze-app-lets-you-map-the-volume-keys-for-many-actions-and-no-root-is-required_id60863"&gt;PhoneArena&lt;/a&gt;, and was one of Computerworld&amp;rsquo;s &lt;a href="https://www.computerworld.com/article/1378366/android-power-favorite-things-october-2014.html"&gt;favorite apps for October 2014&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/projects/noyze/noyze-nexus_hu_91ab52fc8292eb35.webp 320w, https://barrasso.me/projects/noyze/noyze-nexus_hu_887ae13943ef97e6.webp 640w, https://barrasso.me/projects/noyze/noyze-nexus_hu_793aa037bdb3e147.webp 1024w' sizes="100vw" /&gt;
&lt;source type="image/png" srcset='https://barrasso.me/projects/noyze/noyze-nexus_hu_621e358f79eae77c.png 320w, https://barrasso.me/projects/noyze/noyze-nexus_hu_f72c0f018cbfc6d6.png 640w, https://barrasso.me/projects/noyze/noyze-nexus_hu_32017bdc383d8d04.png 1024w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/projects/noyze/noyze-nexus_hu_621e358f79eae77c.png"
alt="Noyze Volume Panel" class="w-full contain"
width="320"
height="195"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;br /&gt;
&lt;h3 id="themes"&gt;
&lt;strong&gt;Themes&lt;/strong&gt;
&lt;a class="heading-anchor" href="#themes" aria-label="Link to Themes"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;&lt;br /&gt;
A key feature of Noyze was the extensive catalog of customizable themes designed to mirror modern and nostalgic styles, from the minimalistic status bar theme to multi-channel controls. Extensive research was done across a variety of systems and interfaces to understand which designs and features resonated most with users.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/projects/noyze/noyze-themes_hu_c3e9c39f1b8d1b2f.webp 320w, https://barrasso.me/projects/noyze/noyze-themes_hu_d0de967c620fb2be.webp 640w, https://barrasso.me/projects/noyze/noyze-themes_hu_65010a66315bcfa5.webp 1024w, https://barrasso.me/projects/noyze/noyze-themes_hu_c03ef8e4119ca95b.webp 1920w' sizes="100vw" /&gt;
&lt;source type="image/png" srcset='https://barrasso.me/projects/noyze/noyze-themes_hu_b2f370b20f3d7248.png 320w, https://barrasso.me/projects/noyze/noyze-themes_hu_8b8c540a55cfe2a5.png 640w, https://barrasso.me/projects/noyze/noyze-themes_hu_18dff1c53927dd2c.png 1024w, https://barrasso.me/projects/noyze/noyze-themes_hu_dcacdcc28cc971a1.png 1920w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/projects/noyze/noyze-themes_hu_b2f370b20f3d7248.png"
alt="Noyze Themes" class="w-full contain"
width="320"
height="213"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/projects/noyze/</guid><pubDate>Wed, 01 Oct 2014 00:00:00 GMT</pubDate></item><item><title>
reClock</title><link>https://barrasso.me/projects/reclock/</link><description>
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
&lt;iframe src="https://www.youtube-nocookie.com/embed/P9W0jKnOMyA?rel=0}&amp;start=654&amp;end=705"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;"
allowfullscreen=""&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;
reClock was a small experiment developed in collaboration with &lt;a href="https://grahammacphee.com/"&gt;Graham Macphee&lt;/a&gt;, designed to be the minimalist clock widget that you actually &lt;em&gt;have&lt;/em&gt; to read. Attention is in short supply and this often leads to a form of amnesia where you forget what you &lt;em&gt;just&lt;/em&gt; read. reClock used random offsets to indicate the time in a format like &amp;ldquo;six until twelve twenty five&amp;rdquo; or &amp;ldquo;quarter past nine o&amp;rsquo;two.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;
Although simple, reClock presented a challenge both in human psychology and efficient rasterization. At the time, &lt;a href="https://developer.android.com/reference/android/app/job/JobScheduler"&gt;&lt;code&gt;JobScheduler&lt;/code&gt;&lt;/a&gt; was not available and the simplest (but less efficient) way to render a widget was to run a background service. reClock pre-rendered several updates for each minute until the countdown occurred, and called &lt;a href="https://developer.android.com/reference/android/appwidget/AppWidgetManager"&gt;&lt;code&gt;AppWidgetManager&lt;/code&gt;&lt;/a&gt; to cycle through renders.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/projects/reclock/reclock-panel_hu_16566666138e1806.webp 320w, https://barrasso.me/projects/reclock/reclock-panel_hu_ba63e4c3ae8b530e.webp 640w' sizes="100vw" /&gt;
&lt;source type="image/png" srcset='https://barrasso.me/projects/reclock/reclock-panel_hu_b795e0a6f9ba1a7b.png 320w, https://barrasso.me/projects/reclock/reclock-panel_hu_2b5809dcac579345.png 640w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/projects/reclock/reclock-panel_hu_b795e0a6f9ba1a7b.png"
alt="reClock Screenshots" class="w-full contain"
width="320"
height="182"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/projects/reclock/</guid><pubDate>Mon, 24 Feb 2014 00:00:00 GMT</pubDate></item><item><title>
PlanGrid</title><link>https://barrasso.me/projects/plangrid/</link><description>
&lt;p&gt;After releasing several notable Android apps including &lt;a href="https://barrasso.me/projects/statusbarplus/"&gt;StatusBar+&lt;/a&gt; and &lt;a href="https://barrasso.me/projects/chargebar/"&gt;ChargeBar&lt;/a&gt;, I was contacted in 2012 by &lt;a href="https://plangrid.com"&gt;PlanGrid&lt;/a&gt; to develop the first version of their blueprint reader for Android tablets. At this time, the Android ecosystem was nascent, fragmented, and unstable. I developed several components including &lt;a href="https://github.com/Tombarr/GridFragment"&gt;GridFragment&lt;/a&gt; and a tile rendering engine to display high-fidelity blueprints on resource-constrained devices (similar to &lt;a href="https://github.com/moagrius/TileView"&gt;TileView&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;
Android Studio wasn&amp;rsquo;t officially released until December 2014, so early versions of PlanGrid for Android used &lt;a href="https://ant.apache.org/"&gt;Ant&lt;/a&gt;, &lt;a href="https://eclipseide.org/"&gt;Eclipse&lt;/a&gt;, and integrated with &lt;a href="https://www.bugsnag.com/"&gt;Bugsnag&lt;/a&gt; for crash detection. PlanGrid for Android Beta (v0.1.1) was publicly released in February 2014&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/projects/plangrid/plangrid-android-release_hu_50d8470aec05efce.webp 320w, https://barrasso.me/projects/plangrid/plangrid-android-release_hu_553ce87838b5442b.webp 640w, https://barrasso.me/projects/plangrid/plangrid-android-release_hu_480d99d1e61e873a.webp 1024w' sizes="100vw" /&gt;
&lt;source type="image/png" srcset='https://barrasso.me/projects/plangrid/plangrid-android-release_hu_80de8fda4420f8d3.png 320w, https://barrasso.me/projects/plangrid/plangrid-android-release_hu_3a61524b485aefc4.png 640w, https://barrasso.me/projects/plangrid/plangrid-android-release_hu_74f58e44dbbdfbd3.png 1024w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/projects/plangrid/plangrid-android-release_hu_80de8fda4420f8d3.png"
alt="PlanGrid for Android Release Email" class="narrow-img"
width="320"
height="356"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/projects/plangrid/</guid><pubDate>Mon, 10 Jun 2013 00:00:00 GMT</pubDate></item><item><title>
ChargeBar</title><link>https://barrasso.me/projects/chargebar/</link><description>
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
&lt;iframe src="https://www.youtube-nocookie.com/embed/bTFaeeBClGs?rel=0}&amp;start=212&amp;end=268"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;"
allowfullscreen=""&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;
Inspired by early designs from Xaiomi&amp;rsquo;s custom Android ROM, &lt;a href="https://www.mi.com/global/miui"&gt;MIUI&lt;/a&gt;, ChargeBar added a thin line on top of your status bar indicating battery charge. ChargeBar surpassed &lt;strong&gt;500,000+ installs&lt;/strong&gt; and was featured on the likes of &lt;a href="https://lifehacker.com/chargebar-gives-you-a-closer-look-at-your-androids-curr-5886706"&gt;Life Hacker&lt;/a&gt;, &lt;a href="https://www.redmondpie.com/chargebar-for-android-is-an-miui-inspired-battery-bar-that-works-on-all-roms/"&gt;Redmond Pie&lt;/a&gt;, and &lt;a href="https://www.droid-life.com/2012/02/20/chargebar-brings-a-visual-battery-meter-to-the-top-or-bottom-of-home-screens/"&gt;Droid Life&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;
ChargeBar was sold on &lt;a href="https://apptopia.com/en/"&gt;Apptopia&lt;/a&gt; (later Fliptopia) in 2012, a marketplace to buy and sell apps.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/projects/chargebar/</guid><pubDate>Sun, 19 Feb 2012 00:00:00 GMT</pubDate></item><item><title>
StatusBar+</title><link>https://barrasso.me/projects/statusbarplus/</link><description>
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
&lt;iframe src="https://www.youtube-nocookie.com/embed/se2LrkCEzj0?rel=0}&amp;start=0&amp;end=0"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;"
allowfullscreen=""&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;
Launched in September 2011, StatusBar+ was the first Android app that I published, achieving &lt;strong&gt;500,000+ installs&lt;/strong&gt;. It was also Android&amp;rsquo;s first status bar replacement app that didn&amp;rsquo;t require jailbreaking or root access! StatusBar+ used the &lt;a href="https://developer.android.com/reference/android/view/WindowManager"&gt;&lt;code&gt;WindowManager&lt;/code&gt;&lt;/a&gt; API to draw &lt;em&gt;on top&lt;/em&gt; of the system status bar. Originally themed like Windows Phone 7, StatusBar+ expanded to offer many more features and themes. StatusBar+ was eventually sold on &lt;a href="https://apptopia.com/en/"&gt;Apptopia&lt;/a&gt; (later Fliptopia) in 2012, a marketplace to buy and sell apps.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;
Check out the video above where &lt;strong&gt;Marques Brownlee of MKBHD&lt;/strong&gt; reviewed a very early version of StatusBar+ on the original Motorola Droid.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/projects/statusbarplus/</guid><pubDate>Wed, 14 Sep 2011 00:00:00 GMT</pubDate></item><item><title>
TextMyFood</title><link>https://barrasso.me/projects/textmyfood/</link><description>
&lt;p&gt;For an internship I developed a prototype of &lt;a href="https://www.linkedin.com/company/textmyfood"&gt;TextMyFood&lt;/a&gt;, a &lt;a href="https://rubyonrails.org/"&gt;Ruby on Rails&lt;/a&gt; service integrated with &lt;a href="https://www.twilio.com/"&gt;Twilio&lt;/a&gt; to let restaurant customers text their waiter to request another round of beer, a new fork, the check, whatever. A decade before the COVID-19 pandemic made QR code menus ubiquitous, TextMyFood was piloted at &lt;a href="https://charlieskitchen.com/"&gt;Charlie&amp;rsquo;s Kitchen&lt;/a&gt; in Cambridge, MA in an attempt to balance lax vs overly-attentive service using simple, readily-available technology.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;
TextMyFood was featured news websites including &lt;a href="https://www.npr.org/2011/01/17/132493034/chk-plz-restaurants-try-texting-to-speed-service"&gt;NPR&lt;/a&gt;, the &lt;a href="https://www.bostonherald.com/2010/10/29/cambridge-eatery-takes-text-orders/"&gt;Boston Herald&lt;/a&gt;, and the &lt;a href="https://thephoenix.com/BLOGS/phlog/archive/2010/12/16/meet-the-mayor-charlie-s-kitchen.aspx"&gt;Boston Phoenix&lt;/a&gt;.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/projects/textmyfood/</guid><pubDate>Tue, 01 Jun 2010 00:00:00 GMT</pubDate></item><item><title>
Digital Independence</title><link>https://barrasso.me/projects/digital-independence/</link><description>
&lt;p&gt;As part of &lt;a href="https://www.ashoka.org/en-us/youth-venture"&gt;Ashoka Youth Venture&lt;/a&gt;, I co-founded the non-profit, &lt;a href="https://www.facebook.com/DigitalIndependence/"&gt;Digital Independence&lt;/a&gt;, in 2009 alongside Bandhan Zishanuzzaman and Arjun Agarwala. Our mission was to educate and empower Boston residents through &lt;strong&gt;free computer literacy courses&lt;/strong&gt;, and to &lt;strong&gt;reduce electronic waste&lt;/strong&gt; (e-waste) by offering discounted computer repair services.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;
&lt;picture&gt;
&lt;source type="image/webp" srcset='https://barrasso.me/projects/digital-independence/Screen%20shot%202012-01-09%20at%201.50.22%20PM_hu_34b2c7bbb7d523fb.webp 320w, https://barrasso.me/projects/digital-independence/Screen%20shot%202012-01-09%20at%201.50.22%20PM_hu_5189163f2d54e83e.webp 640w, https://barrasso.me/projects/digital-independence/Screen%20shot%202012-01-09%20at%201.50.22%20PM_hu_9417ef24df64377f.webp 1024w' sizes="100vw" /&gt;
&lt;source type="image/png" srcset='https://barrasso.me/projects/digital-independence/Screen%20shot%202012-01-09%20at%201.50.22%20PM_hu_19355372fb007924.png 320w, https://barrasso.me/projects/digital-independence/Screen%20shot%202012-01-09%20at%201.50.22%20PM_hu_4d43f3da145d1ca5.png 640w, https://barrasso.me/projects/digital-independence/Screen%20shot%202012-01-09%20at%201.50.22%20PM_hu_882d86981e7dcecb.png 1024w' sizes="100vw" /&gt;
&lt;img
src="https://barrasso.me/projects/digital-independence/Screen%20shot%202012-01-09%20at%201.50.22%20PM_hu_19355372fb007924.png"
alt="Screenshot of mydigitalindependence.com" class="w-full contain"
width="320"
height="147"
loading="lazy"
decoding="async"
draggable="false"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;
We repaired, upgraded, and &lt;strong&gt;donated hundreds of desktops&lt;/strong&gt;, laptops, and smartphones. In both 2010 and 2011, we received the &amp;ldquo;Best of the Best&amp;rdquo; award from the &lt;a href="https://www.cambridgema.gov/Services/mayorssummeryouthemploymentprogram"&gt;Mayor&amp;rsquo;s Summer Youth Employment Program&lt;/a&gt; (MYSEP) where we employed and managed nearly a dozen high school students in Cambridge, MA each summer.&lt;/p&gt;</description><author>blog@barrasso.me (Tom Barrasso)</author><guid>https://barrasso.me/projects/digital-independence/</guid><pubDate>Tue, 15 Dec 2009 00:00:00 GMT</pubDate></item></channel></rss>