[{"content":"","ref":"https://www.chris-saylor.com/link/","tags":null,"title":"Links","type":"link"},{"content":"Not exactly news, but it was news to me. A throwback to my early computer experiences, Space Cadet Pinball has been decompiled and made available for multiple platforms (including Linux). Thanks to Stephen Brennan for sharing and making its existance known to me.\nGithub Repo Archive Link Image credit: Mohamed Chahin\n","ref":"https://www.chris-saylor.com/link/space-cadet-pinpall/","tags":["games"],"title":"Space Cadet Pinball Decompiled","type":"link"},{"content":"This is a fascinating approach to converting an image that would be suitable for rendering in PICO-8. Some potential use-cases for this could be title screens or cut scenes where the image is developed outside of the PICO-8 sprite tool.\nPhoto by Elena Mozhvilo on Unsplash\n","ref":"https://www.chris-saylor.com/link/pico-8-image-pixel-mapping-color-palette/","tags":["pico8","lua","games","coding","python"],"title":"PICO-8 Image Pixel Mapping Color Palette","type":"link"},{"content":"When hiring, coordination and communication between recruiters, HR, candidates, and hiring managers are a real challenge. In this article, I will demonstrate our strategy and workflows to keep everyone in the loop and make the recruiting process as smooth and painless as possible.\nTable of contents Tools Used The Problem Starting with Lists Automation with Slack workflows New Candidate Notification Requesting an Interview Rejecting a Candidate Conclusion Tools Used Slack Calendly - Any scheduling software that you can have a fixed link that connects to your calendar would work. Google Workspace (Gmail) - Microsoft Outlook can also work (or any other email provider that integrates with Slack). The Problem Before diving into the Slack setup, let me describe our hiring process first to show what we\u0026rsquo;re trying to solve.\nWhenever we are hiring employees or contractors, my company tends to engage multiple sourcing partners. This allows us to tap into multiple candidate networks, and we can also get a variety of candidates. The communication with each of these sourcing partners is ad hoc; some have their own portals, but most simply use email.\nCandidates that match our criteria first go through HR to ensure the same candidate hasn\u0026rsquo;t been sourced by one of our other partners or to check if we\u0026rsquo;ve interviewed them in the past. Their information is entered into a shared document, after which a hiring manager reviews the candidate\u0026rsquo;s resume to determine if an interview should be requested.\nAn email is sent to the recruiter, who then communicates with the candidate to schedule an interview with our hiring manager. The interview occurs with the hiring manager, after which a decision is made to either reject the candidate or make an offer.\nAs you can see, there is a lot of communication required:\nHR needs to be in the loop on new candidates. Hiring managers need to be notified to review a resume and later when an interview is to be scheduled. HR always wants to know the status of any candidates in the pipeline. Recruiters need to be told whether to proceed with candidates or not. To begin tackling all of this, I created a recruiting channel in Slack with our HR and hiring managers invited. This will be where all the automation comes into play.\nStarting with Lists The first step to automating the various communication channels is to start with a place to store the data. In this case, I\u0026rsquo;m using Slack lists as a central hub to store all the applicant information needed.1 I created a list with various fields, but the key fields for automating communication are the Name, Source (which recruiter sourced the candidate), and Stage (status of the candidate).\nNext, I created a list that contains the contact details of the recruiters. This list also contains a field called Source, and this is the field that links the two lists together. Think of it as a foreign key in a DBMS. This will be used later to get the contact information per candidate to send external emails to the recruiter responsible for that candidate.\nerDiagram APPLICANTS ||--o{ RECRUITERS : \"sourced by\" APPLICANTS { Text Name \"Candidate full name\" Selection Source FK \"Links to recruiter\" Text Stage \"Current hiring stage\" File Resume \"Resume/CV File\" Date Added \"Application date\" text Notes \"Interview notes\" } RECRUITERS { Text Name \"Contact person\" Selection Source PK \"Recruiting company\" Text email \"Contact email\" } Fig 1. - Pseudo database diagram. Note: The Source value in both lists must match exactly.\nAutomation with Slack workflows With these lists in place, what can be done with the data? Luckily, all of the automations needed can be done with the workflow builder inside of Slack.2\nWhat actions do we want to automate?\nNotify the channel when a new candidate has been entered. This lets the hiring manager know to review the resume. Notify the recruiter via email when a hiring manager requests an interview. Notify the recruiter via email when a hiring manager rejects a candidate. Each of these is triggered by a change to a value of a list item.\nNew Candidate Notification This first automation is the simplest: notify the recruiting channel when a new candidate has been entered.\nFig. 2 - Workflow for notifying the recruiting channel for resume review. In our case, Resume Review is the first stage, and since these workflows work off of \u0026ldquo;updates\u0026rdquo; to list items, we have to start the item with no stage and then move it. To facilitate this, I created a workflow form entry in which the last step updates the stage to Resume Review:\nFig. 3 - New candidate entry form workflow. Requesting an Interview Whenever a hiring manager determines the candidate is good enough to move to the next step, they mark them as Interview Requested. This workflow sends a message to the person who updated the status and will send an email on their behalf.\nFig. 4 - Requesting an interview with a candidate. A crucial step here is making a separate list item\u0026rsquo;s data available to the workflow. Previously in Fig. 1, the Source value is considered a \u0026ldquo;foreign key\u0026rdquo;. This can be used to select the recruiter from the list relevant to the candidate in question.\nFig. 5 - Selecting the relevant recruiter. Now the appropriate recruiter is targeted to request an interview with the candidate via email.\nRejecting a Candidate Similar to requesting an interview workflow, rejecting a candidate follows almost the exact same flow with the addition of communicating the candidate notes to the recruiter. It\u0026rsquo;s important to give feedback about the candidate to the recruiter to allow them to improve their sourcing process.\nFig. 6 - Rejecting a candidate. Conclusion By implementing these Slack-based automation workflows, we\u0026rsquo;ve transformed what was once a chaotic web of emails and manual communications into a streamlined, transparent process. The real power lies not just in reducing manual work, but in creating a consistent experience that benefits everyone involved—from recruiters who get clear, timely feedback to hiring managers who can focus on what they do best: evaluating talent.\nThese same principles can be applied far beyond recruiting. Consider how list-triggered workflows could streamline customer onboarding, project handoffs, or equipment requests. The key is identifying any process where multiple parties need to stay informed as status changes occur.\nIf you\u0026rsquo;re ready to tackle communication chaos in your organization, start small—pick one workflow that causes the most frustration and build from there. Your future self (and your team) will thank you.\nLists in Slack are really powerful. Not only do they serve as a de facto database, but they can also trigger automated workflows when fields change. Additionally, you can create guided forms for entering the data so that you can control data entry like a regular application. Read more about Slack lists.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nBeing able to create and maintain these automations directly in Slack without the need to build something that listens to webhooks makes this a simple process to add no matter what the skill level of the managing team is. Read more about Slack\u0026rsquo;s Workflow Automation.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","ref":"https://www.chris-saylor.com/posts/automate-team-hiring-communication-slack/","tags":["slack","automation","leadership","management"],"title":"Automate Recruiting with Slack","type":"posts"},{"content":"","ref":"https://www.chris-saylor.com/posts/","tags":null,"title":"Posts","type":"posts"},{"content":"Some minor spoilers ahead\nA return to \u0026ldquo;hard\u0026rdquo; science fiction at least for me. Of late, much of the science fiction I\u0026rsquo;ve consumed has largely been fantastical in nature: hand-waving away the technical aspects of how things work in the world described in the novels. This doesn\u0026rsquo;t necessarily make stories less enjoyable, unless it is poorly done or not internally consistent.\nThere\u0026rsquo;s something satisfying in the cornerstone of the story\u0026rsquo;s world being rooted in our own; something familiar and comfortable. The \u0026ldquo;Three Body Problem\u0026rdquo;, takes this familiarity and decidedly makes this a source of discomfort.\nRight from the start, Liu thrusts the reader into a chapter of history often skipped over in Western classrooms: the Chinese Cultural Revolution. This is no mere backdrop. It\u0026rsquo;s a gut-punch of despair, shaping the novel\u0026rsquo;s world and its characters in ways that make their decisions, no matter how extreme, feel chillingly inevitable. Ideological fervor grips them, only to be replaced by another kind of fervor entirely, like swapping out one fever dream for another.\nThe science described in the novel ranges from radio telescopy to futuristic virtual reality to mind bending multi-dimensional projections and folds. It is Flatland ironically on another dimension. The characters, brilliant as they are, discuss these ideas with the ease of someone ordering coffee, while I, the humble reader, occasionally cling to the book as if it were a lifeboat in a sea of incomprehensible physics.\nAnd yet, I was enthralled. I found myself rivited by the story-telling, horrified of the impending apocolypse, and yet the shortcomings come to the forefront: identity and exposition. The narrative ping-pongs between a crime noir and a conspiracy thriller, yet the noir elements feel like a lackluster side quest, making it tricky to summarize without feeling like I\u0026rsquo;m describing two different books. The exposition, unavoidable and occasionally meandering, never truly drags, but rather, like an eccentric professor, it ambles down lengthy corridors of knowledge, always leading somewhere worth going.\nIn the end, The Three Body Problem is a deep dive into a swirling abyss of science, philosophy, and cosmic terror. It lingers in the mind long after the final page, not just for its intricate scientific musings but for the way it reframes humanitys\u0026rsquo; place in the universe. Perhaps that\u0026rsquo;s the mark of truly great science fiction: not just to entertain, but to unsettle, to challenge, and to remind us that the vast unknown is both wondrous and terrifying, waiting just beyond the reach of our understanding.\n","ref":"https://www.chris-saylor.com/books/three-body-problem/","tags":["scifi","reviews"],"title":"A Return to \"Hard\" Science Fiction","type":"books"},{"content":"","ref":"https://www.chris-saylor.com/books/","tags":null,"title":"Books","type":"books"},{"content":"Photo by Eugenio Mazzone on Unsplash\n","ref":"https://www.chris-saylor.com/link/lua-metatable-lazy-load/","tags":["pico8","lua","games","coding"],"title":"Lazy Load Data in PICO-8 with Metatables","type":"link"},{"content":"","ref":"https://www.chris-saylor.com/games/","tags":null,"title":"Games","type":"games"},{"content":"With a renewed interest in Microsoft\u0026rsquo;s MakeCode Arcade1, Sam went to work on a new game. He was happily drawing his backgrounds and sprites with a sprinkling of controls and logic, when he asked me to help him with something.\nIt seems he had some \u0026ldquo;evil\u0026rdquo; pizza slices that he wanted to have come straight at the player randomly from the top of the screen. I have to admit, I wasn\u0026rsquo;t immediately able to call forth the vector math from ninth grade pre-calculus in the heat of the moment.\nLuckily, the internet is a great resource for math and after some googling2, I found a good primer on vectors. To spare other parents who may be asked a similar question as I, let\u0026rsquo;s walk through how to make this happen.\nAssumptions To start, we\u0026rsquo;re going to have two sprites, one called Ship and one called Evil_Pizza.\nBasic Vector Math MakeCode arcade uses a two-dimensional coordinate system for moving around sprites. We know the position of the player and the \u0026ldquo;evil\u0026rdquo; pizza.\nFirst we need to find the vector between these two points. By subtracting the position of the sprite we want to intercept from the position of the sprite that is doing the intercepting, we can get a directional vector to send the sprite.\nIn our example, this would produce an array of values (-45,112).\nSetting our \u0026ldquo;evil\u0026rdquo; pizza sprite\u0026rsquo;s velocity to this vector will cause the pizza to intercept the ship.\nThe above is interactive, try to avoid the evil pizza.\nAdvanced Applications The above works, but it gives little control with how fast or slow the \u0026ldquo;evil\u0026rdquo; pizza approaches the ship.\nWe can improve on this by calculating the unit vector.\nFirst we start with getting the magnitude of the intercept vector:\nNow, calculate the unit vector by dividing the intercept vector by the magnitude:\nHaving the unit vector allows us to easily scale the vector to a more granular level simply by multiplying it by a new magnitude (speed) value.\nYou can see a full working example here:\nAlternatives If you have some javascript coding experience and are feeling adventurous, there is an extension that adds these sorts of vector operations to MakeCode Arcade directly3.\nReferences Microsoft\u0026rsquo;s MakeCode Arcade Vectors MakeCode Arcade extension can be added to your pxt editor, however this extension is not officially approved. ","ref":"https://www.chris-saylor.com/games/make-code-arcade-intercept-sprites/","tags":["games","STEM","makecode arcade","microsoft","physics","coding"],"title":"Intercepting Sprites","type":"games"},{"content":"\u0026ldquo;Ship of Fools\u0026rdquo; is a seafaring cooperative \u0026ldquo;roguelite\u0026rdquo; game developed by Fika Productions and published by Team17 who is probably best known for their smash hit Overcooked. It is available on PC and consoles. You play the Fools, the only creatures fool enough to brave the sea. The Great Lighthouse that once protected the Archipelago is broken and a storm of malice and corruption is coming.\nOne of the key features of the game is its emphasis on teamwork and communication. Players must work together to gather resources, repair the ship, and fend off the various dangers that threaten their survival, include feisty crabs, exploding puffer fish, and other hazards that threaten to sink the ship.\n\u0026ldquo;Ship of Fools\u0026rdquo; allows players to explore the archipelago and discover its many secrets. As players progress in the game, they will unlock new characters and uncover hidden treasures. This encourages players to explore and experiment with different places to visit on the map as well as how different pieces of equipment interact with each other. During the heat of battle, you may even discover that you can put more than just the normal ammo in the cannons.\nRandomly generated map that has lots of points of interest. Another great aspect of the game is its art style and atmosphere. The ship is beautifully rendered and the game\u0026rsquo;s soundtrack perfectly complements the game\u0026rsquo;s adventurous and mysterious tone. The game is full of vibrant colors and detailed animations that bring the ship to life and make the player feel like they are truly on a perilous journey.\nDefend the ship from all sorts of various sea monsters. Overall 4/5 In summary, \u0026ldquo;Ship of Fools\u0026rdquo; is a great game for playing with your kids from eight years old and up. The game\u0026rsquo;s emphasis on teamwork and communication, combined with its humorous characters, make it a perfect choice for families looking to play together. The game\u0026rsquo;s art style and atmosphere also make it an enjoyable and immersive experience for players of all ages.\nThe only draw back to the game is its length. It can feel a bit short, and once you are able to consistently beat the runs, you have to be self-motivated to continue unlocking all the upgrades. However, given it\u0026rsquo;s lower price of (at the time of writing) $15 USD, it is still well worth it.\nBreakdown of skills engaged Spacial Awareness: 5/5 Memory Skills: 3/5 Hand-eye Coordination: 5/5 Problem Solving: 5/5 Reading Skills: 3/5 Entertainment: 5/5 Appropriateness: 4/5 Reviewed on the PC version through Steam.\nSee my Steam Stats for Ship of Fools\n","ref":"https://www.chris-saylor.com/games/ship-of-fools/","tags":["games","reviews"],"title":"Ship of Fools","type":"games"},{"content":"A new project has been greenlit and it has fallen to your team to deliver. With the open field in front of you, thoughts drift to the work ahead, but the current project is concrete, demanding … and unfinished. These scenarios test our experience and judgement as tech leads.\nThe natural tendency is to panic, dive into finishing the current project as fast as possible, and ignore the needs of the upcoming project. These times causes retreat into old habits: of individual contribution. Where is a lead\u0026rsquo;s effort best applied?\nIn my \u0026ldquo;Learning to be a Tech Lead\u0026rdquo; article, a common theme throughout the piece is trust and imbuing ownership; to instill belonging where combined effort and focus to a common goal is to grant opportunities. Appointing one of your team members to take ownership\u0026ndash;with your support\u0026ndash;expands your capabilities and grows the experience of your team. Does it mean you can ignore the current project for the next? It depends on your team. If they are motivated and invested in the outcome, sign off on their plans and leave the rest to them.\nOne of the things we do at Zumba™ is present a demo of the product we\u0026rsquo;re developing. The goal is to provide transparency to stakeholders, but it also affords a unique opportunity for the team. A holistic understanding of how the product works is necessary to successfully present the demo to the stakeholders. That team member I assign to present learns to be keenly aware of edge cases, quality issues, and potential feedback from stakeholders. In other words, they start thinking like a tech lead. If you can get that to happen, handing off the project for that last mile is a natural progression.\nThe real work of a tech lead lies at the beginning of a new project: parsing the problem to solve, culling cruft, and designing a plan to execute. If the project is small enough, this work may be complete before the project even begins. However for a large project, work may start while you are still developing the plan.\nPreparation and team development are key to making this work well.\nEven if you are not interested in growing your team’s management abilities, knowing the goal of the project and how it applies to the business is essential for individual contributors to work well together. Communicate project needs and expectations; this should be priority one (if you aren’t already doing this).\nTracking your team\u0026rsquo;s velocity helps you determine how in-tune your team is with the current project. Expanding on task descriptions, including more mentorship time, or identifying struggling team members is all aided by measuring and understanding velocity. Once under control, estimation of product release readiness becomes more accurate. This helps you determine when you can hand off the remaining details to the team and allow you to focus on the next project.\nBeing a tech lead isn\u0026rsquo;t about making all the decisions. Enabling team agency on how to best accomplish their tasks is absolutely critical \u0026hellip; unless your goal is to have every project screech to a halt whenever you go on vacation. Trust your team, offer guidance, and help them gain confidence in making decisions, otherwise they won\u0026rsquo;t feel like they have bearing on the outcome.\nWith these pieces in place, the present is secured. Delegate this project, and start on the next.\n","ref":"https://www.chris-saylor.com/posts/live-in-the-future-delegate-the-present/","tags":["tech lead","leadership","management"],"title":"Live in the Future; Delegate the Present","type":"posts"},{"content":"What did you do at work three days ago? What about eight days ago? Twelve days?\nDepending on your job, keeping stock of the day-to-day work by memory alone is simply not practical. I sometimes struggle to remember salient details of the morning in the afternoon.\nAm I getting old? Is my mind starting to go? While I\u0026rsquo;m sure sharpness fades over time, this is a product of the ever-increasing complexities of our work. It is challenging to remember accomplishments from the beginning of the year during year-end-review, and this is when a salary raise, bonus, and possibly the job itself is put on the line.\nThree years ago, I began an experiment: I started to write in a journal.\nAt first, it was pen and paper, which worked well enough, but I always had a hard time finding things later. I created jrnl which is an open-source command-line interface that allows very quick journal entries straight from the terminal and then makes it available on a Github wiki. What you use is less important than simply doing the practice of keeping a journal.\nMy role at Zumba has become more and more managerial. I still code quite a bit, but no longer is it my main function. You can read about this transition, but suffice it to say: it has a huge impact on your time and focus. Typically, an individual contributors\u0026rsquo; time is divided into half-days, usually broken up by lunch. A managers\u0026rsquo; day on the other hand is broken into thirty-minute time blocks. \u0026ldquo;Broken\u0026rdquo; is the keyword here because it will break your ability to concentrate on a specific task constantly.\nAs I\u0026rsquo;ve continued this experiment of journaling, I\u0026rsquo;ve been able to regain my ability to hold context. Whenever I start on a task that looks like it could be complex, I immediately start writing down my findings of the task as I discover it. When I inevitably get interrupted and return to the task, I don\u0026rsquo;t need to spend nearly as much time re-gaining all the context previously hard-earned. I catch up on my journal entry and continue.\nEven things like pull requests are improved. A significant amount of time on pull request reviews is saved by having my rationale as to why I did something the way I did readily available and textual. It as if I\u0026rsquo;m creating a changelog.\nJournaling isn\u0026rsquo;t only useful for development, it can also be invaluable in meetings. I have a bad habit of interrupting in meetings, and one of the things I\u0026rsquo;ve done to lessen this behavior is to jot down my questions or concerns in my journal and only bring them up when it is more appropriate. I also now have context for the decisions made in the meeting. Yes, we decided \u0026lsquo;x\u0026rsquo; in the meeting, but why? Now I know.\nAs time goes on, my journal also becomes a source of code snippets, common SQL, and just notes on how obscure systems work. Eventually, as time permits, some of these entries get reformatted (and de-personalized) to be added to the company documentation system.\nWhat I\u0026rsquo;ve described throughout this article is a purpose-driven, technical journal. This is not the same as a personal daily diary, whose focus is on how you feel about the events. Here\u0026rsquo;s what I\u0026rsquo;ve learned from the last three years of technical journaling:\nYou don\u0026rsquo;t need to be religious about it. The act of writing every day isn\u0026rsquo;t the goal. If it becomes a chore to use, then you\u0026rsquo;ll stop using it. The journal doesn\u0026rsquo;t need to be complicated. Use it with other tools like your company\u0026rsquo;s ticket system, a to-do list app, and even a simple calendar. Keep it private. A journal entry is meant to be raw and un-edited. Let the thoughts just pour into the entry. You always have the option of cleaning it up later for public consumption. All the benefits I described in this article are just the tip of the iceberg. Give journaling a try and see if it improves how you work.\nCredits Featured image by Aaron Burden.\n","ref":"https://www.chris-saylor.com/posts/you-should-keep-a-journal/","tags":["leadership","productivity","mindfulness"],"title":"You Should Keep a Journal","type":"posts"},{"content":"In many ways, PHP has come a long way to becoming a competent, typed language. With the newly minted PHP 8, strong types have eliminated a whole host of problems when dealing with class and function parameter input. However, it isn\u0026rsquo;t all just a bed of roses. Thrown exceptions (or Throwables these days) are notoriously absent from any sort of concrete specification within interfaces, classes, and functions. This is particularly troubling if one of our goals is for interchangeable implementations for a business process.\nA common interop activity is swapping out a backend vendor for a specific business process. If your business is constantly re-evaluating its expenditures and contracts with vendors (as they should), the need to swap out software where your business application needs to communicate with the vendor may be a prudent design consideration.\nLet\u0026rsquo;s look at a typical, albeit simplified, example for a fictional email service provider that needs to interop with the application. First, the entry point in the application.\nEmailClientInterface.php\ninterface EmailClientInterface { public function sendEmail( string $emailTemplate, EmailAddress $address, array $parameterMap ) : void } The interface accepts an email template to use, email address, and an array of parameters to map to content. It is a fire and forget function that doesn\u0026rsquo;t specify anything about errors. In the application, an email is sent as a consequence for placing an order.\nOrder.php\nfinal class Order { private $emailClient; public function __constructor(EmailClientInterface $emailClient) { $this-\u0026gt;emailClient = $emailClient; } public function placeOrder(CustomerOrder $order) : void { // charge and persist an order $this-\u0026gt;emailClient-\u0026gt;sendEmail( \u0026#39;order.placed\u0026#39;, $order-\u0026gt;emailAddress(), [ \u0026#39;items\u0026#39; =\u0026gt; $order-\u0026gt;items() ] ); } } What happens if the vendor sending the emails is down? Perhaps it throws a \\RuntimeException? How would our application react to that? It would crash, likely resulting in our web server returning a 500 error response or rolling back the order transaction. Certainly not user friendly. At first blush, the simplest solution would be to surround with a try/catch statement.\nOrder.php\nfinal class Order { private $emailClient; public function __constructor(EmailClientInterface $emailClient) { $this-\u0026gt;emailClient = $emailClient; } public function placeOrder(CustomerOrder $order) : void { // charge and persist an order try { $this-\u0026gt;emailClient-\u0026gt;sendEmail( \u0026#39;order.placed\u0026#39;, $order-\u0026gt;emailAddress(), [ \u0026#39;items\u0026#39; =\u0026gt; $order-\u0026gt;items() ] ); } catch (\\Throwable $e) { // log an error and perhaps send a message to the client } } } The 500 is no longer occurring, but what about recovering from something as simple as a request rate limit? Maybe the client sends a RateLimitExceeded exception. The app could catch that and handle it differently, but now there is a new problem: the client is dictating behavior and is no longer interoperable with other clients.\nThe interface needs to improve to allow for the application to handle error scenarios in an agnostic way from the client. In other words, the client needs to conform to the needs of the application, not the other-way-around. However, in PHP, the interface can\u0026rsquo;t specify thrown exceptions. Comments (@throws) don\u0026rsquo;t count. Instead, a return value should define this behavior. Let\u0026rsquo;s start by thinking about what our response needs.\nEmailSentResult.php\nfinal class EmailSentResult { const REASON_INVALID_EMAIL = \u0026#39;invalid_email\u0026#39;; const REASON_RATE_LIMIT_EXCEEDED = \u0026#39;rate_limit_exceeded\u0026#39;; private $error; private $errorReason; private $errorReasons = [ self::REASON_INVALID_EMAIL, self::REASON_RATE_LIMIT_EXCEEDED, ]; public static function fromSuccess() : EmailSentResult { return new static(); } public static function fromError( \\Throwable $error, string $reason ) : EmailSentResult { if (!in_array($this-\u0026gt;errorReasons, $reason)) { throw new \\UnexpectedValueException(); } $this-\u0026gt;error = $error; $this-\u0026gt;errorReason = $reason; } public function isSuccessful() : bool { return empty($this-\u0026gt;error); } public function error() : ?\\Throwable { return $this-\u0026gt;error; } public function errorReason() : string { return $this-\u0026gt;errorReason ?? \u0026#39;\u0026#39;; } } The EmailSentResult class can indicate if the request was successful, and if it wasn\u0026rsquo;t, the reason. In particular, the reasons are finite and known, thus can be handled specifically by the application. It also includes the original error to be able to log stack traces in the cases were it is unexpected.\nThe interface can be improved to return this result.\nEmailClientInterface.php\ninterface EmailClientInterface { public function sendEmail( string $emailTemplate, EmailAddress $address, array $parameterMap ) : EmailSentResult } The application is now able to handle known reasons of failure.\nOrder.php\nfinal class Order { private $emailClient; public function __constructor(EmailClientInterface $emailClient) { $this-\u0026gt;emailClient = $emailClient; } public function placeOrder(CustomerOrder $order) : void { // charge and persist an order $result = $this-\u0026gt;emailClient-\u0026gt;sendEmail( \u0026#39;order.placed\u0026#39;, $order-\u0026gt;emailAddress(), [ \u0026#39;items\u0026#39; =\u0026gt; $order-\u0026gt;items() ] ); if ($result-\u0026gt;isSuccessful()) { return; } switch ($result-\u0026gt;errorReason()) { case EmailSentResult::REASON_RATE_LIMIT_EXCEEDED: // implement a retry or queue for later send break; default: // Log as an unrecoverable error // using $result-\u0026gt;error() for the stack trace } } } Any email client we wish to interop with the application can be swapped easily as long as it returns the EmailSentResult properly. The client is free to use all the exceptions it wants, as long as it converts those errors into a result. Notice that I didn\u0026rsquo;t include a concrete client in this article? That is intentional: the details of the client are irrelevant. If the properties of the email client can\u0026rsquo;t be defined in the interface, then it is not interoperable with our application, or at the very least, a potential hazard.\nUnexpected errors happen all the time in production environments. Be mindful of them, but don\u0026rsquo;t let them dictate the control flow of your application.\n","ref":"https://www.chris-saylor.com/posts/interop-in-php-should-not-be-exceptional/","tags":["php","design"],"title":"Interop in PHP Should Not Be Exceptional","type":"code"},{"content":"My wife\u0026rsquo;s computer is an iPad and she needed to print something. Our printer is an older Brother network laser printer and as such doesn\u0026rsquo;t support AirPrint. I used this guide to get an Avahi discovery service running on my Raspberry Pi to enable my printer to be discoverable by iOS, iPadOS, and Android.\n","ref":"https://www.chris-saylor.com/link/make-any-printer-airprint-compatible/","tags":["raspberry pi"],"title":"Make Any Printer AirPrint Compatible","type":"link"},{"content":"Like many engineers, I have a life-long passion for learning. I satiate this need by creating side projects that explore new concepts, languages, and tools. Some of my side projects have been successful and used by a wide audience. Of course, like many others, I also have a big list of incomplete and abandoned projects. Each project may require it\u0026rsquo;s own services and other software dependencies, which over time becomes quite hard to maintain on a server. Let\u0026rsquo;s face it: managing software deployments is almost never the exciting part of a side project and in some cases is the reason we abandon it in the first place. In this article, I\u0026rsquo;m going to cover my deployment setup used for all of my web-facing projects.\nTo begin, let\u0026rsquo;s talk about the server itself and the bare-minimum software I have installed to get underway. I currently use Linode as my VPS provider, but any of the major players work just fine. Linode has server bootstrapping mechanism called \u0026ldquo;Stackscripts\u0026rdquo;. I use a custom Stackscript that installs and configures Docker which is a container orchestration manager and is at the heart of deployment automation for my setup. An abridged version of this Stackscript looks like:\n#!/usr/bin/env bash DEBIAN_FRONTEND=noninteractive apt-get install -y apt-transport-https apt-get update apt-get install -y \\ python-pip \\ screen \\ bash-completion command-not-found \\ mlocate \\ htop iotop \\ vim curl wget wget -qO- https://get.docker.com/ | sh pip install docker-compose With the \u0026ldquo;physical\u0026rdquo; server preparation out of the way, let\u0026rsquo;s talk about how apps can safely be exposed to the public internet.\nTopology of our host machine\u0026#39;s exposure network. There are two essential Docker images that work together to automate app exposure:\nNGINX proxy\nNGINX proxy is a Docker image that utilizes NGINX as a \u0026ldquo;reverse proxy\u0026rdquo;, which is essentially a way to pick and choose what we expose to the internet by proxying user requests from the web to the applications. It uses docker-gen to automatically generate NGINX virtual host configurations from Docker container meta data. In essence, I can set an environment variable VIRTUAL_HOST=www.example.com on an application container, have the DNS point to the physical address of the VPS, and when that container boots, NGINX proxy automatically writes nginx configuration files, reloads NGINX, and begins proxying connections to the application. A sample docker container initialization of the NGINX proxy image might look like:\ndocker run -d \\ -p 80:80 \\ -p 443:443 \\ --name nginx-proxy \\ --network public-nw \\ -v /var/run/certs:/etc/nginx/certs:ro \\ -v /etc/nginx/vhost.d \\ -v /usr/share/nginx/html \\ -v /var/run/docker.sock:/tmp/docker.sock:ro \\ jwilder/nginx-proxy Note: If you are concerned with having a container exposed to the internet that has a volume reference to the docker socket, you can run docker-gen as a separate container off the public-nw network.[4]\nLet\u0026rsquo;s Encrypt proxy companion\nThe Let\u0026rsquo;s Encrypt proxy companion works hand-in-hand with NGINX proxy\u0026rsquo;s docker-gen to make and receive requests to Let\u0026rsquo;s Encrypt to automate secure SSL certificates. It is similarly configured like the NGINX proxy image above: setting the LETSENCRYPT_HOST environment variable allows the companion image to configure the certificate creation process[1]. A sample docker container initialization of the Let\u0026rsquo;s Encrypt image might look like:\ndocker run -d \\ --name encrypter \\ --network public-nw \\ -v /var/run/certs:/etc/nginx/certs:rw \\ --volumes-from nginx-proxy \\ -v /var/run/docker.sock:/var/run/docker.sock:ro \\ jrcs/letsencrypt-nginx-proxy-companion Note: The volume for the certs /var/run/certs must be the same as specified in nginx-proxy above, otherwise the reverse proxy won\u0026rsquo;t have a reference to the Let\u0026rsquo;s Encrypt generated certificates.\nLet\u0026rsquo;s suppose you\u0026rsquo;ve made a Go application that uses PostgreSQL as its database. First, you would build the app within a container image that builds and exposes a port to the app via a PORT environment variable[2]. Next, you would pull down a PostgreSQL container image and do whatever configurations your app requires[3]. For this application \u0026ldquo;cluster\u0026rdquo;, when creating these two containers, create a dedicated app network via --network myapp-nw. On the application container, also have it join the public-nw network that contains the reverse proxy. This will allow the NGINX reverse proxy to be able to talk to your application container, but would not allow it to talk to the PostgreSQL container; meaning PostgreSQL would not be exposed to the internet. Finally, specify VIRTUAL_HOST and VIRTUAL_PORT for NGINX to proxy, and LETSENCRYPT_HOST and LETSENCRYPT_EMAIL for SSL certificate generation.\nThe biggest advantage to this setup is the ability to run any application capable of running as a container and have it exposed. Things like your own CI Server, Git server, or even your own data science Jupyter notebook become very easy to deploy.\nThere are many barriers and excuses for side projects to rot or lay dormant never to see light of day. Don\u0026rsquo;t let deployments be one of them.\nFootnotes This may seem like an overkill step if you are using something like Cloudflare\u0026rsquo;s flexible SSL option, however I like to ensure users of my apps are encrypted all the way to my server, not just to Cloudflare. There are many Golang developers that would scoff at building a Go app in a container (since it can compile to a target), however we\u0026rsquo;re interested only in Docker\u0026rsquo;s networking ability and container metadata. Docker is not great for stateful software like databases. Always have the data written to disk via a volume, or use an external service when the project gets serious. You\u0026rsquo;re likely to move to an external service anyways if you need to scale horizontally. Instructions on how to setup docker-gen as a separate container can be found here. Further reading Jason Wilder\u0026rsquo;s original blog post which goes over more thorough details on how the stack I describe above works underneath. [web archive] What is a reverse proxy? Docker networking overview Credits Jason Wilder for his work on these excellent NGINX proxy automation container images, without which much of this article would be much more difficult to setup. Featured image by Lee T. ","ref":"https://www.chris-saylor.com/posts/managing-polylingual-side-projects/","tags":["docker","ops","side projects","nginx","let's encrypt"],"title":"Managing Polylingual Side Projects","type":"code"},{"content":"Do you remember back to your school days of writing a paper, giving it a once over, and turning it in only to be surprised on return of bad editing marks? After all, I reviewed it; how did I miss so many mistakes?\nWhen we start actively writing, our brains devote more processing resources to higher-order thinking. In other words, the brain filters out low-level mundane items like spelling and grammar in favor of conveying meaning. This is a well known phenomenon that has often been written about 1, so you\u0026rsquo;re likely already aware.\n\u0026ldquo;OK, so you\u0026rsquo;ve made me take the painful trip back to high school term papers, what\u0026rsquo;s your point?\u0026rdquo;\nThis isn\u0026rsquo;t just limited to writing papers and articles. It exists in all creative processes. As a software engineer, this happens to us all the time. We\u0026rsquo;re thinking about a problem at a high level, we write code to solve the problem, then the dreaded bugs come. Even worse, things downstream from our changes may be affected. How do we combat this?\nRuminate. Let it stew.\nWhile this is true during active problem solving, the context never fully disappears. I will sometimes wake up in the middle of the night with a solution to a problem from the previous day. Often, we push ourselves to be so productive that we rarely give ourselves time to sit back and think about what we\u0026rsquo;ve done. We\u0026rsquo;ve grown accustomed to doing one task and fixing three bugs. If we were to just slow down and contemplate our solution, we can catch what our brain was helping us gloss over in the first place. Maybe even allow for a holistic solution!\nSo before you push to production, pause and let the code percolate.\nSources Wired.com - What\u0026rsquo;s Up With That: Why It\u0026rsquo;s So Hard to Catch Your Own Typos - Archived Link Credits This is why you should never interrupt a programmer - Archived Link Featured image by Tachina Lee ","ref":"https://www.chris-saylor.com/posts/ruminate-more/","tags":["retrospective","ruminate","deep thinking"],"title":"Ruminate More","type":"code"},{"content":"At Zumba, I implemented CSRF protection to all our state-changing user inputs. With a large and complicated site, implementing CSRF is a very tricky ordeal. There are several strategies with varying degrees of difficulty and effectiveness to consider. The real challenge, which is often not written about, is deploying it to active users with minimal disruption.\nIn my case, I chose the Synchronizer Tokens approach. The gist of this approach is to generate a cryptographically, secure randomized token, and store in the user’s session. Then on the form, render the token and check that the submitted value of the token matches the session’s token. Seems pretty straight-forward at first, but there are a couple of things to consider:\nIf you weren’t storing sessions for un-authenticated users, and there are forms that change state available to them, this causes more a lot more sessions to be created, which could put pressure on your session backend. Consider caching: if you do any edge caching of HTML, Ensure you don’t cache the rendered CSRF tokens.1 Like most development shops, we use an application framework for our user-facing services. Most frameworks come bundled with CSRF support (or have packages available that add support). There are many resources about how to implement the synchronizer token strategy; therefore, I won’t reiterate here. There are, of course, some hindrances that I had to resolve.\nSome frameworks implement “single-use” synchronizer tokens. It hardly adds any security advantages2 at a considerable cost to user experience. The user can encounter errors using the “back button” to a form or browsing with multiple tabs in which one form overrides the token to another form. My advice here is not to use single-use tokens.3\nProtecting AJAX requests is not as straight-forward as protecting HTML rendered forms. In my case, the CSRF token was available in a javascript variable (or within the confines of an iffe). A custom header on the request contains this value. The custom header gets mapped from the header on the server-side into whatever makes sense for the framework/package’s CSRF implementation. If the application framework does not expose a way to extract a generated token for this purpose, you may need to evaluate another solution.\nExpiring tokens can also be problematic and frustrating to users. Consider that a user may have a rendered form open for a long time before deciding to submit. In either the case of HTML rendered forms or AJAX requests, you’ll want to make a mechanism to refresh those tokens periodically.\nFor rendered HTML forms, the framework or CSRF library chosen has a specific way of rendering the token for consumption. In most cases, it is a hidden input field. We deployed a client-side javascript in conjunction with an endpoint to periodically get a fresh CSRF token4 to avoid the user seeing a CSRF error. In our case, it looked something like this:\nconst interval = 1000 * 60 * 30; setInterval(async () =\u0026gt; { const inputs = document.querySelectorAll(\u0026#39;input[name=\u0026#34;data[_Token][key]\u0026#34;]\u0026#39;) try { const res = await fetch(\u0026#39;/user/refresh_csrf\u0026#39;); const token = (await res.json()).csrfToken; inputs.forEach(input =\u0026gt; input.value(token)); } catch (e) { console.error(e); } }, interval); In the case of AJAX requests and token expirations, there are alternatives. In my case, we use Angularjs, which supports the concept of request interceptors. I used this concept to intercept the response, check if it is a CSRF error, make a separate request to get a fresh token, and then re-issue the request with the new token. Even though the request failed due to CSRF, the user was none-the-wiser. Here is an abridged version of the interceptor used:\nconst csrfInterceptor = [\u0026#39;$q\u0026#39;, \u0026#39;$injector\u0026#39;, ($q, $injector) =\u0026gt; ({ responseError: (rejection) =\u0026gt; { if (rejection.data.error_code === \u0026#39;CSRF\u0026#39;) { const $http = $injector.get(\u0026#39;$http\u0026#39;); return $http .get(\u0026#39;/refresh_csrf\u0026#39;) .then(res =\u0026gt; Object.assign(rejection.config, { \u0026#39;X-XSRF-Token\u0026#39;: res.data.token })) .then($http); } return $q.reject(rejection); } })]; To use the above, configure the $httpProvider:\nmyModule.service(\u0026#39;csrfInterceptor\u0026#39;, csrfInterceptor); myModule.config([\u0026#39;$httpProvider\u0026#39;, $httpProvider =\u0026gt; { $httpProvider.interceptors.push(\u0026#39;csrfInterceptor\u0026#39;) }]); Finally, ignoring bots is also pretty critical to not fill up your session backend. Legitimate bots should not make state changes.\nDeploying CSRF to an active site is like working on an engine while it\u0026#39;s running. As for deployment: I was unable to find any war stories of rolling out CSRF protection to actively used sites. Zumba, at any given time, has many active users. Any changes requiring the page to be re-rendered must have a plan for transition. In the case of CSRF, anyone that had a window open to a form that is now CSRF protected, but didn’t have the CSRF token rendered would encounter a CSRF validation error. This validation error can be incredibly damaging for sales and conversion rates. To minimize user disruption as much as possible, I made use of a multi-phase rollout plan:\nPhase 0: Timing. I analyzed the traffic and looked for quiet times. I was in constant communication with my team about timing to avoid rolling it out during a significant sale or when marketing is pushing traffic. Phase 1: Generate tokens. The first thing we did was start generating valid CSRF tokens, storing in user sessions, and rendering on forms. During this phase, we are not validating the tokens on submission yet; we are merely seeding as many pages as possible with rendered CSRF tokens. This phase lasted a few days until we were satisfied that as many users that had our site open in a tab also had a CSRF token rendered ready for submission. Phase 2: Feature flags, aka divide and conquer. To test a global change across a site as large and complex as Zumba is not practical. There are just too many places to check. Users may be adversely affected by a form missing a token render in an area you haven’t gotten to yet upon deployment. I made heavy use of feature flags5 6 so that we could enable small sections of the site at a time to quickly test and monitor. Canary releases were immensely helpful to find issues in places we missed and have a minimal cohort of our users affected. Phase 3: Monitoring. It is crucial to ensure a sweeping change where an error gets produced is easy to find in logs. If your logging platform supports setting up alerting for occurrences over an interval, it is well worth it when doing the canary releases. Following this plan and the implementation details above, I was able to roll out CSRF across the site with minimal apparent disruption to end-users. While there is no one-size-fits-all approach when it comes to CSRF, I hope that by documenting my approach that it may save others from some deployment pain.\nWith edge caching services such as Varnish, you\u0026rsquo;ll need to make use of \u0026ldquo;edge side includes,\u0026rdquo; which bypasses the cache to the backend to render a partial section of the HTML (usually encapsulated in \u0026lt;esi\u0026gt; tags) whenever you are rendering the CSRF token. Just be mindful of the placement of these ESIs as they can harm performance.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nThere are a lot of dissenting opinions on single-use tokens in this forum: https://security.stackexchange.com/a/22936.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nIf you have to do a token per request, then you may need to look into strategies to store multiple tokens in the session with a timestamp, salt, and signature ability.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n@iaincallins goes into some detail on implementation rationale for exposing a CSRF token endpoint in their article: https://medium.com/@iaincollins/csrf-tokens-via-ajax-a885c7305d4a. I agree with the author that it poses little to no security threat as an attacker would have to have access to much more lethal things (such as a session token or some XSS vulnerability) to compromise it.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nUnlike many feature flag libraries that toggle a binary state for a feature, Swivel allows for activating features on pre-configured cohorts of users.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nYou can view a more detailed explanation of the reasons for using swivel via Stephen Young\u0026rsquo;s presentation on Feature Flags are Flawed: Let\u0026rsquo;s Make Them Better.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","ref":"https://www.chris-saylor.com/posts/deploying-csrf-to-an-active-site/","tags":["security","CSRF"],"title":"Deploying CSRF Protection to an Active Site","type":"code"},{"content":"Ever wondered what it would be like to run a kitchen? Chaos. Overcooked 2, when playing with kids, becomes absolute pandemonium, but also incredibly fun. This game gives kids a chance to see what it would be like to be in charge, give orders, and deal with failures.\nFull disclosure, most of our experience with this game is the original with the sequel just being an improved version of the first.\nSam has always been a little helper in the kitchen, whether it\u0026rsquo;s helping to stir the pot or knead the dough for bread, he is eager to learn. One of the most entertaining parts of this game is how they take technical recipes that may include multiple cooking techniques and simplify them. If you need to produce dough for pizza, you don\u0026rsquo;t knead it, you chop it. Hamburger patties? Chop it. Shredded cheese for tacos? Chop it. So it makes recipes very easy to understand and formulaic without requiring a culinary degree.\nWin or lose, you do it together. The crux of the game is multitasking and coordination. Some levels can have complicated steps that take more cognitive load, but always offers a strategy that you can assign your young one that can keep them on task and contribute to winning. I\u0026rsquo;m not going to sugar coat it, though: this game is hard. It\u0026rsquo;s even harder in multiplayer with increased score requirements.\nVariety of cute characters makes this very appealing to kids Overcooked 2 offers a wide variety of human and animal chef avatars that are all pleasing a cute to look at. The imagery, iconography, and graphics are all cartoonish and very clear with what they represent.\nTeamwork is essential in Overcooked 2, and one thing this game does well is training you to work together efficiently. It\u0026rsquo;s a great time to show how to react to failure on a team. Instead of blaming, we each come up with positive ways to improve what we were doing to succeed. Sometimes I will point out something for Sam to do better, and unsurprisingly, he will find things that I could do better as well. It\u0026rsquo;s been an overall positive experience.\nThe controls are also very simplistic. The basic gameplay requires just the control stick and two buttons, one to pick up and put down, and one to perform an action like chop or wash dishes. As you progress, you\u0026rsquo;ll incorporate dashing in between tasks.\nInteresting level themes keep the game fresh all the way through. The design choices in the level design and how they incorporate game elements to interact with them is very smart and noticeable. The environment of the levels is where all of the complications come into play, whether it\u0026rsquo;s pedestrians, earth-quake cracks that separate the stove from the plates, or rats that try to steal unattended food.\nOverall 5/5 Overcooked 2 is a team-building game, that is cute and entertaining. It can be frustrating at times, however, when you finally get the win that you\u0026rsquo;ve struggled with, it\u0026rsquo;s all the more sweater. I highly recommend this game.\nBreakdown Spacial Awareness: 5/5 Memory Skills: 3/5 Hand-eye Coordination: 4/5 Problem Solving: 5/5 Reading Skills: 2/5 Entertainment: 5/5 Appropriateness: 5/5 Reviewed on the PC version through Steam.\nSee my Steam Stats for Overcooked and Overcooked 2\n","ref":"https://www.chris-saylor.com/games/feel-the-heat-in-overcooked-2/","tags":["games","reviews"],"title":"Feel the Heat in Overcooked 2","type":"games"},{"content":"Like any hobby, the type and quality of the tool used to interact with that hobby can make or break its enjoyment. A dull knife makes cooking tedious and frustrating. So, too, can the wrong controller contribute to a distasteful experience with games.\nIn Sam\u0026rsquo;s case (and anyone who follows this series with a little one can attest), the size of his hands becomes a significant factor. Gaming has become mainstream among adults as well as kids. In 2019, nearly 80% of all gamers are aged 18 or older (source: Statista). It stands to reason that a similar proportion of game controllers are designed for an adult demographic (read: adult-sized hands). Most reviews for controllers are also from the perspective of an adult, and this presented a challenge. I needed to find a controller that not only worked for the majority of the games we play but also was comfortable for Sam to use.\nIt\u0026rsquo;s vital to have a foundation of suitable controllers, then filter it down to kid-friendly hardware. Our friends at Cup of Moe have done the leg-work for you, narrowing down the controllers based on traditional requirements.\nWhen assessing a controller for Sam, I evaluate three primary considerations:\nErgonomics: Is the controller small enough for my child\u0026rsquo;s hands to be comfortable? Compatibility: Does the controller work for my gaming systems? Economics: How expensive or versatile is the controller? Size isn\u0026rsquo;t the only consideration from an ergonomic perspective. Sometimes a controller fits in hand, but certain features are out of reach, like a thumbstick or a \u0026ldquo;select\u0026rdquo; button.\nThe D-Pad and right thumbstick are too far away from the grips for Sam to use. This is the typical design of all modern console controllers, so you\u0026#39;ll need to find an alternative. Sam and I tend to play a lot of platformers, and one thing that surprised me is that he disliked using the D-Pad. I almost can\u0026rsquo;t play a platformer very well with a thumbstick, and every time I suggested he try it, he rebuked. It is all about the physicality. It takes a significant amount of thumb strength to roll his small thumb from one side to the other while maintaining enough pressure, especially on large \u0026ldquo;disc\u0026rdquo; pivots. The reality is, the accuracy of the D-Pad isn\u0026rsquo;t necessary when you\u0026rsquo;re five. Therefore, thumbsticks are preferred.\nIn terms of compatibility, I look for multi-platform controllers. In our house, we have a Nintendo switch and a custom-built PC. If a controller works on both, that is a huge time saver as well as a cost-effective choice. However, this is a secondary consideration. Sometimes a controller can connect to multiple systems, but doesn\u0026rsquo;t work well with any of them. Make sure the reviews you read cover connectivity and latency.\nWe bounce between four different controllers depending on the game in our house: 8BitDo Sn30 Pro, Nintendo Switch Joycons, Xbox 360 wireless, and a Steam controller. Of the four, overwhelmingly, we use the 8BitDo Sn30 Pro. It works with all of our game systems, it is small and lightweight, and has great utility. It has all the modern fixings: gyro-controls, rechargeable battery, USB-C connection, and auxiliary buttons for the \u0026ldquo;home\u0026rdquo; key as well as screenshots for the Switch. All of this contained within a Super Nintendo form factor. It is a must-buy if you have a similar variety of game systems.\nWith the smaller form factor of the original SNES body, the compatibility of multiple systems, and the price, this is a must buy for your younger players. The Xbox 360 controllers are what I primarily use for any 3D games. The left thumbstick is in a more comfortable position compared to the 8BitDo; however, the right-side thumbstick is too hard to reach for Sam. He can\u0026rsquo;t reach it without removing his hand from the grip. As a result, he tends to use the 8bitDo regardless of game type. The Steam controller gets used when we stream games from the PC to the Steamlink in the living room. That controller is too overwhelming and ergonomically uncomfortable for use for Sam, so I tend to be the only person that uses that controller.\nWhen considering a controller for your younger players, reflect on what I\u0026rsquo;ve laid out above, scrutinize the pictures, and be mindful of the physical limits of your gaming partner. Also, consider getting a controller that is \u0026ldquo;theirs.\u0026rdquo; Allow them to decorate and put stickers on it and really bring them into the gaming hobby.\nPhoto Credits:\nSean Whelan ","ref":"https://www.chris-saylor.com/games/choosing-controllers-for-younger-players/","tags":["games","reviews"],"title":"Choosing Controllers for Younger Players","type":"games"},{"content":"The short of it: if you take a Zelda game, replace almost all items with toy-themed objects, make it rogue-lite, and add a time-based reset loop, you get Swords of Ditto.\nWe\u0026rsquo;ve revisited this game a few times after mostly completing because it has one major thing that most games of this genre do not have: local co-op. The downsides to this game can almost be overlooked from just being able to share the experience.\nThere is a heavy emphasis on exploration early in the game, followed closely by puzzle-solving dungeons. Unlike other games of this genre, all elements are procedurally generated. Due to being randomly generated, almost all puzzle elements are strictly contained in individual rooms and require very infrequent back-tracking. This is a significant weakness of the generated dungeons: the puzzles are forgettable and dull. They lack the polish of other \u0026ldquo;aha!\u0026rdquo; type puzzle-solving that similar games in the genre do better.\nDiscovering what we are to become. The items found throughout the game are what Sam enjoyed the most. Golf-clubs, laser rings, and drones that fly around and explode are just some of the things that serve a combat role as well as a puzzle element. All of the items are themed after toys using rechargeable battery power as the game-mechanic for limiting their use.\nA giant sasquatch foot is one of many silly \u0026#34;toys\u0026#34; to uncover. The combat is simplistic, and while I got bored of it rather quickly, it kept Sam\u0026rsquo;s interest by being mostly frustration-free. The combat variety revolves around enemies that take different strategies to defeat. Graphics are friendly and cartoonish, and none of the character models frightened Sam. While there is violence in that you swing a sword at things, it is tempered by no gore, and enemies disappear when defeated.\nThere\u0026rsquo;s a cryptic story involved, but Sam never really seemed interested in it. The game loop consists of leveling your character, acquiring items, and then facing the final boss, then the cycle of the game repeats. There is an end-game type aspect that I won\u0026rsquo;t get into to avoid spoilers that break this cycle.\nDiscovering interesting characters is part of the charm. The developers of this game have changed it quite significantly since our first play-through. While much of the improvements are welcomed, I\u0026rsquo;m afraid to say that our most recent session has given the game a much more pressured experience to such an extent that we don\u0026rsquo;t play it anymore. After a game loop reset, we encountered an undefeatable \u0026ldquo;nemesis\u0026rdquo; that would follow you everywhere in the overworld. It is easy to run away from it, but it is a constant annoyance that caused both Sam and me to lose interest in finishing.\nThe most annoying thing introduced in the recent updates (as of this writing) is this undefeatable nemesis that follows and harasses in the overworld. Overall 2/5 Overall, this is a tame adventure RPG that has the promise of a lot of replayability with a trade-off of simple puzzles and tedious game-play. Some skills are engaged and it is a decent co-op adventure to having your child play with you. With the inclusion of a harassing \u0026ldquo;nemesis\u0026rdquo;, I don\u0026rsquo;t recommend this game as it is a major source of stress and frustration especially with kids just getting into games. If in future updates, the creators remove or significantly tone down the \u0026ldquo;nemesis\u0026rdquo; mechanic, then I would recommend.\nBreakdown Spacial Awareness: 2/5 Memory Skills: 2/5 Hand-eye Coordination: 3/5 Problem Solving: 3/5 Reading Skills: 3/5 Entertainment: 3/5 Appropriateness: 3/5 Reviewed on the PC version through Steam.\nSee my Steam Stats for The Swords of Ditto: Mormo\u0026rsquo;s Curse\n","ref":"https://www.chris-saylor.com/games/swords-of-ditto/","tags":["games","reviews"],"title":"The Swords of Ditto: Mormo's Curse","type":"games"},{"content":"For most of my career, I’ve been an individual contributor: focused on the tasks at hand, writing code, among other things. All of that changed about six years ago. I found myself managing products, releases, infrastructure, and making many software decisions. As a result, I was promoted to “Lead Software Engineer.” Other developers were looking to me for answers, and I was involved with high-level decisions for products and features, so it seemed like a natural progression. However, I was ill-prepared for what it takes to be a lead engineer.\nFirst, let me define what being a lead engineer means at Zumba™:\nWork with product managers in determining the technical needs of a product. Ensure product milestones get reached on time. Orchestrate the tasks of the team to reach an ultimate goal. Manage production-related changes (schema updates, third-party provisioning). Provide technical direction to developers on my team. Write some code. These may or may not be or mean the same thing at other companies, but I would imagine they are at least similar in scope.\nThe first real lesson I learned, which seemed counter-intuitive to me at the time, is that I had to let go of the fine details. I had to let go of control of how every task is completed. I just had to let go. It is a tough thing to do when you have been an individual contributor for years. I care deeply for writing quality software, its performance characteristics, and its security implications. The details of each part of the product are essential, but they are no longer my job. They are the job of the engineers on my team. To let go is to trust the individual engineers of my team will care for these things just as I did.\nI have to give my team the same opportunities to learn and fail that was afforded to me.\nLike a carefully planted garden, you have to cultivate these attributes into the team. It has to start with the lead being the embodiment of those values.\nThere’s also a strange phenomenon that occurs when you start to let go of the individual tasks: feeling unproductive. I began to spend a lot less time coding, and more time looking at other’s code or organizing task tickets or assisting with deployment planning. Indeed, my productivity did decrease which is the nature of this role. That lost individual productivity is regained if not amplified by the increases of productivity of my team.\nThere’s a term that floats around in the tech world about the fabled “10x” engineer. It is often misconstrued as one engineer being able to replace the output of ten other “lesser” engineers. The reality is that every organization has multiple “10x” engineers. They don’t replace ten engineers; they make ten engineers better.\nTrain people well enough so they can leave, treat them well enough so they don’t want to. ― Richard Branson\nTo let go of my old detailed concerns, I had to know to whom I was passing the torch. I needed to get to know my team; not just their birthday and favorite foods, but their capabilities, and more importantly: what they want their abilities to be. Only after you know what each team member needs can you begin to select a management style.\nOver the years, I’ve learned the truth about management styles: there is no true management style. Each person on your team may require different styles. Treating a senior engineer the same as a junior engineer will either piss them off or cause them to leave (or both). Junior engineers need more support, and possibly some “micro-management” at the beginning. However over time, that junior engineer that you had to hand-hold doesn’t want to be hand-held forever, and if they do, they are not growing, and you are failing.\nThis concept of ever-changing teams and the growth of the individual team member opened my eyes to the real challenges of management: adapting management styles per person and over time.\nManagement is doing things right; leadership is doing the right things. ― Peter F. Drucker\nWhen you start at the company towards the beginning of their tech stack and stick around, you tend to become a prolific individual contributor to the code base. Often your style ends up becoming a defacto standard way of doing things. This \u0026ldquo;spread\u0026rdquo; happens either from other engineers agreeing with your style and more often than not, from more junior engineers copying and pasting in the code-base. Good or bad, this is an example of passive leadership. It’s usually the first sign that you are on the precipice of leadership.\nOnce promoted, I noticed that my team was continually scrutinizing everything I did. If I criticized how something was done, it couldn\u0026rsquo;t be for things that I do myself. If it is something that I also struggle with, I acknowledge it, and then I follow through on changing it in myself. I can’t expect more of my team than I expect of myself. It is vital to be an example of the qualities I wanted to instill in my team. Showing someone the right thing to do is much more powerful than telling them.\nHonest communication is built on truth and integrity and upon respect of the one for the other. ― Benjamin E. Mays\nIf trust is the foundation of the team, then communication is the cornerstone. Considerable effort has to be employed in establishing regular communication with and amongst the team, not just one-on-one, but also how project details and decisions are shared. I realized that not all communication is going to flow through me. It is impossible to control, and indeed highly undesirable to do so. Encourage participation and open discussions, and more importantly, the rigor of documenting the action items or decisions made due to that discussion.\nIn spite of the accuracy and levity of this comic, taking on the mundane so that the experts on your team can focus is sometimes required. The hardest lesson I had to learn was the delegation of work, of which I still struggle with today. It is yet another strain against the individual contributor’s tendencies. Ultimately, it is a lack of trust in the team that made delegation so hard for me at the beginning. I frequently refer to letting go and trusting the team; delegating tasks of the project (even those difficult ones), is the manifestation of that trust.\nThe constant need to learn and stay up-to-date isn’t just for technical skills for me anymore, and I’m not done yet. Continuous learning, self-evaluation, and checking my hubris served me well as an individual contributor, and I believe these attributes to be equally if not more so important as a tech lead.\nFeatured image by Markus Spiske\n","ref":"https://www.chris-saylor.com/posts/learning-to-be-a-tech-lead/","tags":["tech lead","leadership","management"],"title":"Learning to Be a Tech Lead","type":"posts"},{"content":"It is an unspoken rule that if you utilize something other than Wordpress for a blog that you must include an article on how it is built. This is that article.\nI\u0026rsquo;ve gone through several renditions of this site over the years starting with a custom PHP site, moving to Wordpress, Jekyll (Github pages), raw HTML, Medium, and finally what it is today: Hugo. With the history enumerated, you might be wondering, why expend the effort on changing technologies for a blog?\nThis is the first in a series that will be covering various components of Hugo and how to manage \u0026ldquo;dynamic\u0026rdquo; content in a static way.\nOver the years, my requirements have changed but there are some core requirements:\nCompletely control content Easy to write content Easy to maintain Fast \u0026amp; Scalable Let\u0026rsquo;s address the elephant in the room: Medium. The benefits of Medium are tantalizing: a very nice editing experience, easy publishing, and no server required. However, the limited content customization leaves a lot to be desired. Avoiding kicking a dead horse, suffice it to say, I don\u0026rsquo;t mind having my content on Medium, but I wanted my own domain and platform to be the canonical source in the event that Medium shuts down.\nOne thing that I have gotten used to over the years is writing in markdown. It provides an easy way to add html markup structure while making it readable in its raw format. Platforms like Hugo make this simple as markdown parsing is a first-class citizen. Since it works with regular markdown files, I can use any editor I want and not limited to a crippled web interface.\nHugo, so far, checks the boxes of owning content and easy content writing, but what about hosting? Since the output is static, we can use a myriad of options for serving up plain-old HTML. In my case, I chose Github Pages, since I\u0026rsquo;m already paying for a pro account.\nThis is a diagram showing the build of the site and how it is consumed. I\u0026rsquo;ve been making use of Drone.io for personal use of my side projects as both a CI/CD for over a year now, and it should come as no surprise that I use this tool to deploy this site as well. Indeed, it was quite easy to set up a drone configuration to build the site with hugo and deploy the generated markup to Github Pages:\npipeline: build: image: jojomi/hugo commands: - hugo version - hugo when: branch: hugo event: push publish: image: plugins/gh-pages secrets: [ github_username, github_password ] pages_directory: public/ target_branch: master when: branch: hugo event: push Now whenever I push to the hugo branch, the site is rebuilt and pushed up to github pages automatically.\nAs you can see, it gets started immediately and takes only a few seconds. To further increase scalability in the off-chance an article I wrote gets posted on Hackernews or reddit, Cloudflare sits in between the end-user and Github\u0026rsquo;s page server.\nFurther Considerations Some keen readers might be asking themselves. Since the content is static and has no database server, how does one search the site? In my case, I have hugo generate a consumable search index, then have a client search library called lunajs search and interpret the results. It is extremely fast, with no way back-end search database required. The downside is that the page has to load the entirety of the site\u0026rsquo;s content in order to search it, but with the limited amount of content on my site, it has as much impact as loading a small image. I will cover specific details of this and other \u0026ldquo;dynamic\u0026rdquo; topics in future articles.\n","ref":"https://www.chris-saylor.com/posts/meta-blog-deploy-pipeline/","tags":["continuous deployment","hugo","meta"],"title":"Meta: How this blog is built and deployed","type":"code"},{"content":"Jamestown, released back in 2011, falls under the \u0026ldquo;shmup\u0026rdquo; (Shoot em\u0026rsquo; Up) category. It is a top-down shooter hearkening to the days of Galaxians set in a \u0026ldquo;17th century\u0026rdquo; British colonial Mars. This is one of the first games that Sam and I played cooperatively.\nMost games in this category are incredibly difficult for adults which makes them completely unapproachable for kids. Jamestown is different in the way it approaches cooperation. The easier difficulties are generous with power-ups that \u0026ldquo;revive\u0026rdquo; fallen players. The fallen player will also automatically revive after a short amount of time provided at least one player is alive. If you are good at this kind of game, kids can still contribute and make it through the entire level. It also helps that the hit-box is minuscule to the point that your \u0026ldquo;ship\u0026rdquo; can \u0026ldquo;touch\u0026rdquo; the projectile without dying.\nThe stage bosses offer a good variety whose difficulty are well tuned for progression. The boss battles didn\u0026rsquo;t seem to faze Sam in terms of their demeanor. None of the fights had any puzzling aspects to them, however they did have recognizable patterns. Often, I would get \u0026ldquo;warned\u0026rdquo; about an upcoming attack by Sam based on his precognition from past sessions.\nThe scenery is gorgeous with a steampunk quality and the graphics hold up well given its age. The \u0026lt;abbr=\u0026ldquo;Original Sound Track\u0026rdquo;\u0026gt;OST is also incredible with several swells of the music coinciding with events on screen that really adds to the experience.\nJamestown Soundtrack by Francisco Cerda\nThere are some downsides. The difficulty does ramp up to a point where it is pretty hard to finish, however younger players don\u0026rsquo;t hinder, they just contribute less. Additionally, after an extended play time, noticeable graphical anomalies start to appear. This may be due to the age of the game and the drivers of my specific hardware, so your milage may vary.\nOverall 4/5 Most skills are engaged and it is a great starter to having your child play with you. Sam and I have enjoyed our playtime with this game, and we revisit it from time to time. Recommended.\nBreakdown Spacial Awareness: 5/5 Memory Skills: 3/5 Hand-eye Coordination: 4/5 Problem Solving: 1/5 Reading Skills: 2/5 Entertainment: 4/5 Appropriateness: 3/5 Reviewed on the PC version through Steam.\nSee my Steam Stats for Jamestown\n","ref":"https://www.chris-saylor.com/games/jamestown/","tags":["games","reviews"],"title":"Jamestown: Legend of the Lost Colony","type":"games"},{"content":"Remember that old \u0026ldquo;Snake\u0026rdquo; game you had on your blue Nokia phone brick from the turn of the century? \u0026ldquo;Snakebird: Primer\u0026rdquo; is that game with inventive puzzles mixed in. It is a \u0026ldquo;sequel\u0026rdquo; to \u0026ldquo;Snakebird\u0026rdquo; with a relaxed feel and simpler puzzles for the more casual player or younger kids.\nThe cute, animated birds were the main appeal to Sam. Their facial expressions can be humorous especially when the bottom bird is piled on by three other birds, or when they are squeezed into a small space.\nMuch effort is put into making it a frustration-free experience. Non-linear paths to solving puzzles are available so that if you get stuck on one, you can ruminate on that puzzle while attempting another.\nAlternative paths avail themselves as you progress. The level design is top-notch. New concepts are introduced in such a way that the player can fail at the concept early without major repercussions. Some puzzles can take some time to get the birds into position, and failing towards the end can be frustrating. However, simple mistakes are easily corrected as you can undo an unlimited number of moves made.\nThe backgrounds of the levels change based on your location on the level-selection map and often are themed to the \u0026ldquo;biome\u0026rdquo; they are depicting, including adding some props and hazards that you would expect in that environment.\nLevel backgrounds are varied and wonderful to look at. All the puzzles are small enough to be completed in short sessions, so it is easy to limit the amount of time to play. Even the toughest puzzles that Sam and I played were at maximum 10 minute sessions.\nSometimes props are integrated into the solution. Finally, the controls are simple. With movement, one button to toggle between multiple birds (if applicable), and a button to undo last move. All puzzles are solved through movement alone, so if your child struggles with hand-eye coordination, this game can still be accessible and easily enjoyed.\nSam and I are still working through some of the puzzles, but we\u0026rsquo;ve had fun along the way.\nOverall 3/5 Problem solving skills are highly engaged, but can be a bit frustrating and repetitive. The price and bite-sized levels make this a decent buy.\nBreakdown Spacial Awareness: 3/5 Memory Skills: 3/5 Hand-eye Coordination: 1/5 Problem Solving: 5/5 Reading Skills: 0/5 Entertainment: 3/5 Appropriateness: 5/5 Reviewed on the PC version through Steam.\nSee my Steam Stats for Snakebird: Primer\n","ref":"https://www.chris-saylor.com/games/snakebird-primer/","tags":["games","reviews"],"title":"Snakebird: Primer","type":"games"},{"content":" This is the first entry in a series entitled \u0026ldquo;Games with Sam\u0026rdquo; in which I review games from the context of playing with a pre-school-aged child. We will explore his reactions to the game as well as develop skills that are gained from the experience.\n\u0026ldquo;Blossom Tales: The Sleeping King\u0026rdquo; is a 2D top-down-view adventure game that harkens to the days of Zelda. Every component is an homage to Zelda few other games can not espouse. Opening with another homage to \u0026ldquo;The Princess Bride\u0026rdquo;, a brother and sister are being told the story of “The Sleeping King” by their grandfather. This presents a fascinating technique of story-telling where some parts of the story are “customized” by the choices of the brother or sister or, sometimes, by you playing a mediator between the two. You explore an over-world of Blossom referred to Hyrule all but in name. There are humorous tongue-in-cheek references sprinkled throughout the game to give the adult in the room a chuckle for the recognition and the child a chuckle from proximity. Finding items, exploring dungeons, solving puzzles, and defeating bosses offer a nice harmony of action, exploration, and problem-solving to youthful and hungry minds.\nPeter Falk would be proud. The puzzles vary and intermix between fast-paced combat puzzles and slow-paced moving-blocks puzzles which in combination is a nice balance between hand-eye coordination and problem-solving.\nSam was keen on the environmental puzzles that involved using items found, such as bombing cracked walls or lighting torches by shooting arrows through flames. He often referred to past parts of the dungeons becoming available after gaining a new item or key.\nAlthough there are few dungeons, they each have a unique style and “feel”. All had similar puzzle elements whose mechanics change by the \u0026ldquo;biome\u0026rdquo;. For example, a sliding block puzzle in the “swamp” dungeon would be solved by sliding it one space at-a-time, whereas, on the “ice” dungeon, the blocks would slide on the ice until it hit an obstruction. None of these puzzles were difficult for an adult, however, they all had simple mechanics which made it accessible and sometimes solvable to Sam.\nOne of the first environmental puzzles that Sam could solve independently. Boss battles also have small problem-solving skills required for defeating, involving an item found in that boss’ dungeon. None of the “monsters” in this game were frightening to Sam. In a particular instance where some trees had been corrupted by the main antagonist, Sam was eager to \u0026ldquo;help\u0026rdquo; them get free of the evil influence, rather than destroy them. Usage of weapons by the protagonist such as a sword, bow-and-arrow, and bombs are abundant, yet there is no gore.\nHungry Hungry Hippos ... but with lasers! The save system is generous with progress. It saves the game state on every screen transition and when returning to the game it will enter a small story sequence to remind you of your current aim. This makes it great for short gaming sessions since some dungeons are long.\nTo wrap up on a very pleasant play through, I highly recommend this for playing with a young child (no younger than 4). \u0026ldquo;Blossom Tales\u0026rdquo; is much less cryptic than the typical Zelda-esque games where reading comprehension can be a barrier to pre-school aged kids. There were a plethora of visual cues to show where to go and how to solve the puzzles, and I was pleasantly surprised how little dialogue they used for this purpose. It is slightly derivative, however, it offers quite a lot of charm and the graphics and sound effects are evocative for adult and child alike.\nOverall 4/5 Most skills are engaged and reactions are noticeable. Recommended.\nBreakdown Spacial Awareness: 4/5 Memory Skills: 3/5 Hand-eye Coordination: 2/5 Problem Solving: 4/5 Reading Skills: 2/5 Entertainment: 4/5 Appropriateness: 3/5 Reviewed on the PC version through Steam.\nSee my Steam Stats for Blossom Tales: The Sleeping King\n","ref":"https://www.chris-saylor.com/games/blossom-tales-the-sleeping-king/","tags":["games","reviews"],"title":"Blossom Tales: the Sleeping King Review","type":"games"},{"content":" This is the follow-up post on building a chessbot for Slack.\nThe main focus on the previous segment was on technical engineering: authentication, request handling, image rendering, and chess rules. However, when submitting an app to the Slack App Directory, one has to take a step back and consider the entirety of the app from the user\u0026rsquo;s perspective.\nSubmission process The first thing you are greeted with when submitting an app is a rather intimidating checklist.\nIn its entirety, this checklist probably has about 50 items to consider which are broken into smaller subsections to help manage. They range from testing considerations and security implication to customer service. After submission, a member from Slack\u0026rsquo;s directory review team will go through the application, do some of their own testing interacting with the app, and then either approve or reject with comments to address.\nReview Process A critical factor for apps of this nature is how good is the onboarding process of a user. Can the user without me standing over their shoulder guiding them along interact properly with the software? Are there enough visual (or otherwise) clues to guide the user. As the designer and developer of the bot I am intimately knowledgable with how it works, so I was very pleased with their review feedback catching some areas I wasn\u0026rsquo;t paying attention to that a normal user might struggle with.\nThe first feedback item was a general help message. I had built in a help command to the bot which responds with some general notes and a link to get more help, however the bot itself can be directly messaged without the app_mention event. Messaging the bot in that manner would not generate a response. I implemented listening to the message event which gets fired in the case of direct messages to the bot and display the same help text as the app_mention handler.\nNext, the game challenge workflow: this is the main crux of starting a game in ChessBot. The app review team rejected saying that it didn\u0026rsquo;t work and the bot didn\u0026rsquo;t respond further after a challenge was issued. It wasn\u0026rsquo;t that the app had failed to issue the challenge, it failed to let the challenger know it had successfully issued the challenge. There wasn\u0026rsquo;t feedback to let the challenger know that we were waiting on the challengee to accept. In this case, I implemented an ephemeral message (visible only to the challenger) that gave some feedback on their issued challenge.\nFinally, we needed to setup some ancillary items such as a website with our slack install button, privacy, and a support email address.\nFuture Challenges Slack\u0026rsquo;s platform changes at a very fast clip. Already on my list to keep support of the app:\nImplement secret signing HMAC security token verification. The old request token check has been deprecated. Recently announced in this Slack blog post, they are reversing course on how workspace keys work and are going to require expiring tokens soon. This means I will need to implement rotating bot tokens via a refresh token. They do provide a nice way of keeping up to date with these changes via twitter and RSS, which you will need to stay on top of if you want your app to stay on the app directory.\nOverall, I had a really good experience with getting ChessBot listed on the Slack app directory. The Slack review team were patient and helpful, and did make the app a little bit more accessible to new users.\n","ref":"https://www.chris-saylor.com/posts/submitting-chessbot-to-slack-app-directory/","tags":["slack","bots","golang"],"title":"Submitting Chessbot to the Slack App Directory","type":"posts"},{"content":"With Atlassian\u0026rsquo;s announcement suspending development of Stride and dropping support for Hipchat in favor of Slack, I decided that the time was right to learn and experiment with Slack integrations.\nAny time I take on learning some new technology, I always make a project that utilizes much of the features of that technology. The project needs to be small enough that I can complete it relatively quickly, but deep enough to flex the technology I am learning. Recently, I\u0026rsquo;ve been getting back into Chess and I wondered if I could make an interactive Chess game inside of Slack\u0026rsquo;s platform.\nTable of contents Learn the game Design Phase Implementation Testing complications Deployment A note on security Retrospective Future development Conclusion Credits Learn the game The key to any project is that you need to know the domain of what you are trying to build. How well did I really know the rules of Chess? I knew the piece movements, king checking, castleing, and checkmate. What I didn\u0026rsquo;t know:\nYou can\u0026rsquo;t castle the king while in check, if it would put the king in check, or even pass through a square that could be attacked. The highlighted squares are where the king would go if castling on either side. The king-side castle (O-O) is not valid because the queen would check, and the queen-side castle (O-O-O) is not valid because the king would have to \u0026ldquo;move through\u0026rdquo; the endangered square D1.\nEn passant capture Various automatic draws (stalemate, threefold repatition, fifty-move rule, etc) There are a couple of notations for representing a move. There are a couple of ways to serialize a game (FEN/PGN). Before I even looked at Slack, I learned all the rules in order to know what I needed to implement.\nDesign Phase What does a Chess game inside a team-collaboration chat tool look like? How do I render the board? How do players communicate moves? The game needed to do the following things in order to be successfully playable:\nAllow players to challenge each other and accept or reject those challenges. Display the game board. Notify the player that it is their turn (since this is asyncronous communication). Contain games to message threads (so subsequent board renders and players moves don\u0026rsquo;t clutter up the channel) and also encourage some friendly banter during the game. Communicate moves to the bot. Displaying the winner. To start the game off, I needed a way for a player to challenge another player. In the channel where the chessbot is available, a player would mention the chessbot and \u0026ldquo;challenge\u0026rdquo; another player by mention.\n@ChessBot challenge @cjsaylor The bot would issue a challenge via IM and send an interactive message.\nThe bot would respond to the interactive message in order to remove the buttons.\nDo I draw the board with ascii symbols or render the board as an image? Ascii is the easiest, but also the dullest to look at. It also lacks certain features that images could support: highlighting the previous move, highlight a king that is in check, etc. The image was a better option, and Slack supports displaying images by way of message \u0026ldquo;attachments\u0026rdquo;.\nThe only viable way of communicating moves is by sending text to the bot. There are a couple of notations that could be used, but I chose standard algebraic notation (SAN) for it\u0026rsquo;s ease of use (since it is purely grid positions, d2d4 for example). Otherwise, players would have to know how to express checks, captures, and castling (O-O, O-O-O) with no way to know if it would work until after sending the message.\nImplementation The first part I worked on was rendering the board. I built a stand-alone golang library called chessimage that could be used to render board states as images. Since Slack will display this via image_url, I implemented this into a webserver endpoint. I waffled back and forth if the endpoint would use a FEN serialization to explicitely tell the webserver what to render, or be given a game ID to look up the serialized game and render that. After implementing the game ID lookup, it suffered from one fatal flaw: it would only render the current state, and no previous states of the game. If the player refreshed the browser page or looked at it on another device, then all previous renderings of the board would look like the current state of the game. Because of this, I went back to rendering by passing a FEN string, but also a signature to avoid it being generically used outside of the chessbot service.\nOn the subject of game ID, it turns out that choosing to contain all the messaging into a thread gave us an unintentional, but very useful benefit: The thread ID could also serve as a game ID. This makes retrieving the game state when a player makes a move trivial and also allows the player to have more than one game going on at a time in the same channel. At first, I implemented a very simple memory store for the game state, however if the server needed to be restarted, everyone\u0026rsquo;s game would be lost, so after I stabalized the storage interface, I implemented a Sqlite data store to persist between server restarts.\nFor the game logic, I used an established library for validation and serialization. To store the game in sqlite, I serialized the game state into portable game notation (PGN). This gives us a complete history of all moves of a game. I would then feed in player moves from the app_mention events and then send a message to the user with a rerendering of the board.\nAll the communication is implemented through Slack\u0026rsquo;s event callbacks. The only event the bot listens to is the app_mention event. To do different commands like help, challenge and regular moves, I parse the messages with regular expressions.\nTesting complications One of the first things I did while implementing the game logic was to create a simple REPL that took the same input that slack would but without the long and complicated feedback loop of Slack webhooks. This was tremendously helpful as I could test the game loops, inputs, player setup, and outcomes.\nOf course, the Slack integration would eventually need to be tested. I used ngrok to expose my local Golang webserver to Slack in order to receive and react to all the events and message interactions. This was a tedious process as the hostname would change, which would require updating the Slack configuration for the app. For ease of development, I would always challenge myself, however this left two bugs hidden during development:\nDisplaying the current player\u0026rsquo;s turn was backwards. (fixed in e3ec7a14) Other players could issue move commands when it was not their turn. Indeed, anyone mentioning the bot in the channel could move on anyone\u0026rsquo;s behalf. (fixed in 283d132c) Coincidently enough, Slack\u0026rsquo;s app distribution checklist specifically asks for a clean workspace with 2 users, which I would guess would catch these sorts of quirks. Speaking of app distribution:\nDeployment Up until this point, we\u0026rsquo;ve been using a manually installed app token in our Slack development workspace, however in order to be installed on other workspaces, the manual bot token needed to be replaced with an oauth workflow.\nThe only thing needed was an endpoint on our webserver that would accept an authorization code and exchange it for an authentication token (specifically the bot authentication token). Our app does not request any special scope permissions, since everything is driven through Slack\u0026rsquo;s bot interface, so we discard the authentication token from the user and only keep the bot token. The response includes a team ID, so the bot token can be stored per team in a key/value store. Since we have a very simple case, we simply store this in Sqlite along with all the other game data.\nFollowing the same pattern with our challenge and game data, we implemented both a memory and sqlite implementation from a common interface.\nA note on security As with any side project, this is hosted along other applications on a VPS. Even though Go applications are very simple to run on other environments (change the compile target), all the other applications on the server run in Docker context since they are a mixed bag of technologies. Since we\u0026rsquo;re storing some sensitive access tokens on our server, it\u0026rsquo;s important to be mindful of access to it. I wrapped the web server in a docker image and created an encrypted data volume to house the sqlite file, so that only the docker runtime user has read and write priviledges to that data file on the host machine. An Nginx web server container is exposed through the firewall and a private docker network is used to proxy the request from Nginx to the go webserver.\nTo improve security (or at least the ease of encrypting the data) include Hashicorp\u0026rsquo;s Vault as a container that is only networked to the go webserver container, and then it could store the access tokens in a key-value secrets engine.\nRetrospective During my time developing this bot, Slack provides a great platform with a lot of features that enabled this game to function relatively well. However, it is not without its worts.\nThe API while pretty well documented, is under constant change. Finding information on particular implementation details or questions in general of the Slack platform is challenging due to almost all of it being out of date. What makes matters worse is all the APIs that are deprecated have features that aren\u0026rsquo;t fully supported yet on whatever new API they are promoting.\nSlack\u0026rsquo;s RTM mechanism would be awesome to use for this bot to decrease lag and not require so much serializing/deserializing of the game, however it doesn\u0026rsquo;t yet have all the messaging abilities of the regular events API. It is also a bit clunky to know when a user directly mentions the bot since you have to parse it out of the text of the incoming messages yourself. Finally, we had to implement a web server for the authentication and image rendering, so it would be a lot of effort just to have a quicker receipt of messages.\nFuture development Implementing a DB backend like Redis for the challenge, game, and oauth token storage would enable the bot to scale horizontally, although since it doesn\u0026rsquo;t earn money, I\u0026rsquo;d be loath to scale it up.\nAlternatively, since the app is almost entirely webhook based, this would be a perfect opportunity to implement on a function-as-a-service like AWS Lambda. A simple API gateway coupled with some Lambda functions would be a fantastic use case for Slack bot communication. Additionally, being on AWS would allow usage of S3 or dynamodb as a state storage. Given low volume, this option may be next to free to host, and most of the web handling functions could be re-used almost verbatum with the Lambda functions. You\u0026rsquo;d just toss the webserver part.\nAnother nice to have would be to use a concurrency-safe LRU cache that would allow us to store a fixed number of games in memory in order to avoid having to deserialize the game state from sqlite (or other DBs) on every move command. We could then only serialize when the LRU cache was about to dump the least used games and only deserialize when we have to go to the DB to look up the game. This would be a great mechanism to control the amount of memory pulled in for game storage and for the players that are playing the most, they wouldn\u0026rsquo;t burn up requests to the DB. hashicorp/golang-lru looks like a good candidate for this use case as it also provides a callback when an item is evicted which could be used to serialize the game and store it. The os/signal package would need to be used to listen for SIGTERM and serialize all the games in the LRU cache.\nI also think it would be neat to implement Stockfish as a way of playing against an AI bot as opposed to requiring another player. I initially wanted to do this, but there are so many alternative ways to play chess with a bot that doesn\u0026rsquo;t involve the sending messages, I abandoned as it would probably never be used by anyone, however it would serve as a good way to test the system if they behave like a player.\nConclusion After all this research into Chess, I\u0026rsquo;m still not very good at actually playing Chess yet. I had a lot of fun developing this bot. I\u0026rsquo;ve played a few games in Slack with friends and it works surprisingly well. Does this supplant something like Lichess? Absolutely not, but it can be a good way to keep your remote team socially engaged and connected.\nAll of the code for the chess bot is open source: https://github.com/cjsaylor/chessbot.\nYou can also add the hosted version of the bot to your workspace directly:\nCredits fogleman/gg which made drawing the board graphics a snap. notnil/chess for providing an excellent library for validating and serializing chess games. nlopes/slack which offered a pretty comprehensive library of all slack events, methods, and communications channels. David Lapetina for the featured image All chessboard images were generated by the chessimage library.\n","ref":"https://www.chris-saylor.com/posts/building-a-chessbot-for-slack/","tags":["slack","bots","automation","golang"],"title":"Building a Chess bot for Slack","type":"code"},{"content":"This year, I gave a talk at Syntaxcon in Charleston, SC. Being my first talk on design, I was out of my comfort zone, however I would be remiss if I didn\u0026rsquo;t speak my mind on the subject. The over-arching theme was to get other engineers out of their comfort zones as well and to get involved in the design process.\nEngineers typically don\u0026rsquo;t involve themselves in the design process upfront. They\u0026rsquo;re either not interested in design or still attempting to finish the previous project. Design is where most of the decisions are made, and as a result engineers are considered grumpy for saying \u0026ldquo;no\u0026rdquo; often. The question is: how do we involve ourselves?\nThe talk focused on a tool called storybook.js which allows for React, Vuejs, and Angular components to be rendered in various states. The idea of this tool is to codify designs and make them a living part of the application. Designers and engineers team up, make the components in storybook.js as part of the design phase, and at the end of the design phase, you have usable components.\nEngineers have spent years coming closer and closer to the operations side with \u0026ldquo;devops\u0026rdquo; culture. We did this with code. Maybe we can do it with design as well.\nFull Screen\nFull Screen\nThis is the example storybook that is covered in the slides.\nThe source for the example storybook is available on Github.\n","ref":"https://www.chris-saylor.com/posts/2018-storybook-presentation/","tags":["React","Storybook","Javascript","design"],"title":"Design for Success","type":"posts"},{"content":"I’ve heard many co-workers, friends, and colleagues at meetups acknowledge the existence of generators in PHP, but not understand why they would use them or give them much thought when planning out implementations. In this article, I’m going to explain with concrete examples of when it is a good idea to use a generator.\nIn case you don’t know what a generator is, you can check PHP’s documentation on the subject for a good synopsis.\nSo why would we want to use generators? Any time you have a method that returns a list of something that has additional logic to continue to get things is a great candidate for a generator. Let’s take a look at a simple method that returns a MySQL row cursor:\nclass WidgetFactory { public function getAllWidgets() { return $this-\u0026gt;pdo-\u0026gt;query(\u0026#39;select id, name from widgets\u0026#39;); } } $factory = new WidgetFactory(); $widgets = $factory-\u0026gt;getAllWidgets(); while ($row = $widgets-\u0026gt;fetch()) { echo \u0026#34;{$row[\u0026#39;name\u0026#39;]}\\n\u0026#34;; } We’ve seen the above code many times before. We manually implement an iterator without thinking about it and at the same time couple the code where we use this to the PDOStatement interface of fetch(). What happens if this method is used in many places and the underlying storage engine is changed to something other than MySQL?\nWe can refactor the above code using generators like this:\nclass WidgetFactory { public function getAllWidgets() { $stmt = $this-\u0026gt;pdo-\u0026gt;query(\u0026#39;select id, name from widgets\u0026#39;); while ($row = $stmt-\u0026gt;fetch()) { yield $row; } } } $factory = new WidgetFactory(); foreach ($factory-\u0026gt;getAllWidgets() as $widget) { echo \u0026#34;{$widget[\u0026#39;name\u0026#39;]}\\n\u0026#34;; } From the outset it doesn’t look all that different. It’s still iterating through the PDOStatement with fetch(), but now the consumer of getAllWidgets has no idea about MySQL and indeed no longer has to call fetch(). Let’s suppose we now retrieve the widgets from a restful service that pages the results.\nclass WidgetFactory { public function getAllWidgets() { $next = null; do { list($next, $widgets) = $this-\u0026gt;service-\u0026gt;getWidgets($next); foreach ($widgets as $widget) { yield $widget; } } while (!empty($next)); } } We were able to completely change the mechanism of retrieving widgets without having to change our consumers in any way. We successfully abstracted the iteration in a very simple way.\nOther Uses There are, of course, more reasons to use generators. For example, if your code is making use of asynchronous code with amphp for example, generators allow your consumers to just consume as they resolve from the async process. It also allows for the usage of coroutines (which is not unlike NodeJS’s co library which also uses generators).\nGotchas in Unit tests One key thing to remember is that the internals of the generator method will not begin until something attempts to iterate it. Let’s suppose we are attempting to write a test for our widget factory example:\n// pseudocode... $results = $factory-\u0026gt;getAllWidgets(); $this-\u0026gt;expectsToBeCalled([$factory-\u0026gt;pdo, \u0026#39;query\u0026#39;]); The above code would fail the assertion that the $factory-\u0026gt;pdo-\u0026gt;query was called, because indeed it hasn’t been called yet. We have to iterate before the generator starts executing:\n// pseudocode... $results = iterator_to_array($factory-\u0026gt;getAllWidgets()); $this-\u0026gt;expectsToBeCalled([$factory-\u0026gt;pdo, \u0026#39;query]); This sort of abstraction has always been possible with the Iterator interface since PHP 5.0. However, it is inconvenient to have to implement that interface in a class and then utilize it in the caller, especially if you have multiple ways of iterating over a similar data set in the same class. What I hope is conveyed in this article is to think about abstracting your iterators away from the consumers with generators to simplify future updates.\n","ref":"https://www.chris-saylor.com/posts/why-use-generators-in-php/","tags":["PHP"],"title":"Why Use Generators in PHP?","type":"code"},{"content":" This is the second entry in a series on Elasticsearch and how we use it in our applications. See the previous entry on Rebuilding Indices with No Downtime.\nElasticsearch has a vast number of ways to query the data in your index. How you query your data is very much dependent upon where it is being used, so in this post we’re going to talk about how we use it in our instructor search application.\nHistorically, most of our searches were performed on de-normalized MySQL tables. This included using an arc distance formula as part of a SQL statement with a ton of \u0026ldquo;like\u0026rdquo; statements and rudimentary ordering based on the input by the user. As you no doubt have guessed, this isn’t performant and the results were not very good.\nThe results weren’t relevant.\nRelevancy is key when doing any sort of search, and this is incredibly difficult to accomplish with a database that wasn’t designed for search. It’s up to the engineer to figure out what is relevant to the user based on what was searched. If the user searched for an instructor by their name, it seems logical to sort their name alphabetically. What if they searched for an instructor by name who is within 10 miles of current location? Do you sort by their distance from the user and then alphabetic?\nThese are tough questions to answer and even if you do find an answer, sorting them in any sort of structured or weighted way is very difficult with this sort of setup.\nLucene Lights the Way Elasticsearch is built upon a search engine called Lucene. It is the engine that takes the query you provide Elasticsearch and gets the data from the index. One very important function it serves is a calculation of \u0026ldquo;score\u0026rdquo;. This is a calculation that codifies how relevant a particular result is to the original query.\nAt the beginning when we were first migrating off the search from MySQL to Elasticsearch, it was really tempting to sort the same way we did with the original MySQL query, but we soon learned that the results returned were just as bad (if not worse) than MySQL. We had to learn how to shape our query to take advantage of the \u0026ldquo;score\u0026rdquo;.\nShow Me the Query! First things first, we have a filter of distance available on the application: let’s say we’re trying to find instructors in New York, NY within 5 miles of a central point. We might do something like this:\n{ \u0026#34;query\u0026#34;: { \u0026#34;bool\u0026#34;: { \u0026#34;filter\u0026#34;: [{ \u0026#34;geo_distance\u0026#34;: { \u0026#34;distance\u0026#34;: \u0026#34;8.04672km\u0026#34;, \u0026#34;address.geo\u0026#34;: { \u0026#34;lat\u0026#34;: \u0026#34;40.7643358\u0026#34;, \u0026#34;lon\u0026#34;: \u0026#34;-73.9849351\u0026#34; } } }] } } } This would find instructors within 5 miles of the center of New York, NY:\nChris Saylor — 1.4 miles away Aaron Sonders — 0.5 miles away Bethany Smith — 3 miles away Chris Saylor — 0.2 miles away Notice the results are scattered at varying distances from that center because this query doesn’t have any relevancy. Wait…what? You just said that’s what Elasticsearch does for you! It comes down to filtering versus querying. Anything you filter is a binary thing: what \u0026ldquo;must\u0026rdquo; be in results. As such, it doesn’t calculate a score. On the other hand, when you query for something, it is what \u0026ldquo;should\u0026rdquo; be included and this is where score is calculated. It would be tempting (and easy) to sort by the distance from the center, but that would only work if that is the only factor.\nNow, let’s look for an instructor named \u0026ldquo;Chris Saylor\u0026rdquo; within 5 miles of New York, NY:\n{ \u0026#34;query\u0026#34;: { \u0026#34;bool\u0026#34;: { \u0026#34;must\u0026#34;: [{ \u0026#34;query_string\u0026#34;: { \u0026#34;default_field\u0026#34;: \u0026#34;full_name\u0026#34;, \u0026#34;query\u0026#34;: \u0026#34;chris saylor\u0026#34; } }], \u0026#34;filter\u0026#34;: [{ \u0026#34;geo_distance\u0026#34;: { \u0026#34;distance\u0026#34;: \u0026#34;8.04672km\u0026#34;, \u0026#34;address.geo\u0026#34;: { \u0026#34;lat\u0026#34;: \u0026#34;40.7643358\u0026#34;, \u0026#34;lon\u0026#34;: \u0026#34;-73.9849351\u0026#34; } } }] } } } Chris Saylor — 1.4 miles away Chris Saylor — 0.2 miles away Anthony Saylor — 1.6 miles away The full name query has relevancy (thanks Lucene), but the distances are still scattered because the geo distance filter doesn’t get weighted. Let’s make it contribute to the weight using an Elasticsearch mechanism called function scoring:\n{ \u0026#34;query\u0026#34;: { \u0026#34;function_score\u0026#34;: { \u0026#34;query\u0026#34;: { \u0026#34;bool\u0026#34;: { \u0026#34;must\u0026#34;: [{ \u0026#34;query_string\u0026#34;: { \u0026#34;default_field\u0026#34;: \u0026#34;full_name\u0026#34;, \u0026#34;query\u0026#34;: \u0026#34;chris saylor\u0026#34; } }], \u0026#34;filter\u0026#34;: [{ \u0026#34;geo_distance\u0026#34;: { \u0026#34;distance\u0026#34;: \u0026#34;8.04672km\u0026#34;, \u0026#34;address.geo\u0026#34;: { \u0026#34;lat\u0026#34;: \u0026#34;40.7643358\u0026#34;, \u0026#34;lon\u0026#34;: \u0026#34;-73.9849351\u0026#34; } } }] } }, \u0026#34;functions\u0026#34;: [{ \u0026#34;linear\u0026#34;: { \u0026#34;address.geo\u0026#34;: { \u0026#34;origin\u0026#34;: { \u0026#34;lat\u0026#34;: \u0026#34;40.7643358\u0026#34;, \u0026#34;lon\u0026#34;: \u0026#34;-73.9849351\u0026#34; }, \u0026#34;scale\u0026#34;: \u0026#34;8.04672km\u0026#34; } } }] } } } Chris Saylor — 0.2 miles away Chris Saylor — 1.4 miles away Anthony Saylor — 1.6 miles away What’s going on here? We introduced a linear decaying function that calculates a value (between 1 and 0) that decays the further away a result is from the central point. The relevance of the distance is now included with the relevance of the name we searched. Since there is more than one instructor named \u0026ldquo;Chris Saylor\u0026rdquo; in that location, the instructor closer to that central point will appear higher in the results.\nThis post barely scratches the surface of the power that Elasticsearch has for complex queries of data, but I hope it has given you some insight on how we use it in a real world application. Give it a shot and search for an instructor near you and perhaps even take one of their classes.\n","ref":"https://www.chris-saylor.com/posts/elasticsearch-series-let-it-work-for-you/","tags":["elasticsearch","open source"],"title":"Elasticsearch Series: Let It Work for You","type":"code"},{"content":" This is the first entry in a series on Elasticsearch and how we use it in Zumba\u0026rsquo;s applications.\nMany of Zumba\u0026rsquo;s applications have some form of search. In the beginning, many of these were implemented via querying MySQL. As you can imagine, this doesn’t scale very well. On top of that, certain types of queries, such as geo location, are difficult to accomplish (read: we have to do math). As we grew and our search volume steadily increased, we realized we needed to do something to improve scalability. After some research, a decision was made to use Elasticsearch to solve these issues.\nAs you probably inferred from above, MySQL is the database we use for permanent storage. I’ll be using Elasticsearch as an ephemeral storage throughout this series. This means, we must have a process that will translate our relational data into a document format that Elasticsearch can understand and use.\nThis involves building an index in Elasticsearch. Any query/search a user would perform would be executed against this index. This presents a new challenge: how do we accomplish rebuilding an index that users are actively using?\nThe graph below shows what would happen if we were to rebuild the index directly: disruption and incomplete data.\nRebuilding an index causes total disruption initially and incomplete results until the index is fully rebuilt.\nWe solved this issue by implementing a rotating index. Instead of removing the active index, we create a brand new index, build it, and then swap the “primary” index that the users are actively searching.\nHere the user’s search is never disrupted because we construct a new index and after it is built/settled, we change the what index to search by the client.\nWith our solution the index name becomes dynamic. There are two possible ways to deal with having the client know which dynamic index is the “primary”. In both of the following cases, assume we just created a dynamic index for searching locations named “locations_12345”.\nAlias Approach An alias can be created for a common index name that will always point to the primary index. With Elasticsearch’s “_alias” endpoint, I can create an alias called “locations” on “locations_12345” which would enable the application to do:\nGET locations/location/_search which is equivalent to:\nGET locations_12345/location/_search It was pointed out to me by Zachary Tong from Elastic.co that you can swap out the old and new alias when rotating indices in an atomic fashion:\nPOST /_aliases { \u0026#34;actions\u0026#34;: [ { \u0026#34;remove\u0026#34;: { \u0026#34;index\u0026#34;: \u0026#34;locations_*\u0026#34;, \u0026#34;alias\u0026#34;: \u0026#34;locations\u0026#34; }}, { \u0026#34;add\u0026#34;: { \u0026#34;index\u0026#34;: \u0026#34;locations_12345\u0026#34;, \u0026#34;alias\u0026#34;: \u0026#34;locations\u0026#34; }} ] } Configuration Approach In this approach, you create a special index that is fixed that contains a list of all indexes created for the “locations” dynamic index. Only one entry in this index has the “_id” of “primary” which is what the client would query prior to querying the dynamic index.\nThe client would make a request to the configuration index.\nGET .locations_configuration/configuration/primary The result would contain “locations_12345”. It also has the added benefit of storing a list of the old indices that we can delete later. The downside of this approach is that you need to make two requests to Elasticsearch — one to get the dynamic index from the configuration index, and one to perform the actual query. However, eliminating search disruption out-weighs the overhead of multiple requests in our use-case.\nIf you would like to see (and use) our implementation of these index rotation strategies, I recently open-sourced our Elasticsearch index rotation library.\nIn the next installment of this series I will cover our applications that specifically use Elasticsearch and what sort of queries/mappings these application features require.\n","ref":"https://www.chris-saylor.com/posts/elasticsearch-series-rebuilding-indices-with-no-downtime/","tags":["elasticsearch","open source"],"title":"Elasticsearch Series: Rebuilding Indices with No Downtime","type":"code"},{"content":"Maintaining code quality on projects where there are many developers contributing is a tough assignment. How many times have you tried to contribute to an open-source project only to find the maintainer rejecting your pull request on the grounds of some invisible coding standard? How many times as a maintainer of an open-source project (or internal) have you had a hard time reading code because there were careless tabs/spaces mixed, if statements with no brackets, and other such things. Luckily there are tools that can assist maintainers. In this post, I’ll be going over how to use composer, git hooks, and phpcs to enforce code quality rules.\nThere are a couple of things to keep in mind. First, you want this process to be as simple as possible. Secondly, you want it to be easy to run when necessary. Finally, you want it to be universally accepted as part of your contribution procedure.\nWhat if I told you that it could be done without the developer even knowing it’s happening?\nMost modern PHP projects use composer as their dependency manager. Before you can make anything work, you need to run composer install. This is where the magic happens.\nFirst, we need a development dependency specified to install phpcs. It looks something like this:\n{ \u0026#34;require-dev\u0026#34;: [ \u0026#34;squizlabs/php_codesniffer\u0026#34;: \u0026#34;2.0.*@dev\u0026#34; ] } Composer has a handy schema entry called scripts. It supports a script hook post-install-cmd. We will use this to install a git pre-commit hook. Adding to our example above:\n{ \u0026#34;require-dev\u0026#34;: [ \u0026#34;squizlabs/php_codesniffer\u0026#34;: \u0026#34;2.0.*@dev\u0026#34; ], \u0026#34;scripts\u0026#34;: { \u0026#34;post-install-cmd\u0026#34;: [ \u0026#34;bash contrib/setup.sh\u0026#34; ] } } This will run a bash script called setup.sh when the command composer install is run.\nIn our setup.sh, we will need to copy a pre-commit script into the .git/hooks directory:\n#!/bin/sh cp contrib/pre-commit .git/hooks/pre-commit chmod +x .git/hooks/pre-commit This will copy our pre-commit script from the contrib directory to the hooks section of the special git directory and make it executable.\nWhenever a contributing developer attempts to commit their code, it will run our pre-commit script. Now all we need to do is run the code sniffer rules on relavent files specific to this commit:\n#!/bin/sh PROJECT=`php -r \u0026#34;echo dirname(dirname(dirname(realpath(\u0026#39;$0\u0026#39;))));\u0026#34;` STAGED_FILES_CMD=`git diff --cached --name-only --diff-filter=ACMR HEAD | grep \\\\\\\\.php` # Determine if a file list is passed if [ \u0026#34;$#\u0026#34; -eq 1 ] then oIFS=$IFS IFS=\u0026#39; \u0026#39; SFILES=\u0026#34;$1\u0026#34; IFS=$oIFS fi SFILES=${SFILES:-$STAGED_FILES_CMD} echo \u0026#34;Checking PHP Lint...\u0026#34; for FILE in $SFILES do php -l -d display_errors=0 $PROJECT/$FILE if [ $? != 0 ] then echo \u0026#34;Fix the error before commit.\u0026#34; exit 1 fi FILES=\u0026#34;$FILES $PROJECT/$FILE\u0026#34; done if [ \u0026#34;$FILES\u0026#34; != \u0026#34;\u0026#34; ] then echo \u0026#34;Running Code Sniffer...\u0026#34; ./vendor/bin/phpcs --standard=PSR1 --encoding=utf-8 -n -p $FILES if [ $? != 0 ] then echo \u0026#34;Fix the error before commit.\u0026#34; exit 1 fi fi exit $? This script will get the staged files of the commit, run a php lint check (always nice), and apply the code sniffer rules to the staged files.\nIf there is a code standards violation, the phpcs process will return a non-zero exit status which will tell git to abort the commit.\nWith all of these things in place, the workflow is as follows:\nDeveloper runs composer install. PHP Code Sniffer is installed via a dev dependency. The post-install command automatically copies the pre-commit hook into the developer’s local git hooks. When the developer commits code, the pre-commit hook fires and checks the staged files for coding standards violations and lint checks. This is a relatively simple setup that can save pull request code reviews a significant amount of time preventing back-and-forth on simple things such as mixed tabs/spaces, bracket placement, etc.\n","ref":"https://www.chris-saylor.com/posts/enforcing-code-standards/","tags":["php","standards"],"title":"Enforce code standards with composer, git hooks, and phpcs","type":"code"},{"content":" Note: This is an article restored from archive. This vulnerability hasn\u0026rsquo;t been viable for almost a decade and anyone using old enough browsers that are still vulnerable, are vulnerable to so much more than this.\nIn this article, I will describe a method of attack which allows the attacker to get information from ajax-based requests by use of the browser’s script tag and how to prevent it.\nHow does it work? With a little social engineering, an attacker can get a user with an active session to a web application to visit a malicious page. In this page, the attacker will “include” the common ajax request URL via the script tag, since it is not limited by the cross domain rules of the browser.\nExample attack Let’s say your ajax request /user/emails returns an array of data:\n[ { email: \u0026#34;some-email\u0026#34; } ] The attacker would include this request URL:\n\u0026lt;script src=\u0026#34;/user/emails\u0026#34; type=\u0026#34;text/javascript\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; When the browser loads this ajax request, it’ll automatically execute the response as javascript. The attacker can override the Array object constructor to store it in a local var to be communicated to their servers:\nArray = function(data) { // Upload contents of data to attacker\u0026#39;s server }; How do we prevent this? Since the script tag auto-executes whatever is returned from the request, simply put a while(1); as part of their ajax requests and parse this out when handling the ajax request, since same origin has complete control over the response.\nThis effectively makes it impossible to exploit the request because the while loop will prevent the Array from the response from ever constructing.\nConclusion As you can see, the attack is very simple to execute, however it is also pretty simple to remedy. If you are using a framework that supports layouts, then this is simple to implement application wide.\nSome present-day examples of applications that have implemented this sort of defense are gmail and facebook. Next time you’re on those websites, open up the developer’s console in your browser of choice and examine some of the ajax responses.\nNotes Some modern browsers have opted to make overriding javascript’s Array and Object constructors, unfortunately not everyone is using a modern browser.\n","ref":"https://www.chris-saylor.com/posts/forever-is-a-long-time-to-wait/","tags":null,"title":"Forever Is a Long Time to Wait","type":"code"},{"content":" Note: This is an article restored from archive. There zero reason not to use an IDE that has good PHP xdebug support at this point. The selection of PHP friendly IDEs at this time was poor.\nSometimes tracking down the source of a particularly frustrating bug can be tedious. Simply running var_dump() here and there is problematic and not very elegant. Sometimes it’s nice to step through the code line by line and examine the values of variables at the point of execution.\nThere are several ways to accomplish the above, but on my setup (Mac + VirtualBox), I prefer MacGDBp.\nTo get it running, first you need to make sure Xdebug is installed. For me, I run Ubuntu in a VM, so apt-get install php5-xdebug is enough to get me running. Next, you need to configure Xdebug to broadcast the debugging information and flow control over a port that MacGDBp can listen to:\n[xdebug] xdebug.remote_enable=1 xdebug.remote_handler=dbgp xdebug.remote_port=9000 xdebug.remote_autostart=1 xdebug.remote_connect_back=1 Note: Even though this is an old article restored from archive, this configuration still holds true for most modern setups.\nNow your system has everything you need to listen to the dbgp protocol. After installing MacGDBp, running your php code will automatically stop progress and show up in the debug display. You can configure this to not stop on first launch and instead set breakpoints. Since Xdebug is an extension, you can insert xdebug_break(); at any point in your code to stop execution and step through using this interface.\nWith just a small amount of effort, you can have an environment for properly debugging your code easily and effectively.\n","ref":"https://www.chris-saylor.com/posts/debugging-php-with-xdebug-and-macgdbp/","tags":null,"title":"Debugging PHP with Xdebug and MacGDBp","type":"code"},{"content":" Note: This is an article restored from an archive and the project this refers to is no longer maintained. Google Visualizations Tools API has also changed significantly since this is written.\nOften it is imperative to visualize data in applications. In my previous post, I showed how visualizing data can shape the outcome of a conversation (in this case a tech visit).\nMany of my web applications as of late have been written using CakePHP, and as such I needed a decent go-to graphing tool that didn\u0026rsquo;t require licensing fees. I researched for different graphing tools, and I finally decided to go with Google\u0026rsquo;s Visualization API. It\u0026rsquo;s a javascript SDK that takes client data and adds an iframe containing an interactive graph (or \u0026ldquo;visualization\u0026rdquo; as they call it) to the DOM for view.\nMost of my data isn\u0026rsquo;t available to the client side, so I needed a server-side solution. I decided to create a CakePHP helper that functions as a wrapper for the javascript SDK. This allows me to pull the data on the server side and contain that data within PHP. The wrapper takes the data and wraps the proper javascript to fit Google\u0026rsquo;s SDK for display in the view.\nHere is an example of setting some data and displaying a line graph of that data using the visualization:\n\u0026lt;?php $data = array( \u0026#39;labels\u0026#39; =\u0026gt; array( array(\u0026#39;string\u0026#39; =\u0026gt; \u0026#39;Sample\u0026#39;), array(\u0026#39;number\u0026#39; =\u0026gt; \u0026#39;Piston 1\u0026#39;), array(\u0026#39;number\u0026#39; =\u0026gt; \u0026#39;Piston 2\u0026#39;) ), \u0026#39;data\u0026#39; =\u0026gt; array( array(\u0026#39;S1\u0026#39;, 74.01, 74.03), array(\u0026#39;S2\u0026#39;, 74.05, 74.04), array(\u0026#39;S3\u0026#39;, 74.03, 74.01), array(\u0026#39;S4\u0026#39;, 74.00, 74.02), array(\u0026#39;S5\u0026#39;, 74.12, 74.05), array(\u0026#39;S6\u0026#39;, 74.04, 74.04), array(\u0026#39;S7\u0026#39;, 74.05, 74.06), array(\u0026#39;S8\u0026#39;, 74.03, 74.02), array(\u0026#39;S9\u0026#39;, 74.01, 74.03), array(\u0026#39;S10\u0026#39;, 74.04, 74.01), ), \u0026#39;title\u0026#39; =\u0026gt; \u0026#39;Piston Ring Diameter\u0026#39;, \u0026#39;type\u0026#39; =\u0026gt; \u0026#39;line\u0026#39; ); echo $this-\u0026gt;GChart-\u0026gt;start(\u0026#39;test\u0026#39;); echo $this-\u0026gt;GChart-\u0026gt;visualize(\u0026#39;test\u0026#39;, $data); Produced by the code snippet above. This is produced by similar code as the above substituting \u0026#34;bar\u0026#34; for \u0026#34;line\u0026#34; The helper is available free of charge for use in personal and commercial products under MIT.\nView GChart Source\n","ref":"https://www.chris-saylor.com/posts/google-visualizations-api-cakephp/","tags":null,"title":"Google Visualizations Api Cakephp","type":"code"},{"content":"Being a customer of Time Warner (Road Runner) for nearly three years now, I have had my share of technical issues that required a technician to come out and rummage around in the magic cable box outside my house. Some of the worst issues to correct are intermittent. I share the pain of the Time Warner techs when dealing with seemingly intangible errors, but that doesn’t mean you devote no effort to diagnosing what the issue could have been (still existing just not presenting).\nDiligence to identifying a problem that isn’t currently happening usually wanes fast. I just happen to run a web server in Atlanta, and that server happens to have a monitoring tool installed called Munin. Munin is a tool that graphs many aspects of a node, logs that data, and transmits it back to the Munin server. In this case, the Munin server is at my house.\nSo how did Munin help me convince a technician that something systematic was happening? It turns out that intermittent issues become very obvious when they’re made visual by being graphed over the time period in which it occurred. I was able to demonstrate to the technician exactly when and for how long I was without internet by showing him the interruption in reporting from my Munin node in Atlanta.\neth0 traffic graphed by week The gaps on the left side of the above graph makes it pretty plain that something happened where there are noticeable gaps in the traffic graph. One could argue, well maybe there just was no traffic going to your server during those times (doesn\u0026rsquo;t really explain the sudden drop instead of a drop off). Observe exhibit B:\nNote: This article was restored from archive and this image was lost. It depicted MySQL activity graphed by week which showed a similar gap as other examples.\nStill not convinced?\nDisk Usage graphed by week Disk utilization does not change that quickly on a web server, and certainly is not going to zero without something horribly wrong happening.\nThanks to Munin, the tech acknowledged that there was a problem, quickly determined it was something on their end (hard to BS me), and scheduled a work order for a line technician to take care of the issue. Munin for the win.\n","ref":"https://www.chris-saylor.com/posts/time-warner-defeated-by-munin/","tags":null,"title":"Time Warner Defeated by Munin","type":"code"},{"content":" Note: This is an article restored from archives. MSNBC have abandoned long-form text articles since this was written. I still think this is a neat model to essentially have a table of contents that is always visible with little screen realestate utilized.\nI first stumbled upon a unique visual navigation scheme while browsing MSNBC.com. The concept is to have static navigation for different sections of a page that references the position of that content relative to the scrollbar. It does this by dynamically adding \u0026ldquo;pins\u0026rdquo; to the right side of the screen, seemingly attached to the scrollbar. This allows for the user to quickly move about the page in distinct orderly fashion.\nUsing MSNBC’s model as an inspiration, I decided to enhance this functionality in the form of a jQuery plugin that is both easy to style and easy to integrate. I wanted to have a system that would readily reposition itself not only when the window resizes, but also when objects in the DOM were moved or removed. Secondly, I wanted to allow for event attachments and triggers, so the developer is free to utilize their own events based on how the user utilizes their page. Finally, I wanted to make them dynamically controllable by being part of the jQuery DOM’s object. I didn’t want the developer to have to worry about keeping up with pin names or IDs, so the pins are controllable directly through the object they’re attached to.\n$(\u0026#39;#content\u0026#39;).scrolltab({ hoverin: function() { $(this).stop().animate({width: \u0026#39;200px\u0026#39;}, 500); }, hoverout: function() { $(this).stop().animate({width: \u0026#39;50px\u0026#39;}, 500); }, classname: \u0026#39;scrolltab-content\u0026#39;, title: \u0026#39;Content\u0026#39; }); A very simple example of attaching a Scrolltab pin to an object. This will add mouseover events, name the class of the pin, and set a title (ie what is displayed on the inside of the pin).\nThis jQuery plugin is under the MIT license and is free to use personally and commercially.\nDownload the script | View the source | View a demo\n","ref":"https://www.chris-saylor.com/posts/scrolltab-dynamic-page-navigation-tool/","tags":null,"title":"Scrolltab: Dynamic Page Navigation Tool","type":"code"},{"content":"","ref":"https://www.chris-saylor.com/pico8/","tags":null,"title":"Pico8s","type":"pico8"}]