<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Kevin Wilson</title><description>Software developer, TypeScript enthusiast, whisky lover, coffee snob, sometime musician, and occasional cook.</description><link>https://kwilson.io/</link><item><title>My Claude Team</title><link>https://kwilson.io/blog/my-claude-team/</link><guid isPermaLink="true">https://kwilson.io/blog/my-claude-team/</guid><description>My mini dev team, cranking out all the code</description><pubDate>Wed, 01 Apr 2026 11:54:00 GMT</pubDate><content:encoded>&lt;p&gt;As I&apos;ve been leveraging AI tooling more and more for dev work, I&apos;ve been iterating on what works and doesn&apos;t work for me and the kind of results I&apos;m happy with.&lt;/p&gt;
&lt;p&gt;I spent a good chunk of time working with a custom agentic loop that would iterate through a PRD and tackle one item at a time before passing it through a review cycle with a lower model. This was working quite well, but it was difficult to get the model to only work on &lt;em&gt;one&lt;/em&gt; item at a time while retaining the larger context of everything else — and all within the limited context budget allowed to that agent.&lt;/p&gt;
&lt;h2&gt;Dev Teams&lt;/h2&gt;
&lt;p&gt;Agent team support in Claude Code fixed this. Being able to spin up a team of agents to tackle bigger problems means I don&apos;t need that loop anymore — I can just tell the main agent to orchestrate a team and pass tasks off to sub-agents, each working within their own context budget, before handing off to another agent for review.&lt;/p&gt;
&lt;p&gt;They can take on a big task (or list of tasks) and work it out amongst themselves.&lt;/p&gt;
&lt;h2&gt;Skills&lt;/h2&gt;
&lt;p&gt;With that, here&apos;s the current version of the skill I&apos;ve been using to invoke this — saved as a custom slash command (&lt;code&gt;/review-team&lt;/code&gt;) in Claude Code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Define a team to implement this plan. Include at least 1 reviewer using Sonnet
and working as devil&apos;s advocate who uses /simplify to review code.

You are the orchestrator for this work.

Process tasks using this flow:

1. Task is assigned to Agent
2. Agent works on task
3. When Agent has completed the work, pass to a Reviewer for review with /simplify
4. If the Reviewer accepts the changes, commit and move onto the next task
5. If the Reviewer requests changes, pass back to the original agent
6. If the Agent agrees with the suggestions, the Agent makes the
    changes then GOTO 4 and repeat
7. If the Agent disagrees with the suggestions, note the reason for
    disregarding, then commit and move on to the next task

Commit changes in logical groups. Try to be more granular if possible.

If you are working in a worktree, make sure to run tests/formatting/linting etc.
within that worktree.

As orchestrator, you need to proactively monitor the status of the team. If 
an Agent disconnects or goes idle, you are responsible for either
recovering them or killing them and replacing them with a new team
member. Do not just sit and wait endlessly for an unresponsive team member
to awaken.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My typical workflow is to go through a planning phase with the agent to produce a detailed plan, then invoke &lt;code&gt;/review-team&lt;/code&gt; to kick off execution.&lt;/p&gt;
&lt;p&gt;This allows multiple agents to work in parallel where tasks support it. By letting Claude figure out the dependency graph itself, it&apos;ll usually identify which tasks can be done in parallel and which need to be linear and spin up an N-agent team accordingly.&lt;/p&gt;
&lt;p&gt;Using the &lt;a href=&quot;https://github.com/anthropics/claude-plugins-official/blob/main/plugins/code-simplifier/agents/code-simplifier.md&quot;&gt;/simplify&lt;/a&gt; plugin for reviews — an official Anthropic plugin, not something you need to build — means each change is checked against multiple criteria. The reviewer uses Sonnet specifically for the cost and speed tradeoff; you want reviews to be fast and cheap, not another heavyweight agent.&lt;/p&gt;
&lt;p&gt;Allowing the agent worker to accept or reject reviewer suggestions helps weed out nitpicky feedback or anything that becomes invalid given a wider context.&lt;/p&gt;
&lt;p&gt;The one recurring problem I&apos;ve hit with Claude teams is agents falling offline or failing to respond — hence the strongly worded instruction for the orchestrator to monitor the team and deal with it proactively. It still occasionally needs a nudge, but it does help.&lt;/p&gt;
&lt;p&gt;This skill is ever-evolving as I hit new edge cases. Give it a go and let me know how it works for you.&lt;/p&gt;
</content:encoded></item><item><title>AbortController: the Swiss army knife of JavaScript</title><link>https://kwilson.io/blog/abortcontroller-the-swiss-army-knife-of-javascript/</link><guid isPermaLink="true">https://kwilson.io/blog/abortcontroller-the-swiss-army-knife-of-javascript/</guid><description>AbortController does a lot more than cancel fetch requests. From cleaning up event listeners in React to cancelling streams and managing timeouts, here are several ways to use it.</description><pubDate>Sat, 21 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most people know &lt;code&gt;AbortController&lt;/code&gt; as the thing you use to cancel &lt;code&gt;fetch&lt;/code&gt; requests. That&apos;s fair enough — it was literally designed for that. But &lt;code&gt;AbortController&lt;/code&gt; has quietly become one of the most versatile tools in the JavaScript standard library. Its signal mechanism plugs into event listeners, streams, Node.js APIs, and even your own custom code.&lt;/p&gt;
&lt;p&gt;Let&apos;s go through the ways you can use it, starting with the obvious one.&lt;/p&gt;
&lt;h2&gt;Aborting fetch requests&lt;/h2&gt;
&lt;p&gt;The classic use case. You create an &lt;code&gt;AbortController&lt;/code&gt;, pass its &lt;code&gt;signal&lt;/code&gt; to &lt;code&gt;fetch&lt;/code&gt;, and call &lt;code&gt;abort()&lt;/code&gt; when you want to cancel:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const controller = new AbortController();

fetch(&apos;/api/data&apos;, { signal: controller.signal })
  .then((response) =&amp;gt; response.json())
  .then((data) =&amp;gt; console.log(data))
  .catch((err) =&amp;gt; {
    if (err.name === &apos;AbortError&apos;) {
      console.log(&apos;Fetch was cancelled&apos;);
    } else {
      throw err;
    }
  });

// Cancel the request
controller.abort();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is essential for any situation where a request might become irrelevant — navigating away from a page, a user typing in a search box (where each keystroke triggers a new request), or imposing a timeout.&lt;/p&gt;
&lt;h3&gt;Fetch with a timeout&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;AbortSignal.timeout()&lt;/code&gt; creates a signal that automatically aborts after a given number of milliseconds. No manual controller needed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fetch(&apos;/api/slow-endpoint&apos;, {
  signal: AbortSignal.timeout(5000),
})
  .then((response) =&amp;gt; response.json())
  .catch((err) =&amp;gt; {
    if (err.name === &apos;TimeoutError&apos;) {
      console.log(&apos;Request timed out after 5 seconds&apos;);
    }
  });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that a timed-out signal throws a &lt;code&gt;TimeoutError&lt;/code&gt;, not an &lt;code&gt;AbortError&lt;/code&gt;. This lets you distinguish between explicit cancellations and timeouts.&lt;/p&gt;
&lt;h2&gt;Cleaning up event listeners&lt;/h2&gt;
&lt;p&gt;This is where &lt;code&gt;AbortController&lt;/code&gt; starts to get interesting. You can pass a signal as an option to &lt;code&gt;addEventListener&lt;/code&gt;, and when the signal aborts, the listener is automatically removed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const controller = new AbortController();

element.addEventListener(&apos;click&apos;, handleClick, {
  signal: controller.signal,
});
element.addEventListener(&apos;keydown&apos;, handleKeydown, {
  signal: controller.signal,
});
window.addEventListener(&apos;resize&apos;, handleResize, {
  signal: controller.signal,
});

// Remove all three listeners in one call
controller.abort();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No more keeping track of every handler reference so you can call &lt;code&gt;removeEventListener&lt;/code&gt; later. One &lt;code&gt;abort()&lt;/code&gt; call tears down everything attached to that signal.&lt;/p&gt;
&lt;h3&gt;In React effects&lt;/h3&gt;
&lt;p&gt;This pattern is a perfect fit for React&apos;s &lt;code&gt;useEffect&lt;/code&gt; cleanup. Here&apos;s a comparison.&lt;/p&gt;
&lt;p&gt;Without &lt;code&gt;AbortController&lt;/code&gt;, you need to hold a reference to every handler and remove them individually:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  const handleResize = () =&amp;gt; {
    setWidth(window.innerWidth);
  };
  const handleScroll = () =&amp;gt; {
    setScrollY(window.scrollY);
  };
  const handleKeydown = (e) =&amp;gt; {
    if (e.key === &apos;Escape&apos;) {
      setOpen(false);
    }
  };

  window.addEventListener(&apos;resize&apos;, handleResize);
  window.addEventListener(&apos;scroll&apos;, handleScroll);
  document.addEventListener(&apos;keydown&apos;, handleKeydown);

  return () =&amp;gt; {
    window.removeEventListener(&apos;resize&apos;, handleResize);
    window.removeEventListener(&apos;scroll&apos;, handleScroll);
    document.removeEventListener(&apos;keydown&apos;, handleKeydown);
  };
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With &lt;code&gt;AbortController&lt;/code&gt;, the cleanup collapses to a single line:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  const controller = new AbortController();
  const { signal } = controller;

  window.addEventListener(&apos;resize&apos;, () =&amp;gt; setWidth(window.innerWidth), {
    signal,
  });
  window.addEventListener(&apos;scroll&apos;, () =&amp;gt; setScrollY(window.scrollY), {
    signal,
  });
  document.addEventListener(
    &apos;keydown&apos;,
    (e) =&amp;gt; {
      if (e.key === &apos;Escape&apos;) {
        setOpen(false);
      }
    },
    { signal },
  );

  return () =&amp;gt; controller.abort();
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The signal-based version is shorter and harder to get wrong. You can&apos;t accidentally forget to remove a listener — if it&apos;s attached to the signal, it gets cleaned up.&lt;/p&gt;
&lt;p&gt;This also works nicely for combined fetch + event listener cleanup in a single effect:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  const controller = new AbortController();
  const { signal } = controller;

  // Fetch data
  fetch(`/api/items/${id}`, { signal })
    .then((res) =&amp;gt; res.json())
    .then(setItems)
    .catch((err) =&amp;gt; {
      if (err.name !== &apos;AbortError&apos;) {
        throw err;
      }
    });

  // Listen for live updates
  window.addEventListener(
    &apos;focus&apos;,
    () =&amp;gt; {
      fetch(`/api/items/${id}`, { signal })
        .then((res) =&amp;gt; res.json())
        .then(setItems)
        .catch((err) =&amp;gt; {
          if (err.name !== &apos;AbortError&apos;) {
            throw err;
          }
        });
    },
    { signal },
  );

  return () =&amp;gt; controller.abort();
}, [id]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One controller, one cleanup line, and both the event listener and any in-flight fetches get cancelled together.&lt;/p&gt;
&lt;h2&gt;Composing signals with &lt;code&gt;AbortSignal.any()&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Sometimes you want to cancel an operation if &lt;em&gt;any&lt;/em&gt; of several conditions are met. &lt;code&gt;AbortSignal.any()&lt;/code&gt; combines multiple signals into one:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const userController = new AbortController();
const timeoutSignal = AbortSignal.timeout(10000);

const combinedSignal = AbortSignal.any([userController.signal, timeoutSignal]);

fetch(&apos;/api/data&apos;, { signal: combinedSignal }).catch((err) =&amp;gt; {
  if (err.name === &apos;TimeoutError&apos;) {
    console.log(&apos;Timed out&apos;);
  } else if (err.name === &apos;AbortError&apos;) {
    console.log(&apos;User cancelled&apos;);
  }
});

// User can cancel manually at any time
cancelButton.onclick = () =&amp;gt; userController.abort();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is especially handy in React for combining a component-level abort signal with a user-triggered cancellation or a timeout.&lt;/p&gt;
&lt;h2&gt;Cancelling streams&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;AbortController&lt;/code&gt; works with both readable and writable streams. If you&apos;re consuming a streaming response, aborting the signal tears down the stream:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const controller = new AbortController();

const response = await fetch(&apos;/api/stream&apos;, {
  signal: controller.signal,
});

const reader = response.body.getReader();

try {
  while (true) {
    const { done, value } = await reader.read();
    if (done) {
      break;
    }
    processChunk(value);
  }
} catch (err) {
  if (err.name === &apos;AbortError&apos;) {
    console.log(&apos;Stream cancelled&apos;);
  }
}

// Call this from anywhere to stop the stream
controller.abort();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the right way to handle &quot;stop generating&quot; buttons for streaming AI responses, or cancelling large file downloads midway through.&lt;/p&gt;
&lt;h2&gt;Using the signal in your own APIs&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;AbortSignal&lt;/code&gt; isn&apos;t locked to built-in APIs. You can accept a signal in your own functions and respond to it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function pollForUpdates(url, interval, { signal } = {}) {
  return new Promise((resolve, reject) =&amp;gt; {
    const timer = setInterval(async () =&amp;gt; {
      try {
        const res = await fetch(url, { signal });
        const data = await res.json();
        if (data.complete) {
          clearInterval(timer);
          resolve(data);
        }
      } catch (err) {
        clearInterval(timer);
        reject(err);
      }
    }, interval);

    // Stop polling when the signal aborts
    signal?.addEventListener(&apos;abort&apos;, () =&amp;gt; {
      clearInterval(timer);
      reject(signal.reason);
    });
  });
}

// Usage
const controller = new AbortController();

pollForUpdates(&apos;/api/job/123/status&apos;, 2000, {
  signal: controller.signal,
});

// Stop polling
controller.abort();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By following the &lt;code&gt;{ signal }&lt;/code&gt; convention, your APIs compose naturally with &lt;code&gt;AbortSignal.timeout()&lt;/code&gt;, &lt;code&gt;AbortSignal.any()&lt;/code&gt;, and anything else that speaks the signal protocol.&lt;/p&gt;
&lt;h2&gt;In Node.js&lt;/h2&gt;
&lt;p&gt;Node.js has adopted &lt;code&gt;AbortSignal&lt;/code&gt; across its core APIs. A few examples:&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;setTimeout&lt;/code&gt; / &lt;code&gt;setInterval&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const controller = new AbortController();

const timeout = setTimeout(
  () =&amp;gt; {
    console.log(&apos;This might never run&apos;);
  },
  5000,
  { signal: controller.signal },
);

// Cancels the timer
controller.abort();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Child processes&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { exec } from &apos;node:child_process&apos;;

const controller = new AbortController();

exec(&apos;long-running-command&apos;, { signal: controller.signal }, (err) =&amp;gt; {
  if (err?.name === &apos;AbortError&apos;) {
    console.log(&apos;Process was killed&apos;);
  }
});

// Kill the child process
controller.abort();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;fs.readFile&lt;/code&gt; and &lt;code&gt;fs.writeFile&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { readFile } from &apos;node:fs/promises&apos;;

const controller = new AbortController();

readFile(&apos;/path/to/huge-file.csv&apos;, {
  signal: controller.signal,
}).catch((err) =&amp;gt; {
  if (err.name === &apos;AbortError&apos;) {
    console.log(&apos;File read cancelled&apos;);
  }
});

controller.abort();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Event emitters&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { on } from &apos;node:events&apos;;

const controller = new AbortController();

// Async iteration over events
for await (const [message] of on(chatSocket, &apos;message&apos;, {
  signal: controller.signal,
})) {
  console.log(message);
}
// Loop exits when controller.abort() is called
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;AbortController&lt;/code&gt; started life as a way to cancel fetch requests, but the underlying pattern — a signal that broadcasts &quot;stop what you&apos;re doing&quot; — turns out to be useful everywhere. Event listeners, streams, timers, child processes, your own async functions... anywhere you need cooperative cancellation, &lt;code&gt;AbortSignal&lt;/code&gt; is the standard way to do it.&lt;/p&gt;
&lt;p&gt;The key takeaways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;{ signal }&lt;/code&gt;&lt;/strong&gt; on &lt;code&gt;addEventListener&lt;/code&gt; removes listeners automatically on abort — use it in React effects.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;AbortSignal.timeout()&lt;/code&gt;&lt;/strong&gt; creates a self-aborting signal after a delay.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;AbortSignal.any()&lt;/code&gt;&lt;/strong&gt; lets you combine multiple abort conditions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accept a &lt;code&gt;signal&lt;/code&gt; option&lt;/strong&gt; in your own async functions to make them cancellable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Node.js&lt;/strong&gt; supports signals in &lt;code&gt;setTimeout&lt;/code&gt;, &lt;code&gt;child_process&lt;/code&gt;, &lt;code&gt;fs&lt;/code&gt;, &lt;code&gt;events&lt;/code&gt;, and more.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&apos;re still writing manual &lt;code&gt;removeEventListener&lt;/code&gt; cleanup or rolling your own cancellation booleans, give &lt;code&gt;AbortController&lt;/code&gt; a proper look. It&apos;s already everywhere.&lt;/p&gt;
</content:encoded></item><item><title>Publish Your Mastodon Posts to Astro</title><link>https://kwilson.io/blog/publish-your-mastodon-posts-to-astro/</link><guid isPermaLink="true">https://kwilson.io/blog/publish-your-mastodon-posts-to-astro/</guid><pubDate>Mon, 29 Apr 2024 18:40:04 GMT</pubDate><content:encoded>&lt;p&gt;I&apos;m a big fan of hosting your own content. Partly because I like keeping control over what I&apos;ve posted, partly because platforms like &lt;s&gt;MySpace&lt;/s&gt; some older video sharing sites will just lose all of your content that you hadn&apos;t backed up anywhere else because you were young(er) and stupid(er).&lt;/p&gt;
&lt;p&gt;Since the implosion of &lt;em&gt;The Site Formerly Known as Twitter&lt;/em&gt;, I&apos;ve been using Mastodon to post infrequent nonsense that crosses my mind. With the Fediverse, it&apos;s certainly possible to run your own instance and own your content that way, but I was looking for a simpler solution to just copy the posts into the site here.&lt;/p&gt;
&lt;p&gt;Anyway — whatever your reason — let&apos;s look at automatically importing Toots into Astro.&lt;/p&gt;
&lt;h2&gt;The General Workflow&lt;/h2&gt;
&lt;p&gt;How this is going to work is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Make a post/toot on Mastodon&lt;/li&gt;
&lt;li&gt;Trigger a webhook to grab the content of that toot as JSON&lt;/li&gt;
&lt;li&gt;Write that JSON file directly into an Astro repository on GitHub&lt;/li&gt;
&lt;li&gt;Trigger a build and deployment of the site&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Easy, right? Let&apos;s get things set up to work.&lt;/p&gt;
&lt;h2&gt;Examining the Toot Structure&lt;/h2&gt;
&lt;p&gt;Getting the JSON representation of a Toot is pretty simple — copy the URL of a single post and add &lt;code&gt;.json&lt;/code&gt; to the end of the URL. Doing that with a test toot gives us something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;@context&quot;: [
    &quot;https://www.w3.org/ns/activitystreams&quot;,
    {
      &quot;ostatus&quot;: &quot;http://ostatus.org#&quot;,
      &quot;atomUri&quot;: &quot;ostatus:atomUri&quot;,
      &quot;inReplyToAtomUri&quot;: &quot;ostatus:inReplyToAtomUri&quot;,
      &quot;conversation&quot;: &quot;ostatus:conversation&quot;,
      &quot;sensitive&quot;: &quot;as:sensitive&quot;,
      &quot;toot&quot;: &quot;http://joinmastodon.org/ns#&quot;,
      &quot;votersCount&quot;: &quot;toot:votersCount&quot;,
      &quot;blurhash&quot;: &quot;toot:blurhash&quot;,
      &quot;focalPoint&quot;: { &quot;@container&quot;: &quot;@list&quot;, &quot;@id&quot;: &quot;toot:focalPoint&quot; }
    }
  ],
  &quot;id&quot;: &quot;https://mastodon.social/users/kwilson81/statuses/112357239872765151&quot;,
  &quot;type&quot;: &quot;Note&quot;,
  &quot;summary&quot;: null,
  &quot;inReplyTo&quot;: null,
  &quot;published&quot;: &quot;2024-04-30T00:03:42Z&quot;,
  &quot;url&quot;: &quot;https://mastodon.social/@kwilson81/112357239872765151&quot;,
  &quot;attributedTo&quot;: &quot;https://mastodon.social/users/kwilson81&quot;,
  &quot;to&quot;: [&quot;https://www.w3.org/ns/activitystreams#Public&quot;],
  &quot;cc&quot;: [&quot;https://mastodon.social/users/kwilson81/followers&quot;],
  &quot;sensitive&quot;: false,
  &quot;atomUri&quot;: &quot;https://mastodon.social/users/kwilson81/statuses/112357239872765151&quot;,
  &quot;inReplyToAtomUri&quot;: null,
  &quot;conversation&quot;: &quot;tag:mastodon.social,2024-04-30:objectId=696705597:objectType=Conversation&quot;,
  &quot;content&quot;: &quot;\u003cp\u003ePenny clearly excited to find out if this Cloudflare pipeline thing actually works... 🤞\u003c/p\u003e&quot;,
  &quot;contentMap&quot;: {
    &quot;en&quot;: &quot;\u003cp\u003ePenny clearly excited to find out if this Cloudflare pipeline thing actually works... 🤞\u003c/p\u003e&quot;
  },
  &quot;attachment&quot;: [
    {
      &quot;type&quot;: &quot;Document&quot;,
      &quot;mediaType&quot;: &quot;image/jpeg&quot;,
      &quot;url&quot;: &quot;https://files.mastodon.social/media_attachments/files/112/357/232/898/119/052/original/14e0ac3b6bcebf12.jpg&quot;,
      &quot;name&quot;: &quot;Small Chorkie dog looking pensive.&quot;,
      &quot;blurhash&quot;: &quot;UHD93Kj;Na?F~U%1Ip%1^%%1xHbF%LoMRoWV&quot;,
      &quot;width&quot;: 2499,
      &quot;height&quot;: 3319
    }
  ],
  &quot;tag&quot;: [],
  &quot;replies&quot;: {
    &quot;id&quot;: &quot;https://mastodon.social/users/kwilson81/statuses/112357239872765151/replies&quot;,
    &quot;type&quot;: &quot;Collection&quot;,
    &quot;first&quot;: {
      &quot;type&quot;: &quot;CollectionPage&quot;,
      &quot;next&quot;: &quot;https://mastodon.social/users/kwilson81/statuses/112357239872765151/replies?only_other_accounts=true\u0026page=true&quot;,
      &quot;partOf&quot;: &quot;https://mastodon.social/users/kwilson81/statuses/112357239872765151/replies&quot;,
      &quot;items&quot;: []
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the setup I&apos;m going for (at the moment), I&apos;m only really interested in a subset of this data:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;id&lt;/li&gt;
&lt;li&gt;published&lt;/li&gt;
&lt;li&gt;url&lt;/li&gt;
&lt;li&gt;content&lt;/li&gt;
&lt;li&gt;attachment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So that&apos;s what we&apos;ll be using inside Astro.&lt;/p&gt;
&lt;h2&gt;Creating a Toot Collection in Astro&lt;/h2&gt;
&lt;p&gt;Collections in Astro are stored under &lt;code&gt;~/src/content&lt;/code&gt;, so let&apos;s create a &lt;code&gt;mastodon&lt;/code&gt; folder under there that will host the content.&lt;/p&gt;
&lt;p&gt;We can now add a definition for that collection using Zod inside our &lt;code&gt;~/src/content.config.ts&lt;/code&gt; file. We&apos;ll just ignore attachments for now.&lt;/p&gt;
&lt;p&gt;So that gives us this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const mastodon = defineCollection({
  loader: glob({ pattern: &apos;**/*.json&apos;, base: &apos;./src/content/mastodon&apos; }),
  schema: z.object({
    id: z.string(),
    published: z.coerce.date(),
    url: z.string(),
    content: z.string(),
  }),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Looking at the JSON of the Toot, the ID is just the URL:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://mastodon.social/users/kwilson81/statuses/112357239872765151
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we want to be able to have a local URL pointing at that content (something like &lt;em&gt;mywebsite/notes/[tootId]&lt;/em&gt;) then we&apos;ll need to pull the numeric ID from the end of that so we have something specific that we can use to grab it.&lt;/p&gt;
&lt;p&gt;We can do this at a collection level using a &lt;a href=&quot;https://zod.dev/?id=transform&quot;&gt;Zod transform&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const mastodon = defineCollection({
  loader: glob({ pattern: &apos;**/*.json&apos;, base: &apos;./src/content/mastodon&apos; }),
  schema: z
    .object({
      id: z.string(),
      published: z.coerce.date(),
      url: z.string(),
      content: z.string(),
    })
    .transform((data) =&amp;gt; ({
      ...data,
      tootId: /(?&amp;lt;tootId&amp;gt;[A-z0-9]+$)/.exec(data.id)?.groups?.[&apos;tootId&apos;] ?? null,
    })),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we&apos;ve got a collection set up, let&apos;s look at pulling the data into the repo.&lt;/p&gt;
&lt;h2&gt;Cloudflare Worker&lt;/h2&gt;
&lt;p&gt;Cloudflare Workers are easy to use, fast, and free. So that makes them a good candidate for running our grabbing pipeline.&lt;/p&gt;
&lt;p&gt;How we want this to work is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Worker receives a POST with the Toot URL&lt;/li&gt;
&lt;li&gt;Worker grabs the JSON data of the Toot&lt;/li&gt;
&lt;li&gt;Worker uploads any image attachments to Cloudinary&lt;/li&gt;
&lt;li&gt;Worker creates a new commit with the JSON file directly into the collection folder we created above&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Again, let&apos;s break this down step-by-step.&lt;/p&gt;
&lt;h3&gt;The Basic Worker&lt;/h3&gt;
&lt;p&gt;Creating a basic worker with &lt;code&gt;npm create cloudflare@latest&lt;/code&gt; will give us something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext,
  ): Promise&amp;lt;Response&amp;gt; {
    return new Response(&apos;Hello World!&apos;);
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now running &lt;code&gt;npm run deploy&lt;/code&gt; will take you through the wizard to get that deployed into Cloudflare. I&apos;m not going to go into the specifics of that, but you&apos;ll need to create an account and decide where your worker is going to be deployed to. This is all free though and a pretty simple process.&lt;/p&gt;
&lt;p&gt;Here&apos;s a quick look at what that looks like for me:&lt;/p&gt;
&lt;p&gt;&amp;lt;CloudinaryVideo
cloudinaryId={&lt;code&gt;kwilson.io/blogposts/publish-your-mastodon-posts-to-astro/worker-setup&lt;/code&gt;}
post
/&amp;gt;&lt;/p&gt;
&lt;p&gt;Got that deployed somewhere? Okay, take a note of the URL and let&apos;s continue.&lt;/p&gt;
&lt;p&gt;We want to validate that we can receive a POST with data such as &lt;code&gt;{ &quot;uri&quot;: &quot;https://the-toot-url/id&quot; }&lt;/code&gt; so let&apos;s use Zod again (&lt;code&gt;npm i zod&lt;/code&gt;) to set up validation for that:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { z } from &apos;zod&apos;;

const schema = z.object({
  uri: z.string(),
});

export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext,
  ): Promise&amp;lt;Response&amp;gt; {
    const { uri } = schema.parse(await request.json());
    return new Response(uri);
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we now POST to the URL of your worker, we should get that &lt;code&gt;uri&lt;/code&gt; value echoed back to us:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; curl -d &apos;{&quot;uri&quot;:&quot;http://test.invalid&quot;}&apos; \
       -H &quot;Content-Type: application/json&quot; \
       -X POST https://YOUR-WORKER-URL/

http://test.invalid
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now that we have a worker that can accept the URL as an input, let&apos;s grab the content from Mastodon.&lt;/p&gt;
&lt;h3&gt;Fetching the Toot&lt;/h3&gt;
&lt;p&gt;Remember how we could get the JSON for any toot by appending &lt;code&gt;.json&lt;/code&gt; to its URL? That&apos;s exactly what we&apos;ll do here. We&apos;ll fetch the toot data, then validate it with another Zod schema:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const documentSchema = z
  .object({
    mediaType: z.string(),
    url: z.string(),
  })
  .passthrough();

const mastodonSchema = z
  .object({
    id: z.string(),
    published: z.string(),
    attachment: z.array(documentSchema).optional(),
  })
  .passthrough();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using &lt;code&gt;.passthrough()&lt;/code&gt; means the schema will validate the fields we care about while keeping all the other ActivityPub data intact. This is handy because the JSON structure is quite verbose — we don&apos;t need to define every field, just the ones we want to validate.&lt;/p&gt;
&lt;p&gt;Now we can fetch and parse the toot inside the worker:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { uri } = schema.parse(await request.json());

const response = await fetch(`${uri}.json`);
const postData = mastodonSchema.parse(await response.json());
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Handling Image Attachments&lt;/h3&gt;
&lt;p&gt;If the toot has images attached, we need to deal with them. The attachment URLs point to Mastodon&apos;s file server, but we can&apos;t rely on those URLs being stable forever (which kind of defeats the purpose of archiving our content). So we&apos;ll upload them to Cloudinary and reference them from there instead.&lt;/p&gt;
&lt;p&gt;First, we need a stable identifier for each image. The Mastodon URL itself is long and messy, so let&apos;s generate a SHA-256 hash of it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import crypto from &apos;node:crypto&apos;;

const hashedAttachments = postData.attachment?.map((file) =&amp;gt; ({
  ...file,
  urlHash: crypto.createHash(&apos;sha256&apos;).update(file.url).digest(&apos;base64url&apos;),
}));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives us a short, URL-safe string we can use as both a Cloudinary public ID and a way to reference the image later in our Astro components.&lt;/p&gt;
&lt;p&gt;Now we need to upload the images to Cloudinary. To do that, we&apos;ll use Cloudinary&apos;s &lt;a href=&quot;https://cloudinary.com/documentation/image_upload_api_reference&quot;&gt;Upload API&lt;/a&gt; with a signed request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function uploadImageToCloudinary(
  imageUrl: string,
  imageName: string,
  env: Env,
) {
  const timestamp = new Date().getTime();
  const api_key = env.CLOUDINARY_API_KEY;
  const api_secret = env.CLOUDINARY_API_SECRET;
  const folder_path = env.CLOUDINARY_UPLOAD_FOLDER_PATH;
  const cloud_name = env.CLOUDINARY_CLOUD_NAME;

  const signatureInput = [
    `folder=${folder_path}`,
    `public_id=${imageName}`,
    `timestamp=${timestamp}`,
  ].join(&apos;&amp;amp;&apos;);

  const signature = crypto
    .createHash(&apos;sha1&apos;)
    .update(`${signatureInput}${api_secret}`)
    .digest(&apos;hex&apos;);

  const parameters = [
    `api_key=${api_key}`,
    `file=${imageUrl}`,
    `folder=${folder_path}`,
    `public_id=${imageName}`,
    `timestamp=${timestamp}`,
    `signature=${signature}`,
  ].join(&apos;&amp;amp;&apos;);

  const response = await fetch(
    `http://api.cloudinary.com/v1_1/${cloud_name}/image/upload`,
    {
      method: &apos;POST&apos;,
      body: parameters,
      headers: {
        &apos;Content-Type&apos;: &apos;application/x-www-form-urlencoded&apos;,
      },
    },
  );

  return response;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Cloudinary signed upload API requires you to create a SHA-1 hash of the upload parameters (alphabetically sorted) concatenated with your API secret. It sounds more complicated than it is — the key thing is that Cloudinary can then verify the request is legitimate without you needing to expose your API secret.&lt;/p&gt;
&lt;p&gt;We can then upload all attachments in parallel:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;await Promise.all(
  hashedAttachments?.map((attachment) =&amp;gt; {
    return uploadImageToCloudinary(attachment.url, attachment.urlHash, env);
  }) ?? [],
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Committing to GitHub&lt;/h3&gt;
&lt;p&gt;Now for the clever bit. Rather than having a server pull the repo, make a commit and push, we can use the GitHub API to create a commit directly. This is perfect for a serverless worker where we don&apos;t have a filesystem to play with.&lt;/p&gt;
&lt;p&gt;We&apos;ll use &lt;a href=&quot;https://github.com/octokit/core.js&quot;&gt;Octokit&lt;/a&gt;, GitHub&apos;s official API client:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i @octokit/core base-64 utf8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The GitHub Contents API expects the file content to be base64-encoded, so we need to encode our JSON:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Octokit } from &apos;@octokit/core&apos;;
import utf8 from &apos;utf8&apos;;
import base64 from &apos;base-64&apos;;

const octokit = new Octokit({
  auth: env.GITHUB_ACCESS_TOKEN,
});

const hashedPostData = {
  ...postData,
  attachment: hashedAttachments,
};

const contentBytes = utf8.encode(JSON.stringify(hashedPostData));
const encodedContent = base64.encode(contentBytes);

await octokit.request(&apos;PUT /repos/{owner}/{repo}/contents/{path}&apos;, {
  owner: &apos;YOUR-GITHUB-USERNAME&apos;,
  repo: &apos;YOUR-REPO-NAME&apos;,
  path: `src/content/mastodon/${postData.published}.json`,
  message: `new post - ${postData.published}`,
  committer: {
    name: &apos;Mastodon Post&apos;,
    email: &apos;mastodon@example.com&apos;,
  },
  content: encodedContent,
  headers: {
    &apos;X-GitHub-Api-Version&apos;: &apos;2022-11-28&apos;,
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A few things to note here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The file path uses the toot&apos;s &lt;code&gt;published&lt;/code&gt; timestamp as the filename (e.g. &lt;code&gt;2024-04-30T00:03:42Z.json&lt;/code&gt;). This gives us unique, chronologically sortable filenames for free.&lt;/li&gt;
&lt;li&gt;You&apos;ll need a &lt;a href=&quot;https://github.com/settings/tokens&quot;&gt;GitHub Personal Access Token&lt;/a&gt; with &lt;code&gt;repo&lt;/code&gt; scope to write to your repository.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;committer&lt;/code&gt; can be whatever you like — I set it to something identifiable so I can see at a glance which commits were automated.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Environment Variables&lt;/h3&gt;
&lt;p&gt;The worker needs a few secrets to be configured. You can set these using the Wrangler CLI:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx wrangler secret put GITHUB_ACCESS_TOKEN
npx wrangler secret put CLOUDINARY_API_KEY
npx wrangler secret put CLOUDINARY_API_SECRET
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the non-secret values, you can set them in your &lt;code&gt;wrangler.toml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[vars]
CLOUDINARY_CLOUD_NAME = &quot;your-cloud-name&quot;
CLOUDINARY_UPLOAD_FOLDER_PATH = &quot;mastodon/images&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We also need to enable the &lt;code&gt;nodejs_compat&lt;/code&gt; compatibility flag in &lt;code&gt;wrangler.toml&lt;/code&gt; so we can use the &lt;code&gt;crypto&lt;/code&gt; module:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;compatibility_flags = [&quot;nodejs_compat&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The Full Worker&lt;/h3&gt;
&lt;p&gt;Putting it all together, the complete worker looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Octokit } from &apos;@octokit/core&apos;;
import { z } from &apos;zod&apos;;
import utf8 from &apos;utf8&apos;;
import base64 from &apos;base-64&apos;;
import crypto from &apos;node:crypto&apos;;

const schema = z.object({
  uri: z.string(),
});

const documentSchema = z
  .object({
    mediaType: z.string(),
    url: z.string(),
  })
  .passthrough();

const mastodonSchema = z
  .object({
    id: z.string(),
    published: z.string(),
    attachment: z.array(documentSchema).optional(),
  })
  .passthrough();

export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext,
  ): Promise&amp;lt;Response&amp;gt; {
    const { uri } = schema.parse(await request.json());

    const response = await fetch(`${uri}.json`);
    const postData = mastodonSchema.parse(await response.json());

    const octokit = new Octokit({
      auth: env.GITHUB_ACCESS_TOKEN,
    });

    const hashedAttachments = postData.attachment?.map((file) =&amp;gt; ({
      ...file,
      urlHash: crypto.createHash(&apos;sha256&apos;).update(file.url).digest(&apos;base64url&apos;),
    }));

    const hashedPostData = {
      ...postData,
      attachment: hashedAttachments,
    };

    const contentBytes = utf8.encode(JSON.stringify(hashedPostData));
    const encodedContent = base64.encode(contentBytes);

    await Promise.all(
      hashedAttachments?.map((attachment) =&amp;gt; {
        return uploadImageToCloudinary(attachment.url, attachment.urlHash, env);
      }) ?? [],
    );

    const result = await octokit.request(
      &apos;PUT /repos/{owner}/{repo}/contents/{path}&apos;,
      {
        owner: &apos;YOUR-GITHUB-USERNAME&apos;,
        repo: &apos;YOUR-REPO-NAME&apos;,
        path: `src/content/mastodon/${postData.published}.json`,
        message: `new post - ${postData.published}`,
        committer: {
          name: &apos;Mastodon Post&apos;,
          email: &apos;mastodon@example.com&apos;,
        },
        content: encodedContent,
        headers: {
          &apos;X-GitHub-Api-Version&apos;: &apos;2022-11-28&apos;,
        },
      },
    );

    return new Response(JSON.stringify(result));
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Triggering the Pipeline with IFTTT&lt;/h2&gt;
&lt;p&gt;We&apos;ve got a worker that can process a toot — but we still need something to &lt;em&gt;trigger&lt;/em&gt; it when we actually post something. This is where &lt;a href=&quot;https://ifttt.com/&quot;&gt;IFTTT&lt;/a&gt; (If This Then That) comes in.&lt;/p&gt;
&lt;p&gt;Mastodon provides an RSS feed for every user at &lt;code&gt;https://mastodon.social/@YOUR-USERNAME.rss&lt;/code&gt;. IFTTT can monitor this feed and fire a webhook whenever a new item appears.&lt;/p&gt;
&lt;p&gt;Here&apos;s the setup:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a new IFTTT Applet&lt;/li&gt;
&lt;li&gt;For the &lt;strong&gt;&quot;If This&quot;&lt;/strong&gt; trigger, choose &lt;strong&gt;RSS Feed&lt;/strong&gt; → &lt;strong&gt;New feed item&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Set the feed URL to your Mastodon RSS feed (e.g. &lt;code&gt;https://mastodon.social/@kwilson81.rss&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;For the &lt;strong&gt;&quot;Then That&quot;&lt;/strong&gt; action, choose &lt;strong&gt;Webhooks&lt;/strong&gt; → &lt;strong&gt;Make a web request&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Configure the webhook:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;URL&lt;/strong&gt;: Your Cloudflare Worker URL&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Method&lt;/strong&gt;: POST&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Content Type&lt;/strong&gt;: application/json&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Body&lt;/strong&gt;: &lt;code&gt;{&quot;uri&quot;: &quot;{{EntryUrl}}&quot;}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The &lt;code&gt;{{EntryUrl}}&lt;/code&gt; is an IFTTT ingredient that gets replaced with the URL of the new RSS item — which is exactly the toot URL that our worker expects.&lt;/p&gt;
&lt;h2&gt;Auto-Deployment&lt;/h2&gt;
&lt;p&gt;The final piece of the puzzle is getting the site to rebuild when a new toot is committed. If you&apos;re hosting on Cloudflare Pages (or Netlify, Vercel, etc.) and have it connected to your GitHub repository, this happens automatically — any new commit triggers a build and deployment.&lt;/p&gt;
&lt;p&gt;So the full flow ends up being:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Post a toot on Mastodon&lt;/li&gt;
&lt;li&gt;IFTTT detects the new post via the RSS feed&lt;/li&gt;
&lt;li&gt;IFTTT sends the toot URL to the Cloudflare Worker&lt;/li&gt;
&lt;li&gt;The worker fetches the toot JSON, uploads any images to Cloudinary, and commits the JSON file to GitHub&lt;/li&gt;
&lt;li&gt;Cloudflare Pages detects the new commit and rebuilds the site&lt;/li&gt;
&lt;li&gt;The toot appears on the site&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;All fully automated — post and forget.&lt;/p&gt;
</content:encoded></item><item><title>Switching to Astro</title><link>https://kwilson.io/blog/switching-to-astro/</link><guid isPermaLink="true">https://kwilson.io/blog/switching-to-astro/</guid><pubDate>Wed, 27 Mar 2024 17:30:00 GMT</pubDate><content:encoded>&lt;p&gt;This blog has been neglected for about 5 years and has been burning away compute cycles on an old Wordpress install and MySql DB. And I&apos;ve been too &lt;s&gt;lazy&lt;/s&gt; busy to update it to something that&apos;s not needlessly costing me £10/month.&lt;/p&gt;
&lt;p&gt;But no longer. Now we&apos;re on &lt;a href=&quot;https://astro.build/&quot;&gt;Astro&lt;/a&gt;, taking advantage of static site generation and servicing everything through Cloudflare. Complete overkill for something like this, but it&apos;s a nice framework.&lt;/p&gt;
</content:encoded></item><item><title>Split your cmder window into multiple panels</title><link>https://kwilson.io/blog/split-your-cmder-window-into-multiple-panels/</link><guid isPermaLink="true">https://kwilson.io/blog/split-your-cmder-window-into-multiple-panels/</guid><pubDate>Sun, 26 May 2019 18:33:34 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://cmder.net/&quot;&gt;cmder&lt;/a&gt; is my go-to shell for Windows. Up until recently, I was unaware that it could be split into multiple panels.&lt;/p&gt;
&lt;p&gt;There doesn&apos;t seem to be a menu option to do it, but it’s easily done with these commands (which, yes, I need to look up every time).&lt;/p&gt;
&lt;h3&gt;Split the window horizontally (left/right split):&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;cmd -new_console:s cmd /k &quot;&quot;%ConEmuDir%\..\init.bat&quot; &quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Split the window vertically (top/bottom split):&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;cmd -new_console:sV cmd /k &quot;&quot;%ConEmuDir%\..\init.bat&quot; &quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also sub-split the panels into whatever format works best for you.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;cmder-split.jpg&quot; alt=&quot;cmder pane split into 4 sections&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Fix Error 0x80072EE7 on Windows 10</title><link>https://kwilson.io/blog/fix-error-0x80072ee7-on-windows-10/</link><guid isPermaLink="true">https://kwilson.io/blog/fix-error-0x80072ee7-on-windows-10/</guid><pubDate>Thu, 04 Oct 2018 12:41:31 GMT</pubDate><content:encoded>&lt;p&gt;I just installed the Windows 10 October 2018 update and was hit with an issue that Microsoft Store and Edge wouldn&apos;t connect to the internet. Dreaded error 0x80072EE7.&lt;/p&gt;
&lt;p&gt;Tried repairing etc. as suggested around the web, but the issue for me turned out to be that I didn&apos;t have IPv6 enabled in my network adaptor settings. I&apos;m pretty sure it’s enabled by default, but I&apos;d turned it off while trying to fix something else.&lt;/p&gt;
&lt;p&gt;This setting here:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;ipv6.png&quot; alt=&quot;Windows Dialog: WiFi Properties &amp;gt; Internet Protocol Version 6&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Enabled that and all working again.&lt;/p&gt;
&lt;p&gt;It’s very probable that your issue is unrelated, but this fixed it for me.&lt;/p&gt;
</content:encoded></item><item><title>UX Snippets: Avoid mismatching instructions and actions</title><link>https://kwilson.io/blog/ux-snippets-avoid-mismatching-instructions-and-actions/</link><guid isPermaLink="true">https://kwilson.io/blog/ux-snippets-avoid-mismatching-instructions-and-actions/</guid><pubDate>Thu, 29 Sep 2016 18:46:28 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://moneydashboard.com&quot;&gt;Money Dashboard&lt;/a&gt; is an excellent app. Apart from when it’s telling me that I&apos;ve gone over my monthly coffee budget.&lt;/p&gt;
&lt;p&gt;Or when it gives instructions that don&apos;t match what you see on screen.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;rbs-md.png&quot; alt=&quot;&apos;Please click OK&apos; next to a single button which reads &apos;Got It&apos;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;It’s not a huge deal since it’s pretty obvious what the intention is, but it’s just one more little mental leap that the user has to make to do something.&lt;/p&gt;
&lt;p&gt;And we know that we &lt;a href=&quot;https://www.amazon.co.uk/Dont-Make-Me-Think-Usability/dp/0321965515&quot;&gt;shouldn&apos;t force users to think&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>The horrible UX of the National Lottery website messaging system</title><link>https://kwilson.io/blog/the-horrible-ux-of-the-national-lottery-website-messaging-system/</link><guid isPermaLink="true">https://kwilson.io/blog/the-horrible-ux-of-the-national-lottery-website-messaging-system/</guid><pubDate>Sun, 04 Sep 2016 17:59:46 GMT</pubDate><content:encoded>&lt;p&gt;Yes, I&apos;m one of those fools that plays the lottery. Seeing as there’s no way I&apos;m going to queue at the supermarket to get a paper version of my &lt;em&gt;could-be-worth-millions&lt;/em&gt; lottery ticket, that means I have to suffer the official website if I want to play.&lt;/p&gt;
&lt;p&gt;There are a fair few issues with it but I&apos;m going to be concentrating on the messages system because it’s been driving me mad today.&lt;/p&gt;
&lt;h2&gt;1. I don&apos;t need messages on the site&lt;/h2&gt;
&lt;p&gt;Before we even get to the website, any messages are already emailed to me so 99% of the time (actually probably 100%) I&apos;ve already read them by the time I get to the site so I&apos;ve got no inclination to click through and read them again.&lt;/p&gt;
&lt;h2&gt;2. The message notifications get in the way&lt;/h2&gt;
&lt;p&gt;When I visit the site, I&apos;m usually trying to accomplish something (like, y&apos;know, buy a ticket). This isn&apos;t useful:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;lottery-notifications.png&quot; alt=&quot;Lottery notifications taking up half of the vertical screen space&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Even more galling is that not one of those messages has any more useful information in the body of the message than they have in the title.&lt;/p&gt;
&lt;h2&gt;3. The message notifications break scrolling&lt;/h2&gt;
&lt;p&gt;Okay, so the notifications take up at least 50% of the visible screen – I can still scroll past them, right?&lt;/p&gt;
&lt;p&gt;Not really.&lt;/p&gt;
&lt;p&gt;The 89 requests and 1.2Mb of content take about 4 seconds to load on my machine (and a couple of click trackers never load – thanks, AdBlock!). The main JS file (min.main-[someguid].js) is the last to load. When it does, the site jumps back to scroll position 0.&lt;/p&gt;
&lt;p&gt;So, if you start scrolling down to the action buttons when the page loads, you get about 4s until you&apos;re thrown back up to the top.&lt;/p&gt;
&lt;h2&gt;4. There’s no option to mark all as read/delete all messages&lt;/h2&gt;
&lt;p&gt;Messages are &lt;em&gt;so&lt;/em&gt; important that, when you go to the messages page (My Account &amp;gt; Messages [no direct link from home]) then there’s no way to click something to mark them all as read.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;lottery-messages.png&quot; alt=&quot;List of all lottery messages with some winning tickets&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Also note that it is possible to win sometimes.&lt;/p&gt;
&lt;h2&gt;5. Links a click-jacked so you can&apos;t just bulk-open them all as read&lt;/h2&gt;
&lt;p&gt;By the time I got to this stage, I thought “&lt;em&gt;Right, I&apos;ll just open all the messages in new tabs and that&apos;ll mark them as read&lt;/em&gt;“. Typical foolish optimism of a lottery player.&lt;/p&gt;
&lt;p&gt;My plan was to middle-click (non-context click) each of the message links so that it would look as if I&apos;d read them. But, alas, it seems that there’s a (poorly coded) click listener on the message links so using the middle-click opens a new tab with the message and &lt;em&gt;also&lt;/em&gt; opens it in the current tab. Awesome.&lt;/p&gt;
&lt;h2&gt;Defeat&lt;/h2&gt;
&lt;p&gt;So here I sit – defeated and broken, clicking on each message in turn so that I can clear the notifications.&lt;/p&gt;
&lt;p&gt;It almost makes it not worth winning anything…&lt;/p&gt;
</content:encoded></item><item><title>Death of a Team</title><link>https://kwilson.io/blog/death-of-a-team/</link><guid isPermaLink="true">https://kwilson.io/blog/death-of-a-team/</guid><pubDate>Fri, 11 Mar 2016 12:00:06 GMT</pubDate><content:encoded>&lt;p&gt;Q: What’s the fastest way to run a project into the ground?&lt;br /&gt;
A: Ask the team how long it&apos;ll take, then give them a shorter amount of time to complete it.&lt;/p&gt;
&lt;h2&gt;Recursive Failure&lt;/h2&gt;
&lt;p&gt;Once upon a time, I worked on a project that had the stink of death about it. It was old code, had dozens of hacks scattered throughout, known bugs that were not easily fixable, and dependencies on a buggy internal DLL library for which the source code was missing.&lt;/p&gt;
&lt;p&gt;Prime candidate for a rewrite.&lt;/p&gt;
&lt;p&gt;Since this was all consultancy based work, everything had to be chargeable to a client. When a new tender was released that the powers that be thought that this software could be applied to, the development team would be asked to put together estimates for the effort required to customise the software for the client and, from this (presumably), a bid would go in.&lt;/p&gt;
&lt;p&gt;So, we&apos;d get together and work out what was required.&lt;/p&gt;
&lt;p&gt;Generally, our estimates would include the time to refactor some of the code that badly needed it – the goal was to have this software be something that could be easily configured for dozens of (eventually hundreds of) clients with little effort. Due to the various hacks and nonsense in the code, it&apos;d take a couple of months to pull together something that mostly worked. I have to say “mostly” because, of course, the test coverage was 0%. So, it made sense to include refactoring time since it&apos;d improve the workflow going forward.&lt;/p&gt;
&lt;p&gt;An estimate would go in, something like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;15 days refactoring&lt;/li&gt;
&lt;li&gt;25 days client specific&lt;/li&gt;
&lt;li&gt;5 days technical debt&lt;/li&gt;
&lt;li&gt;5 days testing&lt;/li&gt;
&lt;li&gt;10 days contingency&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;About 12 weeks work to do a good job.&lt;/p&gt;
&lt;p&gt;So, the bid would go in, and we&apos;d win the work. Then we&apos;d be told the budget was lowered to win the work, and we only had 6 weeks to deliver.&lt;/p&gt;
&lt;p&gt;Refactoring and technical debt would be the first to go since it was important to show the client stuff working. Contingency? Haha – no.&lt;/p&gt;
&lt;p&gt;Discussion around tasks would also be a “waste of time” since any time not coding “isn&apos;t productive”.&lt;/p&gt;
&lt;p&gt;So everyone would jump into building stuff as fast as possible. The client would see updates, suggest changes (because the functionality wasn&apos;t quite what they wanted) and project managers would then argue with them because there wasn&apos;t enough time to redo stuff and the code that had been thrown together matched the vague requirements that had been signed off.&lt;/p&gt;
&lt;p&gt;With two weeks of budget remaining, we&apos;d likely have about a month of work still to do – the one consistent thing about working on broken code is that random small tasks will blow up occasionally and eat up days.&lt;/p&gt;
&lt;p&gt;So, the last two weeks of the project, everyone will be working 14 hour days, including weekends (for no extra pay), just to get everything done.&lt;/p&gt;
&lt;p&gt;And we&apos;d meet the deadline.&lt;/p&gt;
&lt;p&gt;The code would be low quality, the software would be buggy, the cumulative technical debt would loom ever higher, the client would be unhappy at some of the functionality, but we&apos;d hit the deadline and ship.&lt;/p&gt;
&lt;p&gt;And then another tender would come in. Rinse and repeat.&lt;/p&gt;
&lt;p&gt;Out of that six person team, four quit within a year, and one requested to be taken off all work for that team. One poor soul is still there.&lt;/p&gt;
&lt;h2&gt;Nails&lt;/h2&gt;
&lt;p&gt;As a developer, I don&apos;t like to check in hacks or knowingly broken code. Sometimes it can&apos;t be helped – hotfixing code in production, last minute changes for a demo etc. – but it should never be the norm.&lt;/p&gt;
&lt;p&gt;I also don&apos;t like to work long days regularly. In general, it’s bad for your health and your relationships.&lt;/p&gt;
&lt;p&gt;Sometimes it’s fine. Hell, sometimes I&apos;ll get caught up in some problem and you&apos;d have to fight me to take my laptop away at 3am. But that’s different. Developers are problem solvers and sometimes the problem becomes and all-consuming monster that will be on your mind until you beat it into submission. In those situations, it makes sense to work on the solution as long as it feels necessary.&lt;/p&gt;
&lt;p&gt;I don&apos;t like to be working at 3am solely because there isn&apos;t enough time to meet the arbitrary deadline imposed for a piece of work.&lt;/p&gt;
&lt;p&gt;In one of the cycles I mentioned above, my manager told me he didn&apos;t want me working long hours. I said that was great and asked if the deadline had moved. He said no. The deadline remained the same and had to be met, the amount of work was the same, and the team size was the same. He just didn&apos;t want me working long hours. I think that was my last cycle.&lt;/p&gt;
&lt;h2&gt;Diminishing Replacements&lt;/h2&gt;
&lt;p&gt;On projects like this, developers will start to look around for other jobs, maybe go on a few interviews.&lt;/p&gt;
&lt;p&gt;Unless you&apos;re paying way above market rates (in which case, why are you penny-pinching with budgets?) or have amazing perks, your best developers are going to leave. And then you&apos;re going to have to try and hire quality replacements quickly since you have no contingency in your project budget. And that’s neither cheap nor easy. And just piles pressure onto the remaining team members.&lt;/p&gt;
&lt;p&gt;Again, I&apos;ve worked in places where a high percentage of developers who could leave, did. So what you mostly get left with are the devs who nobody else wants to hire, and the ones who don&apos;t care. That’s not a fun working environment. Believe me: I know. Ask me for horror stories sometime.&lt;/p&gt;
&lt;h2&gt;Solutions&lt;/h2&gt;
&lt;p&gt;So, as a manager, what can you do? When planning out work, keep this in mind:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;time = features x quality of work&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;If your budget or deadline isn&apos;t big enough, you have to lose either features or quality. The alternative is artificially increasing time by overworking your developers and that’s going to lead to morale drops and team members quitting.&lt;/p&gt;
&lt;p&gt;If you find that developers are working long hours regularly, have a look at the causation. Do you need more developers? Are your deadlines too short? Are you miscommunicating the importance of the work? Can you offer time in lieu or paid overtime to compensate?&lt;/p&gt;
&lt;p&gt;(as an aside, I once worked three 90+ hour weeks in a row, then got given 2 days in lieu as thanks – oh how I laughed)&lt;/p&gt;
&lt;p&gt;Is there a culture of overwork? Are the senior developers/managers/CEO seen to be working late night most nights? If so, they&apos;re setting a bad precedent for your company culture.&lt;/p&gt;
&lt;p&gt;A good work/life balance is important, and that has to be pushed from on-high. Otherwise you&apos;re going to end up with a team of zombies.&lt;/p&gt;
</content:encoded></item><item><title>Authorize Your Azure AD Users With SignalR</title><link>https://kwilson.io/blog/authorize-your-azure-ad-users-with-signalr/</link><guid isPermaLink="true">https://kwilson.io/blog/authorize-your-azure-ad-users-with-signalr/</guid><pubDate>Thu, 25 Feb 2016 09:00:09 GMT</pubDate><content:encoded>&lt;p&gt;As with pretty much everything in the ADAL packages, this is something that seems like it should be pretty straight-forward, but isn&apos;t.&lt;/p&gt;
&lt;p&gt;Before we go on to SignalR, we need to have a look at how we access a Web API endpoint that requires authentication from JavaScript. If you&apos;re already familiar with AD and bearer tokens, you can skip this bit.&lt;/p&gt;
&lt;h2&gt;It’s Easy in WebAPI&lt;/h2&gt;
&lt;p&gt;Okay, let’s say we have an OWIN app already set up to use Azure Active Directory for authorisation.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.UseWindowsAzureActiveDirectoryBearerAuthentication(
  new WindowsAzureActiveDirectoryBearerAuthenticationOptions
  {
    Tenant = AppSettings[&quot;ida:Tenant&quot;],
    TokenValidationParameters = new TokenValidationParameters
    {
      ValidAudience = AppSettings[&quot;ida:ClientId&quot;]
    }
  });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This means that, in our controllers, we can do things like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Authorize]
public abstract class UsersController : ApiController
{
  [HttpGet]
  public async Task&amp;amp;lt;IHttpActionResult&amp;amp;gt; GetUserName()
  {
    return Ok(ClaimsPrincipal.Current.Identity.Name);
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and only authenticated users will be able to hit that endpoint.&lt;/p&gt;
&lt;p&gt;Including &lt;code&gt;Authorize&lt;/code&gt; is enough to force users to authenticate to get through, and to populate &lt;code&gt;ClaimsPrincipal.Current&lt;/code&gt; with the user credentials.&lt;/p&gt;
&lt;h2&gt;Connecting from JavaScript&lt;/h2&gt;
&lt;p&gt;Now we have our API endpoint, how do we access it from JavaScript?&lt;/p&gt;
&lt;p&gt;Microsoft provide an &lt;a href=&quot;https://github.com/AzureAD/azure-activedirectory-library-for-js&quot;&gt;Azure AD library for JS&lt;/a&gt; as part of the general Azure library. The example code only shows samples for using it with Angular. If you want to go down a less masochistic route and just use something like JQuery, then there’s also an annoyingly hard to find &lt;a href=&quot;https://github.com/Azure-Samples/active-directory-javascript-singlepageapp-dotnet-webapi&quot;&gt;library of sample code for ADAL without using Angular&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The ADAL library provides a wrapper through which we can grab a &lt;a href=&quot;https://azure.microsoft.com/en-gb/documentation/articles/active-directory-authentication-scenarios/#web-browser-to-web-application&quot;&gt;bearer token&lt;/a&gt; to attach to any requests.&lt;/p&gt;
&lt;p&gt;I don&apos;t want to go into this in too much detail since there are two full libraries of code samples linked above, but we&apos;ll end up with something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;namespace Security {
  var authContext;

  export function init(config) {
    const authConfig = {
      instance: config.instance,
      tenant: config.tenant,
      clientId: config.clientId,
      postLogoutRedirectUri: window.location.origin
    };

    authContext = new AuthenticationContext(authConfig);

    const isCallback = authContext.isCallback(window.location.hash);
    authContext.handleWindowCallback();

    if (isCallback &amp;amp;&amp;amp; !authContext.getLoginError()) {
      window.location = authContext._getItem(authContext.CONSTANTS.STORAGE.LOGIN_REQUEST);
    }

    if (!authContext.getCachedUser()) {
      authContext.config.redirectUri = window.location.href;
      authContext.login();
      return false;
    }

    return true;
  }

  export function logOut() {
    if (authContext) {
      authContext.clearCache();
    }
  }

  export function acquireAuthToken() {
    var deferred = $.Deferred();
    authContext.acquireToken(authContext.config.clientId, (error, token) =&amp;gt; {
      if (token) {
        deferred.resolve(token);
      } else {
        deferred.reject(error);
      }
    });

    return deferred;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once have have something like that set up, we can initialise then get bearer tokens from our client. Then we can add that token into our AJAX request and pass through to our Users API (from above).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Auth config
if (!Security.init(config)) {
  // Stop app init if there is no user
  return;
}

Security.acquireAuthToken()
  .done((token) =&amp;gt; {
    const jqSettings = {
      url: &apos;/api/users/getusername&apos;,
      method: &apos;GET&apos;,
      beforeSend: (xhr) =&amp;gt; {
        xhr.setRequestHeader(&apos;Authorization&apos;, `Bearer ${token}`);
      }
    };

    $.ajax(jqSettings)
      .done((username) =&amp;gt; {
        console.log(&apos;username&apos;, username);
      });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So now that’s out of the way, how does that fit in with SignalR?&lt;/p&gt;
&lt;h2&gt;It’s Not Easy in SignalR&lt;/h2&gt;
&lt;p&gt;It’s not as simple to wrap up calls to a SinglaR hub to include a bearer token as it is with a raw AJAX request. But, we do have a simple method to append things to the query string.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$.connection.hub.qs = { key: value };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, we&apos;ll use that to append the current bearer token to the hub connection when we have one. With our helper code from above, this becomes as simple as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Security.acquireAuthToken().done((token) =&amp;gt; {
  $.connection.hub.qs = { access_token: token };
  $.connection.hub.start();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;(You may be concerned that the query string isn&apos;t a secure place to store the bearer token but, even using with an API call, the token is still stored unencrypted on the client side and sent with every request so this isn&apos;t much different. Bearer tokens are also transient so they&apos;ll be refreshed periodically.)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;We now need to show the Hub how to get the bearer token.&lt;/p&gt;
&lt;h3&gt;Setting Up The Hub With AD&lt;/h3&gt;
&lt;p&gt;As with the Web API code above, the set-up for using AD with SignalR comes from the &lt;a href=&quot;https://www.nuget.org/packages/Microsoft.Owin.Security.ActiveDirectory/&quot;&gt;Microsoft.Owin.Security.ActiveDirectory NuGet package&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.Map(&quot;/signalr&quot;, map =&amp;gt;
{
  map.UseWindowsAzureActiveDirectoryBearerAuthentication(new WindowsAzureActiveDirectoryBearerAuthenticationOptions
  {
    Provider = new QueryStringOAuthBearerProvider(),
    Tenant = AppSettings[&quot;ida:Tenant&quot;],
    TokenValidationParameters = new TokenValidationParameters
    {
      ValidAudience = AppSettings[&quot;ida:ClientId&quot;]
    }
  });

  map.RunSignalR(hubConfiguration);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is pretty much the same as for Web API, with the exception of &lt;strong&gt;line 5&lt;/strong&gt; specifying a custom bearer token provider. It’s this class that will pull the bearer token from the query string value we injected on the client, and insert it into the AD pipeline.&lt;/p&gt;
&lt;p&gt;There are a bunch of implementations of this around the web, but the version I ended up with is this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class QueryStringOAuthBearerProvider : OAuthBearerAuthenticationProvider
{
  public override Task RequestToken(OAuthRequestTokenContext context)
  {
    var value = context.Request.Query.Get(&quot;access_token&quot;);

    if (!string.IsNullOrEmpty(value))
    {
      context.Token = value;
    }

    return Task.FromResult&amp;amp;lt;object&amp;amp;gt;(null);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All this is doing is pulling the token from the request query string and inserting it into the context. The ADAL helpers then process it as normal.&lt;/p&gt;
&lt;h3&gt;Authorizing the Hub&lt;/h3&gt;
&lt;p&gt;Now that’s done, we can add an &lt;code&gt;Authorize&lt;/code&gt; attribute to our code to authenticate. The &lt;code&gt;ClaimsPrincipal&lt;/code&gt; for the authorised user is stored in the &lt;code&gt;Context.User&lt;/code&gt; property of the hub.&lt;/p&gt;
&lt;p&gt;So, using these together, we get:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Authorize]
public class UsersHub : Hub
{
  public async override Task OnConnected()
  {
    var user = this.Context.User as ClaimsPrincipal;
    Clients.All.newUserHasConnected(user.Identity.Name);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And everything else Hub-related should continue to work as normal.&lt;/p&gt;
</content:encoded></item><item><title>Get Your Web API Playing Nicely With SignalR on OWIN with Autofac</title><link>https://kwilson.io/blog/get-your-web-api-playing-nicely-with-signalr-on-owin-with-autofac/</link><guid isPermaLink="true">https://kwilson.io/blog/get-your-web-api-playing-nicely-with-signalr-on-owin-with-autofac/</guid><pubDate>Thu, 18 Feb 2016 17:02:40 GMT</pubDate><content:encoded>&lt;p&gt;Niche, right? Yes. But this has caused me a day of headaches so posting here to save anyone else from the pain.&lt;/p&gt;
&lt;h2&gt;WebAPI&lt;/h2&gt;
&lt;p&gt;Say we already have Autofac set-up with your WebAPI. So we have something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var builder = new ContainerBuilder();
builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
builder.RegisterAssemblyModules(Assembly.GetExecutingAssembly());
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// Register the Autofac middleware FIRST, then the Autofac Web API middleware,
// and finally the standard Web API middleware.
app.UseAutofacMiddleware(container);
app.UseAutofacWebApi(config);
app.UseWebApi(config);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is fine.&lt;/p&gt;
&lt;p&gt;If we then follow the &lt;a href=&quot;http://docs.autofac.org/en/latest/integration/signalr.html&quot;&gt;Autofac docs&lt;/a&gt;, we&apos;ll end up with something pretty similar for SignalR:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var builder = new ContainerBuilder(); // same as the one used for WebAPI
builder.RegisterHubs(Assembly.GetExecutingAssembly());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and then the Hub configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Get your HubConfiguration. In OWIN, you&apos;ll create one
// rather than using GlobalHost.
var config = new HubConfiguration();

// Register your SignalR hubs.
builder.RegisterHubs(Assembly.GetExecutingAssembly());

// Set the dependency resolver to be Autofac.
var container = builder.Build();
config.Resolver = new AutofacDependencyResolver(container);

// OWIN SIGNALR SETUP:

// Register the Autofac middleware FIRST, then the standard SignalR middleware.
app.UseAutofacMiddleware(container);
app.MapSignalR(&quot;/signalr&quot;, config);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run this and (if you just have a default WebAPI set-up) we&apos;ll probably get a 404 for our SignalR hub. So we need to get WebAPI to ignore anything relevant to SignalR.&lt;/p&gt;
&lt;h2&gt;Ignore Routes&lt;/h2&gt;
&lt;p&gt;Before we map our main routes, we can tell the router to ignore anything under the SignalR path:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Ignore anything to do with SignalR
config.Routes.IgnoreRoute(&quot;signalr&quot;, &quot;signalr/{*pathInfo}&quot;);

// Rest of the routes
config.Routes.MapHttpRoute(
    &quot;Default&quot;,
    &quot;{controller}/{action}/{*id}&quot;,
    new {id = RouteParameter.Optional});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, we&apos;re just telling the WebAPI router to ignore anything that starts with ‘&lt;em&gt;signalr&lt;/em&gt;‘. Now our hub paths should return properly.&lt;/p&gt;
&lt;p&gt;But what about the dependency config?&lt;/p&gt;
&lt;h2&gt;Path Matching Config&lt;/h2&gt;
&lt;p&gt;You might have noticed that we have two calls to &lt;code&gt;app.UseAutofacMiddleware(container)&lt;/code&gt; – which one do we use?&lt;/p&gt;
&lt;p&gt;Rather than configuring everything directly onto the &lt;code&gt;app&lt;/code&gt; object, we can use path matching to only inject the required Autofac dependency settings for SignalR.&lt;/p&gt;
&lt;p&gt;Using this syntax, we can set up our code with a scoped middleware call:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.Map(&quot;/signalr&quot;, map =&amp;gt;
{
    map.UseAutofacMiddleware(container);

    var hubConfiguration = new HubConfiguration
    {
        Resolver = new AutofacDependencyResolver(container),
    };

    map.RunSignalR(hubConfiguration);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(this replaces &lt;code&gt;app.MapSignalR&lt;/code&gt; from above)&lt;/p&gt;
&lt;p&gt;Note that we&apos;re setting the SignalR path explicitly as &lt;em&gt;/signalr&lt;/em&gt;. We could change this if we wanted, but we&apos;d need to also update our ignored routes above to match.&lt;/p&gt;
&lt;p&gt;With that done, we can inject whatever we need from our container. The &lt;a href=&quot;http://docs.autofac.org/en/latest/integration/signalr.html&quot;&gt;Autofac docs&lt;/a&gt; suggest injecting an &lt;code&gt;ILifetimeScope&lt;/code&gt; into the Hub constructor so child objects can be disposed of properly (we do &lt;strong&gt;not&lt;/strong&gt; want a memory leak) so it’s worth reading that if you haven&apos;t already.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MyHub : Hub
{
    private ILifetimeScope _hubLifetimeScope;

    public MyHub(ILifetimeScope scope)
    {
        _hubLifetimeScope = scope;
    }

    public void Ping(string text)
    {
        var clock = _hubLifetimeScope.Resolve&amp;amp;lt;IClock&amp;amp;gt;();
        Clients.Caller.Pong(clock.GetTime());
    }

    protected override void Dispose(bool disposing)
    {
        // Dispose the hub lifetime scope when the hub is disposed.
        if (disposing &amp;amp;&amp;amp; _hubLifetimeScope != null)
        {
            _hubLifetimeScope.Dispose();
        }

      base.Dispose(disposing);
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>An Email To Revolut Customer Services</title><link>https://kwilson.io/blog/an-email-to-revolut-customer-services/</link><guid isPermaLink="true">https://kwilson.io/blog/an-email-to-revolut-customer-services/</guid><pubDate>Mon, 28 Sep 2015 13:26:28 GMT</pubDate><content:encoded>&lt;p&gt;Hi,&lt;/p&gt;
&lt;p&gt;Just some background for context:&lt;/p&gt;
&lt;p&gt;My partner and I got Revolut cards to come to the US last week. We&apos;d both tried to use the cards online to verify they&apos;d work (to pay for the ESTA fees) and all seemed to be well.&lt;/p&gt;
&lt;p&gt;At the airport, I tried to use the card to pay for food and it failed due to an incorrect PIN. My partner tried hers and had the same issue. We&apos;d both changed PINs using the app so thought there might be some issue with that.&lt;/p&gt;
&lt;p&gt;I contacted customer services through the app and was told time and time again that my card was fine and we must be using the wrong PIN. By this time we were at the departure gate, so my partner called in to customer services while I continued to use text. The person she talked to had no ideas for what we could do so, worried that we&apos;d run out of money on holiday, she transferred the money back to her account, being told it would take 3-5 working days.&lt;/p&gt;
&lt;p&gt;After landing, I persisted with the text support and eventually was told that I needed to use the card in an ATM first after changing the PIN. Nowhere in the app does it warn you that this is required. So, that sorted, I now had a working card.&lt;/p&gt;
&lt;p&gt;Today was the 5th working day since my partner transferred the money to her account but there’s still no sign of it. She contacted customer services again and was told there had been a problem with the payment service and nothing had been processed till Thursday 24th and she should wait till 5 working days from then. She’s asked repeatedly that someone help but just keeps being told there’s nothing Revolut can do.&lt;/p&gt;
&lt;p&gt;We&apos;re in America and are running short on money because of your support staff’s continued misinformation and lack of help. This is ruining our holiday.&lt;/p&gt;
&lt;p&gt;Why wasn&apos;t the payment processed till the 24th? And why were we not told this?&lt;/p&gt;
&lt;p&gt;What can you do to help?&lt;/p&gt;
&lt;p&gt;K&lt;/p&gt;
</content:encoded></item><item><title>Switch Out Your Raygun API Key Depending on Web API Cloud Configuration</title><link>https://kwilson.io/blog/switch-out-your-raygun-api-key-depending-on-web-api-cloud-configuration/</link><guid isPermaLink="true">https://kwilson.io/blog/switch-out-your-raygun-api-key-depending-on-web-api-cloud-configuration/</guid><pubDate>Thu, 13 Aug 2015 09:00:17 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://raygun.io/&quot;&gt;Raygun&lt;/a&gt; is an excellent tool that lets you know about more errors in your app than you would ever dare to fear were present.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;no-errors.png&quot; alt=&quot;Error report showing no errors&quot; title=&quot;Obviously, my code has no errors...&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Configuration is easy: install the &lt;a href=&quot;https://nuget.org/packages/Mindscape.Raygun4Net.WebApi/&quot;&gt;NuGet package&lt;/a&gt;, add a couple of lines to your &lt;em&gt;web.config&lt;/em&gt; and you’re ready to go.&lt;/p&gt;
&lt;p&gt;When done, your config file will include:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;section name=&quot;RaygunSettings&quot;
         type=&quot;Mindscape.Raygun4Net.RaygunSettings, Mindscape.Raygun4Net&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;RaygunSettings apikey=&quot;YOUR_APP_API_KEY&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But, what if you want to install it within something like a &lt;a href=&quot;https://azure.microsoft.com/en-gb/documentation/articles/fundamentals-introduction-to-azure/&quot;&gt;Web or Worker Role on Azure&lt;/a&gt;? With these, you have &lt;em&gt;ServiceConfiguration.Local.cscfg&lt;/em&gt; and &lt;em&gt;ServiceConfiguration.Cloud.cscfg&lt;/em&gt; for configuration and they don’t support custom config sections.&lt;/p&gt;
&lt;h2&gt;App Registration&lt;/h2&gt;
&lt;p&gt;When you register the Raygun handler with your app, the default code looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var config = new HttpConfiguration();
RaygunWebApiClient.Attach(config);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This uses the API key from your config file to create a &lt;em&gt;RaygunWebApiClient&lt;/em&gt; to report on errors. But, as we already know, we don’t have a config section.&lt;/p&gt;
&lt;p&gt;To deal with this, the &lt;em&gt;RaygunWebApiClient.Attach&lt;/em&gt; method has an overload that lets us provide an instance of the client. So we can use this to generate a new client using our cloud settings.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var config = new HttpConfiguration();

// Get a reference to whatever we use to pull cloud config
var configManager = container.Resolve&amp;lt;IConfigurationManager&amp;gt;();

// Get the API key for this config
var raygunApiKey = configManager.GetSetting(&quot;Raygun_api_key&quot;);

// Overload attach with a new client
RaygunWebApiClient.Attach(config, () =&amp;gt; new RaygunWebApiClient(raygunApiKey));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, we’re pulling in a config manager instance to read out cloud settings (this example is using an &lt;a href=&quot;http://autofac.org&quot;&gt;Autofac container&lt;/a&gt;, but anything could be used) and then using the key value to construct a new instance (&lt;em&gt;line 10&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;Then, all we have to do is add the setting to our service definition file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ConfigurationSettings&amp;gt;
    &amp;lt;Setting name=&quot;Raygun_api_key&quot; /&amp;gt;
&amp;lt;/ConfigurationSettings&amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and then the correct values for Local&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ConfigurationSettings&amp;gt;
    &amp;lt;Setting name=&quot;Raygun_api_key&quot; value=&quot;my-local-api-key&quot; /&amp;gt;
&amp;lt;/ConfigurationSettings&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and Cloud&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ConfigurationSettings&amp;gt;
    &amp;lt;Setting name=&quot;Raygun_api_key&quot; value=&quot;my-cloud-api-key&quot; /&amp;gt;
&amp;lt;/ConfigurationSettings&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Extended Information&lt;/h2&gt;
&lt;p&gt;If we’re using something like &lt;a href=&quot;http://azure.microsoft.com/en-gb/services/active-directory/&quot;&gt;Azure AD&lt;/a&gt; to manage users, we can also add the user information to the constructor.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var config = new HttpConfiguration();

// Get a reference to whatever we use to pull cloud config
var configManager = container.Resolve&amp;lt;IConfigurationManager&amp;gt;();

// Get the API key for this config
var raygunApiKey = configManager.GetSetting(&quot;Raygun_api_key&quot;);

// Overload attach with a new client
RaygunWebApiClient.Attach(config, () =&amp;gt;
{
    var client = new RaygunWebApiClient(raygunApiKey);

    if (ClaimsPrincipal.Current != null)
    {
        client.UserInfo = new RaygunIdentifierMessage(ClaimsPrincipal.Current.GetUserId())
        {
            FullName = ClaimsPrincipal.Current.GetUserName(),
            Email = ClaimsPrincipal.Current.GetUserEmail(),
            IsAnonymous = !ClaimsPrincipal.Current.Identity.IsAuthenticated
        };
    }

    return client;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;ClaimsPrincipal Get methods (lines 16–19) are using extensions to pull through valid data using null checks; those methods don’t exist on the raw object.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;One important thing to note is that the constructor is called &lt;em&gt;every time&lt;/em&gt; an instance is required, so it’s best to keep things like config value lookups outside of the Attach method.&lt;/p&gt;
&lt;p&gt;Now we can look on in horror as the errors start flooding in.&lt;/p&gt;
</content:encoded></item><item><title>Get Bluetooth Working on Windows 10 on Mac Book Pro</title><link>https://kwilson.io/blog/get-bluetooth-working-on-windows-10-on-mac-book-pro/</link><guid isPermaLink="true">https://kwilson.io/blog/get-bluetooth-working-on-windows-10-on-mac-book-pro/</guid><pubDate>Wed, 05 Aug 2015 11:33:46 GMT</pubDate><content:encoded>&lt;p&gt;This worked for me; it might not work for you. Remember I&apos;m just same random guy on the internet so I take no responsibility for anything that happens to your machine. Okay?&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;p&gt;As yet (5th Aug 2015) there’s no official support from Apple for Windows 10 running through Boot Camp. So, when I upgraded from Windows 8.1 to 10, I was expecting there to be a couple of issues. One of these was a lack of Bluetooth. &lt;a href=&quot;/blog/get-the-fn-key-working-on-windows-10-on-mac-book-pro/&quot;&gt;Also getting the fn key working&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;But, it turns out to be easy to fix.&lt;/p&gt;
&lt;h2&gt;Get the Driver&lt;/h2&gt;
&lt;p&gt;The driver for Windows 8.1 works with 10, so you just need to grab that.&lt;/p&gt;
&lt;p&gt;Go to &lt;a&gt;https://support.apple.com/en-us/HT204048&lt;/a&gt; and find your machine in the list. You want to look for the zip file download of the Boot Camp Assistant software.&lt;/p&gt;
&lt;p&gt;For me, it was this one:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;1.png&quot; alt=&quot;MacBook Pro  (Retina, 15-inch, Early 2013) - Windows 64-but&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Download that file (it’s pretty big – 882Mb for my one) and unzip that once done.&lt;/p&gt;
&lt;h2&gt;Install the Driver&lt;/h2&gt;
&lt;p&gt;Inside the unpacked folder, go into the &lt;em&gt;BootCamp&lt;/em&gt; folder and then into the &lt;em&gt;$WinPEDriver$&lt;/em&gt; folder under that.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The driver needs to be installed via device manager so just confirm that the &lt;em&gt;AppleBluetoothBroadcom64&lt;/em&gt; folder is there for now.&lt;/p&gt;
&lt;p&gt;In Windows, open up device manager (the easiest way to do this is just hit the Windows key and type ‘device manager&apos; – it&apos;ll be the first result).&lt;/p&gt;
&lt;p&gt;Expand &lt;em&gt;other devices&lt;/em&gt; and you should see an unknown device.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Right click it and select &lt;em&gt;Update driver software&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Chose the second option on the pop up to browse your computer for the driver software.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;7.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Browse to the &lt;em&gt;$WinPEDriver$&lt;/em&gt; from your downloads and select that. Make sure &lt;em&gt;Include subfolders&lt;/em&gt; is checked and then click &lt;em&gt;Next&lt;/em&gt; and Windows will install the Bluetooth driver for your Mac Book.&lt;/p&gt;
</content:encoded></item><item><title>Get the FN Key Working on Windows 10 on Mac Book Pro</title><link>https://kwilson.io/blog/get-the-fn-key-working-on-windows-10-on-mac-book-pro/</link><guid isPermaLink="true">https://kwilson.io/blog/get-the-fn-key-working-on-windows-10-on-mac-book-pro/</guid><pubDate>Wed, 05 Aug 2015 11:11:45 GMT</pubDate><content:encoded>&lt;p&gt;This worked for me; it might not work for you. Remember I&apos;m just same random guy on the internet so I take no responsibility for anything that happens to your machine. Okay?&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;p&gt;As yet (5th Aug 2015) there’s no official support from Apple for Windows 10 running through Boot Camp. So, when I upgraded from Windows 8.1 to 10, I was expecting there to be a couple of issues. The main one for me was that the &lt;em&gt;fn&lt;/em&gt; key wasn&apos;t working.&lt;/p&gt;
&lt;p&gt;But, it turns out to be easy to fix.&lt;/p&gt;
&lt;h2&gt;Get the Driver&lt;/h2&gt;
&lt;p&gt;The driver for Windows 8.1 works with 10, so you just need to grab that.&lt;/p&gt;
&lt;p&gt;Go to &lt;a href=&quot;https://support.apple.com/en-us/HT204048&quot;&gt;https://support.apple.com/en-us/HT204048&lt;/a&gt; and find your machine in the list. You want to look for the zip file download of the Boot Camp Assistant software.&lt;/p&gt;
&lt;p&gt;For me, it was this one:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;1.png&quot; alt=&quot;MacBook Pro  (Retina, 15-inch, Early 2013) - Windows 64-but&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Download that file (it’s pretty big – 882Mb for my one) and unzip that once done.&lt;/p&gt;
&lt;h2&gt;Install the Driver&lt;/h2&gt;
&lt;p&gt;Inside the unpacked folder, go into the &lt;em&gt;BootCamp&lt;/em&gt; folder and then into the &lt;em&gt;Drivers&lt;/em&gt; folder under that.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The keyboard is an Apple device, so go into the corresponding folder and you&apos;ll find &lt;em&gt;AppleKeyboardInstaller64.exe&lt;/em&gt;. Run this installer and you get updated drivers and a working &lt;em&gt;fn&lt;/em&gt; key.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Mushroom, Whisky &amp; Mustard Steak Sauce</title><link>https://kwilson.io/blog/mushroom-whisky-mustard-steak-sauce/</link><guid isPermaLink="true">https://kwilson.io/blog/mushroom-whisky-mustard-steak-sauce/</guid><pubDate>Sun, 12 Jul 2015 11:11:18 GMT</pubDate><content:encoded>&lt;h2&gt;Ingredients&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;6 button mushrooms, sliced&lt;/li&gt;
&lt;li&gt;1 clove garlic, crushed&lt;/li&gt;
&lt;li&gt;1tbsp olive oil&lt;/li&gt;
&lt;li&gt;2tbsp whisky&lt;/li&gt;
&lt;li&gt;2tsp wholegrain mustard&lt;/li&gt;
&lt;li&gt;120ml/8tbsp double cream&lt;/li&gt;
&lt;li&gt;salt and pepper&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Method&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Heat the oil over a high heat then fry the sliced mushrooms for about 5 minutes or until golden.&lt;/li&gt;
&lt;li&gt;Reduce the heat and add the crushed garlic, stirring all the time. Fry for a minute but be careful not to burn it.&lt;/li&gt;
&lt;li&gt;Add the whisky and cook until most of it has evaporated.&lt;/li&gt;
&lt;li&gt;Stir in the cream, then the mustard and simmer for 3-4 minutes until it&apos;s reduced enough to coat the steak.&lt;/li&gt;
&lt;li&gt;Taste and season.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Enjoy.&lt;/p&gt;
</content:encoded></item></channel></rss>