diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 000000000..e69de29bb diff --git a/2009/05/anonymous-methods-when-invoking-in-vb.html b/2009/05/anonymous-methods-when-invoking-in-vb.html new file mode 100644 index 000000000..b70d6d0fb --- /dev/null +++ b/2009/05/anonymous-methods-when-invoking-in-vb.html @@ -0,0 +1,319 @@ + + + + + + + + + + + + + + + + + + + + + + Anonymous methods when invoking in VB net + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Thu May 28 2009
+

Anonymous methods when invoking in VB net

+
+
+ + +
+
+ +
+

Well maybe not but you can get close in some circumstances.

+ + + +

I've got a situation where when a timer ticks I want to change the background colour of a textbox on a windows form. Since I don't need to pass in any parameters if I was using c# I could use Control.Invoke and an anonymous method… especially since I know I'll always be accessing this control in this method from a different thread.

+ +

But VB .Net doesn't support anonymous methods. Now I've seen all kinds of verbose ways around this on the web… google it - I dare you.

+ +

But if you use Action as below you're pretty close to hardly any extra code…

+ +
Public Sub removeHighlight() Handles timer.Elapsed
+    timer.Stop()
+	If txtSingleCheck.InvokeRequired Then
+		txtSingleCheck.Invoke(New Action(AddressOf removeHighlight))
+	Else
+		txtSingleCheck.BackColor = Color.White
+	End If
+End Sub
+
+ +

So long as the delegate or action you are calling has the same signature as the method you're calling it in then you call InvokeRequired on the control in question and if true you call a new action with the AddressOf the method you're in otherwise you do what you wanted to do but on the appropriate thread.

+ +

Bot as powerful as anonymous methods I'll grant you but in situations like this it isn't that far removed… is it?

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2009/10/bing-is-not-search-engine.html b/2009/10/bing-is-not-search-engine.html new file mode 100644 index 000000000..5cee07309 --- /dev/null +++ b/2009/10/bing-is-not-search-engine.html @@ -0,0 +1,353 @@ + + + + + + + + + + + + + + + + + + + + + + Bing is not a search engine + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Fri Oct 30 2009
+

Bing is not a search engine

+
+
+ +
+ +
+ + tag-icon + + bing + + + + + tag-icon + + rant + + + + + tag-icon + + search + + + + + tag-icon + + microsoft + + + +
+
+
+
+ +
+

Over the last two days I've been researching using Windows Deployment Services with BDD. I've got 4 workstations to build so I may as well investigate it right?

+ + + +

I am also updating my main machine from build 7100 of Win 7 Ultimate to the RTM so I have a Win XP box on my desk that I've built to give me continuity and a new Win 7 install.

+ +

Both of which had Bing as the default search engine out of the box.

+ +

So I thought I'd run with it. After all I'm searching for Microsoft technologies…

+ +

The first hit from the string "BDD 2007 download" in Google is:

+ +

http://www.microsoft.com/downloads/details.aspx?FamilyId=02A2605D-51E8-469F-BE4A-1DD2AF580502&displaylang=en

+ +

That used to be the download page for BDD 2007 on Microsoft's site.

+ +

The first hit from the same string on Bing.com is:

+ +

http://technet.microsoft.com/en-us/desktopdeployment/default.aspx

+ +

That's not the BDD 2007 page and I can't even see a BDD link on there.

+ +

Bad, bad, bad and bad. I think that's a reasonable search string when I want to download BDD 2007 and so does google whereas Bing seems to think I want to hit some generic front end and read lots of whitepapers…

+ +

That's why I think of Google as a productivity tool.

+ +
+ + +
+
+ + + + + + + + + + diff --git a/2009/10/quack-quack-says-duck.html b/2009/10/quack-quack-says-duck.html new file mode 100644 index 000000000..69f7636cc --- /dev/null +++ b/2009/10/quack-quack-says-duck.html @@ -0,0 +1,329 @@ + + + + + + + + + + + + + + + + + + + + + + Quack Quack says the Duck + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sun Oct 04 2009
+

Quack Quack says the Duck

+
+ +
+ +
+ +

I've released a piece of software that I made for my 18 month old daughter on Codeplex http://qqstd.codeplex.com/.

+ +

It's a small dotNet app for Windows Mobile that creates sound-image pairs by scanning a resource folder and then randomly displays one of the images. When the image is touched the sound associated with the image is played. + +I developed it to occupy my daughter and teach her animal noises but the app doesn't care what if finds so you could use pictures of family and friends and their names said out-loud; Vehicles and their engine noises or anything that enters your transom.

+ + + +

Drop images and .wav files into the \My Documents\qqstd\resources folder and restart the app.

+ +

So if you want to add a leopard:

+ +
    +
  • you'd add as many leopard pictures as you want named leopard1, leopard2, etc
  • +
  • you'd add as many leopard noise WAV files as you like named (yep you guessed it) leopard1, leopard2, etc
  • +
  • restart the app if it's already running
  • +
+ +

and you're good to go…

+ +

It should run on any WinMo 6.1 phone with compact dotNet 3.5 and a touchscreen. But feel free to file a bug report on codeplex if I'm wrong.

+ +

That said it's been tested by the toddler that managed to set my phone not to charge unless powered off which is three clicks deep in a system menu so I'm pretty confident that it'll withstand most baby-based screen bashing.

+ +

At the moment we spend a lot of time on Skype to our relatives in the far south west of the UK so I'm working on another baby-based screen bashing project that I'll release in a few weeks time (work allowing)

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2009/12/c-background-worker.html b/2009/12/c-background-worker.html new file mode 100644 index 000000000..855bfa0f1 --- /dev/null +++ b/2009/12/c-background-worker.html @@ -0,0 +1,340 @@ + + + + + + + + + + + + + + + + + + + + + + c# Background Worker + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Tue Dec 01 2009
+

c# Background Worker

+
+
+ + +
+
+ +
+

I've been meaning to get around to writing a good tutorial on c# background workers. Mainly because I use them to separate the GUI from all the heavy lifting and I always forget how to update things.

+ +

In case I never get around to it. This is about the clearest introduction I've ever found. Well worth a read…

+ +
+ + +
+
+ + + + + + + + + + diff --git a/2010/03/quack-quack-says-duck-now-with-added.html b/2010/03/quack-quack-says-duck-now-with-added.html new file mode 100644 index 000000000..7d848a338 --- /dev/null +++ b/2010/03/quack-quack-says-duck-now-with-added.html @@ -0,0 +1,367 @@ + + + + + + + + + + + + + + + + + + + + + + Quack Quack Says The Duck now with added threading + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Mon Mar 29 2010
+

Quack Quack Says The Duck now with added threading

+
+ +
+ +
+

In a previous post I advertised an application I'd made for WinMo to entertain my toddler.

+ + +

Having watched her play with it and having been reminded to K.I.S.S. I've fixed a bug that highlighted the difference in expectations between myself and a toddler.

+ + + +

No not the world-weary pessimism I'm practising instead when I tested and used the app I would click and then wait for something to happen… whereas my toddler would bash at the screen having got the link between doing so and stuff happening.

+ +

While the UI was blocking the OS would register all the clicks and then process them before updating the screen.

+ +

In the context of this application that meant that you could have a sheep on screen that was barking like a dog…. not teaching my kid the lessons I was hoping!

+ +

I moved the actual sound playing onto a background thread and set a boolean flag to try to control the click event

+ +
Private Sub picBox_Click(ByVal sender As Object, ByVal e As EventArgs) Handles picBox.Click
+    If Not isPlaying Then
+        isPlaying = True
+        Dim t As Threading.Thread = New Threading.Thread(New Threading.ThreadStart(AddressOf playSound))
+        t.Start()
+        t.Join()
+        'playSound has finished now so...
+        refreshScreen()
+    End If
+End Sub
+
+Private Sub playSound()
+    If currentObject.getSoundLocation() <> "" Then
+        currentObject.playSound()
+    End If
+End Sub
+
+Private Sub refreshScreen()
+    picBox.Image = Nothing
+    currentObject = thisCollectionOfThings.getNextObject()
+    If Not currentObject Is Nothing Then addImage()
+    playTimer.Enabled = True
+End Sub
+
+Private Sub playTimer_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles playTimer.Tick
+    playTimer.Enabled = False
+    isPlaying = False
+End Sub
+
+ +

The astute among you (pat yourselves on the back) will notice that there's also a timer in there… I found that if I hit the screen 5 times (for example) during the sound playing the last one or two clicks would be picked up and their audio played +before the image finally refreshed.

+ +

I guess the UI was blocking briefly as processing control passed from the spawned thread back to the UI thread.

+ +

So I added a short timer that is started by the refreshScreen method and which stops itself and resets the isPlaying flag on its first tick.

+ +

That might be a bit hacky and there might be a better way but since that seems to work I'm happy with it.

+ +

And now I have a slightly more toddler-proof toddler game.

+ +

At the time of writing you could view all of the source code and download an install cab at codeplex qqstd.codeplex.com but that site is unavailable now :( You can see it in the wayback machine

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2010/05/theres-more-in-them-that-hills.html b/2010/05/theres-more-in-them-that-hills.html new file mode 100644 index 000000000..73115dd9c --- /dev/null +++ b/2010/05/theres-more-in-them-that-hills.html @@ -0,0 +1,369 @@ + + + + + + + + + + + + + + + + + + + + + + There's more in them thar hills... + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sat May 08 2010
+

There's more in them thar hills...

+
+ +
+ +
+

…than the Factory pattern.

+ +

So anyway I learn about design patterns and begin to use the factory pattern. And much like many other people I settle into a world where there are no other patterns. All is comfortable and fluffy and instantiated from calling code much as it was in days gone by.

+ + + +

Then comes the day I need to handle the responses to a monthly mailing to over 70,000 email addresses and so I write this incredible code. Well maybe not incredible… what would be the right word - oh yeah "messy".

+ +

It all started out really nice and clean but then I realised I needed to handle a couple of more cases than I'd intended when I began… and lots and lots of mail servers have been configured to return non-standard responses to unsuccessful mailings which is great for a human but not so great for a piece of software trying to classify that response.

+ +

So time passes and I'm correctly responding to over 90% of the returns we get (all of which stops evil companies like Yahoo for blocklisting us because we're mailing to non-existent addresses) but my code has got really, really messy.

+ +

Really messy.

+ +

Oh, it's awful.

+ +

I decide to refactor but no matter what I think of I can't get a Factory to solve my problem. Yeah, yeah I know but if you're gonna have a hammer it might as well be shiny. Now, I could go ahead and invent my own solution but as far as I'm concerned writing software is about having to do less and that sounds like too much work.

+ +

A little thought later and I decide it's time to add the command pattern to my arsenal. After all, I'm categorising mail, potentially selecting from a database, potentially updating a database, potentially replying to or forwarding an email and then deleting that mail. Wrap that up and then bash out the various alternatives I need. Bazinga!

+ +

I also like to be sure about what I'm doing before I start. Well, sometimes… So I dig out Patterns in Java Volume 1 and do a little reading and what I saw was such a great idea I realised I had to do everything I could not to forget…

+ +
public abstract class AbstractCommand {
+	public final static CommandManager manager = new CommandManager();
+	public abstract thePointOfThisClass();
+}
+
+ +

Now this made me double take… You might be saying "So What?" but I said "What the what?".

+ +

See I write lots of code like this

+ +
public class MyClass { 
+	ThingManager thisThingManager = new ThingManager();
+	Thing thisThing = new Thing();
+	thisThingManager.doThisThingToThatOtherThing(thisThing);
+}
+
+ +

I dig separation of responsibility so I like to separate out a "manager" or "controller" class from the other classes who don't need the logic that it encapsulates.

+ +

But that pretty tightly couples everything together. If I manage to strip a lot of code out of something (as I did when I bought the excellent Outlook redemption library recently) then there's more to change.

+ +

Here all of the logic for the command is bound up within it even though CommandManager class is still separate. I like that and I hadn't realised you could do this kind of thing by declaring something as static… I like to find a nice little elegant bit of sugar like that.

+ +
public class MyClass{
+    Thing thisThing = new Thing();
+    thisThing.DoThatThingToThisThing();
+}
+
+ +

or even better with a little factory magic in the background

+ +
public class MyClass{
+	Thing.DoSomethingToThisThing(FactoryChoiceEnum.Choice);
+}
+
+ +

So we've got one line of code and with reasonable variable names it doesn't even need comments. For Example…

+ +
public class MyClass {
+	currentMailItem.DealWithThisMailItem(MailResponseEnum.Unsubscribe);
+}
+
+ +

I hope that you've stumbled on this post and it solves your problem.

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2010/08/odd-odd-odd-login-behaviour.html b/2010/08/odd-odd-odd-login-behaviour.html new file mode 100644 index 000000000..ae1577d14 --- /dev/null +++ b/2010/08/odd-odd-odd-login-behaviour.html @@ -0,0 +1,328 @@ + + + + + + + + + + + + + + + + + + + + + + Odd, odd, odd login behaviour + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sat Aug 07 2010
+

Odd, odd, odd login behaviour

+
+ +
+ +
+

I've got a mini 5101. A little HP netbook that I lurve. It runs Windows 7 and Ubuntu 10.04 with aplomb.

+ + + +

My one gripe is that (much like my mac keyboard) the Function key functions are the main action of that key… so in Windows if you hit F5 to refresh a web page the laptop actually sleeps.

+ +

If you hit F3 to search in chrome instead you dim the screen. Annoying, no?

+ +

So I travel the dusty highways to the BIOS settings and there's an option to switch the function, erm, function. Some BIOS' refer to this as switching "media keys". I switch this to enable, boot up and my function keys are my own again.

+ +

All is well… + … + … + …

+ +

except… + +If you let the laptop sleep then when it wakes up it prompts for a password which it rejects as incorrect. Now I typed my password * V * E * R * Y * carefully but no joy.

+ +

I discovered if I hit switch user and chose the same user then the log in screen displayed the user status as "logged in" instead of "locked". Type the same password here and I can log in… What the what!?

+ +

I didn't immediately connect these changes… in my defence this isn't my main machine and I only use it sporadically.

+ +

I created a new user… no change.

+ +

Then I reinstalled Windows… no change.

+ +

I jumped into Google feet first and found almost nothing. Lots of forum posts where nothing is discovered and everyone has a slightly different problem which they describe vaguely. In my experience this generally points to a problem between the chair and the keyboard and so I sat and thought until I had tied the two together in my head.

+ +

As a test I let the laptop sleep, checked it rejected my password and then I held down the function key and typed the password. Voila I could log in.

+ +

Now I have to decide which behaviour is most annoying

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2010/10/refactor-fun.html b/2010/10/refactor-fun.html new file mode 100644 index 000000000..e42f89450 --- /dev/null +++ b/2010/10/refactor-fun.html @@ -0,0 +1,409 @@ + + + + + + + + + + + + + + + + + + + + + + Refactor ==fun + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Wed Oct 20 2010
+

Refactor ==fun

+
+ +
+ +
+

I've been using JetBrains Resharper for a while after a recommendation along the lines of "I can't stand to write code without it now" and…

+ +

I can't stand to write code without it now!

+ + + +

I've got a program that (in a moderately clunky way) gets all of the emails in a couple of mailboxes and checks to see if they are non-delivery reports, reports of address changes (which our customers consistently send in reply to newsletters), unsubscribe requests (despite a link in the mail) and so on…

+ + + +

The class that handled the matching of text against rules had grown to be a real behemoth if not actually a spaghetti monster it was at the minimum a noodle demon. I won't post the code here the internet isn't big enough!

+ +

But it consisted of an enum, five List<string> and then a set of methods that took an email object compared the body and subject to the 5 phrase lists and returned an appropriate result from the enum.

+ +

I realised that I didn't want a list per result…

+ +
_badAddresses.add("no user by that name");
+_outOfOffice.add("on my hols");
+
+ +

it was getting difficult to manage, there was no checking for duplication of the strings, there was no apparent way to keep the enum return and phrase list linked and all the looping was getting confusing.

+ +

So I went through two stages and Resharper helped by being awesome at supporting my laziness.

+ +

First I combined the many lists into one Dictionary<string, phrasecheckresult> to link my candidate strings with my enum result types.

+ +

I used a little of Notepad++'s Find and replace magic to wholesale convert my list initialisation into a Dictionary initialisation and ended up with

+ +
_phraseMap = new Dictionary<string, phrasecheckresult>
+{
+    {"554 qq sorry, no valid recipients}", PhraseCheckResult.BadAddress},
+    {"user doesn't have a yahoo.co.uk account", PhraseCheckResult.BadAddress},
+    {"account has been disabled or discontinued", PhraseCheckResult.BadAddress},
+    {"550 recipient", PhraseCheckResult.BadAddress},
+    {"is invalid", PhraseCheckResult.BadAddress},
+    {"user invalid", PhraseCheckResult.BadAddress}
+};
+
+ + + +

cut short for brevity as there are nearly 300 phrases now… Using an object initialiser meant I had nowhere to go when the program failed at runtime adding duplicate keys to the dictionary. Catching the exception didn't help since I couldn't see what key was duplicated to tidy up my code.

+ +

So I highlighted all the rows of initialisation and what did I see?

+ +

what did i see?

+ +

Resharper's context menu lets me switch the object initialiser out to a series of .Add() calls. I could quickly find the duplicates and then switch back to an object initialiser. Yay!

+ +

I should be writing unit tests but then that's always being put off to the next project and could I check if I've added a key already during an object initialisers run? I guess not but…

+ +

Second I wrote a couple of if braces that checked the subject and body and returned the appropriate results… up pops Resharper and suggests I can convert that to a Linq expression and I get the end result of…

+ +
public enum PhraseCheckResult
+{
+    None, BadAddress, ChangeAddress, OutOfOffice, Unsubscribe, Delete
+}
+
+private static Dictionary<string, PhraseCheckResult> _phraseMap;
+
+public PhraseCheckResult CheckItemAgainstLists(OutlookItem itemIn)
+{
+    return _phraseMap.SingleOrDefault(
+        i =>
+        itemIn.Subject.ToLower().Contains(i.Key.ToLower()) || itemIn.Body.ToLower().Contains(i.Key.ToLower())).
+        Value;
+}
+
+public ProcessPhraseList()
+{
+    _phraseMap = new Dictionary<string, PhraseCheckResult>
+    {
+         {"554 qq sorry, no valid recipients}", PhraseCheckResult.BadAddress},
+         {"user doesn't have a yahoo.co.uk account", PhraseCheckResult.BadAddress}
+    };
+}
+
+ +

A little shift around of the enum was necessary to put None as the first option. That way when the SingleOrDefault method doesn't find any of the candidate strings in the mail item the default action to take is to do nothing and a person can look at it. If you wanted to always delete unidentified messages you could shift Delete to be first in the enum and your program's behaviour would change. Bonza!

+ +
+ + +
+
+ + + + + + + + + + diff --git a/2011/04/ssh-without-password.html b/2011/04/ssh-without-password.html new file mode 100644 index 000000000..4b4e70a71 --- /dev/null +++ b/2011/04/ssh-without-password.html @@ -0,0 +1,396 @@ + + + + + + + + + + + + + + + + + + + + + + SSH without password + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Mon Apr 11 2011
+

SSH without password

+
+
+ +
+ +
+ + tag-icon + + linux + + + + + tag-icon + + windows + + + + + tag-icon + + git + + + + + tag-icon + + ssh + + + + + tag-icon + + learning + + + +
+
+
+
+ +
+ + +

I've resolved to learn more about linux and have been slowly boggling at how easy I find some tasks are in comparison to the MS world…

+ +

Recently I've been working on what was intended to be a small and straight-forward website that has rapidly grown to be a large behemoth that will take credit card payments.

+ + + +

So I need revision control.

+ +

Also, the site uses Drupal and Drupal use GIT for revision control. We're building a custom module and we'd like to contribute it back once it is done so we may as well use GIT now to make life easier.

+ +

Pretty exciting I can commit my changes and they are automatically pushed over to my test server on commit and then if I like them I can push them to my live server. Both pushes are by SSH and both times I have to type in a different, long, complex password.

+ +

Frustrating and inefficient for a god of business whose time is so important - no…

+ +

But the interwebs tell me that you can set up SSH so you don't need a password. They also tell me in a vaguely confusing manner… my resolve now is to add another vaguely confusing explanation to the interwebs.

+ +

confusing explanation

+ +

The task is to set my client.local machine to be able to SSH onto server.remote without any passwords changing hands.

+ +

This was relatively straight forward on my Mac and on my ubuntu box but my main dev machine is Windows 7…

+ +

As an aside switching from Mac, to VMWare fusion Windows, to VMWare fusion, to Windows 7 and remoting between them means I *never* know which key is going to be @ and which " and the windows machines get reset to US keyboard every so often by the Macs which throws a spanner in the works.

+ +

On a linux or unix machine this turned out to be pretty straight forward

+ +
    +
  1. Login to client.local
  2. +
  3. run ssh-keygen -t rsa
  4. +
  5. alter the path offered to rename the file sensibly in my case ~/.ssh/rsa_server.remote
  6. +
  7. ssh-copy-id -i ~/.ssh/rsa_server.remote.pub '-p 8901 dinglehopper@server.remote' +
      +
    • here note that I had to surround with single quotes the -p etc section of the command in order to use a non standard port
    • +
    +
  8. +
  9. +

    still on client.local edit ~/.ssh/config to add +

    + +
    +

    Host server.remote +IdentityFile ~/.ssh/rsa_server.remote

    +
    +
  10. +
  11. type ssh dinglehopper@server.remote -p 8901 and watch in awe and wonder + +Things aren't quite so straightforward on Windows but the basic steps remain.
  12. +
+ +

On Windows I use the excellent PuTTy to enable all things SSHy and I'm going to behave as if you do to…

+ +

First things first ssh onto server.remote as the user you want to use in future eg dinglehopper@server.remote and:

+ +

on your Windows clinet.local fire up puttygen.exe and hit generate. As a bit of fun you are asked to wiggle your mouse in order to provide randomness (I wonder if this is placebo)

+ +

putty key generator

+ +

Once this is generated you'll see a box marked "Public key for pasting into authorized_keys file". Can you guess what that's for?

+ +

So grab that text in your clipboard, fire up ssh and connect to server.remote as the user you want to log in as. +Then

+ +

run echo "YOURKEYHERE" ~/.ssh/authorized_keys

+ +

Now we need to configure PuTTy. So open PuTTy and either load a profile or start a new one. First we scroll down in the tree view to Connection > Data and put in the username we want to connect as…

+ +

putty config

+ +

Then you move to Connection > SSH > Auth and enter the private key file that puttygen created…

+ +

putty config

+ +

Now save this profile so you can fire up the connection in future and away you go…

+ +

Now my git push doesn't bother me for a password.

+ +

There are security concerns with passwordlessness so be mindful!

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2011/06/get-with-programming.html b/2011/06/get-with-programming.html new file mode 100644 index 000000000..6d231f844 --- /dev/null +++ b/2011/06/get-with-programming.html @@ -0,0 +1,352 @@ + + + + + + + + + + + + + + + + + + + + + + Get with the program(ming)! + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Fri Jun 10 2011
+

Get with the program(ming)!

+
+
+ +
+ +
+ + tag-icon + + design + + + + + tag-icon + + ux + + + +
+
+
+
+ +
+ + +

Twice recently I've hit the same problem with two different mobile phone vendor's websites. Vodafone (displayed here) and 3. When I type a phone number I split it into three sections using white-space. "nnnn nnn nnnn" that's how I remember numbers. That's not uncommon I don't think…

+ + + +

using a dash in an input

+ +

nor is it odd to use a dash.

+ +

So why do I need to learn how your website wants phone numbers formatted.

+ +

Whack some javascript on your page… you must be using it for something!

+ +

var correctedNumber = numberTypedOnForm.replace(" ","").replace("-","");

+ +

and with that massive development cost you aren't going to make someone type a number twice only to satisfy your database server. Yes, not everyone will have javascript turned on and it won't catch everyone's weird way of typing phone numbers

+ +
+

"(nnnn)-nn-nn-nn-n"

+
+ +

but it's about not introducing a pain point for customers when you don't have to

+ +

If you want to be really fancy you could

+ +

var correctedNumber = numberTypedOnForm.replace("/\D/g","");

+ +

Computers are supposed to make our lives easier but it's up to you website developers to help them help us.

+ +
+ + +
+
+ + + + + + + + + + diff --git a/2011/08/how-to-design-unsubscribe-link.html b/2011/08/how-to-design-unsubscribe-link.html new file mode 100644 index 000000000..673519e7f --- /dev/null +++ b/2011/08/how-to-design-unsubscribe-link.html @@ -0,0 +1,368 @@ + + + + + + + + + + + + + + + + + + + + + + How to design an unsubscribe link?!? + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Fri Aug 05 2011
+

How to design an unsubscribe link?!?

+
+
+ +
+ +
+ + tag-icon + + GET + + + + + tag-icon + + idempotence + + + + + tag-icon + + detail + + + +
+
+
+
+ +
+

We send out mail to 70,000+ members of our organisation. In theory they know they're getting it cos they're advised when they join the organisation that we'll send the email… yes, I know that implicit opt-ins aren't best practice… I want to polish up our email unsubscribe flow since the amount of mail we send out is steadily climbing as we move from paper to email for more things.

+ + + +

So first idea… you click a link = you get unsubscribed…

+ +

http://unsubscribe.somewhere.co.uk/123435

+ +

where 12345 is your user id.

+ +

Except someone malicious could

+ +
 for(i=0;i<1000000000;i++)
+ {
+ 	$.get('http://unsubscribe.somewhere.co.uk/'+i);
+ }
+
+ +

and unsubscribe every member.

+ +

No, it isn't that likely since this is for a climbing organisation but, it's an avoidable risk!

+ +

Alright, we don't need to make it into

+ +

http://unsubscribe.somewhere.co.uk/{encrypted_something}

+ +

I think that would be overkill so let's

+ +

http://unsubscribe.somewhere.co.uk/email_address

+ +

That way although you could sit and guess the email addresses of members to unsubscribe them at least it is harder and the urls are readable

+ +

Except the HTTP RFC says that a GET request should be idempotent.

+ +

not sure if meme is appropriate or you should feel bad

+ +
+

"Idempotence is the property of certain operations in mathematics and computer science, that they can be applied multiple times without changing the result."

+
+ +

In short someone clicking a link can get information from the database but shouldn't update information.

+ +

The problem is that I think that is counter-intuitive. I know I don't click links hoping that the actions carried out are idempotent. I click a link expecting something to happen and if we confound a user's expectations then we get to do the same job at least one more time… and I'm lazy - so that isn't a solution for me

+ +

But what is the solution since people are not going to want to spend time reading the page. How do I make what someone sees work well?

+ +

I'm a google fanboy so what do they say

+ +
+

Unsubscribing</span>
A user must be able to unsubscribe from your mailing list through one of the following means:

+ +
    +
  1. A prominent link in the body of an email leading users to a page confirming his or her unsubscription (no input from the user, other than confirmation, should be required).
    2) By replying to your email with an unsubscribe request.
  2. +
+
+ +

So I think that we're going to shufty this all-around a bit.

+ +

Two types of mailings = two types of link

+ +

unsubscribe.somewhere.co.uk/areas/email@person.com
unsubscribe.somewhere.co.uk/monthly/email@person.com

+ +

When you hit the page you can click a big button to confirm the action (which ajax-ily updates your displayed state and we can track how many people hit the page without doing anything).

+ +

mockup of UI

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2011/09/unusbscribey-follow-up.html b/2011/09/unusbscribey-follow-up.html new file mode 100644 index 000000000..485ab5d01 --- /dev/null +++ b/2011/09/unusbscribey-follow-up.html @@ -0,0 +1,321 @@ + + + + + + + + + + + + + + + + + + + + + + An unusbscribey follow up + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Tue Sep 20 2011
+

An unusbscribey follow up

+
+
+ + +
+
+ +
+

So recently I blogged a bloggy thing here about unsubscribe links.

+ +

I know a lot of people are of the opinion that an unsubscribe link should unsubscribe you and require no further action and that the whole idempotency thing is software design flim-flam and I was tempted to agree until I was introduced to the concept of pre-fetching…

+ + + +

In short modern browsers and some email clients will try to speed up your experience by following links in the background so that when you click on a link it seems to launch lightening fast. Given the massive bandwidth lots of people have in this Buck Rogers-esque world we live in this is a "good thing". However, if you have recipients of emails with unsubscribe links that require no confirmation and those people are pre-fetching those links then they could be being unsubscribed without even knowing it.

+ +

This is a good example of why standards are worth following… an unsubscribe link made before the advent of pre-fetching that was idempotent on get doesn't need to worry when prefetching is invented because so long as the people implementing prefetching follow the standards too then your software will continue to work as expected.

+ +

As with lots of this stuff - it seems like more work now but it's always more work when it breaks!

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2012/01/setting-up-mvc3-website-using-built-in.html b/2012/01/setting-up-mvc3-website-using-built-in.html new file mode 100644 index 000000000..54a7aa150 --- /dev/null +++ b/2012/01/setting-up-mvc3-website-using-built-in.html @@ -0,0 +1,533 @@ + + + + + + + + + + + + + + + + + + + + + + Setting up an MVC3 website using built-in membership provider + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Thu Jan 12 2012
+

Setting up an MVC3 website using built-in membership provider

+
+ +
+ +
+

Oh wait… this is awful. AWRUCHKA. Right dry heaving done with.

+ +

It's a good job so few websites want to authenticate users and collect data on them otherwise we'd constantly have to write the same code ove… what's that? Oh my! Everyone is going through this.

+ + + +

Jesus no wonder people bang on about RoR. It makes this easier in comparison

+ +

Anyway - I'll forget how to do this before I have to do it again

+ +

So

+ +
    +
  • fire up a new MVC3 web application
  • +
  • Jump into nuget and Install-Package System.Web.Providers 
  • +
  • Sort out a connection string for SQL CE
  • +
  • Add a key to make sure the login link always points to LogOn
  • +
+ +

Now my web.config looks like this (edited out parts I haven't touched for something approximating brevity)

+ +
<?xml version="1.0" encoding="utf-8"?>
+<!--
+  For more information on how to configure your ASP.NET application, please visit
+  http://go.microsoft.com/fwlink/?LinkId=152368
+  -->
+<configuration>
+  <connectionStrings>
+    <add name="users" connectionString="Data Source=|DataDirectory|users.sdf;"
+      providerName="System.Data.SqlServerCe.4.0"/>
+    </connectionStrings>
+  <appSettings>
+    <add key="webpages:Version" value="1.0.0.0" />
+    <add key="ClientValidationEnabled" value="true" />
+    <add key="UnobtrusiveJavaScriptEnabled" value="true" />
+    <add key="loginUrl" value="~/Account/LogOn" />
+  </appSettings>
+  <system.web>
+    <authentication mode="Forms">
+      <forms loginUrl="~/Account/LogOn" timeout="2880" />
+    </authentication>
+    <membership defaultProvider="DefaultMembershipProvider">
+      <providers>
+        <clear />
+        <add name="DefaultMembershipProvider" type="System.Web.Providers.DefaultMembershipProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionStringName="users" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10" applicationName="/" />
+      </providers>
+    </membership>
+    <profile defaultProvider="DefaultProfileProvider">
+      <providers>
+        <clear />
+        <add name="DefaultProfileProvider" type="System.Web.Providers.DefaultProfileProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionStringName="users" applicationName="/" />
+      </providers>
+    </profile>
+    <roleManager enabled="false" defaultProvider="DefaultRoleProvider">
+      <providers>
+        <clear />
+        <add name="AspNetWindowsTokenRoleProvider" type="System.Web.Security.WindowsTokenRoleProvider" applicationName="/" />
+        <add name="DefaultRoleProvider" type="System.Web.Providers.DefaultRoleProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionStringName="users" applicationName="/" />
+      </providers>
+    </roleManager>
+    <sessionState mode="InProc" customProvider="DefaultSessionProvider">
+      <providers>
+        <add name="DefaultSessionProvider" type="System.Web.Providers.DefaultSessionStateProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionStringName="DefaultConnection" applicationName="/" />
+      </providers>
+    </sessionState>
+  </system.web>
+</configuration>
+
+ +

Now start a debug session for the web app. Click logon. Click Register. Fill in the form. Register. Click Logoff and stop the debug session in Visual Studio

+ +

You can see the new SQL CE database and have a look at the schema. The Memberships and Users tables have a new row. The new user.

+ +

ssms screenshots

+ +

ssms screenshots

+ +

ssms screenshots

+ +

ssms screenshots

+ +

Hurrah - all the information you'll ever need is collected.

+ +

What?! You want to know more than name and email. Now that's a turn up for the books.

+ +

It turns out you can store key-value pairs in the profiles table. I think that anyone that wrote ASP dot Net websites will be old-hand at this but I've never had to do that or this…

+ +

While you can do magic up a key-value pair whenever you feel the need to in your code it's probably better to use one of these new fangled Class thing-a-ma-bobs

+ +

+using System;
+using System.Web.Profile;
+
+namespace AcrHack.Models
+{
+    public class CustomProfile : ProfileBase
+    {
+        //magic string
+        public static string ADDRESS = "address";
+
+        public string Address
+        {
+            get { return this[ADDRESS] as String; }
+            set { this[ADDRESS] = value; }
+        }
+    }
+}
+
+ +

Now a quick edit to the web config above so that the providers opening tag becomes

+ +
<profile defaultprovider="DefaultProfileProvider" inherits="AcrHack.Models.CustomProfile"/>
+
+ +

which makes the Profile Provider aware of the new Profile class

+ +

Next step is to find the RegisterModel (this could be the CreateModel or some other model) and add an Address field

+ +
public class RegisterModel
+{
+    [Required]
+    [Display(Name = "User name")]
+    public string UserName { get; set; }
+
+    [Required]
+    [DataType(DataType.EmailAddress)]
+    [Display(Name = "Email address")]
+    public string Email { get; set; }
+
+    [Required]
+    [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
+    [DataType(DataType.Password)]
+    [Display(Name = "Password")]
+    public string Password { get; set; }
+
+    [DataType(DataType.Password)]
+    [Display(Name = "Confirm password")]
+    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
+    public string ConfirmPassword { get; set; }
+
+    //Added Address field
+    [Required]
+    public string Address { get; set; }
+}
+
+ +

and edit the Register method in the controller

+ +

+[HttpPost]
+public ActionResult Register(RegisterModel model)
+{
+    if (ModelState.IsValid)
+    {
+        // Attempt to register the user
+        MembershipCreateStatus createStatus;
+        Membership.CreateUser(model.UserName, model.Password, model.Email, null, null, true, null, out createStatus);
+
+        if (createStatus == MembershipCreateStatus.Success)
+        {
+            FormsAuthentication.SetAuthCookie(model.UserName, false);
+            //Changes here
+            //Create loads or creates a profile based on searching for username
+            var userProfile = ProfileBase.Create(model.UserName) as CustomProfile;
+            userProfile.Address = model.Address;
+            userProfile.Save();
+            //End of changes
+            return RedirectToAction("Index", "Home");
+        }
+        else
+        {
+            ModelState.AddModelError("", ErrorCodeToString(createStatus));
+        }
+    }
+
+    // If we got this far, something failed, redisplay form
+    return View(model);
+}
+
+ +

and finally edit the view to add an editor field for the new property. (I'll leave that as an exercise for the reader)

+ +

Now we can go back to the Register page

+ +

register page

+ +

Register and then have a look in the profile table.

+ +

profile table

+ +

Ta da!

+ +

So there's a mechanism for extending the default profile.

+ +

Honestly, it feels messy and since at this point if there's a need for any data access layer then since there'll be a link on user name or user id anyway it's likely a better idea to have the additional data in the DAL and fangle the authentication and user models together in a ViewModel.

+ + + +

Having gone away and checked some code committed on another project by the lovely OrangeTentacle that's what he's done. So having figured it out for myself I'll probably go and crib off that much tidier code

+ +

Additional Reading:

+ +

Simple.Web.Providers announcement

+ +
+ + +
+
+ + + + + + + + + + diff --git a/2012/02/is-there-really-just-ipad-market.html b/2012/02/is-there-really-just-ipad-market.html new file mode 100644 index 000000000..50b7c2aef --- /dev/null +++ b/2012/02/is-there-really-just-ipad-market.html @@ -0,0 +1,405 @@ + + + + + + + + + + + + + + + + + + + + + + Is there really just an iPad market? + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Tue Feb 07 2012
+

Is there really just an iPad market?

+
+
+ +
+ +
+ + tag-icon + + iOS + + + + + tag-icon + + android + + + +
+
+
+
+ +
+

Disclaimer: I use and love an iPad (1). I've got an iPhone, mac mini, a MBP and an iMac. But I'm not an out and out fanboy - I'm a windows admin and nascent C# developer. I try to use Linux where it fits and find more places it fits all the time. And I've been developing an Android application.

+ +

TL;DR The transformer prime is a beautiful computer but it might be true there's an iPad market and not a tablet market.

+ +

UPDATE

+ +

And then today Google release Chrome for Ice Cream Sandwich. BEST. TABLET. BROWSER. EVAR.

+ + + +

I am fickle and this is enough to sway me to an evens opinion between the two OSs. The keyboard keeps switching back to upper case But it is chrome beta

+ +

One brill feature is when the browser isn't certain what link you meant to click

+ +

link zoom

+ +

A pop-up is launched to give you a larger target. As always with Chrome impeccable attention to detail

+ +
+

I wanted to make a "proper", "empirical" comparison between Android and iOS. So I got a Transformer Prime.

+ +

Actually this is anecdotal and written at 5am <= YMMV

+ +

The prime is more powerful than the sum of the capabilities of all computers I was within a mile of before my twelfth birthday.

+ +

As such I ran it through a full suite of tests

+ +
  1. Watching parrots talking on YouTube with my kids
    1. Result: beautiful screen, speakers have a tendency to buzz, youngest thinks parrot and pirate are synonyms
  2. Reading using Kindle
    1. Result: The wide aspect screen makes a great portrait reader. The screen really is beautiful
  3. Typing stuff into stuff
    1. Android autocorrect isn't as good as iOS. iOS correct is so good that people stop checking it and so you get "damn you autocorrect". With Android I have to break out of my flow to pay attention to what it is suggesting.
  4. Sending emails
    1. Why can't I edit the body of the mail I'm replying to when in HTML?
    2. Do I keep missing the space bar because of the task bar at the bottom of the screen
      1. spoiler: yes
+ +

A) iOS is a consumer operating system in a way that Android isn't

+ + + +

The first time I was handed an iPad I fell in love. I immediately grokked how to use it. I bought one. My kids use it (18 months and 4 year old). They've tried but they haven't broken it. I've seen children and adults with little or no experience of computers pick iOS up super-fast.

+ + + +

I handed the Transformer to my 18 month old and with two-screen presses she had turned off wifi. This device isn't kid proof in the way that an iPad is. My eldest when she under three taught my Dad how to use YouTube on iOS.

+ +

But a lot of this ease of use isn't specific to iOS. Touch-screen visual metaphors more closely mimic how we interact with the physical world - so a touchscreen OS doesn't need explanation in the way that a traditional desktop OS does. And neither Apple nor Google invented touch-screen gestures. Although that's not say they haven't patented them.

+ +

Second) visual metaphors are really important

+ +

The first time I picked up an Android device I had to have it explained to me so I could navigate. At that point in time iOS won out with it's one big button approach.

+ +

But that was Honeycomb and in ICS Google (or whichever genius did it) have sorted the problem I had…

+ +

android UI

+ +

In Honeycomb my eyes didn't immediately get that metaphor. I read it as Left, Up, Windows. Not  as; Back, Home, "Windows" i.e. multi-tasking. So I had a tiny barrier to using the system. And in the MTV-diseased, Radio-1-attention-span world a tiny usability barrier is actually a big usability barrier (you know what I mean - don't confound user's expectations)

+ +

I wanted to change application and my brain only knew how to do that in iOS mode. 

+ +
    +
  1. Go back to the desktop 2) pick an application 3)???? 4) Profit < meme apology />
  2. +
+ +

In ICS the home icon is much, much, much, much clearer. So the barrier to understanding of a new user and specifically to an iOS user is lower.

+ +

But once I was over that initial hump I do like the back button. Although it can take a second to figure out where it will take you. I've found three use-cases

+ +
  • Horizontally through an applications activities
  • Vertically between applications
  • In the browser it appears to function as a traditional back button and only jumps out of the browser back to the previous app when it hits the earliest page in the current tab's history <= I might be wrong here since I do still find it confusing
+ +

Using a tablet or phone you are interrupted by tweets, emails and the like and I do enjoy that in Android I can jump out of what I'm doing; check out the picture of a cat smoking a pipe and then quickly return to my previous task.

+ +

So in my scientific and empirical appraisal of ease-of-use iOS wins. The first iPad2 I bought for the organisation I work for I gave to a member of staff and said "You can have this for a week or two to try it out.". Two days later they came to work and said "Erm, I know you won't believe me but my laptop is broken can I keep the iPad?"

+ +

** IT departments want their staff constrained but enabled. **

+ +

You constrain them because you don't want them hacking away and inventing methods of doing things because that's when they delete all the files or map a share to some level of hell and release a demonic file-type that ruins your afternoon. On the flip-side constrain them too much and they will figure out the most convoluted and surprising mechanism for completing a task (almost always in Excel with VB macros) and then expect you to support the jawless hound they've created because (and here I'm stretching the metaphor): "My dogs tongue keeps getting muddy"

+ +

I'm really not sure which tablet OS (and yes there are only two players) has hit the right mix of constraining and enabling.

+ +

So guess what YMMV. Get the tablet that fits yours or your user's needs. There's not much between them - they're both usable. Why don't we all make web apps (spoiler: I don't know) If you already use and like Android and you want to buy a tablet - definitely get the transformer prime. 

+ +

I'm not really sold on the clip-on keyboard but then I tend to plan my daily typing amount in advance and if I'll go over an arbitrary amount of typing I take a laptop.

+ +

The transformer is a gorgeous piece of hardware. Now to hack iOS onto it… what's that you can't do that kind of thing with iOS…

+ + + +

Kids are awake. Blog post ends.

+ +
+ + +
+
+ + + + + + + + + + diff --git a/2012/07/y-u-no-sell-downloads-hollywood.html b/2012/07/y-u-no-sell-downloads-hollywood.html new file mode 100644 index 000000000..7e84170c7 --- /dev/null +++ b/2012/07/y-u-no-sell-downloads-hollywood.html @@ -0,0 +1,363 @@ + + + + + + + + + + + + + + + + + + + + + + Y U NO SELL DOWNLOADS HOLLYWOOD + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Wed Jul 25 2012
+

Y U NO SELL DOWNLOADS HOLLYWOOD

+
+
+ + +
+
+ +
+ + +

So it occurred to me that my kids might enjoy The Lion King (they like roaring). Our TV is really a computer and is hooked up to the internets allowing all kinds of iPlayer and similar streaming goodness.

+ +

I guess I'm not unusual in that when I want to find something I google it…

+ + + +

google listings

+ +

Notice anything about those results… yup, only one of them is legal (I guess). iTunes is in 6th place there. This means they're likely to only get about 4% of clicks on this result set.

+ +

likelihood of link being clicked based on page

+ +

This was taken from http://www.optify.net/inbound-marketing-resources/new-study-how-the-new-face-of-serps-has-altered-the-ctr-curve an Optify Study that is no longer available.

+ +

My aim here is to buy the film… not to pirate it. To buy it. Google gives me one option.

+ +

I get myself a stiff drink so I can wash away the taste afterwards and search using Bing (I refuse to use it as a verb) and it doesn't even have iTunes as an option…

+ +

bing results

+ +

Am I unusual in that I want to buy downloads of movies? Is it only my choice of search terms?

+ +

Or are the content owners getting it ass-backwards?

+ +

Surely if the content was available cheaply for single-use streams there would be at least hundreds of thousands of regular customers… do they not want that? Am I being naive? 

+ + + +

Do people still have no way to hook the internet up to their TV?! Oh god - that might be true! What an awful, weird idea. My kids have absolutely no idea what is going on when we visit someone and they can't choose what to watch or adverts come on.

+ +

I'd love to be able to get content through a Netflix (or some other subscription). I'd pay more in order to get access to a wider range of content.

+ +
+ + +
+
+ + + + + + + + + + diff --git a/2012/09/obligatory-ios6-maps-post.html b/2012/09/obligatory-ios6-maps-post.html new file mode 100644 index 000000000..d6b16c5a1 --- /dev/null +++ b/2012/09/obligatory-ios6-maps-post.html @@ -0,0 +1,333 @@ + + + + + + + + + + + + + + + + + + + + + + Obligatory iOS6 maps post + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sun Sep 23 2012
+

Obligatory iOS6 maps post

+
+
+ + +
+
+ +
+

For years now I've not bothered buying a satnav because maps on my iPhone has been good enough… sometimes a bit dodgy (once taking a route more fitted for a mountain bike) but generally serviceable.

+ +

Taking a trip from Manchester to Kettering this weekend with only my iPhone on iOS6 and the missus' on iOS5 was eye opening. Also, bleedin' awful… - 'drive around a roundabout twice in confusion' awful.

+ +

I really did give it a good go but this image sums up the difficulty faced using iOS6 maps.

+ + + +

map software screenshots

+ +

Above you can see the difference. Google's maps app on the left has natural features so you can navigate by looking at what is going on. It has contrast so tiny country roads are still visible and it has words on it so that you can… well… see what's going on.

+ +

Apple's maps app (on the right) let's me see I'm where I am… apparently a desert. And that I'm near the A14. Missing roads, very little detail and difficult to read.

+ +

On first sight I thought that iOS6 maps looked clean and fresh in comparison to Google maps and I bet a lot of people who don't really use it will never be disabused of that impression. Unfortunately it's clean and fresh because it doesn't have any stuff on it. And a map without stuff on it is a square.

+ +

This really is a dreadful setback to my one device dream.

+ +
+ + +
+
+ + + + + + + + + + diff --git a/2013/03/automagical-search-ux.html b/2013/03/automagical-search-ux.html new file mode 100644 index 000000000..a0b0a23d5 --- /dev/null +++ b/2013/03/automagical-search-ux.html @@ -0,0 +1,352 @@ + + + + + + + + + + + + + + + + + + + + + + Automagical search UX + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Wed Mar 20 2013
+

Automagical search UX

+
+
+ +
+ +
+ + tag-icon + + design + + + + + tag-icon + + analysis + + + + + tag-icon + + search + + + +
+
+
+
+ +
+

So I'm building a page in a mobile app to find "things".

+ +

Some assumptions:

+ +
    +
  • If you're using the app you are already familiar with the "things"
  • +
  • You've clicked "Find Things" and so you're expecting, as a minimum, to type something into a box (to tell the app what things you want to find)
  • +
  • You're a busy person and you don't want to have to think
  • +
+ + + +

Each thing has a name and a location. The one is, to some extent, meaningless without the other. What I want is that if you enter a name or a part of a name then you get a list of things whose names match. If you enter a place then you get a list of things sorted by distance from that place.

+ +

I'd like the search function to be as unobtrusive as possible and to my mind that means that the user shouldn't have to tell me whether they've entered a name or a place.

+ +

The problem I have is that sometimes the name of the thing is the name of a place. When you type in that text expecting to search in the context of it being a place I currently have no way of letting you override the context of it being the name of a "thing".

+ +

The question is do I catch only that scenario - as in this first set of mockups…

+ +

Mockup

+ +

I like this because the intention is pretty clear and the UI doesn't contain elements to muddy the intention unless we're already in a situation where we might need to make additional decisions.

+ +

But if there's a use-case or an incorrect result state that we haven't accounted for the user could find themselves stuck - I can't think of it but that doesn't mean that it doesn't exist.

+ +

So we could add a toggle that allows people to tell us what they want to do - as in this set…

+ +

Mockup

+ +

I worry that there's more to parse on this screen but also, I wonder if it makes the fact that you can search by address more discoverable.

+ +
+ + +
+
+ + + + + + + + + + diff --git a/2013/11/astronomical-database-identfier.html b/2013/11/astronomical-database-identfier.html new file mode 100644 index 000000000..ab85d3721 --- /dev/null +++ b/2013/11/astronomical-database-identfier.html @@ -0,0 +1,412 @@ + + + + + + + + + + + + + + + + + + + + + + Astronomical Database Identfier + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Fri Nov 22 2013
+

Astronomical Database Identfier

+
+ +
+ +
+

I dealt with an unusual requirement over the last few days. And wish I'd understood some of the more unusual ways that big numbers are handled in C#, Entity Framework, MS SQL and Oracle

+ + + +

That requirement came about in the development of an App that will act as an API for a bunch of sales data. The data is provided by another 3rd party exported from their Oracle database.

+ +

That data ultimately ends up in pretty graphs on an iPad.

+ +

When I received the first set of demo data I noticed both negative IDs and 9 digit ids.

+ +

This immediately made me worry about whether we had the right data formats (everyone worries about data formats, right?) and I asked the question.

+ +

It turns out that the DB schema that the data is ultimately sourced from has the ID column defined as NUMBER(38).

+ +

I've never really used Oracle so I had to dig around to discover that a NUMBER(38) field can hold 99999999999999999999999999999999999999x10125 as its maximum value

+ +

OK, that's a big number. Let's put it into context - the ESA estimates there are something like 1022 to 1024  stars in the known universe

+ +

These IDs are used in the MS SQL DB Schema that we're importing into so I can't ignore the possibility of an ID coming in with this massive value. So there're three distinct problems here…

+ +
    +
  1. How do I represent these numbers in .Net (C# 4.5 to be precise)
  2. +
  3. How do I have Entity Framework 6 map these potentially massive IDs
  4. +
  5. How do I represent these numbers in the schema
  6. +
+ +

Representing a Vigintillion in Dot Net

+ +

A quick journey to MSDN and we can see that if we restrict ourselves to integral types then we have int and long… In short one of those will hold a lot, lot less than NUMBER(38) and the other a lot less.

+ +

All is not lost. Since .Net 4 we have had access to BigInteger which allows for arbitrarily large numbers.

+ +

OK, so we can actually import the number into memory… that's a start

+ +

Using BigDecimal as an ID in EF6

+ +

Let's fire up an EF project, create an entity model with a BigInteger ID, and add a DbSet for that model to a DbContext:

+ +

huge numbers code

+ +

Having an integral type ID at this point and running Enable-Migrations from the console would work without complaint but with BigInteger as the Id an exception is thrown…

+ +
System.Data.Entity.ModelConfiguration.ModelValidationException: One or more validation errors were detected during model generation:
+
+HugeNumbers.Proton: : EntityType 'Proton' has no key defined.
+
+Define the key for this EntityType.
+
+Protons: EntityType: EntitySet 'Protons' is based on type 'Proton' that has no keys defined.
+
+ +

Adding the [Key] data attribute doesn't help.

+ +

How about fangling the ModelBuilder directly?

+ +

huge numbers more code

+ +

Progress! Kind of :

+ +
The property 'Id' cannot be used as a key property on the entity 'HugeNumbers.Proton' because the property type is not a valid key type. Only scalar types, string and byte[] are supported key types.
+
+ +

A negative result is still a result. So this is definitely progress! The scalar types in SQL include numeric which can hold 38 digits. Huzzah! And answers the question of how to represent the ID in the database.

+ +

So can we have a numeric ID in EF?!

+ +

So long as we can define a value type key we can have numeric in the DB. Ta da!

+ +

huge numbers even more code

+ +

We can enable migrations and then generate one:

+ +

huge numbers even more code

+ +

Wrong :

+ +

Decimal has the largest precision of the .Net value types and "only" offers 28-29 significant digits

+ +

Is this peculiar to Entity Framework?

+ +

The NHibernate docs say

+ +
+

Any integral property type is thus supported.

+
+ +

so unless there is some funkiness possible with NHibernate (which I've never used in anger) then I'm guessing they've made a similar design decision to the EF team. And it wouldn't be possible there either…

+ +

In conclusion

+ + + +

Entity Framework is not yet ready for storing an identifier for every proton in the universe and if you might want to be storing 38 digit identifiers (a phrase which I'm assured by my five-year old daughter actually kills int32.MaxValue fairies every time it is uttered) then you aren't going to be using Entity Framework and I'd guess you aren't going to be having a good time.

+ +

And straight from the Magic Unicorns mouth

+ +
+ + +
+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2014/01/comparing-mongodb-and-tokumx.html b/2014/01/comparing-mongodb-and-tokumx.html new file mode 100644 index 000000000..a51076674 --- /dev/null +++ b/2014/01/comparing-mongodb-and-tokumx.html @@ -0,0 +1,380 @@ + + + + + + + + + + + + + + + + + + + + + + Comparing MongoDb and TokuMX + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Thu Jan 23 2014
+

Comparing MongoDb and TokuMX

+
+
+ + +
+
+ +
+

TokuMX is an

+ +
+

"open source, high-performance distribution of MongoDB".

+
+ +

On a current project we're using MongoDB and, as the system is likely to scale fairly heavily, worrying (primarily) about storage. So, I picked up a task to compare MongoDB and TokuMX.

+ + + +

My test machine was a MBP with an SSD and 16GB RAM (Hear me roar!). I created a Debian 7 VM using VMware Fusion with 2GB RAM and then cloned it so that I had two identical linux servers.

+ +

I installed MongoDB on one and TokuMX on the other.

+ +

A NodeJS script was used to repetitively insert 6000 records and then query over the data in a single collection while only one of the two servers was powered on. I didn't clear out the databases between runs although this didn't appear to impact on the results. The script used is available on GitHuband feedback on better tests or mechanism for performing them is welcome!

+ +

The tests were run using asynchronous queues with varying levels of concurrency in order to try and simulate a relatively realistic load.

+ +

Update 2021: The data gathered used to be found on Google Docs but the link is dead now. It must have been in my FootClicks google account :'( Sorry posterity

+ +

The first set of tests were run against a collection with no indexes set.

+ +

This first test showed that TokuMX query time was much better when searching on a non-indexed field.

+ +

Mongo DB vs Toku MX graph

+ +

Mongo DB vs Toku MX graph

+ +

This performance difference larger disappeared when querying an indexed property.

+ +

Mongo DB vs Toku MX graph

+ +

Mongo DB vs Toku MX graph

+ +

TokuMX was still slightly ahead and across all of these datasets was much less affected by the level of concurrency in use.

+ +

The real stand out difference here was looking at the amount of storage being used.

+ +

After the sets of tests against each server I ran du -shb /data/db to get the size of the entire database in bytes.

+ +

MongoDB was using 10303 bytes per record stored and TokuMX only 104 bytes per record stored.

+ +

These might not be the best measures to use or the best way to gather the data (and I'll gladly try other mechanisms) but on a first glance it appears there is a compelling case to consider using TokuMX over MongoDB

+ +
+ + +
+
+ + + + + + + + + + diff --git a/2014/02/websites-cms.html b/2014/02/websites-cms.html new file mode 100644 index 000000000..2df1568ea --- /dev/null +++ b/2014/02/websites-cms.html @@ -0,0 +1,468 @@ + + + + + + + + + + + + + + + + + + + + + + Websites != CMS Platform + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sat Feb 22 2014
+

Websites != CMS Platform

+
+
+ +
+ +
+ + tag-icon + + learning + + + + + tag-icon + + cms + + + + + tag-icon + + design + + + + + tag-icon + + web + + + + + tag-icon + + series + + + +
+
+
+
+ +
+

I was once complaining about having difficulty setting up a very slightly unusual feature in a Drupal site that was taking forever to achieve. The framework made so many assumptions about what I should do that it wouldn't let me do what I wanted to.

+ + + +

A freelancer commented that if he was quoting on a project that had a requirement that it use a given CMS he didn't quote any less than building from scratch. He had found it didn't make enough difference to the effort he'd spend…

+ +

This stuck with me and matches my experience so far. (yeah, yeah, confirmation bias. I know)

+ + + +

I spent this past week doing maintenance work on a Django website. The ceremony involved in the Django part has outweighed the time spent designing the new HTML and creating the new page significantly.

+ +

Some of that delay is that I'm new to Django (and Python), sure, but at points, even when I'd come to understand what Django wanted, I still had to spend time poking it with a stick before it would allow me to display HTML in a browser.

+ +

So?

+ +

My position is that a CMS can be overkill. They speed up the initial setup for a website but then can slow down subsequent features. I'd argue you can provide the features of most CMS with relatively little effort by embracing modularity and the capabilities of modern JS.

+ +

Edit/Addendum

+ +

As Dan points out in the comments Django != CMS either. I call out above that I'm not experienced with Django. I've worked with it twice. And both times Django had been used to build a CMS.

+ +

Importantly, both times I was able to deliver almost every necessary change by editing JS files.

+ +

I'm not saying that Django is bad per se (although I really didn't enjoy working with it). I'm not even saying that having a system to manage content on a website is bad - I can't be I'm suggesting building one!

+ +

Maybe that "heavy-weight" web frameworks may not be appropriate to build that system - on a large .Net project recently I'd argue most of the functionality the customer wanted was built with JS.

+ +

I'm primarily a .Net developer. I love C# - I think the language is powerful and expressive. I think MS are really pushing things with new language development. I grok how to build websites using it but I'm getting to the point where even my BFF language isn't necessarily my first choice.

+ +

Really all I'm saying is that I've discovered I heart JS for making web things because I've found it gets out of the way and lets me build things.

+ +

always be punning

+ +

The basic idea for this blog series had been bouncing around in my head for a while… and the recent work with Django was the kick I needed to actually bother to write it.

+ +

Never say never but sometimes say no

+ +

So I wondered if I really could build an editable website

+ +

Proof, in other words, if proof be need be.

+ +

What is it?!

+ +

Wikipedia has a reasonable definition of a Web CMS (right now at least) as:

+ +
+

A web content management system (WCMS)1 is a software system that provides website authoring, collaboration, and administration tools designed to allow users with little knowledge of web programming languages or markup languages to create and manage website content with relative ease. A robust WCMS provides the foundation for collaboration, offering users the ability to manage documents and output for multiple author editing and participation.

+
+ +
+

Most systems use a content repository or a database to store page content, metadata, and other information assets that might be needed by the system.

+
+ +
+

A presentation layer (template engine) displays the content to website visitors based on a set of templates, which are sometimes XSLT files. Most systems use server side caching to improve performance. This works best when the WCMS is not changed often but visits happen regularly.

+
+ +

+ +
+

Administration is also typically done through browser-based interfaces, but some systems require the use of a fat client

+
+ +
+

A WCMS allows non-technical users to make changes to a website with little training. A WCMS typically requires a systems administrator and/or a web developer to set up and add features, but it is primarily a website maintenance tool for non-technical staff.

+
+ +

I'm not trying to build a CMS… something that could be packaged and distributed. I'm only interested in how long it would actually take me to build a web site that:

+ +
    +
  • Displays Web pages + +
  • +
  • Stores data + +
  • +
  • Edits web pages / Has an Admin section + +
  • +
  • Has templating + +
  • +
  • Allows more than one author + +
  • +
  • Has Server side caching +
      +
    • does anything not have server side caching these days?!
    • +
    +
  • +
  • Can be used by someone non-technical +
      +
    • totally subjective…
    • +
    +
  • +
+ +

(edited with links to the completed work)

+ +

So I'm going to imagineer a fake company called Omniclopse and build them a website from scratch. I'll try to provide what would be provided by a modern CMS and see how much effort that takes. And I'll blog about it as I go.

+ +

I may learn that it isn't quick to build those things (or that I'm not very good at them) but then a negative result is still a result…

+ +

I don't know what rate I'll manage to post at since I have one kid with a broken leg and one about to be born (and another not providing any more than the usual amount of rewarding distraction) but I'd like to practice using NodeJS, Mongo, and Angular. And to practice estimating my work before I begin.

+ +

Read the next post

+ +
+ + +
+
+ + + + + + + + + + diff --git a/2014/03/testing-with-browserstack-and-selenium.html b/2014/03/testing-with-browserstack-and-selenium.html new file mode 100644 index 000000000..08d1b732d --- /dev/null +++ b/2014/03/testing-with-browserstack-and-selenium.html @@ -0,0 +1,443 @@ + + + + + + + + + + + + + + + + + + + + + + Testing With Browserstack and Selenium + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Tue Mar 25 2014
+

Testing With Browserstack and Selenium

+
+
+ + +
+
+ +
+

This post is part of a series where I'm hoping to prove to myself that building a dynamic website with NodeJS is much more fun than using a CMS platform. See the first post for an explanation of why

+ +

Previous Post

+ +

Browserstack

+ +

I love Browserstack's awesome service. It allows you to test your websites on different browsers and operating systems. Helping reduce the need to have access to physical devices for testing and reproducing bugs.

+ +

Selenium WebDriver

+ +

BrowserStack allow automation using a Selenium web driver. You can access this with Python, Ruby, Java, C#, Perl, PHP, or Node.js. It is also possible to test publicly or locally available sites using BrowserStack.

+ + + +

However, after a couple of hours trying to write tests following the documentation and attacking Google I wasn't getting very far. I was able to run tests on Browserstack and take screenshots to prove the page was loaded but I couldn't assert against the page. Frustration had begun to build!

+ +

I haven't used Selenium before and I didn't grok how to assert against the page. I'm sure it was how I was reading the documentation but I wasn't moving forward. And then I discovered nightwatch (by reading to the end of the documentation but still…)

+ +

Nightwatch

+ +

Nightwatch is awesome! It only took a few minutes to get to the point where it was possible to run tests using it. The API is terse and expressive and it will output jUnit results so can be plugged into a CI pipeline.

+ +

A nightwatch test for the front page looks like:

+ +
module.exports = {
+  "Test the home page": function (browser) {
+    browser
+      .url("http://omniclopse-v0-1.herokuapp.com/")
+      .waitForElementVisible("body", 1000)
+      .assert.elementPresent("#homeCarousel")
+      //must have at least one image
+      .assert.elementPresent("#homeCarousel .item img")
+      .end();
+  },
+};
+
+ +

This demonstrates a very clear API. Load the page, wait till the body is visible, then assert that the carousel is present.

+ +

How to run the tests

+ +

Running this at the terminal using: +nightwatch -t end-to-end-tests/* -c end-to-end-tests/settings.json

+ +
{
+  "src_folders": ["./"],
+
+  "selenium": {
+    "start_process": false,
+    "host": "hub.browserstack.com",
+    "port": 80
+  },
+
+  "test_settings": {
+    "default": {
+      "launch_url": "http://hub.browserstack.com",
+      "selenium_port": 80,
+      "selenium_host": "hub.browserstack.com",
+      "silent": true,
+      "screenshots": {
+        "enabled": false,
+        "path": ""
+      },
+      "desiredCapabilities": {
+        "browserName": "firefox",
+        "javascriptEnabled": true,
+        "acceptSslCerts": true,
+        "browserstack.user": "username",
+        "browserstack.key": "password"
+      }
+    }
+  }
+}
+
+ +

Here the settings file sets the location of the tests folder(s), how and where to start Selenium and the capabilities of the browser to use for tests. Also my, fiendishly obfuscated, BrowserStack credentials

+ +

Passing in a settings file like this means that different browser settings can be setup and run separately. For example:

+ +
nightwatch -t end-to-end-tests/* -c end-to-end-tests/settingsWindowsFirefox.json
+nightwatch -t end-to-end-tests/* -c end-to-end-tests/settingsOSXFirefox.json
+nightwatch -t end-to-end-tests/* -c end-to-end-tests/settingsIPhone.json
+nightwatch -t end-to-end-tests/* -c end-to-end-tests/settingsAndroid.json
+
+ +

Which would allow running all of the nightwatch tests against different operating systems and browsers on BrowserStack.

+ +

Viewing results

+ +

Results from the tests are displayed in the console

+ +

Some more realistic tests for the home page

+ +

Switching out the test for carousel by id and instead testing by class (as this is less likely to change) and adding in some other tests for the page contents gives:

+ +
module.exports = {
+  "Test the home page": function (browser) {
+    browser
+      .url("http://omniclopse-v0-1.herokuapp.com/")
+      .waitForElementVisible("body", 1000)
+      .assert.elementPresent("header img#brand")
+      .assert.elementPresent("header .navbar")
+      .assert.elementPresent("header .navbar li a")
+      .assert.elementPresent(".carousel")
+      .assert.elementPresent(".carousel .item img")
+      .assert.elementPresent(".row.info")
+      .assert.elementPresent(".row.info .panel")
+      .end();
+  },
+};
+
+ +

TL;DR

+ +

The combination of BrowserStack and Nightwatch made for a fantastic experience. This is definitely going to be something I wrap into my day-to-day work.

+ +
+ + +
+
+ + + + + + + + + + diff --git a/2014/03/website-cms-display-pages-part-2.html b/2014/03/website-cms-display-pages-part-2.html new file mode 100644 index 000000000..fe68d9ba8 --- /dev/null +++ b/2014/03/website-cms-display-pages-part-2.html @@ -0,0 +1,471 @@ + + + + + + + + + + + + + + + + + + + + + + Website != CMS Platform - Displaying pages - part 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sun Mar 23 2014
+

Website != CMS Platform - Displaying pages - part 2

+
+
+ +
+ +
+ + tag-icon + + learning + + + + + tag-icon + + cms + + + + + tag-icon + + design + + + + + tag-icon + + web + + + + + tag-icon + + series + + + +
+
+
+
+ +
+

This post is part of a series where I'm hoping to prove to myself that building a dynamic website without a CMS is comparable to building one with a known CMS. See the first post for an explanation of why

+ +

Previous Post

+ + + +

In his awesome book, "Don't Make Me Think" (shameless affiliate link), Steve Krug drives home the message that time spent figuring out how your site is supposed to work is not time spent deciding to engage with your site. So, we're not going to do any ground-breaking design work for this company web page.

+ + + +

When people visit the site they should understand straight away how they're supposed to use it. An image search for 'company website' shows the same design over and over again - and, I expect, you'll be instantly familiar with it.

+ +

A logo, a navigation bar, a large carousel or image area, some content in columns below, and a footer.

+ +

A random image from Google images search for 'company website' (not an endorsement){: loading="lazy"}{:loading="lazy"}

+ +

There are relatively few company websites that step away from this basic design. And this site for (the hopefully fake) 'Omniclopse' isn't going to stray from this format.

+ +

Layout

+ +

The site is going to use Twitter Bootstrap for layout and custom styling will be written with SASS instead of directly as CSS.

+ +

Twitter Bootstrap because I'm familiar with it, I can expect others to be familiar with it, and while there is a risk that the site ends up looking like every other site built with Bootstrap the intention is specifically not to worry about breaking design records - the site should aim to use a visual language that the visitor already speaks.

+ +

SASS because it is so much nicer writing SASS than CSS.

+ +

I like my HTML templates to actually be HTML so when Express is setup the default Jade view engine will be removed and a Handlebars view engine will be used instead.

+ +

I haven't used a Handlebars view engine with Express before so I'll need to do a touch of Google-Fu to find one.

+ +

So!

+ +

To grab bootstrap and jQuery (which bootstrap depends on) I'll use Bower. If you're playing along you can download them directly (but that's no fun, right).

+ +

At the terminal: bower install bootstrap -Sa

+ +

Which downloads bootstrap into the project and adds the dependency to the Bower file.

+ +

bower screenshot

+ +

Bower, by default, adds everything into a bower_components directory so we tell Express about that in the Express app config:

+ +

app.use(express.static('/libs',__dirname + '/bower_components'));

+ +

View Engine

+ +

At the terminal: npm install express3-handlebars --save

+ +

installs the Express3 Handlebars view engine.

+ +

Add the view engine's config to the server.js file:

+ +
var express = require("express");
+var app = express();
+var exp3hbs = require("express3-handlebars");
+
+app.use("/libs", express.static(__dirname + "/bower_components"));
+app.engine("handlebars", exp3hbs({ defaultLayout: "main" }));
+app.set("view engine", "handlebars");
+
+app.get("/", function (req, res) {
+  res.render("home");
+});
+
+app.listen(1337);
+
+exports.app = app;
+
+ +

This requires two layout files be added to the site:

+ +

layouts file tree

+ +

Here main.handlebars is the default base layout and home.handlebars is rendered by the method that responds to the root route.

+ +

At this point what the site does hasn't changed how it does what it does so the single test (useless as it is) still passes.

+ +

Building out the Base Template

+ +

Starting to build out the page requires setup to use SASS.

+ +

Gulp

+ +

There is an express plugin that will transpile SASS files when CSS requests are served but, as I want to use Gulp for some linting and minification tasks later on, this is the time to plug Gulp into the project and set up a watch task to transpile SASS to CSS.

+ +

So at the terminal:

+ +
npm install --save-dev gulp
+
+npm install --save-dev gulp-sass
+
+ +

Then a little fangling to generate the gulp file.

+ +
var gulp = require("gulp");
+var sass = require("gulp-sass");
+
+gulp.task("sass", function () {
+  gulp.src("./scss/*.scss").pipe(sass()).pipe(gulp.dest("./public/css"));
+});
+
+gulp.task("watch", function () {
+  gulp.watch("./scss/*.scss", ["sass"]);
+});
+
+gulp.task("default", ["sass", "watch"]);
+
+ +

Typing gulp into the terminal now leaves a task running which watches for changes to .scss files and transpiles them to .css files

+ +

The Omniclopse Brand

+ +

For the imaginary company we're making a site for we can generate a colour palette

+ +

Yes, I am NOT a designer

+ +

The Page

+ +

and after a bit of fangling to build out the (admittedly ugly) page:

+ +

screenshot of the built page

+ +

The code for this page can be found tagged on github and at this point there's nothing groundbreaking (nor should there be). You can visit the site here on Heroku.

+ +

There are bits of the page HTML that I'm not happy with but that can be changed as the site work progresses.

+ +

The single test in the project still passes but that doesn't really prove anything. So the next post is going to be a short aside about using Selenium and Browserstack.

+ +
+ + +
+
+ + + + + + + + + + diff --git a/2014/03/websites-cms-displaying-pages.html b/2014/03/websites-cms-displaying-pages.html new file mode 100644 index 000000000..7bdd5e92b --- /dev/null +++ b/2014/03/websites-cms-displaying-pages.html @@ -0,0 +1,423 @@ + + + + + + + + + + + + + + + + + + + + + + Websites != CMS Platform - Displaying pages + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Mon Mar 17 2014
+

Websites != CMS Platform - Displaying pages

+
+
+ +
+ +
+ + tag-icon + + learning + + + + + tag-icon + + cms + + + + + tag-icon + + design + + + + + tag-icon + + web + + + + + tag-icon + + series + + + +
+
+
+
+ +
+

This post is part of a series where I'm hoping to prove to myself that building a dynamic website without a CMS is comparable to building one with a known CMS. See the first post for an explanation of why

+ +

Previous Post +Next Post

+ +

Setup

+ + + +

So, it's relatively easy to get an Hello World page displaying…

+ + + +

The steps I take when I'm setting up a new project look like:

+ +

project setup steps

+ +

I should probably start using Yeoman but I don't start enough projects to feel the need to automate this step of the setup.

+ +

I already know I want to use express for the server, that I want to test express using mocha, and to use the awesome supertest module, so I can run:

+ +
npm install --save express
+
+npm install --save-dev supertest
+
+npm install --save-dev mocha
+
+ +

Before I carry on I grab the Node .gitignore file

+ +
wget https://raw.github.com/github/gitignore/master/Node.gitignore
+
+mv Node.gitignore .gitignore
+
+ +

and can make an empty but initialised commit

+ +

The First Test

+ +
var request = require("supertest");
+
+var server = require("../server").app;
+
+describe("GET /", function () {
+  it("respond with html", function (done) {
+    request(server)
+      .get("/")
+
+      .set("Accept", "text/html")
+
+      .expect("Content-Type", /html/)
+
+      .expect(200, done);
+  });
+});
+
+ +

There's quite a lot going on there if you haven't used Mocha or Supertest then head off and read about them. How they work is out of the scope of this post. But what we're asserting here is that if you ask our server application for the root route then you get some HTML and HTTP status 200.

+ +

The simplest express server that makes this test pass is:

+ +
var app = require("express")();
+
+app.get("/", function (req, res) {
+  res.send("Hello World");
+});
+
+app.listen(1337);
+
+exports.app = app;
+
+ +

Running node server at the terminal I can point my browser at it:

+ +

screenshot of hello world response

+ +

All of which has us set up to test our server and ready to display something meaningful with very little work at all.

+ +

In the next part of the series we'll look at adding a basic template and making this look a little more like a real website

+ +
+ + +
+
+ + + + + + + + + + diff --git a/2014/04/a-dto-by-any-other-name-would-implement.html b/2014/04/a-dto-by-any-other-name-would-implement.html new file mode 100644 index 000000000..aab2c879c --- /dev/null +++ b/2014/04/a-dto-by-any-other-name-would-implement.html @@ -0,0 +1,354 @@ + + + + + + + + + + + + + + + + + + + + + + A DTO by any other name would implement ISweetSmellEquality + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Tue Apr 01 2014
+

A DTO by any other name would implement ISweetSmellEquality

+
+ +
+ +
+

I've been thinking about what people call the objects they pass around and whether they are the right names and why… and when… and I feel like the dog running behind the television to see where the onscreen dog went - on the verge of a paradigm shifting change in perspective but not quite getting it (and possibly a bit smelly)

+ + + +

DTO

+ +

The most common is DTO or Data Transfer Object. Fowler has a definition "An object that carries data between processes in order to reduce the number of method calls." He extends this clarifying it should be an object that can be serialised.

+ +

This out-of-date article from Microsoft also defines this as an object that is used to reduce the number of calls to a remote interface in a distributed system. I suppose Android's Intent are an example of serializable objects that communicate between processes without using the web - although I don't know enough Android to be sure about that.

+ +

However, in this MSDN article DTOs are defined specifically as objects with properties but no methods used to isolate presentation from the domain - what Fowler calls "localDTO".

+ +

LocalDTO i.e. using DTO to describe objects passed between layers of a single application is so common that Fowler has subsequently written to clarify: +

+
+

Some people argue for them as part of a Service Layer API because they ensure that service layer clients aren't dependent upon an underlying Domain Model. While that may be handy, I don't think it's worth the cost of all of that data mapping. As my contributor Randy Stafford says in P of EAA "Don't underestimate the cost of [using DTOs]…. It's significant, and it's painful - perhaps second only to the cost and pain of object-relational mapping".

+
+ +

A relatively brief online search suggests there are more definitions that describe a DTO as between remote processes as opposed to between layers of an application (here for example or here).

+ +

Of the ten hits for "Data Transfer Object" on Google right now eight agree with Fowler's definition, one is Fowler's Value Object page, and one is a J2EE definition for a transfer object which specifies that it can be used for transferring data between tiers - in PoEAA Fowler tells us that the Java community have since moved away from calling these classes Transfer Objects.

+ +

So it appears that while it is common to call objects passed between application tiers (at least in MS circles) DTOs it isn't technically correct but grew out of an out-of-date J2EE usage of DTO including in its definition moving data between tiers.

+ +

Domain Model

+ +

In the quote above local DTOs are used instead of passing Domain Models. Fowler defines a Domain Model as "An object model of the domain that incorporates both behaviour and data." In Patterns of Enterprise Application Architecture (shameless affiliate link) he expands and in describing a Domain Model says:

+ + +
+

As a result I see two styles of Domain Model in the field. A simple Domain Model looks very much like the database design with mostly one domain object for each database table. A rich Domain Model can look different from the database design, with inheritance, strategies, and other Gang of Four patterns, and complex webs of small interconnected objects.

+
+ +

Further Fowler describes the anemic domain model where the domain model objects have little or no behaviour. This anemic model seems to be a good fit for the local DTOs described above. The solution to this anti-pattern seems to be to have read Eric Evan's DDD (shameless affiliate link) and where to implement as rich a domain model as appropriate for the application being built.

+ +

Value Object

+ +

I have a tendency to call local DTOs "value objects" but using Evan's definition this isn't strictly true. I had missed that a value object isn't only about representing the value. It's more than that. Value objects should be immutable and any two value objects are only equal when their properties are equal. As such they don't map to the local DTOs described above.

+ +

However, I've been experimenting recently with passing structs around as immutable value objects when traversing layers (and at a colleague's suggestion have amended my R# auto property shortcut to create a private setter). I prefer these immutable objects as responses from queries into the domain but I haven't done any reading around whether that's a bad idea lots of people have already had.

+ +

In conclusion…

+ +

…it seems that I really need to read DDD and maybe that the job isn't to find the correct name for an object passed between tiers but to start passing the domain model and lose the "DTOs" entirely

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2016/yarn.html b/2016/yarn.html new file mode 100644 index 000000000..a55437ed7 --- /dev/null +++ b/2016/yarn.html @@ -0,0 +1,365 @@ + + + + + + + + + + + + + + + + + + + + + + Yarn! + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Tue Oct 18 2016
+

Yarn!

+
+
+ +
+ +
+ + tag-icon + + yarn + + + + + tag-icon + + npm + + + + + tag-icon + + js + + + + + tag-icon + + CI + + + +
+
+
+
+ +
+

Yarn is a new JS package manager that promises to be fast, secure, and reliable. My initial experience is that it is fast. I'm excited about making time to use it for real at work. Kudos to the developers!

+ + + +

Yarn description

+ +

It was announced by Facebook and has been worked on by real luminaries. There's real weight behind it!

+ +

Repeatable builds

+ +

Anyone that uses NPM has probably been hit by their build suddenly failing because a dependency of a dependency of a dependency has introduced a breaking change in a patch version update. To be fair to the JS community these issues tend to be fixed quickly but that's no use while it is broken. So the fact that Yarn includes npm shrinkwrap without me having to figure out how shrinkwrap works is a boon.

+ +

Fast builds

+ +

But the biggest reason I'm excited is that yarn has a cache of downloaded packages. Because people don't check node modules into source control and npm doesn't cache them we all download lodash and its friends over and over and over again. The build for the main project I work on at the moment spends 5 minutes downloading npm packages. I resent each of those 5 minutes. each. and. every. one.

+ +

How to convert an existing project

+ +

you type:

+ +
yarn
+
+ +

seriously that's it!

+ +

and you'll see something like this

+ +

Yarn run

+ +

There you can see that the first run for this project with few dependencies was 2.25s but subsequent runs are more like 0.75s

+ +

NPM is consistently around 4 seconds for the same project.

+ +

NPM run for the same project

+ +

If the difference was really only between 4 and 0.75 seconds I wouldn't be too excited (although not relying on other people's infrastructure to build and deploy is a big deal™)

+ +

But setting up a project with fifteen dependencies had a much more striking improvement. NPM took over a minute, yarn a little under 5 seconds.

+ +

yarn being awesome

+ +

I'm excited to get our build agents set up to see what yarn turns the five minute plus npm run at work into.

+ +

evolution

+ +

So yarn made a sensible decision. It doesn't reinvent the wheel, doesn't ask us to abandon things that work. But it promises to reduce impact of changing dependencies, reduce necessity of an external network when building software, and reduce the time spent building software. Those are really great things to improve.

+ +
+ + +
+
+ + + + + + + + + + diff --git a/2017/05/big-pile-of-soil.html b/2017/05/big-pile-of-soil.html new file mode 100644 index 000000000..2c725463e --- /dev/null +++ b/2017/05/big-pile-of-soil.html @@ -0,0 +1,349 @@ + + + + + + + + + + + + + + + + + + + + + + Big Pile of Soil + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sun May 14 2017
+

Big Pile of Soil

+
+ +
+ +
+

During Kevin Rutherford's guided discussion on clean code at Agile Manchester 2017 we talked briefly about whether there was a difference between 'cleaning code' and 'clean code'.

+ +

I suggested that I might expect to have to make code dirtier on the road to making it cleaner. Being of the opinion that sometimes you need to add duplication in order to see your way to removing it.

+ +

As I am a creature of bad habit I jumped immediately into tortuous metaphor.

+ + + +

Brace yourselves

+ +

If I'm concreting in a post in my garden there's a period of time where there's a big pile of soil. I have to get rid of that to finish the job but I can't do the job without making the pile.

+ +

The thing is…

+ +

…even though it occurred to me in the moment I actually quite like this metaphor. +

+
    +
  • The reason the mess is there is understood by almost anyone that sees it
  • +
  • The path to clearing the mess is understood by almost anyone that sees it
  • +
  • For each additional uncleared pile of soil next to a post it becomes more obviously important to think about whether it's time to start finishing the work by almost anyone that sees it
  • +
+ +

Also, this only holds for a task I can complete in a day or two. If we're laying a foundation then we should probably know where the soil is going and clear it as we go.

+ +

If you were to go into somebody's garden and there were tens of posts still with a pile of soil next to each. You'd be tempted to either help them clear up or sit them down and ask them why they hadn't. Either way it would be clear it was wrong and unfinished as it was.

+ +

Can you stretch the metaphor too far?

+ +

Easily :) but I'll try not to. It is common to use physical engineering, building, and DIY metaphors for software but they often fall down because the link between one block of code and another is nowhere near as viscerally clear as the link between a hole in the ground and a pile of soil.

+ +

This is where naming, patterns, conventions, context, and physical position can be used to communicate to the future developer. And where jumping in to version control history can let you see what other files changed when this file was created or amended into its present confusing state.

+ +

But what does it look like?

+ +

I started writing up an example and then remembered that the best possible example already exists!

+ +

Sandi Metz covered this wonderfully in this talk from RailsConf 2014. It's a little under 40 minutes and well worth your time with a worked and brilliantly explained solution to the Gilded Rose kata.

+ +

refactorings are small steps

+ +

The road to clean code is not paved with many-day blocks of work. Those aren't refactorings they're only redesigns (which are fine but should probably be infrequent). + +That means it can be OK to add something dirty on the path to something better because you know how and when it's going to be cleaned up.

+ +

Remember to look around the garden for yesterday's mess before you start a new job

+ + +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2017/05/ubictionary.html b/2017/05/ubictionary.html new file mode 100644 index 000000000..f6d0d55ff --- /dev/null +++ b/2017/05/ubictionary.html @@ -0,0 +1,359 @@ + + + + + + + + + + + + + + + + + + + + + + Ubictionary + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Wed May 10 2017
+

Ubictionary

+
+ +
+ +
+ + +

I've spent a great first day at Agile Manchester 2017. One of the slides at a talk from Anna Dick was the stand-out point of the day for me.

+ +

"Find a common language, don't rely on agile jargon"

+ + + + +

slide from of Anna Dick and Co-Op Digital

+ +

George Washington was the first to say that "the hardest problem in computer science is naming things and cache invalidation" (citation needed).

+ +

However, often where we do focus on getting a name right in the code (and it's really important that we do that) we don't focus on making sure that name comes from a language that everyone understands and uses.

+ +

As a trivial example don't call something ChangeJobTitle when your users are looking for Promote.

+ +

This reminded me of…

+ +

A recent job where I was at a startup working with maths wizards to try and track shoppers around physical retail stores using only their smartphones.

+ +

The shoppers' phones not the wizards'.

+ +

The maths wizards had a complex language and so did retailers and we wanted to make sure that the retailers and shoppers didn't have to care about or understand the maths wizards' language in order to use the system.

+ +

We spent time trying to track our language use even publishing an "ubictionary". A portmanteau of 'Dictionary' and 'Ubiquitous language'. It didn't always work but we felt we were doing ok.

+ +

As we were dealing with physical retail one of the names we struggled with was 'site' vs 'store'. The maths wizards didn't need to think about the outside world so they called the indoor shopping area the 'site'. Whereas we cared about the physical location of the 'store' which we called the 'site', using 'store' to mean the indoor shopping area.

+ +

Arguably more communication sooner could have avoided this confusion but we treated these as separate bounded contexts. So we documented the two usages and moved on pleased that we'd got the name right.

+ +

Another thing we struggled with was that we could only really talk to customer proxies so the language we were trying to capture didn't come to us first-hand. Several months after putting the site/store schism to bed we managed to arrange time with two retail contacts to pick their brains and I used the word 'site'.

+ +

They knew what I meant but there was a clear moment of friction as they had to translate in order to follow me. We dug into that and they then spent five or ten minutes discussing whether it was a 'plot' or a 'lot'.

+ +

Five minutes with an actual customer had invalidated one of our most basic uses of language.

+ +

That's why I was really pleased to see this talk call out not only that we should avoid jargon and find the right language but that it needs to be a common language.

+ +

In other words (pun intended)

+ +

If you aren't talking to your users and customers and you aren't absorbing how they think and talk about the things you're working on then you're putting up barriers to communication and usability that don't need to exist.

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2017/06/radarban.html b/2017/06/radarban.html new file mode 100644 index 000000000..7d81a7924 --- /dev/null +++ b/2017/06/radarban.html @@ -0,0 +1,441 @@ + + + + + + + + + + + + + + + + + + + + + + Where we're going we don't need columns + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sat Jun 24 2017
+

Where we're going we don't need columns

+
+ +
+ +
+

A few years ago while waiting for a user group to start at the Manchester ThoughtWorks office I bothered a couple of the devs there about their board. That conversation, after a bit of fangling, led to my convincing the team I was on at the time to use a radar board to represent our backlog.

+ +

It allowed us to combine a fluid representation of the business's priorities with a physical representation of the cost of reorganising those priorities. But also, in a way you don't get with a columnar board, gave an immediate feedback mechanism when too much work had been proposed or accepted.

+ +

Apologies to the two ThoughtWorks devs if I misrepresent any of their good ideas as mine or my bad ideas as theirs.

+ + + + +

The board was really simple. At the top right there was a small quarter circle labelled "now". Then a slightly larger one labelled "next". After that a slightly larger one labelled whatever you like to mean not-next-but-after-that. We experimented with a fourth quadrant but it covered ground so far away in time that it didn't add much distinction to the plan.

+ +

By chance I still have a photo I took as we drew an early version of the quadrants on the board.

+ +

Very early version of the board

+ +

You can see it's very straight-forward.

+ +

The radar board had the kanban board alongside it so you could see work getting closer to "now" until it was promoted onto the kanban board. We called it: radarban

+ +

A diagram showing the board

+ +

Several points here:

+ + + +
    +
  • I am well known for my incredible artistic skills and ability at writing clearly. As this diagram shows.
  • +
  • Bugs are a separate stream of work. You pull preferentially from that row running across the top of the board.
  • +
  • The radar doesn't have to be huge. We had room for hand-recorded metrics alongside.
  • +
  • The 'value' column is an obsession of mine. We didn't always have it. We didn't really get it to work. I think it's probably the most important thing most teams don't do.
  • +
+ +

What is it?!

+ +

Now

+ +

When there is capacity the team commits to work on something and moves it into 'Now'. For the team in question that's when we'd have the kick-off meeting and split it out into various tasks and stories (see below).

+ +

The quadrant represents in broad brushstrokes (what some would call 'epic' level) what the team is working on right, erm, now.

+ +

If someone has been out of the office they should be able to see at a glance what's happening.

+ +

Next

+ +

The 'Next' quadrant is whatever the business has prioritised to happen as soon as any current piece of work is signed off. Something getting into 'Next' is not a commitment to work on it.

+ +

Tracking cycle time let's you see when the tickets in Next should start.

+ +

Later / Maybe / Possibly / Probably

+ +

The third quadrant is weeks if not months out (all depends on your flow). And these tickets should be very vague because items here might never be worked on, or might change significantly before they start. If you're spending effort in this part of the board then something is wrong.

+ +

So far, so exactly like a backlog column, right?

+ +

What goes on it?

+ +

Everything goes on this board. Recruitment, holidays, business trips, when the new printer is arriving… (the list by virtue of being everything could keep going).

+ +

… …. Well, not everything, if nobody cares that a particular thing is on there stop putting it on.

+ +

Why a radar?

+ +

Physical feedback

+ +

The 'Now' quadrant is small. Those three tickets in the image above are pretty high-level and they're all that will fit in there. If somebody tried to add more work to that quadrant they'd have to overlap tickets, or crowd then together, turn them sideways… In other words they get immediate, visceral, physical feedback that they are overloading the team.

+ +

** So it would promote the conversation about the cost of a change to work in-flight or a higher load on the team. **

+ +

Expresses fluidity

+ + + +

It seemed that there was less mental barrier to reorganising proposed work when it was a set of concentric clouds of tickets than when it was a series of columns.

+ +

We had two. whole. whiteboards. of. backlog. at one point - guess how many people cared about the backlog when it was that big… In contrast it was a frequent sight to see the CEO and CTO stood by the radar reorganising the quadrants based on what had changed for them since it was last looked at.

+ +

Because there wasn't the implied (or explicit) priority of something being at the top of a column of tickets we avoided effort prioritising or discussing work until we were ready to move one or more of the tickets closer to 'now'. When we were ready to move a ticket we only moved whichever was best right now.

+ +

** So it would promote the conversation about what the priorities were. **

+ +

Not every idea is equal

+ +

Backlogs have a tendency to attract ideas that will never be worked on (for one reason or another). Limiting the amount of space for the backlog. And as a result limiting what can go in it forces you to maintain the backlog.

+ +

Keeping it incredibly low fidelity means people aren't invested in the idea as much and makes this process easier

+ +

** So it would promote the conversation about whether we need to or would ever work on an item **

+ +

It always works!

+ +

No, there are no golden bullets (or boards).

+ +

The context where I propose it helps is when business priorities are fluid, where they're not clear to everyone, or where the business doesn't feel the cost of changing work in-flight.

+ +

It's ok for needs and priorities to change but when they change frequently making sure everybody knows what is going on can be very difficult.

+ +

It's (sometimes) OK to drop everything because of emergencies or opportunities but it can also become the norm. It can be hard to notice that happening and to communicate the cost of that to the people asking for the changes. Walking over to the radar to say "we can move it in but we have to move something out" helps clarify this and let's everyone make a concrete decision.

+ +

Kick-off and split - as an aside

+ +

That 'kick-off and split' process was moving us towards a single-piece flow approach introduced to us by one of our colleagues Michael Dickens. There's an example here on Twitter where you can see in the centre the various tasks required to finish this piece of work stuck over a diagram of the moving pieces.

+ +

Or similarly described here by Kevin Rutherford where a diagram helps guide and frame the discussion of what work needs doing.

+ +

That should give you an idea of the scope of the tickets in now. Up to several pieces of work for several developers for several days.

+ +

At one point we did erase the kanban board, draw a complex process flow we needed to implement in its place, and stick post-its over that. It worked really well!

+ +

What do you absolutely need?

+ +

You need lots of space.

+ +

We had more than one whiteboard per person. So could afford three boards to describe our current and upcoming work. We were also colocated so we didn't need an electronic board (although we did have to have one and it was never up-to-date :/)

+ +

I've tried this recently with a different team. We have very little whiteboard space. We were trying to convey a lot of information in one place - it didn't work for everyone so we've moved away from it

+ +

All crammed into one board

+ +

It's a shame we couldn't make it work because even with a lack of space it was great for communicating what was coming towards the team, focussing on what we were working on, and promoting conversation about the things we were doing.

+ +

You need to talk to each other

+ +

Ultimately that's what this is about - if I haven't laboured that point enough.

+ +

This is a planning mechanism intended to force the right people to stand next to each other and agree about what is happening.

+ +

If you try this…

+ +

… I'd love to hear about it.

+ +

Tell me what worked and didn't, send me a picture, ask me questions.

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2017/07/retrosperiment.html b/2017/07/retrosperiment.html new file mode 100644 index 000000000..fcb5756f6 --- /dev/null +++ b/2017/07/retrosperiment.html @@ -0,0 +1,388 @@ + + + + + + + + + + + + + + + + + + + + + + Retrosperiment + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Thu Jul 06 2017
+

Retrosperiment

+
+
+ + +
+
+ +
+

(originally posted on the code computerlove blog. At the now unreachable link: https://lean.codecomputerlove.com/a-retrosperiment/)

+ +

Experimenting with a "new" retro format

+ +

For our team's most recent retro we decided to try a new format to see how it affected our discussion. We thought we'd share it here in case it has value for other teams.

+ +

What is it?

+ +

A retrospective is a practice from XP described on the c2.wiki as

+ +
+

A practice which has an XP team asking itself, at the end of each iteration : What went well ? +What could be improved ? +What could we experiment with ?

+
+ +

We've recently had several discussions trying to focus on the real and perceived progress of our work and thought it would be beneficial to run the retro with a focus on the impact of our team's principles and practices. Specifically how they relate to delivery of value and speed of delivery.

+ + + +

We first drew axes on a white-board

+ +

The axes of the retro graph

+ +

Since any and all software communication must take the form of some set of quadrants…

+ +

the quadrants this describes

+ +

Then the whole team wrote on post-it notes what they thought our principles and practices were and put them on the board.

+ +

The faster that practice helps us move the further right the post-it goes. The more value it lets us deliver the higher it goes.

+ +

So what did this look like in practice?

+ +

the post-its

+ +

This let us see straight away where we had different opinions across the team

+ +
    +
  • feature flags
  • +
  • estimates
  • +
  • road-maps
  • +
  • Slack / comms
  • +
+ +

And where, when we agreed roughly on position, we needed to focus on if we could speed-up or get more value

+ +

This was a very different discussion than we would usually have. Anchored more in what we could change and how we might change it than how we feel about things…

+ +

the discussion

+ +

It's still important to address the team's morale and individual concerns but this list of discussion points felt more focused, as we'd hoped, on what we can change to deliver more value faster.

+ +

Retroception

+ +

After a retrospective of the retrospective format the team members felt that it might have been useful to constrain the number of things we were allowed to put on the board. Or to dot vote on the items before discussing them to allow us more time to dig in to the discussion.

+ +

Why not try it and let me know if it works for you too?

+ +
+ + +
+
+ + + + + + + + + + diff --git a/2017/10/constructiphor.html b/2017/10/constructiphor.html new file mode 100644 index 000000000..758dc2034 --- /dev/null +++ b/2017/10/constructiphor.html @@ -0,0 +1,373 @@ + + + + + + + + + + + + + + + + + + + + + + Constructiphor + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sun Oct 15 2017
+

Constructiphor

+
+ +
+ +
+

On Twitter I…

+ +

…made a toot-storm about using construction as a metaphor for software engineering.

+ +
+

I've never really got on with construction metaphors for software. The cost of mistakes and rework is high in construction

+
+ +

the toot itself

+ +

This isn't saying that Software isn't putting things together but rather I've seen people justify not 'being agile' by using construction metaphors.

+ + + +

For example

+ +
+

we have to agree up front what we're going to do so that we know we're building the right thing… now go plan 19 sprints

+
+ +

(guess whether the client was certain they knew what they wanted)

+ +

So, why, do we have to agree up front what we're doing?

+ +

The builders pouring the foundations in the image in that toot were really careful they got things right before they started pouring concrete. If you pour that concrete and it's wrong, it's a big deal. Doing it twice would be an expensive problem. It isn't inconceivable that you make a mistake where it might be impossible to recover from the cost.

+ +

The bricklayers that built on top of the foundations couldn't start before the foundations were ready. Once they could start they went really, really slowly until the first few rows were in and true. After that it is amazing how fast they can add new rows of bricks.

+ +
    +
  • They have to work in series.
  • +
  • They have to be incredibly intolerant of mistakes
  • +
+ +

In short they have to agree up front what they're doing.

+ +

In comparison I could run infrastructure scripts to create complex utility computing environments, test the results, tear down the infrastructure, and repeat. All for the cost of the compute time. AWS recently started billing by the second so if that only takes minutes to run it's even cheaper than before.

+ + + +

I can reset the state of the software to just about any point in history to see what it was like. I can experiment with swingeing changes cheaply and without impacting other people's work.

+ +
    +
  • We don't have to work in series
  • +
  • We can be tolerant of mistakes
  • +
+ +

So, we don't have to agree up front what we're doing?

+ +

Also…

+ +

…I saw a few folks tweet that Allan Kelly makes the point that renting compute in the 70s cost the equivalent of 1.25 million dollars monthly but a similar amount of compute can now be bought for something more like $35.

+ +

In the context of the 70s pricing planning was cheap. But planning is now comparatively expensive.

+ +

seen at https://twitter.com/ojuncu/status/913688587576778752

+ +

That's all the confirmation bias I need :)

+ +

And so…

+ +

… in construction the cost of the work, or the cost of the work being wrong is higher than the cost of planning the work. Measure twice, cut once is still good advice.

+ +

That was true in Software but isn't anymore.

+ +

Because the cost of planning is comparatively expensive it is now the item to minimise. Software systems can be put together by taking many small, cheap, reversible steps. What Deng Xiaoping would have described as "crossing the river by feeling the stones."

+ +

If your software development still spends a high proportion of time planning then you need to be sure that is an unavoidable aspect of what you are doing and not a signal that you're falling into obsolescence.

+ +

For example if you're writing the software that determines whether to insert or remove the control rods in a nuclear reactor then, yes, you probably need to be very sure you know every edge case is handled correctly or successfully passed off to a human before it goes into production.

+ +

What can I do..?

+ +

For most software development now we need to be asking what the measurable outcome is, ensure we're measuring it, and start doing as fast as possible.

+ +

The harder work then is using user research to determine what direction to head in. Followed by more user research and the telemetry coming out of the application to stay on course or make appropriate corrections.

+ +

Cut once, Measure twice.

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2017/generating-static-amp.html b/2017/generating-static-amp.html new file mode 100644 index 000000000..fdee0ecff --- /dev/null +++ b/2017/generating-static-amp.html @@ -0,0 +1,700 @@ + + + + + + + + + + + + + + + + + + + + + + Generating static AMP pages + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Wed Mar 22 2017
+

Generating static AMP pages

+
+
+ +
+ +
+ + tag-icon + + series + + + + + tag-icon + + blog + + + + + tag-icon + + recursion + + + + + tag-icon + + AMP + + + + + tag-icon + + jekyll + + + +
+
+
+
+ +
+ + +

AMP or Accelerated Mobile Pages is a Google-backed project that allows you to use restricted HTML to delivery static content quickly. Since AMP HTML is restricted it isn't a fit for every site.

+ +

Since this blog is published as static HTML articles it is a good candidate for publishing an AMP version. An open source AMP jekyll plugin was amended to add AMP versions of pages.

+ +

The major discovery was that the validation tooling around AMP is awesome. Compare that to Facebook Instant Articles where there is almost no validation tooling (that I could discover at least)…

+ +

This didn't feel like a topic that justified several posts so to avoid taking too long this is a bit of a whistle-stop tour of adding AMP pages to this blog.

+ + + +

Jekyll Plugin

+ +

The basic idea is adapted from a Jekyll plugin on github.

+ +

There are several parts here:

+ +
    +
  • Adding an AMP layout to the site
  • +
  • Adding a 'generator' to the Jekyll module
  • +
  • Adding an 'amp_images' filter
  • +
  • Adding an 'amp_tweets' filter
  • +
+ +

This was a very manual process but not particularly onerous. Jekyll proved to be well-made for extension.

+ +

Adding an AMP layout

+ +

AMP has some required markup. So an amp-post.html was added to the _layouts folder.

+ +

+<!DOCTYPE html>
+<html
+  amp
+  lang="en"
+>
+  <head>
+    <meta charset="utf-8" />
+    <title>{{ page.title }}</title>
+    <link
+      rel="canonical"
+      href="{{ site.url }}{{ page.url }}"
+    />
+    <meta
+      name="viewport"
+      content="width=device-width,minimum-scale=1,initial-scale=1"
+    />
+    <link
+      href="https://fonts.googleapis.com/css?family=Khula"
+      rel="stylesheet"
+    />
+    <style amp-custom>
+      {% include syntax.css %}
+      {% capture include_to_scssify %}
+        {% include main.scss %}
+      {% endcapture %}
+      {{ include_to_scssify | scssify }}
+    </style>
+    <style amp-boilerplate>
+      body {
+        -webkit-animation: -amp-start 8s steps(1, end) 0s 1 normal both;
+        -moz-animation: -amp-start 8s steps(1, end) 0s 1 normal both;
+        -ms-animation: -amp-start 8s steps(1, end) 0s 1 normal both;
+        animation: -amp-start 8s steps(1, end) 0s 1 normal both;
+      }
+      @-webkit-keyframes -amp-start {
+        from {
+          visibility: hidden;
+        }
+        to {
+          visibility: visible;
+        }
+      }
+      @-moz-keyframes -amp-start {
+        from {
+          visibility: hidden;
+        }
+        to {
+          visibility: visible;
+        }
+      }
+      @-ms-keyframes -amp-start {
+        from {
+          visibility: hidden;
+        }
+        to {
+          visibility: visible;
+        }
+      }
+      @-o-keyframes -amp-start {
+        from {
+          visibility: hidden;
+        }
+        to {
+          visibility: visible;
+        }
+      }
+      @keyframes -amp-start {
+        from {
+          visibility: hidden;
+        }
+        to {
+          visibility: visible;
+        }
+      }
+    </style>
+    <noscript
+      ><style amp-boilerplate>
+        body {
+          -webkit-animation: none;
+          -moz-animation: none;
+          -ms-animation: none;
+          animation: none;
+        }
+      </style></noscript
+    >
+
+    {% if page.body contains "florp-wrapper" %}
+    <script
+      async
+      custom-element="amp-twitter"
+      src="https://cdn.ampproject.org/v0/amp-twitter-0.1.js"
+    ></script>
+    {% endif %}
+
+    <script
+      async
+      custom-element="amp-analytics"
+      src="https://cdn.ampproject.org/v0/amp-analytics-0.1.js"
+    ></script>
+    <script
+      async
+      src="https://cdn.ampproject.org/v0.js"
+    ></script>
+
+    <link
+      rel="shortcut icon"
+      href="/favicon.ico"
+    />
+
+    {% include openGraph.html %}
+  </head>
+  
+</html>
+
+ +

So, there's an <style amp-boilerplate/> element which has to be included and the <html amp lang="en"> declaration.

+ + + +

script elements are declared async. Not just any javascript can be included. Here the amp-analytics script is loaded to allow adding google analytics to the page.

+ +

Currently the AMP validator considers including an unnecessary script a warning and not an error but that could change in future. So the amp-twitter script is loaded but only if there is an embedded tweet in the page.

+ +

Styles

+ +

All styles are included in the head in the <style amp-custom/> element. It was found to be easier to load all styles that way even on non-AMP pages. There was no measurable difference in page rendering with styles in a linked stylesheet versus in a style tag in the head.

+ +

Previously the site used bootstrap v3 for styling (which is burned into my muscle memory). But assessing how much of bootstrap was being used (hardly any) vs. how much was being copied into the head of the page (oodles) for AMP made bootstrap a difficult choice to keep.

+ +

Bootstrap is MIT licensed so only the used styles were copied into the site's scss file. Mixed in with the custom styles there are only c400 lines of styles.

+ +

Presumably it is not true for all sites that there is no performance difference between an in-page style element and a linked sheet but there's only 12Kb of SCSS to be compiled for this site… and a third of that is for syntax highlighting of code blocks.

+ +

The Body

+ +
 {% capture header %}{% include header.html %}{% endcapture %} {{
+header | amp_images: false, 32, 32 }}
+<div class="main">
+  {% include structuredData.html headline=page.title genre=page.category
+  keywords=page.keywords content=page.body link=page.permalink date=page.date %}
+
+  <article>
+    {% capture post_header %}{% include post_header.html %}{% endcapture %} {{
+    post_header | amp_images }}
+    <div class="post">
+      {{ page.body | markdownify | amp_images | amp_tweets }}
+    </div>
+  </article>
+</div>
+
+{% capture footer %}{% include footer.html %}{% endcapture %} {{ footer |
+amp_images: false, 25, 25 }} 
+
+ +

All images have to be fed to the amp_images filter (see below).

+ +

Structured data is apparently not required for AMP but Google's webmaster tools were unhappy if it was not present so the structured data include is added.

+ +

The main content is also passed through the amp_tweets filter as well as the amp_images filter.

+ + + + + + + + + + +
` {{ page.bodymarkdownifyamp_imagesamp_tweets }} `
+ +

So far so straightforward

+ +

Adding a generator

+ +

Jekyll generators run as part of Jekyll's build and "create additional content based on your own rules".

+ +

This generator is almost exactly the same as found on Github.

+ +
require 'thread'
+require 'thwait'
+
+  # Generates a new AMP post for each existing post
+  class AmpGenerator < Generator
+    priority :low
+    def generate(site)
+      dir = site.config['ampdir'] || 'amp'
+      threads = site.posts.docs.map do |post|
+        Thread.new do
+          index = AmpPost.new(site, site.source, File.join(dir, post.id), post)
+          index.render(site.layouts, site.site_payload)
+          index.write(site.dest)
+          site.pages << index
+        end
+      end
+      ThreadsWait.all_waits(*threads)
+    end
+  end
+end
+
+ +

For each of the posts in the site this initializes an AmpPost as a copy of that non AMP post and adds that new post into an amp folder in the output.

+ +

Site build was taking around 18 seconds after adding this generator (and the image and twitter filters). Amending the generator so that it creates a new thread for each AmpPost and then waits for all of those threads to finish reduce build time to around 7 seconds!

+ +

Adding an 'amp_images' filter

+ +

AMP images must be given an explicit size. And this filter, which is unchanged from that found on github, uses nokogiri to find each img element and convert it to an amp-image element.

+ +

So markup like

+ +
<p>
+  <img
+    src="/images/yarn-desc.png"
+    alt="Yarn description"
+  />
+</p>
+
+ +

becomes

+ +
<p>
+  <amp-img
+    src="/images/yarn-desc.png"
+    alt="Yarn description"
+    width="900"
+    height="304"
+    layout="responsive"
+  >
+  </amp-img>
+</p>
+
+ +

Adding an 'amp_tweets' filter

+ +

If you want to embed a tweet in a blog post (and I, for my sins, often do) then twitter provide HTML something like

+ +
<blockquote
+  class="twitter-tweet"
+  data-lang="en"
+>
+  <p
+    lang="en"
+    dir="ltr"
+  >
+    <a href="https://twitter.com/some_user">@some_user</a> the content content
+    @sender (@sender)
+    <a href="https://twitter.com/sender/status/IDFORTHETWEET"
+      >August 20, 2016</a
+    >
+  </p>
+</blockquote>
+
+<script
+  async=""
+  defer=""
+  src="//platform.twitter.com/widgets.js"
+  charset="utf-8"
+></script>
+
+ +

AMP insists this element has a known height and width so that has to be manually edited to

+ +
<div
+  class="florp-wrapper"
+  data-width="292"
+  data-height="350"
+>
+  <blockquote
+    class="twitter-tweet"
+    data-lang="en"
+  >
+    <p
+      lang="en"
+      dir="ltr"
+    >
+      <a href="https://twitter.com/some_user">@some_user</a> the content content
+      @sender (@sender)
+      <a href="https://twitter.com/sender/status/IDFORTHETWEET"
+        >August 20, 2016</a
+      >
+    </p>
+  </blockquote>
+
+  <script
+    async=""
+    defer=""
+    src="//platform.twitter.com/widgets.js"
+    charset="utf-8"
+  ></script>
+</div>
+
+ +

The amp_tweet filter then uses that .florp-wrapper class to find each tweet and convert it to an amp-twitter element.

+ +
<div
+  class="florp-wrapper"
+  data-width="292"
+  data-height="350"
+>
+  <div>
+    <amp-twitter
+      layout="responsive"
+      data-tweetid="IDFORTHETWEET"
+      width="292"
+      height="350"
+    ></amp-twitter>
+  </div>
+</div>
+
+ +

The necessity to manually remember to wrap the embedded tweets in a div with the correct class is the least nice part of this whole process (but it's not the worst thing in the world).

+ + + +

(the tweets aren't really wrapped with florp-wrapper but using the real class meant the script was included and so failed AMP validation :/)

+ +

AMP Validation

+ +

The AMP validator is fudging awesome! It was invaluable in figuring out if I'd set this all up correctly and then identifying old posts which were only imported HTML and not Markdown that Jekyll was building. Those old posts held the majority of the AMP issues identified.

+ +

You can

+ + + +

Google Webmaster tools

+ +

Webmaster tools AMP pages

+ +

Google webmaster tools are also, slowly, picking up that the AMP pages are present. Highlighting warnings and errors and linking out to the validator.

+ +

And so…

+ +

If you're already generating articles using Jekyll it's well worth investigating a little time to get this setup. Either because it'll be interesting to do or because you believe you enough traffic from mobile devices to justify not making those readers wait before they can consume your awesome content.

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2017/testing-meaning.html b/2017/testing-meaning.html new file mode 100644 index 000000000..86482534e --- /dev/null +++ b/2017/testing-meaning.html @@ -0,0 +1,452 @@ + + + + + + + + + + + + + + + + + + + + + + Testing Meaning in HTML! + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Thu Aug 17 2017
+

Testing Meaning in HTML!

+
+ +
+ +
+ + +

One of the benefits of generating a site as a static artefact (here using Jekyll but there are a gazillion tools) is that the finished product is a known quantity. Anything that's a known quantity can be tested!

+ +

A previous post in this series looked at testing the generated HTML for technical correctness… Things like if the HTML is well-formed or that links go to real destinations.

+ +

This post describes testing the meaning of the text in the generated HTML. Checking spelling, and keeping myself honest in my attempt to use more inclusive language.

+ + + +

Test the markdown itself

+ +

Since the HTML is generated from markdown. Is that markdown valid?

+ +
node_modules/.bin/remark . --use lint --frail
+
+ +

Remark is a tool that allows the use of more than one plugin for processing markdown.

+ +

Here the --use lint adds the linting plugin. --frail set it to exit with a non-zero code on warnings as well as errors.

+ +

This doesn't help test the meaning. But, it does help ensure that other errors that are found are located in the meaning and not in a typo. There are still old posts I grabbed from Blogger that are very messy HTML. Periodically I'll switch one to MarkDown and this helps catch errors fast.

+ +

Even better - test the spelling in the markdown

+ +

After yet another occasion where I proofread a post, published it, read it, and immediately saw a spelling mistake. It was time to automate the solution.

+ +
  node_modules/.bin/mdspell _posts/**.md \
+    --en-gb \
+    --ignore-numbers \
+    --ignore-acronyms \
+    --report
+
+ +

MarkDown spellcheck is a tool for doing exactly that.

+ +

Here it

+ +
    +
  • grabs all the .md files
  • +
  • sets the dictionary to --en-gb
  • +
  • ignores numbers and acronyms
  • +
  • and is set to run in report mode
  • +
+ +

The tool has a report mode which outputs spelling errors and then exits with a non-zero code. And an interactive mode that pauses on each potential mistake allowing you to choose to ignore, add to a dictionary, or to correct.

+ +

example interactive spelling output

+ +

The interactive spelling mode can be pretty slow at checking the dictionary. There is an open issue about this.

+ +

As you train this tool, it populates a .spelling file so that you don't have to keep teaching it the domain-specific language you use. Mine's already hundreds of lines long.

+ +

Testing for inconsiderate language…

+ +

Alex is a tool for catching inconsiderate or insensitive language.

+ +

There is very little cost to modifying your language (replacing "guys" with "everyone" or "his" with "their"). And compared to the cost of excluding even one person, I consider it a worthwhile thing to try to improve.

+ +

Alex is run using this command: npx alex _posts --why

+ +
    +
  • _posts tells alex which directory to start in
  • +
  • --why tries to output a source for the warning
  • +
+ + + +

he-she rule

+ +
_posts/2014-06-01-promises-part-2.md
+  197:160-197:162  warning  `he` may be insensitive, use `they`, `it` instead            he-she          retext-equality
+
+ +

In that text I am referring to a man. So, I could ignore the warning. By adding an HTML comment to the markDown <!--alex ignore he-she-->. In each case replacing he-she with the reported rule in the output.

+ +

Or spend the (literal) second to convert that reference to they.

+ +

A file with more errors

+ +
_posts/2010-05-08-theres-more-in-them-that-hills.md
+    17:123-17:130  warning  Don’t use “bitchin”, it’s profane                           bitchin         retext-profanities
+    17:153-17:160  warning  Don’t use “bitchin”, it’s profane                           bitchin         retext-profanities
+    19:229-19:235  warning  Be careful with “failed”, it’s profane in some cases        failed          retext-profanities
+    25:4-25:7  warning  Reconsider using “God”, it may be profane                           god             retext-profanities
+    31:360-31:365  warning  `idiot` may be insensitive, use `foolish`, `ludicrous`, `silly` instead
+Source: http://www.autistichoya.com/p/ableist-words-and-terms-to-avoid.html
+
+ +

Here's another example of the importance of context but also the unthinking use of language.

+ +

I wrote that post in 2010. I don't use that voice any more. But, I'm ok with bitchin' in the context it was used in. But it isn't about what I'm ok with. I don't know the reader and can rephrase without it.

+ +

Next failed is profane in some cases… in this post it's talking about software failing to send emails. I think it's ok. But I can also see how to rephrase the sentence. This is about trying to include as many people as possible. It takes seconds to rephrase the sentence.

+ +

Then a warning about "Oh God it's awful" - and there I'm talking about software I wrote :/ If you've worked with me, you may recognise the feeling :p

+ +

I feel relatively strongly that blasphemy is allowed. We should have freedom from religion as well as freedom of religion. I also feel strongly that I don't go out of my way to blaspheme.

+ +

So I might choose to set Alex not to warn me about the word 'god'

+ +

I can set my .alexrc file to contain

+ +
{
+  "allow": [
+    "god"
+  ]
+}
+
+ +

Or to rephrase the sentence. There's always another way to make yourself clear.

+ +

I use words like Idiot less and less since it takes little effort to replace them. On reading the paragraph it was in so many years after writing it doesn't add anything to the post at all. So I remove the entire paragraph.

+ +

And so…

+ +

These are small changes that help make writing more accessible. I am an imperfect human and find great value in automation that helps me avoid mistakes.

+ +

Update 2021: This has been in my drafts for four years. I am going to publish it with minimal editing in the interest of progress over perfection

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2017/testing-static-sites.html b/2017/testing-static-sites.html new file mode 100644 index 000000000..a014ed05f --- /dev/null +++ b/2017/testing-static-sites.html @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + Testing Static HTML! + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sun Mar 19 2017
+

Testing Static HTML!

+
+ +
+ +
+ + +

One of the benefits of generating a site as a static artefact (here using Jekyll but there are a gazillion tools) is that the finished product is a known quantity. Anything that's a known quantity can be tested!

+ + + +

Test the generated HTML

+ +

I chose a wonderful tool called htmlproofer which, since it has a CLI, can be invoked as part of the build.

+ +
#! /bin/bash
+
+set -eu
+
+bundle exec htmlproofer \
+  _site \
+  --file-ignore /amp/,/.git/ \
+  --check-favicon \
+  --check-html \
+  --check-opengraph
+
+ +

This checks the generated site directory. Ignoring the AMP folder. The list of checks this carries out (reproduced here from the project readme):

+ +
+ +

Images

+ +

img elements:

+ +
    +
  • Whether all your images have alt tags
  • +
  • Whether your internal image references are not broken
  • +
  • Whether external images are showing
  • +
  • Whether your images are HTTP
  • +
+ + + +

a, link elements:

+ +
    +
  • Whether your internal links are working
  • +
  • Whether your internal hash references (#linkToMe) are working
  • +
  • Whether external links are working
  • +
  • Whether your links are HTTPS
  • +
  • Whether CORS/SRI is enabled
  • +
+ +

Scripts

+ +

script elements:

+ +
    +
  • Whether your internal script references are working
  • +
  • Whether external scripts are loading
  • +
  • Whether CORS/SRI is enabled
  • +
+ +

Favicon

+ +
    +
  • Whether your favicons are valid.
  • +
+ +

OpenGraph

+ +

Whether the images and URLs in the OpenGraph metadata are valid. +HTML

+ +

Whether your HTML markup is valid. This is done via Nokogiri, to ensure well-formed markup.

+ +
+ +

If the following invalid elements are added to the page and the htmlproofer script run…

+ +
<img src="foo.png"/>
+
+<a href="/does-not-exist">invalid link</a>
+
+ +

…then the output highlights five errors.

+ +
Running ["HtmlCheck", "FaviconCheck", "ImageCheck", "LinkCheck", "ScriptCheck", "OpenGraphCheck"] on ["_site"] on *.html... 
+Checking 310 external links...
+Ran on 55 files!
+- _site/2017/testing-static-sites.html
+  *  External link http://pauldambra.github.io/2017/testing-static-sites.html failed: 404 No error
+  *  External link http://pauldambra.github.io/amp/2017/testing-static-html failed: 404 No error
+  *  image foo.png does not have an alt attribute (line 678)
+  *  internal image foo.png does not exist (line 678)
+  *  internally linking to /does-not-exist, which does not exist (line 680)
+     <a href="/does-not-exist">invalid link</a>
+htmlproofer 3.5.0 | Error:  HTML-Proofer found 5 failures!
+The command "./htmltest.sh" exited with 1.
+
+ +

Three of these were expected:

+ +
    +
  • that the image element doesn't have an alt attribute
  • +
  • that foo.png does not exist
  • +
  • and that the internal link to /does-not-exist does not, erm, exist
  • +
+ +

ruh roh

+ +

Interestingly this also reveals a bug in the setup.

+ +
  *  External link http://pauldambra.github.io/2017/testing-static-sites.html failed: 404 No error
+  *  External link http://pauldambra.github.io/amp/2017/testing-static-html failed: 404 No errors
+
+ +

Grepping the generated html for those two external links finds them in the HEAD of the document.

+ +

I'd only run this process on existing, published blog posts since adding it. This is the first time that it has run against a repo with a new, unpublished blog post and it's correctly highlighting that the open graph URL for this article and the amphtml link rel for this article don't exist. Because they don't - this article hasn't been published yet.

+ +

The site's .travis.yml file currently has:

+ +
script:
+  - "./build.sh"
+  - "./htmltest.sh"
+  - "./amp-validate.sh"
+after_success:
+  - "./deploy.sh"
+
+ +

this will have to become

+ +
script:
+  - "./build.sh"
+  - "./amp-validate.sh"
+after_success:
+  - "./deploy.sh"
+  - "./htmltest.sh"
+
+ +

so that the HTML test only runs after the deploy has occurred. Ideally any published documents would be tested before deploy and could fail the build and newly published documents only after their first deploy as a smoke test. But this will do for now.

+ +

Test the generated AMP

+ +

An AMP version of the site is generated at build time too. HTML-Proofer can't test the AMP site so these pages could be broken and that test doesn't protect us.

+ +

AMP is a dream to work with because the AMP debugger is well built and provides clear, actionable errors. Brilliantly that online debugger is available as an NPM package so as can be seen above there is an amp-validate.sh as part of the build.

+ +
#! /bin/bash
+
+set -eu
+
+npm install -g amphtml-validator
+
+for f in `find _site/amp -type f -name '*.html'`; do
+  amphtml-validator $f
+done
+
+ +

Because the AMP debugger was so helpful when adding AMP generation the only warning this generated when it was added to build was many instances of

+ +
+

_site/amp/2009/05/anonymous-methods-when-invoking-in-vb/index.html:633:6 The extension 'amp-twitter extension .js script' was > found on this page, but is unused (no 'amp-twitter' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-twitter.html)

+
+ +

Each AMP page had the amp-twitter extension included whether or not there was a tweet embedded in the page. This was fixed.

+ +

And a single, old page which the AMP generator couldn't handle and so

+ +
+

_site/amp/2011/04/ssh-without-password/index.html:636:3 The attribute 'style' may not appear in tag 'span'. +_site/amp/2011/04/ssh-without-password/index.html:667:15 The tag 'paste' is disallowed.

+
+ +

Again fixed by updating the HTML of the non-AMP post.

+ +

And so?

+ +

When these two types of test were added there were 237 HTML errors and 9 AMP warnings and 2 AMP errors. From as little as missing a favicon through to genuinely malformed pages. Adding these tests was straight-forward, added value to the CI for this blog, and is another good indication of the benefits of statically generated sites.

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2018/01/direction.html b/2018/01/direction.html new file mode 100644 index 000000000..00cf33430 --- /dev/null +++ b/2018/01/direction.html @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + Is where we're going where we're going + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Mon Jan 29 2018
+

Is where we're going where we're going

+
+
+ +
+ +
+ + tag-icon + + physics + + + + + tag-icon + + software + + + + + tag-icon + + agile + + + + + tag-icon + + cargo-cult + + + + + tag-icon + + rant + + + +
+
+
+
+ +
+

Velocity is…

+ +

A way of measuring the progress being made by a software team. Not all teams use velocity. I've been on quite a few that do. So at least some teams still use it as a measure.

+ + + +
+

Velocity is the number of story points completed by a team in an iteration. +scrum alliance 2014

+
+ + + +
+

define velocity as simply a measure of how fast a team is going +mountain goat software 2014

+
+ +

In fact both of those articles go on to expand on the physical metaphor…

+ +
+

How do you measure your velocity while driving? (Imagine the speedometer is broken.) You've been driving for the last two hours, you've gone 160 kilometers, so you know your average velocity is 80 km per hour. scrum alliance 2014

+
+ +

A little physics

+ +

Notice that in the quote above there is a switch between "velocity" and "speedometer".

+ +

If you were driving too fast you don't get a velociting ticket for exceeding the velocity limit.

+ +

In physics (and policing) there is a distinction between speed and velocity.

+ +

Speed only has magnitude but velocity is speed and direction.

+ +

Wait, what?

+ +

If you are told someone is moving 30mph can you tell me how long it will take them to get to your house?

+ +

No! You don't know where they are and you don't know which way they're travelling. You need to know they are, for example, due south from your house and travelling north.

+ +

You can't say anything about when and where they will arrive only from their speed.

+ +

So, what do we need?

+ +

Simplistically (let's not stretch the metaphor to routing on a map) you need:

+ +
    +
  • a destination
  • +
  • a starting point
  • +
  • a direction of travel
  • +
  • a speed
  • +
+ +

Let's, for now, assume we're talking about a fixed destination and starting point (spoiler: we're not).

+ +

(Most?) Teams that measure velocity do so as if direction doesn't exist. It's a count of the work completed… it assumes you know where you are, where you're going, and that you're heading in the right direction.

+ +

It assumes that every (task|story|ticket|feature) you are asked to complete is the correct thing to do.

+ +

Do we care? Should we care?

+ +

High speed with no progress

+ +

some toddlers demonstrating you can have speed without progress

+ +

So logically you can have a high speed system with low progress. And you can have a low speed system with high progress.

+ +

Let's labour the point…

+ +

Take two cyclists and start them at the same place with some desired destination for them to travel to. Point one of them in the right direction and the other randomly. No matter how fast the randomly pointed cyclist travels they are far less likely to reach the destination at all.

+ +

Mix in a closer to reality metaphor. Make it a journey of many legs and the likelihood that the randomly directed cyclist will ever reach the destination approaches zero pretty quickly. The other cyclist could be travelling at any speed but is guaranteed to get to the destination.

+ +

1 step forward and 1 step back means 0 progress

+ +

So, what

+ +

I assume a few things:

+ +
    +
  • you want to achieve something to solve a problem
  • +
  • you want to get better at doing that
  • +
  • you don't want to waste your own or somebody else's time or money
  • +
+ +

In which case you have to regularly measure

+ +
    +
  • where you are
  • +
  • where you're going
  • +
  • the direction you're travelling
  • +
  • that your speed isn't zero
  • +
+ +

Otherwise, like the cyclist that chooses random directions you can't expect to ever reach your destination.

+ +

In reality you aren't given random tickets to work on (or at least for your sake I hope not). Instead, with what we all know right now some group of us choose what to work on next.

+ +

You're not being pointed randomly but instead in roughly the right direction.

+ +

Taking the cyclist example again - If you can stop and re-assess your roughly correct destination you'll get there eventually but you'll still take longer than a cyclist that has better directions provided than you.

+ +

So, ok, direction is important.

+ +

But why regularly measure

+ +

Because the landscape you're building software in probably doesn't look like this:

+ +

a picture of the peak district with great visibility

+ +

Since we're generally operating under imperfect conditions. Trying to figure out where we are is more like being in the fog:

+ +

a picture of the peak in foggy conditions

+ +

A friend was for a while a member of mountain rescue (who are incidentally incredible - you should give them money). They once described to me how they navigate when they have very low visibility.

+ +

a lost Lego hiker

+ +

In pairs:

+ +
    +
  • use the map to figure out where you are
  • +
  • use that information to figure out what direction to go
  • +
  • using a compass one of you slowly walks in that direction
  • +
  • the other stays still and calls out when the walker is about to disappear into the fog
  • +
  • then that person catches up with the walker
  • +
  • repeat
  • +
+ +

Looking at the context of where they are against what they know about the world. Working together to understand what that means, right then. Watching each other and relying on communication. Chopping the journey into many safer parts.

+ +

Can this apply to software teams? (spoiler, yes, I think so.)

+ +

I've tried a number of times to work this out during sessions at Co-op Digital and XP Manchester. Many thanks to the people who shared their time and brains with my confused grasping at an idea.

+ +

Maybe I've not worked it out completely yet… toot me and tell me what you think!

+ +
    +
  • +

    We can use tools like the Cyenfin framework, Wardley mapping, and user research to understand where we are and how we want to get to our destination.

    +
  • +
  • +

    We can remember that we have low visibility and work closely together to make sure we aren't trying to move too far in one go. Slicing work as thinly as our context tells us makes sense.

    +
  • +
  • +

    We can use data from people using our software and more user research as our compass to check whether we strayed from our desired path.

    +
  • +
+ +

TL;DR

+ +

We can and should care if we're being asked to do something meaningful. And we mustn't treat it as somebody else's work to check where we should go or whether we got there.

+ +

The number of tickets you complete is not a measure of progress by itself. Start by measuring value and only then, if ever, start counting tickets or points

+ +
+ + +
+
+ + + + + + + + + + diff --git a/2018/02/serverless-1.html b/2018/02/serverless-1.html new file mode 100644 index 000000000..89e01c4ad --- /dev/null +++ b/2018/02/serverless-1.html @@ -0,0 +1,477 @@ + + + + + + + + + + + + + + + + + + + + + + Serverless - Part One + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sat Feb 03 2018
+

Serverless - Part One

+
+ +
+ +
+

Anyone who knows me knows that I like to talk about Event-driven systems. And that I'm very excited about serverless systems in utility computing.

+ +

I started my career in I.T. having to order network cables, care about fuses, and plan storage and compute capacity. It was slow, frustrating, and if you got it wrong it could take (best case scenario!) days to correct.

+ +

Over a few articles I hope to communicate what serverless is, why you should find it exciting, and how to start using it.

+ +

Let's start by defining our terms…

+ + + +

Utility Computing

+ +

As a name, "The Cloud™️" is terrible. It's meaningless. It totally fails to communicate what it is. Maybe it's a place you put computers? Maybe it's because applications can "scale" there?

+ +

Far better to think of "Utility Computing". United Utilities provides water as a utility to properties. Their customers know, vaguely, that there are water mains, and reservoirs, water treatment plants, and more but don't have to care. They don't think about that detail, they turn on a tap.

+ +

That's the cloud. Computing as a utility. You don't have to care if the provider is running servers or containers, if they have enough fuses in stock, or what model of switch they bought. You turn on your application and let it run.

+ +

Event-driven systems

+ +

Events are facts. They are things that happened so they are immutable. An application can store the events.

+ +

In systems that are not event driven the events are still there only they are ephemeral, implied in the API call, the change in state, the UI interaction, etc. In the event-driven system they are central to what happens.

+ +

Fowler describes multiple types of Event-driven systems:

+ +
    +
  1. Event Notifications
  2. +
+ +

One system registers with another. That system raises an event: PersonChangedAddress. If the "subscriber" cares it takes some action. In a system where events are notifications they might carry no information. So the subscriber still needs to call an API or in some other way load the information it needs to take an action.

+ +

event notifications system diagram

+ +
    +
  1. Event Carried State Transfer (should obvs be "Event Assisted State Transfer" or E.A.S.T.)
  2. +
+ +

One system registers with another. That system raises an event: PersonChangedAddress and includes at least the new address and the identifier for the person. The subscriber now has all the information it needs to respond to the event.

+ +

event carried system diagram

+ +
    +
  1. Command Query Responsibility Segregation (CQRS)
  2. +
+ +

An application that separates writing to the system (commands) from reading from it (queries).

+ +

Arguably not an event-driven architecture since it can be achieved without events. But Greg Young asserts it was a necessary step to a world that has EventSourcing (in this video IIRC).

+ +

Here one application receives the command ChangeAddress. It acts on it. That action might raise an event, write to a queue, write to a database… the mechanism doesn't matter for CQRS.

+ +

Another application (or the same one in a different code path) has the responsibility for querying the system. It lets people view a list of addresses but the work of reading an address for display is much simpler (generally) than the work of accepting, validating, transforming, and storing the address on the command side.

+ +

CQRS is a big topic. Fowler's description is a good starting point. And Rob Ashton has a good article showing varying levels of complexity of CQRS approaches

+ +

CQRS system diagram

+ +
    +
  1. EventSourcing
  2. +
+ +

An event-sourced system doesn't only respond to events but builds its state by replaying the events. For example instead of storing an order:

+ +

{"user": "12345", "items": [{"sku": "54321", "paid": "£1.23"}]}.

+ +

You would store events:

+ +

+[
+ {"type": "userStartedOrder", "user": "12345"},
+ {"type": "userAddedItemToBasket", "item": {"sku", "54321"}},
+ {"type": "userPaidInFull", amount: "£1.23"}
+]
+
+
+ +

An application can now read all three of those events to generate the state of the order.

+ +

Or it could read all of the events of type PersonChangedAddress and generate a list of all addresses in the system.

+ +

event sourcing system diagram

+ +

The event-driven approach has a number of benefits. Most strikingly flexibility to changes in business logic, the ability to audit what has happened, and composability. Imagine we need to report on stock and accounts changes - we don't even need to change any deployed module.

+ +

building on an event driven system diagram

+ +

This additive approach means that every application that only reads from the stream can never add defects to existing applications!

+ +

Ok, never say never, the chance of introducing a defect at the system level exists but is far, far lower than in a change that directly affects the already deployed application's code.

+ +

Serverless

+ +

Serverless continues this journey. It doesn't mean there aren't any servers. But it does mean that you hardly have to care there are servers.

+ +

Before I started this I was conflating "Serverless" with "Functions-as-a-service" (FaaS).

+ +

FaaS is a system where a utility compute provider runs arbitrary code on your behalf in response to events occuring. No virtual machine, No network config, no capacity planning. Think AWS Lambda or Azure Functions.

+ +

Serverless implies event-driven!

+ +

Also serverless isn't only functions!

+ +

Storage, database, queues, and more can be provided in such a way that they are distributed, highly available, elastic, and you don't have to manage, or maintain any infrastructure. Well, ish, you have to create the serverless components and their connections… but not the infrastructure they're going to run on (and it's patches, and new versions, and foibles, and …)

+ +

So that last system diagram could be rewritten:

+ +

serverless event driven system diagram

+ +

Globally distributed, resilient, highly available, scalable, event-driven system. And somebody else manages all the pieces while you fill it with code.

+ +

I'm sold!

+ +

Let's use a toy system to explore it?

+ +

I love building event-driven systems but they're not the norm so it's a long time since I've had one in production. While I was off work recently I thought I'd practice. Since Serverless is the future I decided to make a serverless system. Because I know how to have fun.

+ + + +

Finding somewhere to take your kids can be difficult and, since it was half-term, was on my mind. It seems like there are no websites that are aware of where you are, where you could go, and what the weather might be like when you get there…

+ +

So let's make that.

+ +

Level 1: System Context Diagram

+ +

the first level of a c4 diagram

+ +

Level 2: Container Diagram

+ +

the second level of a c4 diagram

+ +

Level 3: Component Diagram

+ +

the third level of a c4 diagram

+ +

(check out Simon Brown's C4 diagrams - they're 💯)

+ +

Some of this is pretty speculative but it's roughly the right shape. So it's not worth designing any more until I've learnt some more.

+ +

This is already a massive post so I'll stop here. Next time I'll try and have fewer of my terrible drawings and more code!

+ +
+ + +
+
+ + + + + + + + + + diff --git a/2018/02/serverless-2.html b/2018/02/serverless-2.html new file mode 100644 index 000000000..a7a4edd7c --- /dev/null +++ b/2018/02/serverless-2.html @@ -0,0 +1,624 @@ + + + + + + + + + + + + + + + + + + + + + + Serverless - Part Two + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sun Feb 04 2018
+

Serverless - Part Two

+
+ +
+ +
+

After describing event-driven and serverless systems in part one it is time to write some code. Well, almost. The first task is a walking skeleton: some code that runs on production infrastructure to prove we could have a CI pipeline.

+ +

I think I'll roll my AWS credentials pretty frequently now - since I can't imagine I'll get through this series without leaking my keys somehow

+ +

¯\_(ツ)_/¯

+ +

Putting authentication and authorisation to one side, because the chunk is too big otherwise, this task is to write a command channel to allow editors to propose destinations on the visitplannr system.

+ +

This requires the set up of API Gateway, AWS Lambda, and DynamoDB infrastructure and showing some code running. But doesn't require DynamoDB table streams or more than one lambda.

+ +

That feels like a meaningful slice.

+ + + +

The moving pieces

+ +

the second level of a c4 diagram

+ +

Infrastructure as Code

+ +

We use terraform at work so it would be quite productive to use that - but I wanted to try out SAM local to understand its local development story and deployment using CloudFormation.

+ +

From the docs: "SAM Local can be used to test functions locally, start a local API Gateway from a SAM template, validate a SAM template, and generate sample payloads for various event sources."

+ +

The Serverless Application Model or SAM is based on CloudFormation. With the aim of defining "a standard application model for serverless applications".

+ +

CloudFormation is an AWS specific infrastructure as code resource letting you author entire application stacks as JSON or YAML. That let's you launch "application stacks", in this case: API Gateway, Lambda, and DynamoDB.

+ +

Code as Code

+ +

AWS lambda now runs many awesome languages (NodeJS, Python, C#, Java, and Go). I 💖 JavaScript and have already experimented a few times over the last few years with NodeJS in Lambda. So will write the application in Node.

+ +

There are a number of frameworks that sit on top of AWS Lambda and API Gateway. Such as Claudia.js or Serverless. But I didn't want any of the details hidden away from me so haven't investigated them at all (which may be cutting off my arm to spite my face).

+ +

The eventstream

+ +

It is common to use dynamodb as the storage mechanism for Lambda. "Amazon DynamoDB is highly available, with automatic and synchronous data replication across three facilities in an AWS Region."

+ +

Which highlights one of the benefits of the serverless model - geographically distributed HA database by default.

+ +

It can read and write JSON, and allows you to subscribe to the stream of changes for a table. So most likely fits the needs of this application.

+ +

The SAM template

+ +

The SAM template is (once you're used to the syntax) pretty straightforward.

+ +

A header describing this template and the versions of the language used:

+ +
AWSTemplateFormatVersion: "2010-09-09"
+Transform: AWS::Serverless-2016-10-31
+
+Description: |
+  A location and weather aware day-trip planner for parents
+
+ +

a list of the resources to be created:

+ +
Resources:
+
+ +

A dynamodb table definition:

+ +
EventsTable:
+  Type: "AWS::DynamoDB::Table"
+  Properties:
+    AttributeDefinitions:
+      - AttributeName: StreamName
+        AttributeType: S
+      - AttributeName: EventId
+        AttributeType: S
+    KeySchema:
+      - AttributeName: StreamName
+        KeyType: HASH
+      - AttributeName: EventId
+        KeyType: RANGE
+    ProvisionedThroughput:
+      ReadCapacityUnits: 5
+      WriteCapacityUnits: 5
+
+ +

"In DynamoDB, tables, items, and attributes are the core components that you work with. A table is a collection of items, and each item is a collection of attributes. DynamoDB uses primary keys to uniquely identify each item in a table and secondary indexes to provide more querying flexibility."

+ +

There are two types of primary key in dynamodb. And this is the first design decision which will need validation in future. In fact in a "real" project this would need a lightweight architecture decision record. So let's add one here.

+ +

The first kind of primary key in DynamoDB is having only a partition key. The partition key is hashed and determines where on physical storage the item is placed. The partition key must be unique.

+ +

The second kind is a composite primary key. It consists of a partition key and a sort key. The partition key no longer needs to be unique in isolation. Rather the sort key/partition key pair must be unique.

+ +

In a real system this would probably push towards StreamName as the partition key: so that events that logically live together physically live together. And EventNumber in the stream as the sort key. So that the order of items as they are stored on physical media matches the order they are likely to be read.

+ +

This would introduce a bunch of complexity in code for tracking event numbers so for now instead of an EventNumber as the sort key the decision is to introduce a UUID EventId. This will need performance testing to check that there is no significant impact of the items being sorted by UUID.

+ +

The "ProvisionedThroughput" setting show where the abstraction starts to leak and the fact that these services run on infrastructure bleeds through. Under the hood AWS is reserving capacity for dynamodb - after all they definitely do have to capacity plan their infrastructure so that we don't have to.

+ +

From the docs:

+ +

"One read capacity unit = one strongly consistent read per second, or two eventually consistent reads per second, for items up to 4 KB in size.

+ +

One write capacity unit = one write per second, for items up to 1 KB in size."

+ +

So the system needs to be sized against the expected read and write throughput.

+ +

The AWS SDK has retries built in for if the AWS service throttles your reads or writes when you are over capacity. This would be an area that would need testing and monitoring in a real system.

+ +

It's important to note that the "cost" of managing that capacity setting is probably lower than the cost of creating, managing, and maintaining your own distributed, highly-available, (potentially) multi-master database cluster.

+ +

And worth saying that on Prime Day 2017 Amazon's own use of DynamoDB peaked at nearly 13 million reads per second. So the need for throughput limits in config isn't because the service can't cope with your load.

+ +

The lambda and API gateway definition:

+ +
ProposeDestinationFunction:
+  Type: AWS::Serverless::Function
+  Properties:
+    Runtime: nodejs6.10
+    Handler: proposeDestination.handler
+    Timeout: 10
+    Policies: AmazonDynamoDBFullAccess
+    Environment:
+      Variables:
+        EVENTS_TABLE: !Ref EventsTable
+    Events:
+      ProposeDestination:
+        Type: Api
+        Properties:
+          Path: /destination
+          Method: POST
+
+ +

This sets a lambda function with a given handler, and runtime. The handler is the code that will run when the event is received. And sets an environment variable to reference the created dynamodb table.

+ +

Finally sets that this lambda function will be triggered by an API POST to /destination. Which is all SAM needs in order to create an API gateway to trigger the lambda.

+ +

With 39 lines of YAML SAM will provision an API gateway, a lambda function, and a dynamodb function. All highly available, elastic, and distributed geographically - that's pretty funky!

+ +

The handler code

+ +
exports.handler = (event, context, callback) => {
+  console.log(`received event: ${JSON.stringify(event)}`);
+  callback(null, {
+    statusCode: 200,
+    body: "OK",
+  });
+};
+
+ +

First things first:

+ +
    +
  • No semi-colons. Live with it. It's great.
  • +
  • standardjs - I don't agree with all of standardjs' decisions but I do recognise that since they're largely arbitrary I shouldn't care.
  • +
+ +

This is a lambda function. It's about as small a function as you can write to respond to an event from API gateway. First it logs the received event. Then it tells API gateway to return a http status 200 with the body 'OK' to the client.

+ +

Anchors away!

+ +

After incurring the incredible cost of $0.02 because I kept forgetting to strip chai and mocha from the bundle I wrote a deployment script.

+ +

+#! /bin/bash
+
+set -eux
+
+rm -rf node_modules/
+
+npm install --only=production
+
+if aws s3 ls "s3://visitplannr" 2>&1 | grep -q 'NoSuchBucket'
+then
+  aws s3 mb s3://visitplannr
+fi
+
+sam package --template-file template.yaml \
+  --s3-bucket visitplannr \
+  --output-template-file packaged.yaml
+
+sam deploy --template-file ./packaged.yaml \
+  --stack-name visitplannr \
+  --capabilities CAPABILITY_IAM
+
+
+ +
    +
  • remove node_modules directory
  • +
  • npm install all of the non-dev dependencies (there aren't actually any yet!)
  • +
  • make sure there's an s3 bucket to upload the code into
  • +
  • run sam package which translates to CloudFormation and uploads the code to s3
  • +
  • run sam deploy which launches the application stack
  • +
+ +

Running that creates everything necessary in AWS. Looking at the created stack there are more pieces created than needed to be specified.

+ +

the created resources

+ +

This includes the IAM roles to allow these resources to talk to each other. These at least in part result from the config line: Policies: AmazonDynamoDBFullAccess applied to the lambda function.

+ +

This is much more access than we need. But in the interest of not getting diverted the necessity for finer grained access goes on the to-do list - it's possible but not necessary right now.

+ +

The wider than necessary access can be seen in the lambda console which lists out the resources the function can access and the policy that makes that access possible.

+ +

the lambda console and its permissions

+ +

The API Gateway console shows the new endpoint

+ +

the api gateway console

+ +

The endpoint can be tested right in the console:

+ +

testing the api

+ +

and the results are logged in the page

+ +

the api test results

+ +

And finally, the cloudwatch logs show the output from running the lambda.

+ +

That

+ +
console.log(`received event: ${JSON.stringify(event)}`);
+
+ +

results in logging:

+ +

+2018-03-04T22:45:26.854Z  bb57cb6c-1ffd-11e8-b4a3-d7ae7c406190  received event:
+{
+    "resource": "/destination",
+    "path": "/destination",
+    "httpMethod": "POST",
+    "headers": null,
+    "queryStringParameters": null,
+    "pathParameters": null,
+    "stageVariables": null,
+    "requestContext": {
+        "path": "/destination",
+        "accountId": "123456",
+        "resourceId": "0yfso6",
+        "stage": "test-invoke-stage",
+        "requestId": "test-invoke-request",
+        "identity": {
+            "apiKeyId": "test-invoke-api-key-id",
+            "userAgent": "Apache-HttpClient/4.5.x (Java/1.8.0_144)",
+            "snip": "much more info"
+        },
+        "resourcePath": "/destination",
+        "httpMethod": "POST",
+        "apiId": "lvcba3r3ia"
+    },
+    "body": "{\n    \"test\": \"this endpoint\"\n}",
+    "isBase64Encoded": false
+}
+
+
+ +

and each invocation also logs a line like:

+ +

REPORT RequestId: bb57cb6c-1ffd-11e8-b4a3-d7ae7c406190 Duration: 15.92 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 20 MB

+ +

Showing you the actual and billed durations and the used memory compared to the provisioned memory.

+ +

More leaky abstractions

+ +

So, it isn't that you don't have to care at all that there are servers. AWS Lambda charges per 100ms of function invocation. The amount of time you get for free and the cost per 100ms varies depending on how much RAM you allocate to the function.

+ +

In fact, increasing the RAM actually increases the underlying compute, network, threads, and more. In some experiments at Co-op Digital we saw that scaling for a network and compute bound workload was pretty important.

+ +

DynamoDB

+ +

The table viewed in the AWS console

+ +

The table has been created and is ready to be used. The config we've used doesn't actually setup the table for autoscaling. But we'll loop back around and tidy that up later. It's another detail that doesn't need nailing right now.

+ +

Let's review

+ +

With 39 lines of YAML we've created a walking skeleton to prove we can write code locally and deploy it to AWS.

+ +

We've had to learn a little about the details of dynamodb and AWS lambda where they leak their underlying infrastructure into our worlds - although presumably there's a setting equivalent to "I care less about how much I spend than how much resource you use - charge me what you like and don't bother me again". I don't want to turn that on (yet).

+ +

All the code for this stage can be found on github

+ +

And we're finally ready to write some tests. In the next post we'll look at some tests, talk about the final state of the handler, and look at how to set up locally to run integration tests.

+ +

And delete

+ +

aws cloudformation delete-stack --stack-name visitplannr && aws s3 rb s3://visitplannr --force

+ +

This tells AWS to delete everything since I don't want to pay money for an application stack that nobody is using and only exists for my (unashamedly very low readership) blog.

+ +
+ + +
+
+ + + + + + + + + + diff --git a/2018/02/serverless-3.html b/2018/02/serverless-3.html new file mode 100644 index 000000000..f53d85a2b --- /dev/null +++ b/2018/02/serverless-3.html @@ -0,0 +1,594 @@ + + + + + + + + + + + + + + + + + + + + + + Serverless - Part Three + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Mon Feb 05 2018
+

Serverless - Part Three

+
+ +
+ +
+

Part One - describing event-driven and serverless systems

+ +

Part Two - Infrastructure as code walking skeleton

+ +

In this post we will look at how SAM local let's you develop locally and write the first lambda function. To take a ProposeDestination command and write a DestinationProposed event to the eventstream.

+ +

"SAM Local can be used to test functions locally, start a local API Gateway from a SAM template, validate a SAM template, and generate sample payloads for various event sources."

+ + + +

SAM Local

+ +

You have to have Docker running locally and then you can npm install -g aws-sam-local.

+ +

To start the API Gateway and Lambda example from part two navigate to the directory containing the template.yaml file and run sam local start-api

+ +

This starts lambda in Docker and shows what endpoints are mounted:

+ +

the start-api command console output

+ +

You can then POST to the endpoint curl -H "Content-Type: application/json" -X POST -d '{"geolocation":"xyz", "name":"test"}' http://127.0.0.1:3000/destination

+ +

Which outputs the same information as you would see in cloudwatch logs:

+ +

the api console output

+ +

DynamoDB

+ +

This took several attempts to get running - mostly because of unfamiliarity with Docker - AWS were super helpful on twitter despite my silliness. Without that confusion this would have been very straightforward.

+ +

There are three steps to this:

+ +

1) Start dynamodb

+ +

DynamoDB needs to be run as a named container and on the same Docker network as SAM local

+ +
docker network create lambda-local
+sam local start-api --docker-network lambda-local
+docker run -d -v "$PWD":/dynamodb_local_db -p 8000:8000 --network lambda-local --name dynamodb cnadiminti/dynamodb-local
+
+ +

2) Create the DynamoDB table

+ +

SAM local can't create DynamoDB tables from the template.yaml in the way that CloudFormation will when the SAM application is deployed so the table needs manually creating.

+ +

The following AWS CLI command will create the table as defined in the template.yaml:

+ +
aws dynamodb create-table \
+  --table-name visitplannr-events \
+  --attribute-definitions \
+        AttributeName=EventId,AttributeType=S AttributeName=StreamName,AttributeType=S \
+  --key-schema AttributeName=StreamName,KeyType=HASH AttributeName=EventId,KeyType=RANGE \
+  --provisioned-throughput \
+  ReadCapacityUnits=5,WriteCapacityUnits=5 \
+  --endpoint-url http://0.0.0.0:8000
+
+ +

3) Write some code to have the one talk to the other

+ +

The client connection code is:

+ +
const AWS = require("aws-sdk");
+const awsRegion = process.env.AWS_REGION || "eu-west-2";
+
+let dynamoDbClient;
+const makeClient = () => {
+  const options = {
+    region: awsRegion,
+  };
+  if (process.env.AWS_SAM_LOCAL) {
+    options.endpoint = "http://dynamodb:8000";
+  }
+  dynamoDbClient = new AWS.DynamoDB.DocumentClient(options);
+  return dynamoDbClient;
+};
+
+module.exports = {
+  connect: () => dynamoDbClient || makeClient(),
+};
+
+ +

This module exposes a connect method that lazily initializes the db client.

+ +

SAM local sets an AWS_SAM_LOCAL environment variable so the code checks for that and if it is present sets the endpoint URL to http://dynamodb:8000. This is the container name and the port it exposes.

+ +

For the production code you don't need to set any endpoint and can let lambda figure out what to connect to.

+ +

The propose destination handler

+ +

The handler should act as a composition root. It creates the application's object graph and lets the parts do the work. This allows unit tests to inject fakes. If those tests were to exercise the handler directly the code would run the dynamoDbClient and timeout waiting for dynamodb.

+ +

The handler ends up looking like this:

+ +
const httpResponse = require("./destinations/httpResponse");
+const mapCommand = require("./destinations/mapCommand");
+
+const guid = require("./GUID");
+
+let streamRepo;
+const dynamoDbClient = require("./destinations/dynamoDbClient");
+const makeStreamRepository = require("./destinations/make-stream-repository");
+
+const commandHandler = require("./destinations/commandHandler");
+
+exports.handler = (event, context, callback) => {
+  const proposeDestination = mapCommand.fromAPI(event, "proposeDestination");
+
+  streamRepo =
+    streamRepo || makeStreamRepository.for(dynamoDbClient.connect(), guid);
+
+  commandHandler
+    .apply({
+      command: proposeDestination,
+      type: "destinationProposed",
+      streamName: "destination",
+      streamRepository: streamRepo,
+    })
+    .then(() => httpResponse.success(proposeDestination, callback))
+    .catch((err) => httpResponse.invalid(err, proposeDestination, callback));
+};
+
+ +

Here the handler converts the API gateway event to a ProposeDestination command. It then either uses the existing stream repository or creates one currying the dynamodb client and guid generator.

+ + + +

The command handler is then called. It either converts the command to a destinationProposed event and returns an HTTP 200 success. Or fails and returns an HTTP 400 invalid request.

+ +

Testing this with SAM local

+ +

I haven't wrapped this up into something useful that could be run in a CI pipeline but as a sense check before deployment this is a good starting point.

+ +

First ensure SAM local is running:

+ +

AWS_REGION=eu-west-2 sam local start-api --docker-network lambda-local

+ +

Then also that dynamodb is running:

+ +

docker run -d -v "$PWD":/dynamodb_local_db -p 8000:8000 --network lambda-local --name dynamodb cnadiminti/dynamodb-local

+ +

Then write a test to exercise the system:

+ +
const request = require("supertest");
+var exec = require("child_process").exec;
+const expect = require("chai").expect;
+
+const rootUrl = process.env.rootUrl || "http://127.0.0.1:3000";
+const dynamoDbUrl = process.env.dynamoDbUrl || "http://0.0.0.0:8000";
+
+const countItemsInTable = () => {
+  return new Promise((resolve, reject) => {
+    exec(
+      `aws dynamodb scan --table-name visitplannr-events --endpoint-url ${dynamoDbUrl}`,
+      (error, stdOut, stdErr) => {
+        if (error) {
+          reject(new Error(error));
+          return;
+        }
+        const scanResult = JSON.parse(stdOut);
+        resolve(scanResult.Items.length);
+      }
+    );
+  });
+};
+
+describe("propose destination", function () {
+  it("can write an event to dynamodb", function (done) {
+    this.timeout(5000);
+
+    countItemsInTable()
+      .then((startCount) => {
+        request(rootUrl)
+          .post("/destination")
+          .send('{"name":"test destination","geolocation":{"x": 0, "y": 0}}')
+          .end((err, res) => {
+            if (err) {
+              done(err);
+              return;
+            }
+            countItemsInTable()
+              .then((finalCount) => {
+                expect(finalCount).to.equal(startCount + 1);
+                done();
+              })
+              .catch(done);
+          });
+      })
+      .catch(done);
+  });
+});
+
+ +

This is not a great example of a test for a number of reasons but it does demonstrate that the running system can receive an HTTP post after which there is one more item in the dynamodb table.

+ + + +

The devil is always in the detail so this test wouldn't be good enough for a real system. But it does show that the lambda functions can be integration tested locally with real HTTP calls, writing to a local dynamodb.

+ +

Unit testing

+ +

The composition root approach means that the handler can be unit tested without relying on the dynamodb client. As an example testing the behaviour in the repository against a fake dynamodb, here the test locks in that the repository adds a correlation id to the item written to the stream:

+ +
const streamRepoFactory = require("../destinations/make-stream-repository");
+const expect = require("chai").expect;
+
+describe("the stream repository", function () {
+  const fakeGuidGenerator = {
+    generate: () => "a-generated-guid",
+  };
+
+  const fakeDynamoDbClient = {
+    put: (params, callback) => {
+      callback();
+    },
+  };
+
+  const streamRepo = streamRepoFactory.for(
+    fakeDynamoDbClient,
+    fakeGuidGenerator
+  );
+
+  let writeToTheStream;
+
+  beforeEach(function () {
+    writeToTheStream = streamRepo.writeToStream({
+      streamName: "arbitrary-string",
+      event: { winnie: "pooh" },
+    });
+  });
+
+  it("decorates the event with a correlation id", function (done) {
+    writeToTheStream
+      .then((writtenItem) => {
+        expect(writtenItem.Item.event).to.deep.equal({
+          winnie: "pooh",
+          correlationId: "a-generated-guid",
+        });
+        done();
+      })
+      .catch(done);
+  });
+});
+
+ +

Writing to the event stream can be tested with a guid generator that always generates the same guid, and a dynamodb client that doesn't connect to dynamodb. This lets other behaviour be tested without those dependencies complicating or slowing down the tests.

+ +

Testing in AWS

+ +

The integration test above is bound to querying dynamodb using the AWS CLI. It would not take a lot of fixing to have that test run against an actual API Gateway endpoint and dynamodb instance.

+ +

At this point the code is still coming together but demonstrates that there is a local dev story, the system could be tested in CI, and can run in AWS.

+ +

Extending the system

+ +

So now POSTing to Lambda can write events to dynamodb. In the next post we will look at subscribing to and responding to that event stream.

+ +

All the code for this stage can be found on github

+ +
+ + +
+
+ + + + + + + + + + diff --git a/2018/02/serverless-4.html b/2018/02/serverless-4.html new file mode 100644 index 000000000..1996510f2 --- /dev/null +++ b/2018/02/serverless-4.html @@ -0,0 +1,659 @@ + + + + + + + + + + + + + + + + + + + + + + Serverless - Part Four + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Thu Mar 15 2018
+

Serverless - Part Four

+
+ +
+ +
+

Part One - describing event-driven and serverless systems

+ +

Part Two - Infrastructure as code walking skeleton

+ +

Part Three - SAM Local and the first event producer

+ +

In this post we start to see how we can build a stream of events that lets us create state. We'll do this by adding an event subscrber that waits until a user proposes a destination to visit and validates the location they've provided.

+ +

the slice being built in this article

+ + + +

Overview

+ +

This slice will prove that the system can subscribe to events occurring, react to them, and write new events back to the stream. That would only leave authentication, and a read model website to build to provide all the parts needed.

+ +

Subscribing and reacting to events demonstrates one of the benefits mentioned in part one. That these systems are composable. The additional code added here won't need any changes to the existing deployed applications. But can still add new behaviour to the system as a whole.

+ + + +

In part three we added a command handler that could write ProposedDestination events. Here a user is saying they think there is a place that parents would like to take their kids. The application accepts this to smooth their experience (and capture any proposal) and then responds to that event by checking the provided details before listing the new destination.

+ +

the event flow

+ +

So:

+ + + +
    +
  • one or more ProposedDestination events occur
  • +
  • The Location Validator is subscribed to those events
  • +
  • It reads each one and validates the provided location
  • +
  • Writing the success or failure event to the stream + +Notice here that the validator doesn't need to know what happens in case of success or failure. It doesn't even need to know whether there are applications that do something - there's no coupling of config or orchestration.
  • +
+ +

Twee Example

+ +

The first iteration will be a validator that confirms that an event has a geolocation key which has a numeric latitude and longitude.

+ +
{
+  "geolocation": {
+    "latitude": 1.23,
+    "longitude": 2.34
+  }
+}
+
+ +

This is a bit silly but the point here isn't to see what useful location validation looks like. Think of it as a walking skeleton into which more realistic geolocation like checking the coordinate is in the UK could be placed.

+ +

Infrastructure Changes

+ +

As discussed in part two DynamoDb already has the concept of streams of changes to tables as triggers for lambdas. Updating the SAM template to add the stream changes the definition to:

+ +
EventsTable:
+  Type: "AWS::DynamoDB::Table"
+  Properties:
+    AttributeDefinitions:
+      - AttributeName: StreamName
+        AttributeType: S
+      - AttributeName: EventId
+        AttributeType: S
+    KeySchema:
+      - AttributeName: StreamName
+        KeyType: HASH
+      - AttributeName: EventId
+        KeyType: RANGE
+    ProvisionedThroughput:
+      ReadCapacityUnits: 5
+      WriteCapacityUnits: 5
+    StreamSpecification:
+      StreamViewType: NEW_IMAGE
+
+ +

This change to add the StreamSpecification YAML key sets the stream of changes to only include the new version of the item. The valid options for StreamViewType are:

+ +
    +
  • KEYS_ONLY - Only the key attributes of the modified item are written to the stream.
  • +
  • NEW_IMAGE - The entire item, as it appears after it was modified, is written to the stream.
  • +
  • OLD_IMAGE - The entire item, as it appeared before it was modified, is written to the stream.
  • +
  • NEW_AND_OLD_IMAGES - Both the new and the old item images of the item are written to the stream.
  • +
+ +

Referring back to Fowler's four types of event driven systems from part One:

+ +
    +
  • KEYS_ONLY works for "Event Notification": the receiver knows a property changed and whether it wants to act (but not what changed).
  • +
  • NEW_IMAGE could map to "Event Assisted State Transfer" (EAST) unless the receiver needs access to the old version of the data. For example to write to your old and new address when your postal address changes. And could map to CQRS where the new_image could be either the command or the result of accepting the command
  • +
  • OLD_IMAGE doesn't map to any type but would be great for an audit log or system where data mustn't be lost.
  • +
  • NEW_AND_OLD_IMAGES maps well to EAST and CQRS.
  • +
+ +

25 characters to add the change. Over 1200 to disect it.

+ +

The Lambda…

+ +

… is again a composition root to allow unit testing without the external dependencies.

+ +
const mapDomainEvent = require("./destinations/location-validation/dynamoDbMap");
+
+const guid = require("./GUID");
+
+let streamRepo;
+const dynamoDbClient = require("./destinations/dynamoDbClient");
+const makeStreamRepository = require("./destinations/make-stream-repository");
+
+const geolocationValidator = require("./destinations/location-validation/geolocation-validator");
+
+const geolocationEventWriter = require("./destinations/location-validation/geolocation-validation-event-writer");
+let eventWriter;
+
+const makeEventSubscriber = require("./destinations/location-validation/event-subscriber");
+
+exports.handler = (event, context, callback) => {
+  const receivedEvents = mapDomainEvent.from(event);
+
+  streamRepo =
+    streamRepo || makeStreamRepository.for(dynamoDbClient.connect(), guid);
+  eventWriter = eventWriter || geolocationEventWriter.for(streamRepo);
+
+  const eventSubscriber = makeEventSubscriber.for(
+    geolocationValidator,
+    eventWriter
+  );
+
+  eventSubscriber.apply(receivedEvents, callback);
+};
+
+ +

Again some dependencies are initialised in the handler but memoised outside of it to reduce start-up time when a lambda is re-used.

+ +

It maps from the list of DynamoDB events received to a list of domain events and then passes those off to an eventSubscriber. The event subscriber has the validator and the resulting events writer injected.

+ +

The event subscriber is only interesting because it does some Promise fangling:

+ +
module.exports = {
+  for: (geolocationValidator, eventWriter) => ({
+    apply: (events, callback) => {
+      console.log(`received events: ${JSON.stringify(events)}`);
+
+      const writePromises = events.map((e) => {
+        return geolocationValidator
+          .tryValidate(e)
+          .then(() => {
+            return eventWriter.writeSuccess(e);
+          })
+          .catch((err) => {
+            return eventWriter.writeFailure(err, e);
+          });
+      });
+
+      Promise.all(writePromises)
+        .then(() =>
+          callback(null, `wrote ${writePromises.length} events to dynamodb`)
+        )
+        .catch((err) => callback(err));
+    },
+  }),
+};
+
+ +

Both the validator and the event writer return promises. The validator only to provide a nicer API. The writer because it is IO. Because of JavaScript's single-threaded "helpfulness" this could mean that your code finishes before the promises finish handing back to the Lambda's callback and terminating your code before it can complete.

+ +

This naive version does that:

+ +
module.exports = {
+  for: (geolocationValidator, eventWriter) => ({
+    apply: (events, callback) => {
+      console.log(`received events: ${JSON.stringify(events)}`)
+
+      events.forEach(e => {
+        geolocationValidator
+          .tryValidate(e)
+          .then(() => {
+            eventWriter.writeSuccess(e)
+          })
+          .catch(err => {
+            eventWriter.writeFailure(err, e)
+          })
+      })
+
+      callback(null, `wrote ${events} events to dynamodb`))
+    }
+  })
+}
+
+
+ +

Instead each Promise is captured and Promise.all is used to convert that list of promises into a single promise that only completes when they have all completed.

+ +

Testing that takes a bit of juggling but is relatively straight-forward:

+ +
const chai = require("chai");
+const dirtyChai = require("dirty-chai");
+chai.use(dirtyChai);
+const expect = chai.expect;
+
+const geolocationValidator = require("../../destinations/location-validation/geolocation-validator");
+const geolocationEventWriter = require("../../destinations/location-validation/geolocation-validation-event-writer");
+const makeEventSubscriber = require("../../destinations/location-validation/event-subscriber");
+
+const severalFakeEvents = () => {
+  const fakeEvent = {
+    event: {
+      geolocation: {
+        latitude: "0",
+        longitude: "0",
+      },
+    },
+  };
+  return [fakeEvent, fakeEvent, fakeEvent];
+};
+
+const simulateSlowWriteToDynamo = () => {
+  const now = new Date().getTime();
+  while (new Date().getTime() < now + 200) {
+    /* do nothing */
+  }
+};
+
+const assertAllOfTheEventsHaveWritten = (actual, expected) => {
+  expect(actual).to.equal(expected);
+};
+
+describe("the event subscriber can handle multiple events", function () {
+  it("without calling back it is finished before they write to dynamo", function (done) {
+    let writesCompleted = 0;
+
+    const fakeSlowStreamRepo = {
+      writeToStream: () => {
+        return new Promise((resolve, reject) => {
+          simulateSlowWriteToDynamo();
+          writesCompleted++;
+          resolve();
+        });
+      },
+    };
+
+    const eventWriter = geolocationEventWriter.for(fakeSlowStreamRepo);
+
+    const eventSubscriber = makeEventSubscriber.for(
+      geolocationValidator,
+      eventWriter
+    );
+
+    const events = severalFakeEvents();
+
+    eventSubscriber.apply(events, (err, complete) => {
+      expect(err).to.be.null();
+      expect(complete).to.not.be.null();
+
+      assertAllOfTheEventsHaveWritten(writesCompleted, events.length);
+
+      done();
+    });
+  });
+});
+
+ +

Here a fake, slow write is introduced and captures a count of completed writes.

+ +
let writesCompleted = 0;
+
+const fakeSlowStreamRepo = {
+  writeToStream: () => {
+    return new Promise((resolve, reject) => {
+      simulateSlowWriteToDynamo();
+      writesCompleted++;
+      resolve();
+    });
+  },
+};
+
+ +

This let the change above be test-driven. Running the test showed it failing before any delays occurring until the Promise.all change was introduced.

+ +

Woo-Hoo! Event-Driven!

+ +

Running deploy.sh and pushing a test API event in results in a DynamoDB table with the expected two events.

+ +

two events in a dynamodb table

+ +
{
+  "event": {
+    "correlationId": "2237661b-851b-4e78-3dfd-efe2436717d4",
+    "geolocation": {
+      "latitude": 3.14,
+      "longitude": 4.13
+    },
+    "name": "1",
+    "type": "destinationProposed"
+  },
+  "EventId": "a49fc63b-889f-4311-3b7f-efb6251d39c3",
+  "StreamName": "destination-2237661b-851b-4e78-3dfd-efe2436717d4"
+}
+
+ +
{
+  "event": {
+    "correlationId": "2237661b-851b-4e78-3dfd-efe2436717d4",
+    "triggeringEvent": "a49fc63b-889f-4311-3b7f-efb6251d39c3",
+    "type": "geolocationValidationSucceeded"
+  },
+  "EventId": "d8b75eb2-347f-4275-3ea4-7d1c934e6589",
+  "StreamName": "destination-2237661b-851b-4e78-3dfd-efe2436717d4"
+}
+
+ +

This is fantastic. All of the pieces for the event-driven back-end now exist.

+ +

It's not all golden. There's still quite a bit of manual testing necessary to check that the lambda's dependencies are declared correctly and wired together as expected. And to check that the system hangs together as expected.

+ +

At the moment that's not enough pain to stop moving forwards with the broad-brushstrokes implementation but it is getting close.

+ +

Next time we will add a read model and (depending on the length of the blog post that generates) view it via HTML.

+ +

All the code for this stage can be found on github

+ +

An aside on cost

+ +

So far this blog series has cost $0.09 in AWS charges relating to vistplannr. Almost all of which has been avoidable S3 charges.

+ +
+ + +
+
+ + + + + + + + + + diff --git a/2018/03/harmful-dry.html b/2018/03/harmful-dry.html new file mode 100644 index 000000000..e0c1c86a7 --- /dev/null +++ b/2018/03/harmful-dry.html @@ -0,0 +1,974 @@ + + + + + + + + + + + + + + + + + + + + + + DRY - considered harmful + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Fri Mar 30 2018
+

DRY - considered harmful

+
+
+ + +
+
+ +
+

DRY or WET?

+ +

DRY, in software development, stands for Don't Repeat Yourself. This is often taken to mean remove any duplication of lines of code. See the anti-example in the wiki page comparing to WET code - which stands for Write Everything Twice. This reinforces the idea that this is about the amount you type.

+ +

Below we're going to look at what the impact of removing duplication of lines of code does to some software, hopefully demonstrate that it isn't desirable as an absolute rule, and show what the better way might be.

+ + + +

We're making an internet cafe and so we need software to make internet drinks

+ +

+class Coffee(val milk: Int, val sugar: Int) {
+    override fun toString() = "pour a Coffee(milk=$milk, sugar=$sugar)"
+}
+
+fun main(args: Array<String>) {
+    val whiteCoffeeNoSugar = Coffee(1, 0)
+    println(whiteCoffeeNoSugar)
+}
+
+
+ +

This instructs the barista

+ +
pour a Coffee(milk=1, sugar=0)
+
+ +

Great, customers like it. Let's add tea!

+ +

+class Coffee(val milk: Int, val sugar: Int) {
+    override fun toString() = "Coffee(milk=$milk, sugar=$sugar)"
+}
+
+class Tea(val milk: Int, val sugar: Int) {
+    override fun toString() = "Tea(milk=$milk, sugar=$sugar)"
+}
+
+fun main(args: Array<String>) {
+    val whiteCoffeeNoSugar = Coffee(1, 0)
+    println(whiteCoffeeNoSugar)
+
+    val buildersTea = Tea(1, 3)
+    println(buildersTea)
+}
+
+
+ +

Woah! Wait! That violates DRY! There's loads of duplication. Let's remove it.

+ +

+abstract class Drink(
+  val name: String,
+  val milk: Int,
+  val sugar: Int
+) {
+    override fun toString() = "pour a $name(milk=$milk, sugar=$sugar)"
+}
+
+class Coffee(milk: Int, sugar: Int) : Drink("Coffee", milk, sugar)
+
+class Tea(milk: Int, sugar: Int) : Drink("Tea", milk, sugar)
+
+fun main(args: Array<String>) {
+    val whiteCoffeeNoSugar = Coffee(1, 0)
+    println(whiteCoffeeNoSugar)
+
+    val buildersTea = Tea(1, 3)
+    println(buildersTea)
+}
+
+
+ +

Phew, now we won't get thrown out of the Agiles and can carry on building our internet cafe global super-company.

+ +

Brilliant, that's DRY?

+ +

Yep, no repetition. Everything is hunky-dory.

+ +

The next feature request comes in.

+ +
As a User
+I want to add warm milk
+So that I can buy a warmer drink
+
+ +

(yep, I know that's an awful user story but this imaginary dev team think they are nailing it)

+ +

Taking advantage of named and default parameters we can add a warm milk property to drinks.

+ +

+abstract class Drink(
+  val name: String = "Drink",
+  val milk: Int = 0,
+  val warmMilk: Int = 0,
+  val sugar: Int = 0
+) {
+    override fun toString()
+      = "pour a $name(milk=$milk, warmMilk=$warmMilk, sugar=$sugar)"
+}
+
+class Coffee(milk: Int = 0, warmMilk: Int = 0, sugar: Int = 0)
+  : Drink("Coffee", milk, warmMilk, sugar)
+
+class Tea(milk: Int = 0, warmMilk: Int = 0, sugar: Int = 0)
+  : Drink("Tea", milk, warmMilk, sugar)
+
+fun main(args: Array<String>) {
+    val whiteCoffeeNoSugar = Coffee(milk = 1, sugar = 0)
+    println(whiteCoffeeNoSugar)
+
+    val buildersTea = Tea(milk = 1, sugar = 3)
+    println(buildersTea)
+
+    val warmerCoffee = Coffee(warmMilk =  2, sugar = 2)
+    println(warmerCoffee)
+}
+
+
+ +

And the next request:

+ +

+As a User
+I want to order chocolate sprinkles
+So that I can spend more money
+
+
+ +

+abstract class Drink(
+  val name: String = "Drink",
+  val milk: Int = 0,
+  val warmMilk: Int = 0,
+  val sugar: Int = 0,
+  val chocolateSprinkles: Int = 0
+) {
+    override fun toString()
+      = "pour a $name(milk=$milk, warmMilk=$warmMilk, sugar=$sugar, chocolateSprinkles=$chocolateSprinkles)"
+}
+
+class Coffee(milk: Int = 0, warmMilk: Int = 0, sugar: Int = 0, chocolateSprinkles: Int = 0)
+    : Drink("Coffee", milk, warmMilk, sugar, chocolateSprinkles)
+
+class Tea(milk: Int = 0, warmMilk: Int = 0, sugar: Int = 0)
+    : Drink("Tea", milk, warmMilk, sugar)
+
+fun main(args: Array<String>) {
+    val whiteCoffeeNoSugar = Coffee(milk = 1, sugar = 0)
+    println(whiteCoffeeNoSugar)
+
+    val buildersTea = Tea(milk = 1, sugar = 3)
+    println(buildersTea)
+
+    val warmerCoffee = Coffee(warmMilk =  2, sugar = 2)
+    println(warmerCoffee)
+
+    val mocha = Coffee(warmMilk =  2, sugar = 2, chocolateSprinkles = 4)
+    println(mocha)
+}
+
+
+ +

The money is pouring in. And our code is as DRY as possible. We added chocolate sprinkles without breaking a sweat.

+ +

Next we can add lemon in tea and hazelnut syrup in coffee in a single code change. We're on fire!

+ +

+abstract class Drink(
+  val name: String = "Drink",
+  val milk: Int = 0,
+  val warmMilk: Int = 0,
+  val sugar: Int = 0,
+  val chocolateSprinkles: Int = 0,
+  val lemon: Int = 0,
+  val hazelnutSyrup: Int = 0
+) {
+
+    override fun toString() =
+      "pour a $name(milk=$milk, " +
+        "warmMilk=$warmMilk, " +
+        "sugar=$sugar, " +
+        "chocolateSprinkles=$chocolateSprinkles, " +
+        "lemon=$lemon, " +
+        "hazelnutSyrup=$hazelnutSyrup)"
+}
+
+class Coffee(milk: Int = 0, warmMilk: Int = 0, sugar: Int = 0, chocolateSprinkles: Int = 0, hazelnutSyrup: Int = 0)
+    : Drink("Coffee", milk, warmMilk, sugar, chocolateSprinkles, hazelnutSyrup)
+
+class Tea(milk: Int = 0, warmMilk: Int = 0, sugar: Int = 0, lemon: Int = 0)
+    : Drink("Tea", milk, warmMilk, sugar, lemon)
+
+fun main(args: Array<String>) {
+    val whiteCoffeeNoSugar = Coffee(milk = 1, sugar = 0)
+    println(whiteCoffeeNoSugar)
+
+    val buildersTea = Tea(milk = 1, sugar = 3)
+    println(buildersTea)
+
+    val warmerCoffee = Coffee(warmMilk =  2, sugar = 2)
+    println(warmerCoffee)
+
+    val mocha = Coffee(warmMilk =  2, sugar = 2, chocolateSprinkles = 4)
+    println(mocha)
+}
+
+
+ +

Pretty soon after deployment disaster strikes! Barista Mike Acawfe reports

+ +
+

this latest software version is a disaster. It's adding lemon to hazelnut coffee and chocolate sprinkes to tea ordered with lemon.

+
+ +

ugh, I knew we should have tried that new fangled unit testing. All the code compiles but the position of the parameters matters in how they get from the Coffee and Tea classes to the base Drink class.

+ +

You can fix this with more named parameters!

+ +

+abstract class Drink(
+  val name: String = "Drink",
+  val milk: Int = 0,
+  val warmMilk: Int = 0,
+  val sugar: Int = 0,
+  val chocolateSprinkles: Int = 0,
+  val lemon: Int = 0,
+  val hazelnutSyrup: Int = 0
+) {
+
+    override fun toString() =
+      "pour a $name(milk=$milk, " +
+        "warmMilk=$warmMilk, " +
+        "sugar=$sugar, " +
+        "chocolateSprinkles=$chocolateSprinkles, " +
+        "lemon=$lemon, " +
+        "hazelnutSyrup=$hazelnutSyrup)"
+}
+
+class Coffee(milk: Int = 0, warmMilk: Int = 0,
+  sugar: Int = 0, chocolateSprinkles: Int = 0, hazelnutSyrup: Int = 0)
+  : Drink("Coffee", milk = milk, warmMilk = warmMilk,
+  sugar = sugar, chocolateSprinkles = chocolateSprinkles, hazelnutSyrup = hazelnutSyrup)
+
+class Tea(milk: Int = 0, warmMilk: Int = 0, sugar: Int = 0, lemon: Int = 0)
+  : Drink("Tea", milk = milk, warmMilk = warmMilk, sugar = sugar, lemon = lemon)
+
+fun main(args: Array<String>) {
+    val whiteCoffeeNoSugar = Coffee(milk = 1, sugar = 0)
+    println(whiteCoffeeNoSugar)
+
+    val buildersTea = Tea(milk = 1, sugar = 3)
+    println(buildersTea)
+
+    val warmerCoffee = Coffee(warmMilk =  2, sugar = 2)
+    println(warmerCoffee)
+
+    val mocha = Coffee(warmMilk =  2, sugar = 2, chocolateSprinkles = 4)
+    println(mocha)
+
+    val lemonTea = Tea(lemon = 1)
+    println(lemonTea)
+}
+
+
+ +

But, something is bothering you. It was hard to spot this bug because even though there's no duplication of code there's actually lots of duplication of names. This lovely DRY code uses the word milk nine times. In fact each of the ingredients is mentioned nine times. So any new ingredient means edits in nine places.

+ +

And the call through to the base class constructor duplicates the constructor on the line above. Any changes to the ingredients and you'll need to change both constructors.

+ +

You meet a friend for coffee and, since it's on your mind, ask how they would remove this last duplication?!

+ +

Each idea once and only once

+ +

Your friend explains that DRY isn't about code. It's about ideas! The reason you're struggling is that the idea that some drinks have milk and others lemon is hidden because you've treated removing lines of code as an absolute rule.

+ +

They offer to help you rewrite your code with that in mind.

+ +

The first idea that's missing is that there are types of ingredients.

+ +

The second idea is that each drink is a collection of ingredients that should be printed out for the baristas.

+ +

So you start with a marker interface and a set of data classes. Each drink then allows you to add a subset of the possible interfaces and prints out the barista's instructions.

+ +

At the same time you add the concept of temperature to milk so you don't have to have implicitly cold milk separately from warm milk.

+ +

+interface Ingredient
+
+enum class Temperature {
+    WARM, COLD
+}
+
+data class Sugar(val spoons: Int) : Ingredient
+data class Milk(val glugs: Int, val temperature: Temperature = Temperature.COLD) : Ingredient
+data class ChocolateSprinkles(val pinches: Int) : Ingredient
+data class HazelnutSyrup(val shots: Int) : Ingredient
+data class Lemon(val squeezes: Int) : Ingredient
+
+class Coffee {
+    private val ingredients: MutableList<Ingredient> = mutableListOf()
+
+    fun withIngredient(ingredient: Sugar): Coffee {
+        ingredients.add(ingredient)
+        return this
+    }
+
+    fun withIngredient(ingredient: Milk): Coffee {
+        ingredients.add(ingredient)
+        return this
+    }
+
+    fun withIngredient(ingredient: ChocolateSprinkles): Coffee {
+        ingredients.add(ingredient)
+        return this
+    }
+
+    fun withIngredient(ingredient: HazelnutSyrup): Coffee {
+        ingredients.add(ingredient)
+        return this
+    }
+
+    override fun toString()
+      = "pour a Coffee(${ingredients.joinToString(",")})"
+}
+
+class Tea {
+    private val ingredients: MutableList<Ingredient> = mutableListOf()
+
+    fun withIngredient(ingredient: Sugar): Tea {
+        ingredients.add(ingredient)
+        return this
+    }
+
+    fun withIngredient(ingredient: Milk): Tea {
+        ingredients.add(ingredient)
+        return this
+    }
+
+    fun withIngredient(ingredient: Lemon): Tea {
+        ingredients.add(ingredient)
+        return this
+    }
+
+    override fun toString()
+      = "pour a Tea(${ingredients.joinToString(",")})"
+}
+
+fun main(args: Array<String>) {
+    val whiteCoffeeNoSugar = Coffee().withIngredient(Milk(1))
+    println(whiteCoffeeNoSugar)
+
+    val sugaryCoffee = Coffee().withIngredient(Sugar(3))
+    println(sugaryCoffee)
+
+    val warmerCoffee = Coffee().withIngredient(Milk(2, Temperature.WARM)).withIngredient(Sugar(3))
+    println(warmerCoffee)
+
+    val mocha = Coffee()
+      .withIngredient(Milk(2, Temperature.WARM))
+      .withIngredient(Sugar(2))
+      .withIngredient(ChocolateSprinkles(4))
+    println(mocha)
+
+    val theFancyCoffee = Coffee()
+      .withIngredient(Milk(2, Temperature.WARM))
+      .withIngredient(Sugar(2))
+      .withIngredient(ChocolateSprinkles(4))
+      .withIngredient(HazelnutSyrup(2))
+
+    println(theFancyCoffee)
+
+    val buildersTea = Tea().withIngredient(Milk(1)).withIngredient(Sugar(3))
+    println(buildersTea)
+
+    val lemonTea = Tea().withIngredient(Lemon(1))
+    println(lemonTea)
+}
+
+
+ +

This prints out

+ +

+pour a Coffee(Milk(glugs=1))
+pour a Coffee(Sugar(spoons=3))
+pour a Coffee(WarmMilk(glugs=2),Sugar(spoons=3))
+pour a Coffee(WarmMilk(glugs=2),Sugar(spoons=2),ChocolateSprinkles(pinches=4))
+pour a Coffee(WarmMilk(glugs=2),Sugar(spoons=2),ChocolateSprinkles(pinches=4),HazelnutSyrup(shots=2))
+pour a Tea(Milk(glugs=1),Sugar(spoons=3))
+pour a Tea(Lemon(squeezes=1))
+
+
+ +

Notice the awesome toString output that Kotlin's data classes give you for the Ingredients.

+ +

Now the word milk is only in the code three times. Once when it is declared and once in each drink.

+ +

But there is still duplication of the idea that a drink can have ingredients added. In fact each drink has almost the same method repeated for each ingredient. All to avoid being able to put chocolate sprinkles in tea.

+ +

So the idea that chocolate sprinkles aren't a tea ingredient is implicit in the fact that there's no method for it. It isn't represented once and only once.

+ +

One option is to accept any ingredient to the method but explicitly refuse ones that shouldn't be added

+ +

+class Coffee {
+    class ItIsNotOKToPutLemonInCoffee : Throwable()
+
+    private val ingredients: MutableList<Ingredient> = mutableListOf()
+
+    fun withIngredient(ingredient: Ingredient): Coffee {
+        if (ingredient is Lemon) {
+            throw ItIsNotOKToPutLemonInCoffee()
+        }
+
+        ingredients.add(ingredient)
+        return this
+    }
+
+    override fun toString()
+      = "pour a Coffee(${ingredients.joinToString(",")})"
+}
+
+class Tea {
+    class ItIsNotOKToPutThisIngredientInTea(ingredient: Ingredient)
+      : Throwable("It is not OK to put $ingredient in tea")
+
+    private val ingredients: MutableList<Ingredient> = mutableListOf()
+
+    fun withIngredient(ingredient: Ingredient): Tea {
+        if (ingredient is HazelnutSyrup
+            || ingredient is ChocolateSprinkles) {
+            throw ItIsNotOKToPutThisIngredientInTea(ingredient)
+        }
+
+        ingredients.add(ingredient)
+        return this
+    }
+
+    override fun toString()
+      = "pour a Tea(${ingredients.joinToString(",")})"
+}
+
+
+ +

But there's still duplication of the idea. You'll have to change Tea or Coffee any time you add a new ingredient. And even though the withIngredient method only knows about the marker interface in its signature it has to know about concrete implementations of the interface to work. Yuk!

+ +

+enum class Temperature {
+    WARM, COLD
+}
+
+interface Ingredient {
+    fun canBeAddedTo(drink: Drink) = true
+}
+
+data class Sugar(val spoons: Int) : Ingredient
+data class Milk(val glugs: Int, val temperature: Temperature = Temperature.COLD) : Ingredient
+
+data class ChocolateSprinkles(val pinches: Int) : Ingredient {
+    override fun canBeAddedTo(drink: Drink) = drink is Coffee
+}
+
+data class HazelnutSyrup(val shots: Int) : Ingredient {
+    override fun canBeAddedTo(drink: Drink) = drink is Coffee
+}
+
+data class Lemon(val squeezes: Int) : Ingredient {
+    override fun canBeAddedTo(drink: Drink) = drink is Tea
+}
+
+abstract class Drink {
+    class IsNotFitForConsumptionWithThisIngredient(ingredient: Ingredient, drink: Drink)
+      : Throwable("It is not OK to put $ingredient in ${drink.javaClass.simpleName}")
+
+    private val ingredients: MutableList<Ingredient> = mutableListOf()
+
+    fun withIngredient(ingredient: Ingredient): Drink {
+        if (!ingredient.canBeAddedTo(this)) {
+            throw Drink.IsNotFitForConsumptionWithThisIngredient(ingredient, this)
+        }
+
+        ingredients.add(ingredient)
+        return this
+    }
+
+    override fun toString()
+      = "pour a ${this.javaClass.simpleName}(${ingredients.joinToString(",")})"
+}
+
+class Coffee : Drink()
+
+class Tea : Drink()
+
+
+ +

So now ingredients know whether they can be added to a drink. They default to it being ok that they are added to any drink

+ +

+interface Ingredient {
+    fun canBeAddedTo(drink: Drink)= true
+}
+
+
+ +

but can be specified as allowed only for certain drinks

+ +

+data class ChocolateSprinkles(val pinches: Int) : Ingredient {
+    override fun canBeAddedTo(drink: Drink) = drink is Coffee
+}
+
+
+ +

This means that new ingredients that are added shouldn't need any modifications to the drinks.

+ +

Now Drink as an abstract class reappears. The individual drinks now only need to have a type for the canBeAddedTo(drink:Drink) check. It's ok to allow code to get more complex while you're working on it as happened here when the withIngredient methods were exploded into Coffee and Tea.

+ +

+abstract class Drink {
+    class IsNotFitForConsumptionWithThisIngredient(ingredient: Ingredient, drink: Drink) : Throwable("It is not OK to put $ingredient in ${drink.javaClass.simpleName}")
+
+    private val ingredients: MutableList<Ingredient> = mutableListOf()
+
+    fun withIngredient(ingredient: Ingredient): Drink {
+        if (!ingredient.canBeAddedTo(this)) {
+            throw Drink.IsNotFitForConsumptionWithThisIngredient(ingredient, this)
+        }
+
+        ingredients.add(ingredient)
+        return this
+    }
+
+    override fun toString()
+      = "pour a ${this.javaClass.simpleName}(${ingredients.joinToString(",")})"
+}
+
+
+ +

I'm still confused by Java allowing methods in interfaces. Ingredient can be an interface but because Drink wants to override ToString it has to be an abstract class. Without that it could be an interface too ¯\_(ツ)_/¯

+ +

One idea that is still implicit is that the ingredients are printed out for the barista. So let's add an OrderPrinter and take the need to descibe itself out of the Drink

+ +

We can also take the opportunity, since we're exposing the drink's ingredients, to make them an immutable list.

+ +

+enum class Temperature {
+    WARM, COLD
+}
+
+interface Ingredient {
+    fun canBeAddedTo(drink: Drink) = true
+}
+
+data class Sugar(val spoons: Int) : Ingredient
+data class Milk(val glugs: Int, val temperature: Temperature = Temperature.COLD) : Ingredient
+
+data class ChocolateSprinkles(val pinches: Int) : Ingredient {
+    override fun canBeAddedTo(drink: Drink) = drink is Coffee
+}
+
+data class HazelnutSyrup(val shots: Int) : Ingredient {
+    override fun canBeAddedTo(drink: Drink) = drink is Coffee
+}
+
+data class Lemon(val squeezes: Int) : Ingredient {
+    override fun canBeAddedTo(drink: Drink) = drink is Tea
+}
+
+abstract class Drink {
+    class IsNotFitForConsumptionWithThisIngredient(ingredient: Ingredient, drink: Drink)
+        : Throwable("It is not OK to put $ingredient in ${drink.javaClass.simpleName}")
+
+    var ingredients: List<Ingredient> = emptyList()
+        private set
+
+    fun withIngredient(ingredient: Ingredient): Drink {
+        if (!ingredient.canBeAddedTo(this)) {
+            throw Drink.IsNotFitForConsumptionWithThisIngredient(ingredient, this)
+        }
+
+        ingredients += ingredient
+        return this
+    }
+}
+
+class OrderPrinter {
+    companion object {
+        fun instructBarista(drink:Drink)
+          = "pour a ${drink.javaClass.simpleName}(${drink.ingredients.joinToString(",")})"
+    }
+}
+
+class Coffee : Drink()
+
+class Tea : Drink()
+
+fun main(args: Array<String>) {
+    val whiteCoffeeNoSugar = Coffee()
+      .withIngredient(Milk(1))
+
+    println(OrderPrinter.instructBarista(whiteCoffeeNoSugar))
+
+    val sugaryCoffee = Coffee()
+      .withIngredient(Sugar(3))
+
+    println(OrderPrinter.instructBarista(sugaryCoffee))
+
+    val warmerCoffee = Coffee()
+      .withIngredient(Milk(2, Temperature.WARM))
+      .withIngredient(Sugar(3))
+
+    println(OrderPrinter.instructBarista(warmerCoffee))
+
+    val mocha = Coffee()
+      .withIngredient(Milk(2, Temperature.WARM))
+      .withIngredient(Sugar(2))
+      .withIngredient(ChocolateSprinkles(4))
+
+    println(OrderPrinter.instructBarista(mocha))
+
+    val theFancyCoffee = Coffee()
+      .withIngredient(Milk(2, Temperature.WARM))
+      .withIngredient(Sugar(2))
+      .withIngredient(ChocolateSprinkles(4))
+      .withIngredient(HazelnutSyrup(2))
+
+    println(OrderPrinter.instructBarista(theFancyCoffee))
+
+    val buildersTea = Tea()
+      .withIngredient(Milk(1))
+      .withIngredient(Sugar(3))
+
+    println(OrderPrinter.instructBarista(buildersTea))
+
+    val lemonTea = Tea().withIngredient(Lemon(1))
+
+    println(OrderPrinter.instructBarista(lemonTea))
+
+    try {
+        Tea().withIngredient(ChocolateSprinkles(2))
+    } catch (e: Drink.IsNotFitForConsumptionWithThisIngredient) {
+        println("excellently did not allow chocolate in tea: $e")
+    }
+}
+
+
+ +

This is about twice as much code as the original DRY version. But is much more flexible for adding new ingredients without changing existing code. What DRY misses is the much more expressive four rules of simple design.

+ +
    +
  1. Runs all the tests
  2. +
  3. Has no duplicated logic. Be wary of hidden duplication like parallel class hierarchies
  4. +
  5. States every intention important to the programmer
  6. +
  7. Has the fewest possible classes and methods
  8. +
+ +

These are in order of importance. The code in this article is manually tested but doesn't pass this as the rule is runs all the tests. Before fixing anything else my fictional friend should have made me write tests.

+ +

Rules 2, 3 and 4 are in tension with each other. If I want to state every intention to the future reader I can't only remove as many classes and methods as possible. The wonderful design pressure as I tried to show here is that you want the smallest amount of code to communicate the largest amount of the ideas it represents.

+ +

So, stop looking for duplicated lines of code. Stop automatically making every string a constant. And start having empathy for the future reader of your code. Leave as little of the information needed to change the code in your brain as possible by putting it in the code.

+ +

All of the code can be found on Github

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2018/06/serverless-5.html b/2018/06/serverless-5.html new file mode 100644 index 000000000..793d5a8e5 --- /dev/null +++ b/2018/06/serverless-5.html @@ -0,0 +1,590 @@ + + + + + + + + + + + + + + + + + + + + + + Serverless - Part Five - Read Models + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sun Jun 10 2018
+

Serverless - Part Five - Read Models

+
+ +
+ +
+

Part One - describing event-driven and serverless systems

+ +

Part Two - Infrastructure as code walking skeleton

+ +

Part Three - SAM Local and the first event producer

+ +

Part Four - Making streams of events

+ +

OK, four months since part four. I got a puppy and have written the code for this part of the series in 2 minute blocks after sleepless nights. Not a productive way to do things!

+ + + +

Getting ready to make some HTML

+ +

Now that the API lets clients propose destinations to the visit plannr the home page for the service can be built. It's going to show the most recently updated destinations.

+ +

In a CRUD SQL system the application would have been maintaining the most up-to-date state of each destination in SQL and you'd read them when the HTML is requested. But this application isn't storing the state of the destinations but the facts that it has been told about the destinations.

+ +
+

As an aside a lot of people don't realise that CRUD SQL stands for C an we R eally not U se SQL D atabases they may S eem familiar but all the ORM stuff is well over our Q uota for comp L icated dependencies.

+
+ +

In an event driven system applications subscribe to be notified when new events occur. They can create read models as the events arrive. Those read models are what the application uses to, erm, read data. So they're used in places many applications make SQL queries. Now this visit plannr application needs a read model for recently updated destinations.

+ + + +

What even is a Read Model?

+ +
+

The query (or read) model is a denormalized data model. It is not meant to deliver the domain behaviour, only data for display (and possibly reporting).

+
+ +
+

CQRS-based views can be both cheap and disposable … any single view could be rewritten from scratch in isolation or the entire query model be switched to completely different persistence technology

+
+ +
    +
  • both from Page 141 Implementing Domain Driven Design by Vaughn Vernon
  • +
+ +

A CQRS system (see part 1) separates the parts of the application(s) that receives commands to change from those that receive queries for data. Read models are the data models for the read side of the application. This lets you optimise different areas for their specific tasks.

+ +

Read models are a representation of the data built for a particular query. You can reuse read models. However in a CQRS or eventsourced system you tend to make many read models.

+ +

If Sam and Jamie both come to my house to help me garden my eventstream would be:

+ +
[
+  {name: "Sam", type: "CameToGarden"},
+  {name: "Jamie", type: "CameToGarden"}
+]
+
+ +

I can build two readmodels from this:

+ +
{helperCount: 2}
+
+ +
{peopleHelping: ["Sam", "Jamie"]}
+
+ +

So each read model in a system is a different way of representing written data in order to serve a particular need. Think of them as different SQL projections or views over tables. They aren't the data they're something built from the data that lets you show it to someone.

+ +

A wonderful thing about read models (in an eventsourced system at least) is that you can throw them away. Imagine a SQL database that you can delete once you don't like its shape. In a system with read models you can change your code, reset the system that builds the read model to start at the beginning of history, and let it create the new read model.

+ +

Work an example

+ +

without any events

+ +

Let's imagine an eventsourced ecommerce application with no events. Sales and fulfilment teams need to know how much money we've made, how many orders we've taken, and what products have been sold.

+ +

We've deployed 3 separate applications that are subscribed to the empty event stream.

+ +

after one event

+ +

Big day - the first sale! myshop.com writes an event to the stream that we've sold a t-shirt. The sales, order count, and products sold read models update and any UI or report being generated using them can update accordingly.

+ +

after many events

+ +

Many days and events have passed and after the most recent cancelled order the fulfilment team let you know that it's really hard for them to figure out what's happening when an order is cancelled. They'd like a view to help them manage cancellations.

+ +

when the new read model is deployed

+ +

So a new read model is built and deployed to track order cancellations. The existing read models are all up-to-date on event 300. When the new application starts its read model isn't showing any cancelled orders and it has read 0 events.

+ +

(important to note that no other applications had to change at all to support this!)

+ +

when the new read model is caught up

+ + + +

The new application reads through the event stream until it has caught up. There's a period of time where it is reading through the event stream and performing any calculations or running any logic where it isn't caught up with the other read models or with the write side of the applications.

+ +

This is 'eventual consistency'. An event sourced system embraces the benefits of not trying to force all the parts of the application to stay exactly in sync with each other all the time.

+ +

adding a graph database read model

+ +

As the website gets more popular storing the products sold in an array is limiting what business intelligence the sales team can gather. You can add a consumer that stores products sold in a graph database.

+ +

As your new data science capability learns what structure they want in this new data store it is possible to keep deleting the graph store and letting it recreate from the event stream. Again this is an addition that doesn't need changes to the existing applications.

+ +

Why a Read Model now?

+ +

The system has a command channel to propose destinations, and an event subscriber that validates the proposed destination. Now a new event subscriber can respond to each event in a destination stream and create or update a read model used to let people view the destinations on the website.

+ +

How to make a read model in this system?

+ +

If this system was a long running process it would start, read all the events from the beginning of time (or the last snapshot), build a read model in-memory, and start serving requests once the read model was up-to-date with the event stream.

+ +

It also subscribes to the event stream so each subsequent event written to the stream is applied to the read model store. Even with millions of events in a stream once the system has caught up it is only applying one event at a time. Only applying one event can be incredibly fast!

+ +

And as in the graph database example above read models don't have to be in-memory. They can be pretty much anywhere. You can run graph databases, document databases, sql databases, and flat files side-by-side as read models for different uses.

+ +

Serverless systems only run for the lifetime of each request and so need to start as fast as possible. Building the read model from scratch on-start can be treated as too slow and we'll decide to store the read model in dynamodb.

+ +

The lambda

+ +

This is kept as a port into the system

+ +
exports.handler = async (event) => {
+  streamReader =
+    streamReader ||
+    makeStreamRepository.for(
+      eventsTableName,
+      dynamoDbClient.documentClient(),
+      guid
+    );
+
+  readModelWriter =
+    readModelWriter ||
+    makeReadModelRepository.for(
+      readModelsTableName,
+      dynamoDbClient.documentClient(),
+      guid
+    );
+
+  const writes = await readModelUpdateHandler
+    .withStreamReader(streamReader)
+    .withReadModelWriter(readModelWriter)
+    .allowingModelsWithStatus(terminalEventType)
+    .writeModelsFor(event);
+
+  return Promise.all(writes);
+};
+
+ +

It initialises a stream reader and a model writer then curries a handler function which receives the event that triggered the lambda. Accepting a terminalEventType so destinations that shouldn't be shown to users yet can be filtered out. Finally waiting for any dynamodb writes to be gathered and passes those promises back to the executing environment so it can wait for them to complete.

+ +

The handler is small.

+ +
const destinationReadModel = require("./destinationReadModel.js");
+const streamNames = require("./streamNames");
+
+module.exports = {
+  withStreamReader: (streamReader) => ({
+    withReadModelWriter: (modelWriter) => ({
+      allowingModelsWithStatus: (status) => ({
+        writeModelsFor: async (event) => {
+          console.log(`processing trigger event: ${JSON.stringify(event)}`);
+
+          const readPromises = streamNames
+            .from(event.Records)
+            .map((cs) => streamReader.readStream({ streamName: cs }));
+
+          const streamsOfEvents = await Promise.all(readPromises);
+
+          const writes = streamsOfEvents
+            .map((streamOfWrappedEvents) =>
+              streamOfWrappedEvents.map((x) => x.event)
+            )
+            .map(destinationReadModel.apply)
+            .filter((m) => m.status === status)
+            .map(modelWriter.write);
+
+          return writes;
+        },
+      }),
+    }),
+  }),
+};
+
+ +

The triggering event could contain more than one dyanamodb update so:

+ +
const readPromises = streamNames
+  .from(event.Records)
+  .map((cs) => streamReader.readStream({ streamName: cs }));
+
+const streamsOfEvents = await Promise.all(readPromises);
+
+ +

Remember each event is appended onto the end of a stream of events that represents an instance of a particular domain concept. So each destination has its own stream of events that make up the history of that destination. This code reads the stream name from each of the events that triggered the lambda and reads all of the events from each of those streams from dynamodb.

+ +
const writes = streamsOfEvents
+  .map((streamOfWrappedEvents) => streamOfWrappedEvents.map((x) => x.event))
+  .map(destinationReadModel.apply)
+  .filter((m) => m.status === status)
+  .map(modelWriter.write);
+
+ +

each stream of events is applied to a destinationReadModel which are filtered to keep only those with the desired status. Those models are then written to dynamodb so other applications can query them.

+ +
module.exports = {
+  apply: (events) => {
+    const readModel = events.reduce(
+      (model, event) => {
+        switch (event.type) {
+          case "destinationProposed":
+            model.name = event.name;
+            model.geolocation = event.geolocation;
+            model.timestamp = event.timestamp;
+            break;
+          case "geolocationValidationSucceeded":
+            model.status = "locationValidated";
+            break;
+          case "geolocationValidationFailed":
+            model.status = "failed";
+            break;
+        }
+
+        return model;
+      },
+      { status: "pending", type: "destination" }
+    );
+
+    console.log(
+      `built readmodel ${JSON.stringify(
+        readModel
+      )} from events ${JSON.stringify(events)}`
+    );
+    return readModel;
+  },
+};
+
+ +

Building the read model involves taking each event and updating a model based on the event type. Here you can see how this code is tolerant of events it isn't expecting - it will ignore them.

+ +

There's no validation that the data being read from the events is present. Whether there should be validation at this stage is context dependent. Here we wrote the event producers and know that for there to be a geolocationValidationSucceeded event both name and geolocation have to be present. We can trust that the read model will be good enough for now.

+ +

What's next?

+ +

Now that read models are being stored in dynamodb the next step is to generate a home page. Because the read models are writing to a dynamodb table they can be treated as a projection (read models that can be treated as an eventstream and subscribed to) and we can generate static HTML when the read models change.

+ +

All the code for this post can be found here on github.

+ +
+ + +
+
+ + + + + + + + + + diff --git a/2018/07/serverless-6.html b/2018/07/serverless-6.html new file mode 100644 index 000000000..00c35f984 --- /dev/null +++ b/2018/07/serverless-6.html @@ -0,0 +1,640 @@ + + + + + + + + + + + + + + + + + + + + + + Serverless - Part Six - Making a view + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sun Jul 01 2018
+

Serverless - Part Six - Making a view

+
+ +
+ +
+

Part One - describing event-driven and serverless systems

+ +

Part Two - Infrastructure as code walking skeleton

+ +

Part Three - SAM Local and the first event producer

+ +

Part Four - Making streams of events

+ +

Part Five - Making a read model

+ +

In part 5 the code was written to make sure that whenever a destination changes the recent destinations read model will update. Now that read model can be used to realise a view that a human can use. We'll add code to create a HTML view behind AWS cloudfront. This will demonstrate how event driven systems can be created by adding new code instead of changing existing code.

+ + + +

Where were we?

+ +

a sequence diagram of the system so far

+ +

The system so far allows an API call to propose a destination someone might want to visit. When that ProposeDestination command is received after a little validation a DestinationProposed event might be saved to DynamoDB. A lambda is subscribed and when that event is raised validates the location of the destination - you can't visit somewhere that isn't anywhere after all. That lambda saves either a geolocationValidationSucceeded or geolocationValidationFailed event to DynamoDB.

+ +

Right now there are no consumers of the geolocationValidationFailed event. When one is necessary, for example to let the destination proposer know we need their help to correct the record, nothing already written has to change. A new subscriber would be added alongside.

+ +

The last change was to add a subscriber to any event in the stream. An event being received is a strong indication that a destination changed so its job was to make sure that destination is stored or updated in DynamoDB.

+ +

Most systems only save that final data and throw away all the other lovely information. Maybe they come pretty close to saving the same information because they write it as log messages. A form of event stream that is very difficult to consume.

+ +

ReadModels are Projections are ReadModels

+ +

If you're familiar with SQL then you've typed something like Select name, thing, clink, andStuff from myTable before now. That list of properties is the projection. The structural representation of the data in the store that will be the query result.

+ +

Since a read model is a representation of the data that is provided for one or more reads or queries then a read model is a projection.

+ +

Generally speaking you can say either ReadModel or Projection. Some people distinguish between ReadModel as a set of data and Projection as a stream of that data which might be a useful distinction.

+ +

Where does this change take us?

+ +

This set of changes puts the system in a position to be able to serve a HTML home page that shows the most recently changed destinations. Get the champagne on ice this start-up is heading to exit.

+ +

the new sequence diagram

+ +

This change adds two new lambdas (or subscribers, or consumers).

+ +

Creating HTML

+ +

The destination stored to DynamoDB is our read model but in the beauty of an event driven system can also be subscribed to. This means we don't have to read from DynamoDB when somebody wants to put that data into HTML to display to a user.

+ + + +

Because the system is event driven we know whether the data has changed so we have an hook for cache invalidation. That means the system can generate HTML whenever the set of destinations changes.

+ +

The handler should look familar now:

+ +
const AWS = require("aws-sdk");
+const region = process.env.AWS_REGION || "eu-west-2";
+const s3 = new AWS.S3({ region });
+
+let documentClient;
+const dynamoDbClient = require("./destinations/dynamoDbClient");
+const lambdaHandler = require("./destinations/homepage/handler");
+const readModelsTableName =
+  process.env.READMODEL_TABLE || "vistplannr-readmodels";
+
+exports.handler = async (event) => {
+  documentClient = documentClient || dynamoDbClient.documentClient();
+
+  const handler = lambdaHandler
+    .withTableName(readModelsTableName)
+    .withDocumentClient(documentClient)
+    .withStorage(s3, "visitplannr-site-home");
+
+  const promise = handler(event);
+
+  promise.catch((err) => console.log(err, "error generating homepage"));
+  return promise;
+};
+
+ +

It acts as the composition root, gathers dependencies, injects the dependencies into the system, and handles the event. So the code can be tested completely decoupled from the dependencies.

+ +

The actual code is straight forward. Read something from one place, transform it, and write it to another

+ +
const guid = require("../../GUID.js");
+const makeReadModelRepository = require("../destinations-read-model/make-readmodel-repository.js");
+const generate = require("./htmlGenerator");
+
+const writeToS3 = (html, s3, bucketName) => {
+  const params = {
+    ACL: "public-read",
+    Body: html,
+    Key: "index.html",
+    ContentType: "text/html",
+    Bucket: bucketName,
+  };
+
+  return s3.upload(params).promise();
+};
+
+module.exports = {
+  withTableName: (tableName) => ({
+    withDocumentClient: (dynamoDbClient) => ({
+      withStorage: (s3, bucketName) => {
+        const readModelRepo = makeReadModelRepository.for(
+          tableName,
+          dynamoDbClient,
+          guid
+        );
+
+        return async (event) => {
+          console.log(event, "triggering event");
+
+          // we don't care about the event o_O
+          const destinations = await readModelRepo.read(5);
+          console.log(
+            destinations,
+            "loaded destinations for home page generation"
+          );
+
+          const html = generate.homepage(destinations.Items);
+          return writeToS3(html, s3, bucketName);
+        };
+      },
+    }),
+  }),
+};
+
+ +

So now whenever there's an event the HTML is templated and written to S3.

+ +

Invalidating the Content Delivery Network's Cache

+ +

The bucket that the templated HTML is written to is being served as a static site behind the CloudFront CDN. A CDN is a bunch of computers that cache a copy of your content close to the edge of the network so that it can be delivered to users as quickly as possible.

+ +

Because the HTML is behind a CDN writing to the bucket isn't enough. The CDN carries on serving the old cached content. So writing to S3 will also need to invalidate the CDN's cache.

+ +

That could be done in the same lambda as writes the event to S3 but writing to S3 is itself a lambda trigger. So we can encode the behaviour "when the static content changes invalidate the cache" instead of "when this particular reason for the content to change happens invalidate the cache".

+ +

Having a separate lambda continues to demonstrate you can take advantage of the additive nature of an event driven system.

+ +
const AWS = require("aws-sdk");
+const cloudfront = new AWS.CloudFront();
+const ssm = new AWS.SSM();
+const fileChanged = require("./destinations/cloudfront/handler.js");
+const timestamps = require("./destinations/timestamps.js");
+
+let cloudfrontDistributionIdParam;
+
+exports.handler = async (event) => {
+  cloudfrontDistributionIdParam =
+    cloudfrontDistributionIdParam ||
+    (await ssm.getParameter({ Name: process.env.PARAM_NAME }).promise());
+
+  const invalidations = await fileChanged
+    .withCDN(cloudfront)
+    .withDistribution(cloudfrontDistributionIdParam.Parameter.Value)
+    .withTimestampSource(timestamps)
+    .invalidate(event);
+
+  return Promise.all(invalidations);
+};
+
+ + + +

In what should be a familiar pattern now the dependencies are gathered, curried into the actual application code, invoked, and the results passed back out to the lambda environment to signal success or failure.

+ + + +

The notable difference here is the introduction of a new AWS dependency: AWS SSM. Simple Systems Manager (SSM) is a wide set of services to let you manage and configure Amazon AWS systems. The piece being used is Parameter Store.

+ +

This is a service that allows you to store plain text or encrypted config. Used here to store and provide the cloudfront distribution id. Why Parameter Store is being used is covered below.

+ +

The handler then uses the timestamp and the event to make a unique(ish) id for the invalidation and shapes the correct call to CloudFront to invalidate the cache.

+ +

a gif demoing API calls being translated to HTML

+ +

What made this a fast change?

+ +

Almost everything necessary already existed

+ +
    +
  • test mechanism
  • +
  • event streams
  • +
  • CloudFormation templates + +Continuing to bang the drum for why event driven systems are so productive… Almost the entire change was the functional code to read, transform, and then write. Because the system complexity has been pushed up to the architecture the individual blocks can be simple.
  • +
+ +

Both lambdas were written in an evening.

+ +

What blew up and stopped it being a fast change?

+ +

CloudFormation was not happy with what I was trying to do… In setting out the template to add the CloudFront distribution, static site bucket, policies allowing public read from the bucket, and the two lambdas I created a circular dependency.

+ +

And had no idea what to do next :(

+ + + +

Luckily I know how to toot! And the lovely Heitor Lessa from AWS gave me some pointers. I particularly love that he laid out part of the path without giving me the solution - I didn't have the tools to investigate myself but will do next time now.

+ +

In the CloudFormation template the cloud front distribution ID was being set as an environment variable on the lambda that would need it. But from the help provided

+ +
# This Environment block creates the circular dependency
+## CF needs S3 to be created first
+#### Lambda needs CF and S3 to be created first
+##### S3 needs S3->Lambda permission to be created first
+###### [Fails] S3->Lambda permission needs Lambda to be created first
+###### --> This circles back to point 2
+
+ +

This seems to be an unavoidable effect of how CloudFormation works partly because I couldn't use an S3 bucket as an event source for a lambda if it wasn't defined in the same template. So I couldn't split the templates and pass data as identifiers from one to the other.

+ +

My colleagues were particularly helpful

+ +

advice on twitter to not use cloudformation

+ +

The best solution (we could think of) was to put the ID into parameter store from the cloudformation template to break the circular dependency.

+ + + +

I've been avoiding abstractions like terraform or the confusingly named serverless framework while writing this series so that I understood the nuts and bolts and this was the first time I came close to regretting this decision. Always frustrating to have things broken without knowing what to do next :'(

+ +

Two standout pieces of advice I received:

+ +
    +
  1. The SAM template will generate additional CloudFormation resources for you (to save you typing them). You can reference them in the template.
  2. +
+ +

So in this resource description

+ +
  CloudfrontFunctionPermissions:
+      Type: "AWS::IAM::Policy"
+      Properties:
+          PolicyName: "CloudfrontCacheInvalidation"
+          PolicyDocument:
+              Version: "2012-10-17"
+              Statement:
+                  -
+                      Effect: "Allow"
+                      Action: "cloudfront:CreateInvalidation"
+                      Resource: "*"
+          Roles:
+              - !Ref CloudfrontInvalidatingFunctionRole
+
+ +

The !Ref CloudfrontInvalidatingFunctionRole is referencing a role in the template that isn't in the template until SAM has converted it to a full CloudFormation template o_O.

+ +

I think this is confusing but it's good to know.

+ +
    +
  1. You can use cfn-python-lint to lint CloudFormation templates. It gives much better output than you get elsewhere!
  2. +
+ +

Cost

+ +

There was a lot of CloudFormation stack creation and deletion as a result of all of this. So I was very disappointed to see that it had pushed my monthly bill up gigantically.

+ +

the aws bill for 6 cents

+ +

This might seem like a silly point but creating a similarly resilient application with a serverful architecture would probably be

+ +
    +
  • 1 load balancer and 2 virtual machines for the application
  • +
  • 3 virtual machines for the eventstore
  • +
  • 1 load balancer and 2 virtual machines for an API gateway
  • +
+ +

(yep, and networks and security groups and and and)

+ +

That gives a monthly cost of at least $100 standing idle. I'm much happier to be stung for 6 cents.

+ +

What's the TODO list now?

+ +

We have most of the basic building blocks but only someone comfortable calling an API directly can propose a destination. The next steps from a system behaviour perspective will be to start to add a UI to propose destinations. This will start to call out the need for authentication and authorisation if it doesn't demand it outright.

+ +

From a developer's health perspective we've got quite a lot of code now. There's no bundling so the upload to S3 contains more than it needs to and it's JS - I love JS - but there're no types which can start to get confusing.

+ +

Also any system level testing is all manual at the moment which isn't good enough. There needs to be a way to visualise what is there, what it is doing, and that it works.

+ +
    +
  • visualise deployed system
  • +
  • observe deployed system
  • +
  • test deployed system
  • +
  • bundle JS
  • +
  • add Typescript
  • +
  • add a propose destination form
  • +
  • add auth to the system
  • +
+ +

The code for this part can be found on github

+ +
+ + +
+
+ + + + + + + + + + diff --git a/2019/11/serverless-lessons-learned.html b/2019/11/serverless-lessons-learned.html new file mode 100644 index 000000000..7d8d8d78d --- /dev/null +++ b/2019/11/serverless-lessons-learned.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + Serverless - Lessons learned + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sat Nov 30 2019
+

Serverless - Lessons learned

+
+ +
+ +
+

At the 2019 Manchester Java Unconference I attended a discussion on "Cloud Native Functions". It turned out nobody in the group had used "cloud native" but I've been working with teams using serviceful systems.

+ +

I have a bad habit of talking more than I should but, despite my best efforts, the group expressed interest in hearing what teams at Co-op Digital had learned in the last ten months or so of working with serviceful systems in AWS.

+ +

We defined some terms, covered some pitfalls and gotchas, some successes, and most of all our key learning: that once you can deploy one serviceful system into production you can move faster than you ever have before.

+ +

Let's spend a little while defining our terms…

+ + + +

Cloud Native Functions

+ +

The Cloud Native Computing foundation is a group of companies seeking to define open standards for systems built to run on "the cloud".

+ +
+

Cloud native technologies empower organizations to build and run scalable applications in modern, dynamic environments such as public, private, and hybrid clouds. Containers, service meshes, microservices, immutable infrastructure, and declarative APIs exemplify this approach.

+
+ + + +

from: https://github.com/cncf/foundation/blob/master/charter.md

+ +

Seeking to build "a constellation of high-quality projects that orchestrate containers as part of a microservices architecture."

+ +

So, cloud native functions are (container based) systems that allow you to run functions as a service (Faas).

+ +

Function as as a Service (FaaS)

+ +

These are compute environments that let someone deploy a function that will run in response to events triggered by the environment.

+ +

AWS, Azure, and Google Cloud Platform all have a FaaS offering. There are systems like kubeless that let you run infrastructure (or rent it from someone else) and run your own FaaS environment on top of that.

+ +

CNCF has a landscape view at time of writing that has 58 serverless products on it.

+ +

If I only convince you of one thing in this post I want it to be this: none of the items in the "installable platform" section are Serverless. Doesn't mean they aren't potentially valuable to someone but…

+ +

Serverless

+ +

There's quite a bit of definition of serverless in a previous post.

+ +

Boil it down to this: there is no installation, configuration, or maintenance of servers for the owners, and builders of a service in a Serverless system.

+ +

In most cases your team (or worse a different team in your organisation) will provision, manage, patch, security scan, and deploy servers either physical or virtual. Unless you sell the compute those servers represent then you don't make money only by running those servers.

+ +

what you do and don't manage in a serverless system

+ +

In this image we can see that adding containers or kubernetes might make your systems "Serverless" to a traditional development team, one that has no access to or responsibility for infrastructure. But it increases the amount of infrastructure to provision, patch, and scan for vulnerabilities for your organisation. It increases the amount of things to manage that don't directly add value.

+ +

It's only as you move to a system like EC2 fargate or GCP cloud run where you only bring the containers that you start to reduce the amount of infrastructure management you need to carry out.

+ +

I'm partly excluding managed kubernetes from that or at least withholding judgement. A little because I'm not familiar enough to say if it meets this definition but also partly because in Amazon EKS you are still responsible for bring the machine images that kubernetes worker nodes run on. So you're still responsible for the scanning and patching of those images.

+ + + +

On the right you then have what most people think of when you say Serverless which is something that should probably be called "Serverless with FaaS compute". Serverless has existed (if not named) since tools like S3 became available. FaaS allows you to more obviously include your business logic to tie together the various available serverless services.

+ +

Here you tradeoff not being able to bring your own application framework with the freedom of having an almost zero maintenance load. So long as you scan your dependencies, and perform some static or dynamic analysis of your code you can offload the responsibility for the rest of the maintenance and management of the system to the utility provider.

+ +

Serviceful Systems

+ +

Some folk don't get on with the name serverless. Myself it is because of the confusion between FaaS and Serverless making it hard for people to understand how to approach building these systems. My colleague Chris Sewart introduced me to the idea of calling it "serviceful" instead. The earliest reference for the name I can find is Patrick Debois at Serverlessconf. A 30 minute video here: https://www.youtube.com/watch?v=bYCPbKHivMA

+ + + +

Instead of concentrating on not having servers. Concentrate on making best use of services. The example my colleague uses is that if you want a file system you almost certainly want NFS because it's an excellent file system. But that generally speaking you don't really want a file system you only want somewhere it is easy to put files. As a result you should use S3 (if you're in AWS) because that's a really easy way to store files.

+ +

In a serviceful system you should default to consuming the service. The service doesn't come with the provisioning and maintenance burden of the not-service. Even if the not-service is in some way better it needs to be a lot better to justify its cost.

+ +
    +
  • Yes, NFS is great but use S3
  • +
  • Yes, RabbitMQ is great but use SQS
  • +
  • Yes, ${MVC Framework of choice} is great but use API Gateway and Lambda
  • +
  • etc
  • +
  • etc
  • +
+ +

Technical debt vs Accidental complexity

+ +

To aid some of the below…

+ +

Technical Debt

+ +

Most teams call an awful lot of things "technical debt". I like to restrict it to one particular thing… decisions we made on purpose to do something with a poor level of technical correctness because it let's us get to production faster. Technical debt is not a bad thing - so long as you are disciplined about replacing the bad thing with a better version once you've proven the need for it.

+ +

Accidental Complexity

+ +

A lot of teams call this "technical debt" without distinguishing it from "technical debt". Accidental complexity (defined by Brooks in 1986 in the "No Silver Bullet" paper) is complexity that we add that doesn't need to be in the system. As distinct from essential complexity that does need to be in the system.

+ +

E.g. we wrote a tax processor which handles complex tax rules… and we wrote our own queueing software to do it. The essential complexity of the tax rules might be swamped by the accidental complexity of the home grown queue.

+ +

Or we repurposed the existing Oracle analytical DB to support our website because it already handled the complex business logic. The essential business logic complexity might be outweighed by the workrarounds needed to make an analytical DB look like an online transaction processing DB.

+ +

(not that I've been burned by inheriting decisions that look like either of those two ;))

+ +

Blimey charlie that's a lot of definition of terms!

+ +

Let's see if it helps…

+ +

Background / Context for my experiences

+ +

I work with a team that build customer, member, and offers systems for the Co-op. At one point the team was 200 people from 3 different consultancies. It's now only a few more than 20.

+ +

200 people working to a short deadline even bringing their best selves every day can introduce an awful lot of technical debt and accidental complexity.

+ +

Dealing with that debt while adding to and fixing our systems was making us very slow. We chose the principle of preferring immutability and composability at every level. Choosing serviceful systems has enabled that and meant that we make most things such that they can be added alongside what already exists. That means we can work without adding to the already high maintenance burden of the serverful systems that exist.

+ +

That lets us deal with technical debt and accidental complexity at a different cadence than we deal with our sponsors' and users' needs. We already run in AWS so we chose to use AWS lambda for FaaS and DynamoDB for (sort of) key-value storage. We were already using SQS (queue), SES (email), and S3 (storage).

+ +

Event driven and asynchronous or GTFO

+ +

The first thing to accept is that this is an event-driven approach. You have to approach the design of your system as lots of little things talking to each other by raising events (albeit implicitly). If you can't or don't want to then you're not going to get on with this way of building things.

+ +

Where something is synchronous (e.g. an API call) you have to know that you can process and respond in a short enough time or that you can fake a synchronous system. For example if you can always succeed (at least after retry) then return 20x to the calling client, put their request into SQS, and move on.

+ +

In most cases you should already be thinking of your system as little, independent things talking to each other by sending messages. However, it was fascinating to have someone in the JManc discussion group that worked at Elastic on ElasticSearch. Such a different development context and you could see that things that were absolutely true for them didn't make sense for me and vice versa.

+ +

(Always important to remember that we all say "pattern" a lot and that means: a problem, a solution, and a context. Here we saw how a change of context meant a good solution in one context was a bad solution in the other)

+ +

Empowering if you empower

+ +

When I joined this team only QAs were allowed to deploy to production and only platform engineers made any infrastructure changes. It was inherited behaviour and it was debilitating for productivity. It also meant that folk with deep expertise in important tasks were snowed under with trivial tasks that didn't require their expertise. Because they were siloed the different groups sat separately and worked separately so shared very little understanding of each others needs and difficulties.

+ + + +

The stability, reduced complexity, and reduced attack surface of Serviceful systems has helped give us the confidence to collapse those silos. Software engineers now regularly write terraform, platform engineers and QAs join the mob, and folk sit together.

+ +

We also noticed people starting to thank each other as they got to know each other and understand the work being done. Of all the things we've achieved together this is the one I'm most proud of. So while I wouldn't argue the behaviours are unique to serviceful systems I wouldn't want to leave out the contribution they made.

+ +

Cheap and fast and slow

+ +

Cheap

+ +

Cost isn't the most important thing - developers can cost much more than infrastructure. But we've been building entirely servicefully for more than a year now and our systems do more than they used to but at worst our AWS bill has been flat over that year. We use cloudability to track our spending and that predicts a 10-20% drop in bill over the next 12 months based on change over the last year.

+ + + +

In fact one of our engineers has paid his salary in cost reductions on our inherited Serverful systems. That almost certainly means we've invested upwards of $200,000 since the team was launched that could have been avoided. Engineers are more expensive than infrastructure so let's guess that we invested $1.5M to create that avoidable $200k. Arguably, that's going on for $2M invested not to achieve any value at all. At best it was scaffolding that enabled the valuable work. At worst, avoidable in its entirety.

+ +

Serviceful systems were less mature back when that investment was being made so it may well have been the right investment then… but they're much more mature now. To the point that it should be your default choice. Your context might force a different choice. But my assertion is that teams should assume they're building Servicefully and discover where they can't.

+ +

S3 and dynamo are our highest serverless cost. Lambda is effectively free still despite running production workloads and underpinning the majority of our scheduled infrastructure tasks.

+ +

DynamoDB was rising in cost. We discovered this was because we were setting tables to fixed provisioned capacities. In order to fix a performance issue we set Dynamo to "on demand" i.e. serverless mode. Not only did that fix our performance problems but also reduced cost by about 80%. The moral of the tale here is you get forensic visibility into the cost of what you're running. But you have to make sure you're using a service like cloudability and are checking what you're spending.

+ +

https://twitter.com/pauldambra/status/1180157778419179523

+ +

You have to make sure you are looking at the cost profile of the services… AWS Cognito is cheap as chips, AWS Cognito with Advanced Security suuuuupeeerrrrrr expensive.

+ +

AWS API Gateway is super cheap and has per request pricing. While Azure API Management service you pay to reserve capacity so at much lower traffic levels (comparitively) you could end up spending more than running an API gateway yourself. You can't assume Serviceful is cheaper but when you cut with the grain there's a good chance it is.

+ +

and fast

+ +

These services are (in our experience, in AWS) rock-solid, stable, and fast. But they're also fast to build. Once you know how! The group building Offers took two weeks to get their first API Gateway > Lambda > DynamoDB system into production. They took one day to get the second out.

+ +

It's now faster for the team to create two competing designs of a thing and then measure them than to research which might perform better. As you build capability at this way of working your pace can grow much more easily.

+ +

and slow

+ +

But you are also accepting that you are leaning on a framework that you can't play around with. We use a number of existing dependencies that live inside a VPC (a private network in AWS) and so we have to deploy some lambdas inside that VPC.

+ +

At the time of our first implementations cold start of a lambda function in a VPC took a pretty consistent ten seconds. For an offline batch process that doesn't really matter but if you connect that up to an API that's abysmal.

+ +

Since that JManc discussion AWS have released a fix to that performance issue. But it's a great example of how you may have to accept the tradeoff of not being able to build exactly what you want in the way you want in order to get the benefits of the serviceful approach.

+ +

It's also a great example of why I'd recommend AWS for Serviceful/utility hosting. The speed at which they iterate and improve based on customer feedback is startling.

+ +

Commit to learning

+ +

This is a relatively new way of making systems and pushes you into less familiar approaches. If you start down this road you should make a point of introducing protected time for individual and group learning. We definitely missed a trick here and it took longer than necessary to get good at this.

+ +

You should have protected learning time anyway but especially while you introduce something so new to everyone.

+ +

One of the things that helped fantastically was the team's practice of preferring to mob on work. That's helped keep everyone moving their understanding along at the same rate.

+ +

The next steps the team needs to take are to start to formalise and describe some of what we did so that other teams can start to take advantage of it.

+ +

This is forking awesome

+ +

We're doing more, with fewer people, at greater value, and lower cost. And it's been a genuinely joyful process.

+ +

I'm more than happy to stick my flag in the ground and repeat from above that serviceful systems are more than mature enough and more than valuable enough that you should have to justify why you're not using them.

+ +
+ + +
+
+ + + + + + + + + + diff --git a/2020/01/year-notes.html b/2020/01/year-notes.html new file mode 100644 index 000000000..44aa52eaf --- /dev/null +++ b/2020/01/year-notes.html @@ -0,0 +1,461 @@ + + + + + + + + + + + + + + + + + + + + + + 2019 Year Notes + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Tue Jan 07 2020
+

2019 Year Notes

+
+ +
+ +
+

Since October 2017 I've been keeping week notes. I've found them a fantastic tool to track my focus and to remember to reflect on success (or lack of it).

+ +

Some colleagues and some tooters have written year notes as 2019 ends. And here are mine… It feels egotistical… but hopefully it's useful to me in the future even if it isn't to anyone else.

+ + + +

Being a Principal Engineer

+ + + +

I spent 2018 seconded into this role and 2019 officially in it. The teams I work with are achieving great things: reduced complexity, reduced cost, increased stability and throughput, and regularly deliver same day fixes (down from 10 days for simple fixes). Check the Customer and Member section in this Co-op blog. But even two years in I find it hard to see the value I'm providing and to unlearn a lot of the not-principal-engineer habits I'd worked hard to learn.

+ +

The biggest being…

+ +

Feedback loops

+ +

My feedback loop is really long - months sometimes. Maybe that was always the case and I didn't see but it makes tracking impact hard. It feels like this got better over the year but large organisations tend towards silos and I've found that tends to increase the length of and decrease the quality of feedback loops.

+ +

This is important to me and I can't think of a goal… that's worrying.

+ +

Staying out of the way vs getting in the way

+ +

Making a balance between letting the team get on with things and stepping in to influence, coach, and set guide rails is really hard. A lot of my experience was working solo as the only tech person at a not-for-profit or in roles where I was embedded as a member of a team. Acting alongside several teams is very different…

+ +

As an example from recent months: remembering to care that we lint our JS and not which linter we use was hard (although no semicolons 4 life obvs). Then noticing that folk kept replacing the linter and that I need to step in and make the arbitrary choice and let them get on with smashing it is important. (maybe growth would be knowing that somebody needed to step in and helping someone else do it)

+ +

Knowing which gear to change into so I'm letting the teams be creative but keeping them on track is something I have no idea how to measure if I'm doing well… So I suppose my goal is to start 2021 with a better relationship with being a "leader" (I still put that in air quotes every time I say it).

+ +

Making time to write code

+ +

Making time to write code with the teams has been critical for my sanity (and I think my effectiveness). Recently I spent time with the folk building offers.

+ +

Partly because a tight opportunity/deadline needed support, partly because it was fun, partly because my instinct was that was the next important thing for me to do.

+ + + +

But it gave me space to talk to them about the four rules of simple design, get confidence in their abilities, help them get used to a new (to them) codebase, and expand some of our design decisions while in context. Plus I 💖 cutting some code.

+ +

I have to figure out a way to make this a regular part of my role without using it as a mechanism for procrastination. And I should ask that group for feedback on how it felt for them!

+ +

WeekNotes

+ +

I've really valued the focus on reflection. Particularly around trying to find a new feedback loop I can lean on. I started them without any plan and now realise it's a shame they're images on twitter. I can't even find the first one :(

+ + + +

At the moment they're really in google docs. Cos I can make them on the train home. I need to find a train-friendly way to be able to write and publish them easily without relying on a walled garden.

+ +

weeknotes image

+ +

Being part of a big organisation

+ +

Sometimes you can get a glimpse of how a tiny improvement in a big organisation can have a huge impact and that keeps me going. Things like visiting one of our food distribution centres and realising the massive technological and human effort that we're a tiny supporting player to. Or being part of the systems that have helped Co-op members give £17m to local causes.

+ +

We started a monthly conference instead of our weekly community of practice. I was worried that the extra time commitment would mean people didn't attend and I was so wrong. There are enough of us that the monthly unconference is one of the best things about working at Co-op. Kudos to Gemma Cameron for having the vision and making it happen.

+ + + +

But, despite the openness and willingness to improve we see in colleagues across this massive beast of a company, large organisations are inherently pathological. Some groups work in such a way that shows they see the plan as the goal. A thing being on-time and on-budget becomes more important than it being valuable. And that thing being valuable is a side note. Things are made that are either not measurable or not measured.

+ + + +

I'm not sure how best to contribute to improving that. It's easy to fall into an agile echo chamber - thinking if you turn up with some post-its, a whiteboard, and a jenkins server everything is solved. It'll be hard, but important, to hold true to our principles while showing the same willingness to listen and change that we're asking of folks in the "Enterprise" (disappointingly not a spaceship).

+ +

For now I'll concentrate on moving to a place where we deliver frequently and show commitment to measuring value. Tidying our own house before we complain about someone else's.

+ +

Talking

+ +

2019's Week 1 weeknotes mentioned a colleague Graham Thompson saying: "we're aligned because we talk". That turned out to be the theme for the year. Over and over we discovered our problems by talking and solved them by talking.

+ +

But multiple times we also saw that the team spent less time fixing or building something than we'd spent talking about whether we should.

+ +

This year I want to try to swim against the current of our meeting driven organisation and focus on face-to-face communication as part of what we're working on.

+ +

I also have a 140+ day streak on duolingo learning Italian. Non c'è un serpente nei miei stivali. I'll aim to practice every day this year.

+ +

Go-live is marketing

+ +

In 2016 I went to watch James Jeffries talk at the leanmanc usergroup and the other speaker, Andy Mayer, said something along the lines of "because you're releasing to production and learning constantly go live should be driven by marketing".

+ +

We've seen the value in that approach over and over in 2019.

+ +

Week 4: "🍾 “Legacy” DB System we've been replacing has now been strangled away and nothing uses it anymore"

+ +

Week 40: "🦄 team's third significant "go live" that was so smooth it was almost an anti-climax"

+ +

Week 45: "💪 Deployed a new system to replace part of another system while that system was under sustained load. Bold and seamless"

+ +

That week 4 release was the end of 9 months and more of at least two people working full time. It was a significant change and affected multiple business units.

+ +

Go-live was announced at standup with: "oh yeah, MODS is primary for reads and writes now". Running in production as soon as and as meaningfully as possible is a forking super power.

+ + + +

Master, Black list, Guys, and more

+ + + +

I've consistently spent time trying to refocus my use of language this year. Things like using primary or trunk instead of master, exclusion list instead of blacklist, folk or skipping the word instead of guys.

+ +

This seems like the least I can do to promote inclusion. It's not enough but it's something.

+ +

For a while we had a non-binary colleague who used they/them as pronouns. I definitely wasn't good enough at managing that. It was such a clear example of how unconsciously I use gendered language. I tend to speak in a stream of consciousness style and I need to practice speaking purposefully.

+ +

Even if I only make one person feel included or avoid excluding them then the effort has been worth it.

+ +

Running

+ +

I used to cycle 1000km a year plus. I don't now. Largely because cycling in Central Manchester is a horrible experience. But I still snack as if I'm cycling twice a week all year :(

+ +

So I decided I needed to step up my game. I ran 35 weeks of the year. And averaged 6.5km in those weeks. Better than I expected. And a good start. But I want to double that this year.

+ +

Which suggests my maths need to improve since there aren't 70 weeks in the year. Let's say: 10km average over 40 weeks of the year.

+ +

I'll also give up shaming myself for not cycling 27km to work and aim to go for some rides for fun when the weather picks up.

+ +

Walking

+ + + +

This is the first full year I've had a dog since I was a teenager. Best decision in a long time. As much work as having a baby but it adds joy to life.

+ +

dog on new years day 2019

+ +

dog on new years day 2019

+ +

dog on new years day 2020

+ +

dog on new years day 2020

+ + + +

Kids

+ + + +

god of death

+ + + +

All three kids have said they don't want to be on social media so I won't mention much here. Watching them growing into sensible, curious, wonderful, talented nerds despite my terrible parenting is the most incredible thing.

+ +

They also have learned to amuse themselves by saying: "blink and I'll be in college" when I'm distracted by my phone and they want my attention.

+ +

They deserve my attention. We used to have "family screen-free day" once a week. That's coming back!

+ +

What writing this taught me I want to do in 2020

+ + + +
    +
  • decide what feedback loops I want to shorten
  • +
  • find others that want to do that and work with them
  • +
  • expect to find better ways of doing it by including others
  • +
  • read about leadership and get over myself
  • +
  • make time to write code and find out from the teams how they'd like me to do that
  • +
  • figure out how I can write weeknotes as easily without images in twitter being the main record
  • +
  • move to a place where we regularly measure and report on our work
  • +
  • speak face-to-face with individuals instead of in big meetings
  • +
  • practice Italian every day
  • +
  • use the unfair super power of being a white, middle-class, middle-aged, straight man to lift others up
  • +
  • 10km running on average over 40 weeks of the year
  • +
  • 4 leisurely cycle rides
  • +
  • put my phone down and talk to my kids
  • +
+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2021/01/year-notes.html b/2021/01/year-notes.html new file mode 100644 index 000000000..be47ee2a6 --- /dev/null +++ b/2021/01/year-notes.html @@ -0,0 +1,564 @@ + + + + + + + + + + + + + + + + + + + + + + 2020 Year Notes + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sun Jan 03 2021
+

2020 Year Notes

+
+ +
+ +
+

Since October 2017 I've been keeping week notes. I've found them a fantastic tool to track my focus and to remember to reflect on success (or lack of it). I didn't write them for most of 2020. When pandemic hit it seemed too self-centered. I wish I'd kept them up now.

+ +

I wrote year notes last year. Surprisingly that was the only blog post I wrote last year 😱

+ +

Here are mine for 2020… It feels egotistical… but it's intended to remind me to reflect. Hopefully it's useful to me in the future even if it isn't to anyone else.

+ + + +

Goals from last year's year notes

+ +

and whether I achieved them or not

+ +
    +
  • ❌ decide what feedback loops I want to shorten
  • +
  • ½ find others that want to do that and work with them
  • +
  • ✅ expect to find better ways of doing it by including others
  • +
  • ½ read about leadership and get over myself
  • +
  • ½ make time to write code and find out from the teams how they'd like me to do that +
  • +
  • ✅ figure out how I can write weeknotes as easily without images in twitter being the main record
  • +
  • ❌ move to a place where we regularly measure and report on our work
  • +
  • 🤣 speak face-to-face with individuals instead of in big meetings
  • +
  • ½ practice Italian every day +
  • +
  • ❓use the unfair super power of being a white, middle-class, middle-aged, straight man to lift others up
  • +
  • ✅ 10km running on average over 40 weeks of the year
  • +
  • ❌ 4 leisurely cycle rides
  • +
  • ❓put my phone down and talk to my kids
  • +
+ +

Maybe measuring goals once a year is a bad way to achieve them 🤔

+ +

Running and Cycling

+ +

I went for 1 bike ride. I could easily have made time to do more.

+ +

I ran 644km in 2020. That's 12.4km a week on average. I'm really pleased with that. I bet I wouldn't have managed that if it wasn't for…

+ +

…Lockdown

+ +

I suppose it would be weird not to mention lockdown and COVID this year. I'm lucky to live close to the countryside, in a house with space, and to have a job I can do remotely. Not everyone is that lucky but lockdown has been nice for me. Instead of 2 hours a day crammed on a train I've seen my family, played the guitar, or walked the dog.

+ + + +

Because I'm in the house I could help with the kids more. We realised that because I could do the school run my wife could work more.

+ +

It's definitely affected the "speak face-to-face" with individuals goal 🤣. But I've been for a walk a couple of times with colleagues who live nearby and both times that was golden.

+ +

Working remotely has been pretty great despite a background of massively increased anxiety. Given I shouldn't expect to live every year with the worries of this one I think I'm sold.

+ +

Being a Principal Engineer

+ +

I'm still uncomfortable with saying I'm a leader. Less so than in the past. But I think it's because culturally we have a hierarchical view where the leader is important and has all the power and ideas. I don't think I behave like that. I certainly try not to. I can see the pressure towards and ease of becoming an ivory tower architect.

+ +

In a world where the leader is there to help, to have a wider viewpoint, to join things together, and to lift people up, then I'm not uncomfortable. Then I'm scared. That sounds hard. It is hard.

+ +

In 2020 I worked with the Co-operate, and the Customer & Member teams and we all contributed to a "membership evolution" programme. You can read about what we achieved in our end-of-year blogpost. That meant I was working across five teams and with a programme group. At times it was too much.

+ +

Doing too much

+ +

COVID massively reduced what we were asked to do. That was such a gift.

+ +

As a result of doing less we did things better. We did (probably) the important things. And at the end of it all we still celebrated. We still achieved goals. Nobody was mourning the missed things. The company turned a profit.

+ +

Every year we should routinely chop a third out of what we aim to do. (obvs people would start to include sacrificial work to game the system but…)

+ +

Context, direction, and measurement

+ +

I need to get better at clarifying and measuring things. And at talking to people about that. My colleague Nathan Langley was incredible at that this year. They stuck at it. From day-to-day influencing, to rolling their sleeves up and making prototypes, months locked in a room working to make things better. Finally pushing through adoption of a way of communicating strategy and vision and linking them to concrete activities. Such a cool thing. And now other teams are picking it up. It is scary how rare it is for people to communicate the basics. And it's amazing how powerful it is.

+ + + +

For most of my time on Membership there was no communicated and agreed vision. Many of us believed we knew what it was, some of us even agreed. But we couldn't write it down and point at it. Nate changed that. 🔥💖 (like, loads of people were involved but he was influential and consistent)

+ +

I'm joining a new team in the new year. Finding a way to orient myself, choose action, and check the outcome is still going to be as important. But will be even more in a new (to me) business where I don't have years of context.

+ +

I need to remember it's new to me… not to the folk already there. I'll bring my perspective but before that I need to bring my ears

+ +

(which is a clunky way of saying I need to remember to listen)

+ +

A slight aside to say what I think "the basics" are

+ +
    +
  • Why would I start?
  • +
  • When do I stop?
  • +
  • How do I know it is working?
  • +
+ +

I see so much work that can't answer those questions. Any framework or process that doesn't remind you to answer those questions should be yeeted into the sea.

+ +

I love the accelerate metrics.

+ +
    +
  • lead time,
  • +
  • deployment frequency,
  • +
  • mean time to restore (MTTR)
  • +
  • and change fail percentage
  • +
+ +

But if you're not measuring value then you don't need the accelerate metrics. They might help you do the wrong thing faster.

+ + + +

Black lives matter

+ +

Supporting and growing inclusion is the most important thing I can do

+ + + +

This year was already raw. And then George Floyd was murdered by the police in America and something broke. He wasn't the first or last person killed by America's racist system but something caught fire. Doing nothing, saying nothing wasn't ok. I said something about it at our weekly show and tell. I wanted folk on the team to know that their BAME colleagues hurting and for those BAME colleagues to know that if they needed time or support we'd try to help them. It was the hardest public speaking I've ever done.

+ + + +

A couple of times over the last few years I've been asked to come to a meeting to repeat something a woman has been trying to have heard with my man-voice so people will hear it. I'd not given the feedback to the people not listening. I promised myself I'd used up my feeling-too-awkward-to-say-something credit and the next time I saw misogyny or exclusion I'd say something. And the next time I saw misogyny it was on a call with hundreds of other people. So, I said something… I nearly didn't because I wasn't comfortable. Luckily I was able to "say something" with text which made it easier. But I'm glad I did. It was the right thing to do.

+ +

I'm not claiming some expertise or moral high ground on this. I'm sharing some of the small things that I have done cos I want a world where we all do the small things. They add up to impact if enough people do them.

+ +

I know I'm not doing enough, I know I need to learn more, you almost certainly need to as well.

+ +

Things I've thought about more thoroughly than this blog post

+ +

Since I joined Co-op I've worked on Membership. I've been there three and a half years. This year I move to work with Funeralcare. This year has been characterised by knowing that I was likely to move but not when or where to. Figuring out how to work so that the things important to me might carry on when I'm not there is hard.

+ +

I'll be watching to see what happens and trying not to judge myself too harshly

+ +

There are two bits of writing as a result of leaving membership that it felt right to record here

+ +

tooting about my time on membership

+ +

I tooted a twoot-thread at the end of the year.

+ +

That text is copied here for "posterity"…

+ +
+ +

Today's my last day on Membership at Coop Digital. After three and a half years I move to funeralcare in the new year. I thought I'd reflect on my time…

+ +

lines of code is a terrible metric: was the opinion I held until I checked and discovered that since July 2017 I've deleted more code than I've added. Overall, I've deleted 1.5 million lines of code. That makes me happy even though I know it's a terrible metric

+ +

Kindness and empathy are key. When I've got that right it's been 💯. When I've got it wrong it's been 💩

+ +

This year we made 600 changes, at a higher change success rate, and with better availability. And the systems we built and work we did had more value for the business. We've more than doubled the rate we deploy changes over the last few years

+ +

We used to have very slow deploy pipelines and joke we could deploy in-between visits to our sites. In an emergency we can now get (some) changes tested and to prod in minutes. And we have hundreds of thousands of active users

+ +

I still feel a bit uncomfortable thinking of myself as a leader but I'm not (completely) scared of it any more. Maybe time to stop winging it and learn something. I'm lucky to have great peers to learn from

+ + + +

I'm increasingly convinced that giving positive feedback is a super power, and that it pays back much more than negative feedback. But I can see times where I've avoided giving negative feedback and things have been much harder than just having the "difficult conversation"

+ +

shutting up and asking questions is really hard (for me). knowing when to stop asking questions and make statements is even harder. But the times when I've got that balance right have been incredible

+ +

Twice in the last few years people have told me they feel safe on the team. Very few things feel as good as that

+ +

Once someone was brave and told me how I was achieving my goals made their job harder. That didn't feel good, but I value it as much, if not more

+ + + +

leadership (maybe just how I do it) magnifies your reach but so also your mistakes. A bad decision I made in Jan of 2018 is still sat in need of defuckulation now. It's hard not to obsess about those mistakes

+ +

Building a culture of celebration and sharing is really hard and really, really important

+ +

Drawing diagrams is a super power.

+ +

The best thing has been learning how much I still have to learn. I had no idea what I was getting into when I took the Principal role, how hard the shift would be from working in a team to working with teams. I'm so glad I did it, I'm so proud of what the teams have achieved

+ +
+ +

slacking about my time on Membership

+ +

I wrote some words in our slack channel when I left the membership team (which I can't completely recreate here cos of secrets and intrigue)

+ +
+ +

keep up the kindness

+ +

When someone does something well, tell them. +When you wish someone had done something better, tell them. +When someone breaks something, tell them it’s ok, tell them about when you broke something. Have I told you about the time I deleted the record of every insurance sale at The BMC?

+ +

We have to have a job, it’s up to us to make sure we enjoy it

+ +

people and interactions over process and tools

+ +

Process isn’t a good in and of itself. scrum, kanban, user stories, squads, and more are all attempts by people to describe what worked for them. There’s a risk you’re taking advice from a pastry chef while making a casserole.

+ +

We should be being agile not doing it.

+ +

keep releasing small pieces of things

+ +

We made almost a third of Co-op Digital’s recorded changes in 2020, at higher availability, and with better success than the years before. There are few engineering practices more effective than ensuring that changes to code and config make it, safely, to production in the shortest possible time.

+ +

Aim for minutes from commit to prod! What is needed to make that possible?!

+ +

The things you need to do to make this possible are what good engineering is.

+ +

Slow down

+ +

I see us regularly spend all week smashing out feature work. It’s wonderful that we’re committed to what we’re working on. But we have to force ourselves to make time for socialising, learning, and tidying up.

+ +

There’s a good chance we’re moving faster than sponsors, users, and the team can maintain. It’s time to slow down a little and find a more sustainable pace. You have to weed the garden as well as growing plants.

+ +

We’ve seen over the last year folk giving the teams space to make things in the right way. It’s on us to take that time and use it well.

+ +

Take part in service and support

+ +

Our work is not only about making great things, it’s about keeping them great. Learning from and reacting to what really happens is a super power

+ +

Take part in design

+ + + +

We have an incredible design team. If you haven’t worked somewhere that doesn’t value design you might not realise what a wonderful gift it is. Take every opportunity to work with the designers in the team. When there’s user research go along and take notes. Ask them about the designs. The nuance and depth that goes into seemingly simple things is fascinating and can help you understand why you should put in the extra effort (cos sometimes that detail is hard engineering)

+ +

There aren’t many investments guaranteed to pay back but this is one that will pay back.

+ +

Keep being amazing

+ +

3 or 4 years ago Membership systems broke frequently, cost Co-op money, and were an isolated island of functionality. Now we’re rock solid at 10x usual traffic, turn a profit, and support some of the most important things Co-op are working on.

+ +

I hope it’s not egotistical to say that I know I contributed to that and feel pride in what we’ve achieved. But I know that you all contributed to it far more than I did. I’m so excited to see where you go from here.

+ +

I don’t know what I’m talking about…

+ +

…or at least I often feel I’m making it up as I go along. I get the impression that’s true of lots of people, if not everyone. So if you disagree with any or all of this that’s fine. Decide what you think is important and work with people to make that happen.

+ +
+ +

Walking

+ +

This is the second full year I've had a dog since I was a teenager. Best decision in a long time. As much work as having a baby but it adds joy to life.

+ +

dog on new years day 2019

+ +

dog on new years day 2019

+ +

dog on new years day 2020

+ +

dog on new years day 2020

+ +

dog on new years day 2021

+ +

dog on new years day 2021

+ +

Note that in all three photos the dog is soaking wet.

+ +

Kids

+ +

a homemade cardboard sculpture of a house

+ +

All three kids have said they don't want to be on social media so I won't mention much here. I continue to be amazed that my bad influence isn't reducing the kids well-rounded, excitement at the world.

+ +

What writing this taught me I want to do in 2021

+ +
    +
  • read about leadership and get over myself
  • +
  • make time to write code for days at a time
  • +
  • start weeknotes again
  • +
  • by March understand what business and team goals I'm contributing to
  • +
  • meet one-on-one with everyone on my team at least once
  • +
  • keep those meetings going with some of them
  • +
  • practice Italian every day
  • +
  • use the unfair super power of being a white, middle-class, middle-aged, straight man to lift others up
  • +
  • 15km running on average over 40 weeks of the year
  • +
  • 4 leisurely cycle rides
  • +
  • record a video of event sourcing from scratch
  • +
+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2021/07/tech-debts.html b/2021/07/tech-debts.html new file mode 100644 index 000000000..1bd162d6e --- /dev/null +++ b/2021/07/tech-debts.html @@ -0,0 +1,567 @@ + + + + + + + + + + + + + + + + + + + + + + Tech Debts + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Wed Jul 21 2021
+

Tech Debts

+
+ +
+ +
+

Here is something I had to write down at work. Something I've tried to say (with varying success) more than once. That I'm publishing here so I can refer back to it in future and in case it is useful for someone else

+ +

When I am thinking about this, I am thinking about three things

+ +

Two of the agile principles

+ +
    +
  • +
    +

    Continuous attention to technical excellence and good design enhances agility.

    +
    +
  • +
  • +
    +

    Simplicity–the art of maximising the amount of work not done–is essential.

    +
    +
  • +
+ +

And the XP Rule

+ + + + + +

Owning software is like having a garden

+ +

/images/tree.jpg

+ +

In managing a garden, you need to

+ +
    +
  1. frequently and persistently weed
  2. +
+ +
+

"Argh, the young apple tree is being choked by bindweed again."

+
+ +
    +
  1. look at the consequences of your decisions as the plants grow
  2. +
+ +
+

"hmmm, the apple tree is healthier now that I moved it, but it means it's grown a low branch across the path."

+
+ +
    +
  1. deal with the consequences of decisions made before it was your garden
  2. +
+ +
+

"I would never have put an apple tree that near to the path."

+
+ +
    +
  1. delay work based on your context
  2. +
+ +
+

x: "The apple tree is growing too large but, I can't prune it until late winter."
+y: "It could make the tree ill. But, we could tie the branch back until we prune it."

+
+ +

With tech debt, too, there are four things, not one

+ +

People say tech debt and mean more than one thing.

+ +
    +
  1. Mess
  2. +
  3. Things we did that we no longer like
  4. +
  5. Things someone else did that I do not like
  6. +
  7. Classical technical debt
  8. +
+ +

That means we can think we agree when we're talking about different things. I have seen multiple people and teams (including myself) struggle with this.

+ +

TL;DR

+ +

You don't need permission for making incremental improvements in the course of work. That's engineering. It should be included if and when you estimate. When estimating think of both the ideal day and the most horrible day. We tend to think of the ideal but rarely experience it. Meaning our estimates fall short

+ +

Any technical improvement that can be done in the course of your work, should be done.

+ +

We don't budget time for writing tests or searching StackOverflow. Many small changes will have a large cumulative impact. The waves shape the beach by moving the sand a tiny amount.

+ +

The team should work with the Principal Engineer to determine the direction of travel and then follow it. This is particularly true for category one below: mess

+ +

Any technical improvement that can't be done in the course of work should be described, proposed, and measured.

+ +

Anything else is often "something the team wants to do". See categories 2 and 3 below. They should be described and capture the impact of doing or of not doing that work. They can then be prioritised, and reported on. In other words, it is treated the same as a request from outside of the team.

+ +

The team should work with the Principal Engineer and the Product Manager. To determine how to measure the desired improvement. And how to propose it for prioritisation

+ +

And you should switch between those two modes

+ +

Sometimes you start what you think is a quick task. And discover it isn't. You should stop, write it down and treat it as a separate piece of work.

+ +

The four things we call technical debt

+ +

1. Mess

+ + + +

Foote and Yoder propose that all architectures become a big ball of mud. It's a great paper. I suggest that software always ends up looking like the home of a hoarder.

+ +

a picture of a hoarders room, very messy, with piles of things

+ +

What is it?

+ +

People solve problems by adding things far more than they do by removing them. That's what hoarders do. Nobody set out to create the room in the image. They kept adding things that they thought were useful. The hoarder is used to it. It becomes their normal. Anyone new to the hoarder's home can't imagine how they can live there.

+ +

At some point, the software industry started calling this technical debt. It isn't. It's mess. The only response to mess. Is to accept it is inevitable and to tidy up at a faster rate than we add mess.

+ +

In the physical world when the mess is particularly bad we have no choice but to stop everything, hire a skip, and throw everything away. Sometimes this is the right choice for software but we aren't constrained by mess in the same way that we are in the physical world.

+ +

How do we react?

+ +

In the software world we deal with this mess by refactoring. One of the XP rules is to refactor mercilessly. Modern IDEs have tooling to help make this safe. A great guide to this is to use code smells as a guide to what to do next.

+ +

This is a constant activity. You consider this for every task you complete. You are, however, allowed to be pragmatic. You may say: "I won't fix that now because I know folk are waiting for this bug fix so we can start making money again". But you watch for always choosing the pragmatic path. The road to hell is paved with good intentions.

+ +

Some people have had success putting "golden" tickets in the backlog and everyone has to play a golden ticket in ever iteration. One person may use theirs to learn, another to go back and refactor to remove the code smell that had bugged them in some recent work.

+ +

You may not know where to start. If you haven't been tidying enough you should start anywhere rather than worry about which is the best bit of tidying. You will learn what to do by trying to do it.

+ +

What to watch out for?

+ +

If you find yourself pulling new tickets into your iteration, time-box, or sprint that is a good signal that you are rushing through implementing without tidying.

+ +

It is tempting to try to put value on individual pieces of tidying. But the value is in the constant application of effort. Not in any one individual piece of tidying.

+ +

What we would have to put value on is if we want to treat tidying as a task separate to our work. "I want to stop doing other things for M days, in order to tidy up. I believe this is necessary because X, Y, and Z". Here you have to put value on it because you are asking other people to stop and wait while you tidy.

+ +

You may not know where to start. If you haven't been tidying enough you should start anywhere rather than worry about which is the best bit of tidying. You will learn what to do by trying to do it.

+ + + +

"courage" to make the changes you believe will improve the code, no matter how big they are. Modern tooling makes large changes safe. If you find you can't make large changes safely, that's a new signal of how to improve your testing.

+ +

And "respect". Firstly, respect that your team mates are right to try and tidy this *now.* And secondly, respect for others working in or with the team. Tidying might mean they're waiting longer than they expect. Tell them what is happening.

+ +

2 Things we did that we no longer like

+ +

I loved that coat. A genuine first world war camel hair trench coat. It kept me dry and warm through many an Oldham winter snowstorm. I thought it was great. As time passed I had to come to terms with the fact that when it was wet it smelled like a dead dog.

+ +

What I had thought was a great coat, was actually a smelly coat. It served me well until I realised it stank.

+ +

a very old picture of a very young me wearing an original world war one camelhair coat

+ +

What is it?

+ +

We are always growing and learning. Decisions we make that we are proud of eventually become decisions we wish we hadn't made. Sometimes it is easy to change the decision. Other times it is hard.

+ +

It is tempting for this to be treated as concrete ("Monoliths are bad now") when it is often preference ("We would like to use Webpack that we don't understand instead of Gulp that we don't understand")

+ +

How do we react?

+ +

In one of two ways.

+ +

1) slowly applying a direction of travel

+ +

We choose a direction "we will more carefully apply the interface segregation principle". Maybe we run sessions to get or keep the team aligned on the direction. And then we fix the thing as it falls in front of us.

+ +

In this specific case:

+ +
    +
  • every time we edit a file,
  • +
  • if the interface is too large,
  • +
  • we use our brains and our IDE tools to break it into smaller interfaces
  • +
+ +

As these are in the moment changes we don't even tell people we're doing it. It's a part of the work

+ +

2) clearing the slate

+ +

Sometimes the change is too large to be done in tiny pieces or would take so long to complete in small pieces you may as well never start.

+ +

In this case:

+ +
    +
  • we figure out why it is a problem +
      +
    • e.g. every time we make a particular ten minute config change (X times per year) we lose 3 hours of time for at least two people.
    • +
    +
  • +
  • and we say what happens if we fix it +
      +
    • e.g. If we spend 10 days fixing this we will save 15 days this year, and 20 days every year after that
    • +
    +
  • +
+ +

Similarly to the need for respect for other people's time when dealing with mess. When clearing the slate we are asking other people to wait while the engineering team seemingly achieves nothing. We don't always have to be right that we should stop and spend time changing our minds but we do have to be careful

+ +

What to watch out for?

+ +

Watch out for fads! Yes, SvelteJS might be getting lots of social media traction but do we want to invest tens of thousands of pounds changing JS frameworks.

+ +

Watch out for swapping a known set of problems for almost the same problems in different clothes. You may hate Dropwizard and wish we were using Spring. Or think we shouldn't use Jenkins because GoCD is better. etc etc. There's much more to adopting a technology than the text files.

+ +

Replacing "Things we did that we no longer like" with "things we now like" can cause your products and systems to stand still while you do the work. Watch out for replacing things without knowing when to stop or how to measure it

+ +

Equally, watch out for never replacing decisions you've outgrown. There's a lot of room for manoeuvre between "never replace existing tech" and "OMG we should use Rust!"

+ +

3. Things someone else did that I do not like

+ +

Jessica Kerr talks about "downhill invention, uphill analysis" from Vehicles by Valentino Braitenberg. Explaining that it is easier to think about how to replace a system than it is to think about how it works. And, so, we naturally tend to think that systems we inherit are bad, and the ones that we replace them with are good

+ +

the different styles of the same thing meme, one thing drawn in multiple styles

+ +

What is it?

+ +

Because of how the brain works, the people that made a thing in a particular way are often too slow to accept they should have made it a different way (see Things we did that we no longer like). And, people that are fresh to the thing are often too quick to suggest that it should have been made in a different way. Not being present when the context forced your hand, or when mistakes were made, mean you are less willing to accept the current state.

+ +

How do we react?

+ +

It is good to take advantage of fresh perspectives. At a minimum we use this to help set long-term direction. E.g. "I found it very confusing to onboard and there are four different ways of deploying the applications. I can't see a reason for having more than one. But the work to reduce the number of ways we have looks complicated"

+ +

And we should also be looking for low-hanging fruit. E.g. "I see we're using CloudFront but not setting cache-control headers. I've seen cache-control headers have positive impact in multiple systems. I reckon I could add them in less than two days"

+ +

What to watch out for?

+ +

We always under-estimate the effort required to replace something and over-estimate the effort required to understand it.

+ +

Also watch out for justifications based on something being best or current practice. All practice is context dependent. One person's best practice is another's terrible idea. Use tools like Wardley Mapping and Cynefin to help determine what practice to apply.

+ +

4. Classical Technical Debt

+ +

Technical debt is the purposeful decision to defer some necessary work in order to meet a deadline. The debt metaphor is well chosen for descriptive purposes. As the impact of the debt gets worse over time, particularly if we pay back the minimum charge or less. But, engineers always talk about debt as if it is bad. Businesses don't think of debt as bad. There is always debt in running a business and you are always choosing what debt to ignore.

+ +

Tech debt has been short hand for so long now that everyone means a different thing but thinks they agree. It may be better to talk about whether we are keeping scope and delaying implementation or reducing scope and managing consequences.

+ +

several credit cards and a bill behind them

+ +

What is it?

+ +

Technical debt is a purposeful choice to make a version of something that is working from the perspective of the customer. But that is either made badly or missing necessary work that means it is hard to maintain or change.

+ +

It is only bad when the work to add technical debt is more common than work to remove it. Think of each piece of debt as a new credit card and not a single purchase on one card.

+ +

It isn't reducing scope with no intention to pay back the debt.

+ +

How do we react?

+ +

We track it. Ideally we keep a ticket in Jira or whatever equivalent is in use. That ticket is assigned to the business person that made the decision to incur the debt. As the cost of the debt becomes apparent the ticket is kept up-to-date so prioritisation can occur.

+ +

Technical debt is deferred work not avoided work. If you already can't pay off your monthly debt bills you should think very hard about taking on more debt. At some point you have to defer spending instead of accruing debt.

+ +

What to watch out for?

+ +

Treating technical debt as avoiding work instead of deferring work. It is common to say "if we do X this way that accrues technical debt". People hear "I want to gold plate this but you can invest less and still have it". Better to talk about whether to delay the implementation (and to when!). Or whether to cut scope with no inferred expectation the thing will still be done.

+ +

Repeating the tl;dr

+ +

Avoid saying technical debt. Instead, say what you mean.

+ +

Subsequently:

+ +

Any technical improvement that can be done in the course of your work, should be done.

+ +

We don't budget time for writing tests or searching StackOverflow. Many small changes will have a large cumulative impact without needing others working in or with the team to wait.

+ +

Any technical improvement that can't be done in the course of work should be described, proposed, and measured.

+ +

Anything else is often "something the team wants to do". See categories 2 and 3. They should be described and capture the impact of doing or of not doing that work. They can then be prioritised, and reported on. In other words it is treated the same as a request from outside of the team.

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2021/09/emotional-advice.html b/2021/09/emotional-advice.html new file mode 100644 index 000000000..56f86febc --- /dev/null +++ b/2021/09/emotional-advice.html @@ -0,0 +1,328 @@ + + + + + + + + + + + + + + + + + + + + + + Advice given on ending four years at Co-op + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Fri Sep 17 2021
+

Advice given on ending four years at Co-op

+
+ +
+ +
+

I've finished at the Co-op after four years. I was feeling emotional and wrote some "wise words". I thought I'd record them here. In the future, when I'm reminiscing, they can transport me back to this feeling.

+ + + +
    +
  • keep being kind to yourselves and each other
  • +
  • keep being bold but stay humble
  • +
  • people and interactions over processes and tools (that's one of the best bits of the agiles)
  • +
  • keep releasing small things +
      +
    • then try and release smaller
    • +
    +
  • +
  • slow down, start less, and you'll finish more
  • +
  • ask three questions +
      +
    1. why should I start this work
    2. +
    3. how will I know when to stop
    4. +
    5. how will I know if it is still working tomorrow
    6. +
    +
  • +
  • make the loosely coupled version of the service or system
  • +
  • make the simpler version of the service or system
  • +
  • delete things
  • +
  • help everyone take part in service and support
  • +
  • help everyone take part in design and user research
  • +
  • keep being amazing
  • +
  • as long as you are being kind to yourself, you are allowed to hold yourself to a higher standard (but be kind first!)
  • +
  • Nolite te Bastardes Carborundorum
  • +
+ +

a gif of all the photos I took at work over four years

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2022/07/weather-vane-or-sign-post.html b/2022/07/weather-vane-or-sign-post.html new file mode 100644 index 000000000..ffc29c9ec --- /dev/null +++ b/2022/07/weather-vane-or-sign-post.html @@ -0,0 +1,315 @@ + + + + + + + + + + + + + + + + + + + + + + Are you a weather vane or a sign post + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Fri Sep 17 2021
+

Are you a weather vane or a sign post

+
+ +
+ +
+

Tony Benn once said: "I have divided politicians into two categories: the Signposts and the Weathercocks. The Signpost says: 'This is the way we should go.' And you don't have to follow them but if you come back in ten years time the Signpost is still there. The Weathercock hasn’t got an opinion until they've looked at the polls, talked to the focus groups, discussed it with the +spin doctors."

+ +

I heard this quote recently and it has really struck me…

+ +

Having changed problem domain, work environment, stack, and programming language this last year I'm wondering what signpost I want to be.

+ + + + + +

In ASP .Net I was a sign post for feature folders. In flaccid scrum teams for less planning and more measuring. In BLOBs (boring line of business applications) for an event-driven core. But having changed environment so completely some days I feel like a weather-vane.

+ +

What are things you are a signpost for? Or what are the things about which you are a weather vane on purpose or by accident?

+ +

Click "ask a question" below to sign in and tell me what you're a signpost for

+ + +
+
+ +
+
+ + + + + + + + + + diff --git a/2022/08/solar-panels.html b/2022/08/solar-panels.html new file mode 100644 index 000000000..80e69ed40 --- /dev/null +++ b/2022/08/solar-panels.html @@ -0,0 +1,383 @@ + + + + + + + + + + + + + + + + + + + + + + Five years of Solar Panels + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Mon Aug 22 2022
+

Five years of Solar Panels

+
+
+ +
+ +
+ + tag-icon + + solar + + + + + tag-icon + + energy + + + +
+
+
+
+ +
+

We've had photovoltaic (PV) solar panels generating electricity on our roof for exactly 5 years. I've explained the impact a few times privately or on the tooter website. I'm writing it down here so that I don't have to re-remember all the details each time. And since electricity prices are in the news at the moment and it might be useful to some folks.

+ +

We had 14 panels installed on 25th August 2017. It cost £4,793.25 which included 5% VAT. Our house is south facing at the rear. They've generated 17.06MWh of electricity in the last five years.

+ +

That's 17,060Kwh or a little over 17,000 "units".

+ +

And that represents around 4 tonnes of CO2 "saved".

+ + + +

What we have

+ +

We have 11 panels on the south facing roof.

+ +

the layout and lifetime generation of the south facing panels

+ +

And 3 on the east facing roof.

+ +

the layout and lifetime generation of the east facing panels

+ +

You can see the lifetime generation of each panel in those images. The south-facing panels have each generated around 1.3MWh and the east-facing panels around 1MWh.

+ +

There is an inverter in the attic and a generation meter alongside our gas and electricity meters in the hall. The inverter from SolarEdge emits some metrics over wi-fi and we have an app that lets us see them.

+ + + +

Solar Energy sales people behaved horribly. It was like the worst experience of buying a car. We were lucky that we found "Just Energy Solutions" who seemed trustworthy and had soft-touch sales. They drove up from Bristol and completed the installation in a day. I would happily recommend them but they aren't trading any more.

+ +

Generation

+ +

We did lose some data when I changed my wifi network and forgot about the metrics for a few weeks. So we've generated an unknown amount more than 17.06MWh. You can see the drop in Q3 2019 below. Which suggests we've actually generated closer to 17.5MWh.

+ +

production (by quarter) showing 2 or 3 MWh per year

+ +

We don't have a battery. So we do still draw from the grid. Because, while we generate almost as much as we use, we don't always generate electricity when we are using it. :)

+ +

A battery was at least £3000 at time of installation. And this was already an expensive luxury. We chose a slightly more expensive inverter. So that we could still add a battery in future.

+ +

Feed-in-tariff

+ +

We receive some payments from the UK feed-in-tariff. Roughly 5p for every KWh we generate and another 5p for every KWh we export. For small installations you aren't required to meter generation and export. So the tariff assumes that we export 50% of what we generate.

+ +

Since Nov 2018 (I don't have all the records available online and I'm too lazy to find the paper records) we've received £950.21. Naively that's an interest rate of around 4%.

+ +

Immediate use

+ +

Our generation meter shows 16,655KWh. So I think that represents 405KWh that have been consumed within the house before making it to the meter (or possibly that the panels metrics don't match production exactly ¯\(ツ)/¯ )

+ +

At 18.9 p/kWh that's another £76.54 we've not had to spend on electricity

+ +

Electricity we've not had to pay for

+ +

Then there's the electricity we've used that we would have otherwise paid for. Our monitoring isn't detailed enough to know this value. Using Ofgem figures British Gas suggests our household would use 4,300kWh electricity annually.

+ +

Our usage is around 3100kWh annually. Not all of that difference will be due to the panels - we're super careful about energy use - but that's up-to £200 a year more that we're "saving".

+ +

Price rises

+ + + +

The UK is having a terrible time of absent government and unrestrained, self-interested capitalism after an extended period of government by money-vampire. So, electricity is expected to soon be 52p/KWh. That potential 1200KWh a year less is then £600 a year.

+ +

## Payback time

+ +

Payback time is the time it takes for income and savings to pay for the cost of installation. Before Solar PV installation we had other work done on the roof. The payback time of that investment in the roof was… … infinity. No part of our house generated income before we got the panels.

+ +

We were in the lucky position to care more about offsetting our environmental impact than the financial return on investment.

+ +

However, based on the last five years and assuming (despite what's happening right now) that electricity prices rise with inflation then the payback time is 15-20 years.

+ +

Over the coming years prices will rise, my kids will move out (🙏), and our electricity demand will fall. So I'd expect payback to be more like 10-15 years.

+ +

But I'm not relying on it!

+ +

Would I recommend you get panels?

+ +

Having £4k to invest is a privileged position. If you have that money, a south or east facing roof, and can expect not to move house for at least 5 to 10 years. Then I think this is a fantastic way to do something positive with your money.

+ +

However

+ +

Insulate, insulate, insulate. Then do more insulation. You should reduce the need to heat your home as much as practically possible before doing anything else. Both for income and eco- reasons.

+ +

Angry moralising

+ +

It is very hard to avoid angry moralising while writing about this. There is such a failure of vision about how we could build and use energy infrastructure. Leaving those most in need with the least support. All the while the shiny-faced suits-full-of-piss in government squeeze the system of every penny for their friends and family.

+ +

I really hope we can figure out how to be better.

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2023/01/year-notes.html b/2023/01/year-notes.html new file mode 100644 index 000000000..c7d4b95b4 --- /dev/null +++ b/2023/01/year-notes.html @@ -0,0 +1,514 @@ + + + + + + + + + + + + + + + + + + + + + + 2022 Year Notes + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sun Jan 15 2023
+

2022 Year Notes

+
+ +
+ +
+

Forgive me, for I have sinned, it's been 2 years since my last year notes 👼

+ +

I wrote year notes for 2019, and 2020. I've been super un-inspired to write for the last few years. Which is a shame - because it's a a great way to learn.

+ +

So much happened last year that it feels way longer than a year. So, discipline over motiviation - here are my 2022 year notes. Or at least as much as I can write while the house is empty of other people.

+ + + +

Goals from 2021 year notes

+ +

and whether I achieved them or not

+ +

(slightly reogranised since I see themes with hindsight that I didn't at the time)

+ +

leadership

+ +
    +
  • ❓ read about leadership and get over myself
  • +
  • ❓ by March understand what business and team goals I'm contributing to
  • +
  • 😅 meet one-on-one with everyone on my team at least once
  • +
  • 😅 keep those meetings going with some of them
  • +
+ +

In retrospect I was struggling with a role that was poltical and not technical. 2021 saw me move teams, see that the grass wasn't greener, realise that the garden was (for me) poisoned, and move jobs to a technical role.

+ +

writing software

+ +
    +
  • 🙃 record a video of event sourcing from scratch + +
  • +
  • ✅ make time to write code for days at a time
  • +
+ +

And the move (back) to a technical role was harder than I anticipated. New stack, new org, new culture. But I wouldn't change the decision for all the money in the world (well, maybe all the money)

+ +

myself

+ +
    +
  • ❌ start weeknotes again
  • +
  • ✅ practice Italian every day
  • +
  • ❌ 15km running on average over 40 weeks of the year +
      +
    • 43 runs totalling 168km = 3.9km per run
    • +
    • I ended up struggling with achilles pain in 2021 and hardly running at all in 2022
    • +
    • but physio is helping
    • +
    +
  • +
  • ✅ 4 leisurely cycle rides +
      +
    • not in 2021, but I did in 2022
    • +
    +
  • +
+ +

I'm more who I want to be, but there's more to do

+ +

my world

+ + + +
    +
  • 👀 use the unfair super power of being a white, middle-class, middle-aged, straight man to lift others up
  • +
+ +

This is the least I could do. I should take it for granted and figure out the answer to "Great, you want to help others, so what?"

+ +

So, 2022

+ +

Travel

+ +

Working at PostHog comes with a number of benefits. Freedom to travel because the work is remote, and travel for the work, is one that I've been loving!

+ +

2022 travel map from Google Maps showing the countries and places I visited

+ +

In 2022 I visited 6 countries

+ +
    +
  • Barcelona, Spain +
      +
    • for the engineering offsite
    • +
    +
  • +
  • Reykjavik, Iceland +
      +
    • for the all company offsite
    • +
    +
  • +
  • Forte Di Marme, Italy +
      +
    • because I wanted to take the kids to Italy with their Nonno
    • +
    +
  • +
  • Pescara, Italy +
      +
    • for ten days because my family are wonderful and I wanted to spend time in Italy
    • +
    +
  • +
  • Rome, Italy +
      +
    • for the product analytics team offsite
    • +
    +
  • +
  • Paris, France +
      +
    • because I wanted to take the kids to Paris
    • +
    +
  • +
  • Lisbon, Portugal +
      +
    • because we have a budget to meet each other,
    • +
    • my colleague let us use their house for free,
    • +
    • so Ben/Paul super-fun-time could happen to make real user monitoring.
    • +
    • I've never worked anywhere where we are as free to choose our own goals
    • +
    +
  • +
+ +

I spent nearly 20 days in Italy in 2022. More than I've spent there for 20 years. The older I get, the more I value my heritage (#SoCliche). My spoken Italian has progressed from "Nouns and pointing" to "Hangry three year old".

+ +

Travelling for work has been an incredible thing. Lisbon was superb. It was fun to have a goal, work hard all day, and then have food and chat in the evenings. Barcelona, Rome, and Reykjavik were amazing. I'm convinced that intermittently coming together is one of the things that makes remote work, erm, work. Not only that though. Having a budget to meet and socialise is a super power.

+ +

Pescara was like breathing out. It's only the second time in my life I've travelled overseas by myself. And it's the longest I've spent not needing to parent for a decade and a half. I'm incredibly lucky that my family put up with me being away.

+ +

Work

+ +

2022 contributions graph from GitHub showing a step-change around June

+ +

Interestingly, there's a step change in my GitHub contributions around the time I went to Pescara too. And, without wanting to seem big-headed, I think, a step change in my performance at work too.

+ +

Changing back from a leadership role to a typey-typey-software role, at the end of 2021, was way more of a change than I anticipated. Alongside going all-remote and discovering which habits that helped in an office job that don't help anymore. It's amazing how much engineering you can forget in four years of talking to people about engineering.

+ +

What do I think has helped 🤔

+ +
    +
  • cadence +
      +
    • I like to have a large(r) spike PR where I can experiment and gather feedback
    • +
    • and then splitting smaller pieces of work from that
    • +
    • the smaller pieces are easier to engineer well
    • +
    • and way more safe to release
    • +
    • aiming for merging more than one PR a day
    • +
    +
  • +
  • stopping and thinking +
      +
    • decide a goal, figure out how to get there, figure out how much of your time to give it
    • +
    • and then do that
    • +
    +
  • +
  • extreme ownershp and turn the ship around +
      +
    • a very incredible colleague bought me "turn the ship around"
    • +
    • another incredible colleague suggested "extreme ownership"
    • +
    • they're both great books +
        +
      • although I struggled with the "yee-haw shoot people" presentation of extreme ownsership
      • +
      +
    • +
    • "This isn't going to happen until I make it happen! -> How do I make it happen? -> How do I remove things stopping it happening?"
    • +
    +
  • +
  • improvements accrue if you let them +
      +
    • this is maybe a corollary of "stopping and thinking"
    • +
    • I had a great pairing session where a colleague made (to them) a throw away comment about how React works
    • +
    • it changed the model of how I think about it.
    • +
    • Spotting that, I asked myself how that should change how I approach work
    • +
    • the last three months I've been working with another colleague on application performance (the back-end of the back-end)
    • +
    • they're incredible, if I can learn 1% of their skill I'll consider it a success
    • +
    • but now I want to find other work I can prioritise to practice what I think I've learned so I can trick my brain into storing the knowledge
    • +
    +
  • +
  • talking to users +
      +
    • I've spent time supporting users
    • +
    • joining video calls to help them
    • +
    • running user interviews
    • +
    • understanding the users and seeing the struggles they have is 💯
    • +
    • I've been working a lot on our dashboards, not because I thought it was important but because they did.
    • +
    +
  • +
+ +

Annoyingly, I not sure I know what made the difference. I really want to figure it out so I can take advantage of it well. I'm surrounded by amazing people and finding that wonderfully motivating.

+ +

### Open Source

+ +

A brief aside about working on open source software. An unexpected (for me) side-effect has been how incredible it is to be able to share exactly what I mean when talking to people about software. "I think it is good to do X" becomes "Here's a PR (or set of them) that I think demonstrate a way to do X well".

+ +

I think that's awesome.

+ +

Also, sometimes in remote work I miss the power of someone looking over your shoulder while you work. It's way harder to cut corners when someone is watching. Remembering that anyone can watch my work helps remind me to take the step from "make it work" to then "make it right"

+ + + +

For example, I fixed a bunch of bugs in our dashboards product (I think more than I introduced 😅). In doing that we learned about what made it easier to introduce those bugs than to avoid them. I could have moved back on to my main priority… but the world is watching, so I figured out a way to make it harder to introduce the bugs than to avoid them ("four rules of simple design" for the win) https://github.com/PostHog/posthog/pull/13630

+ +

Kids

+ +

toddle and dog sitting on a path

+ +

Since last year notes I've graduated from three kids to four kids. It's still incredible. I'm still always very tired. So amazingly worth it. They have said they don't want to be on social media so I won't mention much here.

+ +

And now they're home… so I'm going to publish without editing and procrastinating.

+ +

What writing this taught me I want to do in 2023

+ +
    +
  • continue becoming a better engineer and team-mate
  • +
  • practice Italian every day
  • +
  • train at the gym at least twice a week every week
  • +
  • 8 leisurely cycle rides
  • +
  • visit Italy at least twice
  • +
+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2023/02/jan-month-notes.html b/2023/02/jan-month-notes.html new file mode 100644 index 000000000..f6b4c610c --- /dev/null +++ b/2023/02/jan-month-notes.html @@ -0,0 +1,346 @@ + + + + + + + + + + + + + + + + + + + + + + Jan 2023 Month Notes + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Fri Feb 03 2023
+

Jan 2023 Month Notes

+
+ +
+ +
+

Year Goals for 2023

+ +
    +
  • continue becoming a better engineer and team-mate
  • +
  • practice Italian every day
  • +
  • train at the gym at least twice a week every week
  • +
  • 8 leisurely cycle rides
  • +
  • visit Italy at least twice
  • +
+ +

How did January go?

+ + + +

In reverse order

+ +

Visit Italy at least twice

+ +

Cancelled a trip to Genoa because our next offsite is in Aruba the same month (hard life).

+ +

But added a trip to Rome with the kids.

+ +

So, still need to figure out what my second trip will be

+ +

8 leisurely cycle rides

+ +

0 cycle rides in Jan.

+ +

The weather in the peak district and leisurely have not overlapped this month 🤣

+ +

train at the gym at least twice a week

+ +

Managed 3 times a week. I ache everywhere all the time 🥵

+ +

But I went for a short jog with the dog yesterday. It felt light and effort-free. First time both achilles have been pain free in a long time. So, I'm hopeful that I'll be able to get back to running soon. Although I need to not do my common failure mode and ramp up to 3 hour runs with the dog too soon and injure myself again 🤣

+ +

Practice Italian every day

+ +

✅ only a few minutes at a time, but I did it every day.

+ + +

Still trying to get my Dad to talk to me in Italian habitually. 🇮🇹

+ +

Continue becoming a better engineer and team-mate

+ +

I didn't think about how I'd measure this 🙈

+ +

I've done a lot of solo-work this month. So arguably not being a great team mate. But it has been soaking up bugs and customer issues so the others on the team can focus.

+ +

Have also managed to stick to small PRs. Despite working on a bunch of tricky frustrating things that lend themselves to sprawling PRs that never get merged… So, I'm pleased with that discipline.

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2023/03/feb-month-notes.html b/2023/03/feb-month-notes.html new file mode 100644 index 000000000..dca4cd653 --- /dev/null +++ b/2023/03/feb-month-notes.html @@ -0,0 +1,350 @@ + + + + + + + + + + + + + + + + + + + + + + Feb 2023 Month Notes + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Wed Mar 01 2023
+

Feb 2023 Month Notes

+
+ +
+ +
+

Year Goals for 2023

+ +
    +
  • continue becoming a better engineer and team-mate
  • +
  • practice Italian every day
  • +
  • train at the gym at least twice a week every week
  • +
  • 8 leisurely cycle rides
  • +
  • visit Italy at least twice
  • +
+ +

How did February go?

+ + + +

In reverse order

+ +

Visit Italy at least twice

+ +

I'm still figuring out when Italy #2 will be. I'd love to take daughter #1 to Sicily and then catch the train back up through the country. I can work, she can study, and we can both enjoy the food and culture. Maybe a pipe-dream…

+ +

8 leisurely cycle rides

+ +

0 cycle rides in Feb.

+ +

Lots of parenting and responsibilities this month, so very little time for myself. And the bags under my eyes are proof of that 🙈

+ +

train at the gym at least twice a week

+ +

We were away in Cornwall for half term and so I managed 3 times a week while I was at home. And 2 times a week the 2 weeks that the trip overlapped with. But the dog and I went for a long run while in Cornwall and I accidentally went for a very long walk there. Turns out South West and South East are not the same direction 🤣

+ +

Here I am just after turning around, trying to figure out how to get back on track. At this point in time I thought I was all the way on the right-hand edge of that map segment 😅

+ +

an OS map showing my position in Cornwall

+ +

I don't ache everywhere all the time anymore. And, actually, feel pretty good. Plus time at the gym is uninterrupted pod-cast time. So, I'm happy with that.

+ +

Practice Italian every day

+ +

✅ only a few minutes at a time, but I did it every day.

+ + +

Still trying to get my Dad to talk to me in Italian habitually. 🇮🇹

+ +

Continue becoming a better engineer and team-mate

+ +

I've been concentrating on this more this month. We joke a lot about being a group of lone wolves and in sprint planning I was described as "wolfing with everyone". I guess that's a good thing 😅

+ +

My GCSE physics teacher told us to always start solving a problem with a diagram. This month's work was tricky, slow, and frustrating. But when I took the time to draw a diagram or two, and then go for an accidentally long walk, my brain was prepared, and my subconscious figured out how to make the complicated thing much, much less complicated.

+ +

a diagram of the problem I was trying to solve

+ +

One of my favourite engineering aphorisms is from Kent Beck: "for each desired change, make the change easy (warning: this may be hard), then make the easy change".

+ +

This particular piece of work, I had three attempts at that had to be abandoned because of side-effects to the change. By the time I'd simplified it, it was a less than 1-day change. Here's an example of one of the pieces of simplication https://github.com/PostHog/posthog/pull/14348.

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2023/03/mar-month-notes.html b/2023/03/mar-month-notes.html new file mode 100644 index 000000000..63b497063 --- /dev/null +++ b/2023/03/mar-month-notes.html @@ -0,0 +1,349 @@ + + + + + + + + + + + + + + + + + + + + + + March 2023 Month Notes + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sun Apr 02 2023
+

March 2023 Month Notes

+
+ +
+ +
+

Year Goals for 2023

+ +
    +
  • continue becoming a better engineer and team-mate
  • +
  • practice Italian every day
  • +
  • train at the gym at least twice a week every week
  • +
  • 8 leisurely cycle rides
  • +
  • visit Italy at least twice
  • +
+ +

How did March go?

+ + + +

In reverse order

+ +

Visit Italy at least twice

+ +

Excitement building this month because "Italy Trip Number One" is in April…

+ +

8 leisurely cycle rides

+ +

0 cycle rides in Mar.

+ +

train at the gym at least twice a week

+ +

✅ Three times a week even while in the Caribbean with work. Still enjoying it.

+ +

Practice Italian every day

+ +

✅ only a few minutes at a time, but I did it every day.

+ +

Continue becoming a better engineer and team-mate

+ +

This was my last month on team product analytics. I'm moving to team session replay. Ben and I built network performance monitoring in December and had a great work vibe - exciting to be building more monitoring tools.

+ +

It makes sense to move teams now because there's a natural gap… 1 week in Aruba with work, and then 2 weeks in Italy. So, I can start fresh when I get back.

+ +

Yep, that's right - Aruba

+ +

We had our annual offsite in Aruba this month. It was a ridiculously beautiful place.

+ +

a pina colada by the pool

+ +

The highlight is always the hackathon. This year I worked on a team building issue tracking into PostHog.

+ +

a gif of the issue tracking page we built

+ +

Hackathon always reminds me of how powerful it is to start work together and excited.

+ +

My new favourite planning method is "post-it notes on a table with food and drink".

+ +

a table with post-it notes on the surface

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/2023/03/office-365-mega-thread.html b/2023/03/office-365-mega-thread.html new file mode 100644 index 000000000..6a2f5ae45 --- /dev/null +++ b/2023/03/office-365-mega-thread.html @@ -0,0 +1,1551 @@ + + + + + + + + + + + + + + + + + + + + + + Office 365 mega-thread + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sun Mar 05 2023
+

Office 365 mega-thread

+
+ +
+ +
+

Between the end of 2019 and when I left the Co-op on Sep 18th 2021 I used Office 365 and never found a single redeeming feature.

+ +

Well, maybe one, a small set of Co-op employees had access to slack and g-suite in Co-op Digital. But in the rest of Co-op they were using installed (i.e. local only) old versions of Office (without video-conferencing and chat). For them, maybe Office 365 was an improvement - and certainly it made remote work during the pandemic possible.

+ +

But for me, it was a constant source of frustration.

+ +

I'm sure that there are great people working on Office with care and attention but I didn't experience that. It was like being haunted and losing your mind all in one go. I had a habit of tooting my frustrations. I'm aware of them having been submitted as evidence in one procurement process. I don't think they swung the decision.

+ +

If the tooter-web dissappeared they'd be the one thing I missed and so I've copied them here.

+ + + +

I didn't start the thread until after I had already made several of the toots. So the initial few dates might appear out of order. I've kept the order that I added them to the thread instead of listing them in date order.

+ +
+
10:20AM Oct 20, 2019
+ My life with office 365 is not richer +
+ +
+ +
+ +
+
3:52 PM Oct 8, 2019
+ Me (a person at work): I'd like to import* a calendar
+Office 365: "YOU MUST WANT SPORTS!"
+
+This should not be the default behaviour of anything
+
+* I want to view a calendar +
+ + + +
+ +
+ +
+
7:46PM Sep 26, 2019
+ Interesting to learn that all one time password apps should behave the same because it's described in an RFC...
+
+And yet Office 365 can only use Microsoft authenticator
+
+#NewMicrosoft
+
+#VeryStaringFace +
+ +
+ +
+ +
+
12:01PM Oct 18, 2019
+ Hey @office365 can you opensource? It would be quicker for me to contribute code to fix Word than to figure out how to amend a numbered list in this garbage fire +
+ +
+ + + +
+ +
+
8:33PM Oct 9, 2019
+ In word you click "Give feedback to Microsoft".
+
+ 3 apps, 3 different feedback mechanisms.
+
+that gives me a sad +
+ +
+ +
+ +
+
7:41PM Oct 9, 2019
+ Where's the approprate place to report that the visual affordance for giving feedback is different in Outlook, Word, and Powerpoint (in the browser)? +
+ +
+ +
+ +
+
4:48PM Oct 8, 2019
+ Hey @office365 what is going on with pasting into lists in Word in Chrome?!
+
+(gif: MS Word in the browser being very odd about lists) +
+ +
+ + + +
+ +
+
4:29PM Oct 4, 2019
+ Me: "a new Word document just look many times already in the last couple of days"
+O365: "Please just this time but not the others could you mindlessly click this box accepting a certificate"
+Me: *just wanting to write some text *clicks OutlookO365: "H! Psyche! Nothing happened . Lol" +
+ +
+ +
+ +
+
6:03PM Oct 9, 2019
+ I just selected a time for an invite in outlook calendar in a browser on my phone.
+
+Great example of why you should use native inputs instead of building your own
+
+(Spoler that was not a native input and it was too hard) +
+ +
+ +
+ +
+
10:20AM Oct 20, 2019
+ And today, I found a uservoice entry for snoozing email (blimey do I miss @inboxbygmail)
+
+I can sign in using google or facebook but not office 365
+
+I have twice given consent for storage of PII but my vote hasn't registered +
+ + + +
+ +
+ +
+
10:24AM Oct 20, 2019
+ I've just realised that I can "like" an email in Outlook
+
+What am I supposed to imagine happens when I do that?!
+
+Does the other person get an email saying that I've liked a different email? What is it for? +
+ +
+ +
+ +
+
10:39AM Oct 20, 2019
+ Outlook: "try the new focussed inbox. we'll stop moving messages to the 'Clutter' folder"
+Me: "What's the clutter folder?" Where is that?! Ugh I guess I'll learn more"
+Outlook: "lol video is unavailable. psyche!" +
+ + + + + +
+ +
+ +
+
10:41AM Oct 20, 2019
+ Nope, no clutter folder :/ +
+ + + +
+ +
+ +
+
1:39PM Oct 21, 2019
+ Listening to someone use Outlook for the first time
+
+"this isn't nice"
+"that's unexpected"
+
+#UX +
+ +
+ +
+ +
+
8:00PM Oct 21, 2019
+ Insert a picture with the cursor in a cell in excel. Adds image at full size.
+
+Insert a picture in a powerpoint slide. Adds image as small as it feels it can get away with.
+
+As a user
+I want adding images to be as frustrating as possible
+So that I close my laptop and go outside +
+ +
+ +
+ +
+
11:10AM Oct 24, 2019
+ Me: *I wonder if I can book a meeting with someone
+Calendar: I WILL SHOW YOU MYSELF HORZONTL SO ALL TEXT IS HIDDEN. I HALP YOU MAKE A DECIDE +
+ + + +
+ +
+ +
+
8:35PM Oct 29, 2019
+ Calendar: "Don't worry some of what you need to click on is off th ebottom of the screen but despite it being lierally the default behaviour of a web page you can't scroll to it"
+Calendar: *holds up hand for high five +
+ +
+ + + +
+ +
+
1:00PM Nov 6, 2019
+ Presented without comment #calendar #search +
+ + + +
+ +
+ +
+
9:58AM Nov 25, 2019
+ The two states of opening an email in Outlook on poor signal
+
+(NB I have gmail open in another window in the same browser. Guess whether it can open my mail) +
+ + + + + +
+ +
+ +
+
11:42AM Dec 3, 2019
+ Me: find this document
+OneDrive: here are some results... including the folder they are in...
+Me: oh, useful can I open that folder from here
+OneDrive: No! +
+ +
+ + + +
+ +
+
11:44AM Dec 3, 2019
+ And just randomly someone else's name is the title of the left hand column of the OneDrive page.
+
+Really is the least discoverable UI I've worked with for quite some time. +
+ + + +
+ +
+ +
+
12:56PM Dec 3, 2019
+ When the browser tab has this red thing it's Outlook making it look like you've new mail when actually you haven't +
+ + + +
+ +
+ +
+
9:28AM Dec 12, 2019
+ Me: navigates to a week in the calendar
+Me: "yep, that's the one" *clicks new event
+Office 364.5: Ah, you must want to default to today not the week you're looking actually +
+ +
+ + + +
+ +
+
6:58PM Dec 19, 2019
+ My office 420 session just expired *while* I was typing into a Word document.
+
+It told me to refresh the page.
+
+I did.
+
+3 paragraphs of text gone.
+
+I have literally never lost a character of text in over a decade of using Google. +
+ +
+ +
+ +
+
12:23PM Jan 28, 2020
+ Today in my-life-editing-word-documents-in-chrome the cursor moves around while I'm typing and so every sentence is a battle +
+ +
+ + + +
+ +
+
10:17AM Feb 11, 2020
+ Me: fuck it, ok, I'll open Outlook as a native app
+Outlook: you have to quit word first, Lol" +
+ +
+ + + +
+ +
+
10:21AM Feb 11, 2020
+ Outlook (native app): log in twice now please.
+Outlook (native app): click allow or deny on this meaningless tech message.
+Outlook (native app): and now here are two appointments in the past that nobody has asked about
+
+#FuckingHell +
+ +
+ +
+ +
+
10:55AM Feb 11, 2020
+ Outlook (native app): here're 1664 reminders for the past, human I am halp +
+ +
+ +
+ +
+
9:05AM Feb 12, 2020
+ Even though the cursor is in it I can't type in the box to thell them what I don't like...
+
+I don't feel like my feedback is valued. +
+ + + +
+ +
+ +
+
6:36pM Mar 2, 2020
+ Me: "please save this spreadsheet with this password to open it"
+Me: *pastes password into box
+Excel: "please confirm the password into this new box"
+Me: *pastes password into box
+Excel: "they are not the same"
+Me: "I feel like you have secret password format restrictions" +
+ +
+ + + +
+ +
+
2:27PM Mar 24, 2020
+ Searching in GMail: "did you mean this email that doesn't actually have the words you typed into search but we had a feeling you might actually want?"
+
+Searching in Outlook: +
+ + + +
+ +
+ +
+
9:21AM Jun 7, 2020
+ The mail icon on the left has no little notification. That means I don't have mail. If it had a little notification it would mean I had mail
+
+The mail icon on the right has a little notification. That rarely means I have mail. How is even that little detail so badly implemented?! +
+ + + +
+ +
+ +
+
9:30AM Jun 7, 2020
+ I checked. There was no mail. Now, there's a notification on twitter. I checked. There was something new.
+
+Twitter can get it right and they've made showing a list of snippets of text complicated. +
+ + + +
+ +
+ +
+
7:50PM Jun 15, 2020
+ Me: *signs in to Word desktop app
+Word: you have to sign in
+Me: *clicks sign in
+Word: *with no feedback "you have to sign in"
+Me: *clicks sign in
+Word: *with no feedback "you have to sign in"
+...
+Me: *clicks sign in
+Word: "seventh times the charm" *saves changes +
+ +
+ +
+ +
+
7:54PM Jun 15, 2020
+ Me: *highlights line of text
+Me: *paste
+Word: "Don't worry, I've stuck this pasted text as the start of the next nearest heading in the document." +
+ +
+ + + +
+ +
+
12:18PM Jun 16, 2020
+ Me: *clicks a calendar appointment in O365 web calendar
+O342: "here's your little white diamond"
+Me: "no, that's not what should happen" *clicks again
+O213: "yep, little white diamond, as requested"
+Me: *waits a minute and clicks again
+O420: "your diamond, good sir" +
+ + + +
+ +
+ +
+
10:18AM Jun 19, 2020
+ wait, so teams works in the app or in chrome?
+
+sorry, I was late for the meeting. I foolishly thought having two browsers on my computer would be enough +
+ +
+ +
+ +
+
12:02PM Jun 26, 2020
+ me: scroll down please
+word in the browser: I DoNT sCroLl An1m0r
+me: it's just a browser window
+word: I DoNT sCroLl An1m0r
+me: refreshes window
+word: NO SCROLL ONLY RENDER +
+ +
+ + + +
+ +
+
12:08PM Jun 26, 2020
+ Ha, forking hall MS, One instance of firefox, all tabs scroll except for MS Word tabs. even newly opened ones.
+
+This has happened to multiple open tabs (and now any new tabs) at the same time.
+
+Office 365 must be a burning nightmare of a code base. +
+ +
+ +
+ +
+
12:10PM Jun 26, 2020
+ Oh, my bad, it was Chrome not firefox. Teams can't do video in Firefox so I have to run two different browsers. +
+ +
+ + + +
+ +
+
1:04PM Jun 26, 2020
+ I have the outlook calendar integration in Slack now. It's great except...
+
+slack: you have a meeting!
+me: * clicks link
+link: * opens in my default browser
+teams: I can't a video in this browser
+me: * copies link from browser URL bar
+me: * pastes into Chrome
+
+1/2 +
+ +
+ +
+ +
+
1:04PM Jun 26, 2020
+ teams: You want to open in app or browser?
+me: IN THE BROWSER
+teams: would you like to join this call
+me: * clicks join now
+
+in another timeline
+
+me: * clicks link
+zoom: * want to join call?
+me: * clicks join now
+
+2/2 +
+ +
+ +
+ +
+
11:52AM Jul 15, 2020
+ Me: highlights text at the beginning of a bullet point and presses delete
+Outlook: I R DELETE THE BULLET POINT AND NOT THE WORDIES AS PER YOUR RECENT REQUEST +
+ +
+ +
+ +
+
11:37AM Jul 16, 2020
+ ORGANISE WHAT MESSAGES, OUTLOOK?! YOU ARE LITERALLY TELLING ME THERE ARE NO MESSAGES AND THAT I SHOULD ORGANISE THEM +
+ + + +
+ +
+ +
+
8:16PM Jul 16, 2020
+ How I make a line chart in excel (web version) where the first column should be the Y axis values.
+
+1) copy data to google sheets +
+ +
+ +
+ +
+
8:57AM Jul 29, 2020
+ me: open the next email, please
+outlook: here you go
+me: close email
+me: oh, actually, open email again
+outlook: I can't display that email
+me: but... but... +
+ +
+ +
+ +
+
10:31AM Aug 6, 2020
+ powerpoint: "Here's how text selection works"
+me: "Yes, that is what I expected"
+narrator: "It was not what he expected"
+
+(look at the difference in the same key commands when the cursor is on "predictable" vs. when it is on "Not") +
+ +
+ + + +
+ +
+
10:36AM Aug 6, 2020
+ After I stopped recording the screen I deleted the text by holding down backspace. Sometimes the cursor moved left without actually removing the character to its left.
+
+This is fine becuase text editing is new
+
+NB it is not new and not fine +
+ +
+ + + +
+ +
+
8:48AM Aug 21, 2020
+ me: *typing in box
+teams: wHy NoT rEfErsH teH paJ +
+ + + +
+ +
+ +
+
8:50AM Aug 21, 2020
+ Let's not worry about the fact that while Teams wants me to refresh the page cos I'm not connected to the internet. I can toot from the same computer.
+
+TIL: https://Twitter.com runs on my laptop
+
+Turns out it's called teams cos it's teeming with bugs +
+ + + +
+ +
+ +
+
2:53PM Sep 2, 2020
+ Me: ...
+Me: ...
+TeAmS: I HAVE ANTICIPATED YOUR NEEDS AND MUTED THE LIVE STREAM AGAIN FOR YOU HUMAN. NO NEED TO THANK ME +
+ +
+ +
+ +
+
5:16PM Sep 2, 2020
+ This isn't just an Office 365 complaint. But look at all that unused space... What are all of those buttons?!
+
+Can we just all agree that icons need to have text alongside them? +
+ + + +
+ +
+ +
+
10:37AM Sep 9, 2020
+ I don't have the energy for snark today
+
+fucking teams! +
+ +
+ +
+ +
+
11:18AM Sep 15, 2020
+ I've edited text in probably 10 applications already today. In all bar one of them the only thing that has caused mistakes is my fat fingers
+
+I'm on my fourth attempt trying to edit a line of text in powerpoint and whole blocks of the text keep disappearing +
+ +
+ + + +
+ +
+
9:00PM Sep 15, 2020
+ Award for the most value-less interruption ever goes to...
+
+*opens golden envelope
+
+message to say that you clicked on a link for content you have access to and would you like to go to that content? +
+ + + +
+ +
+ +
+
3:54PM Nov 9, 2020
+ Kid in the same room playing an online game, I'm watching a video, and using Slack. My house couldn't currently have more internet.
+
+Me: * clicks a link in Teams
+Teams: "I don't feel too well" +
+ + + +
+ +
+ +
+
7:42AM Nov 9, 2020
+ me: "I'd like to edit this bulleted list"
+outlook: "gotcha"
+me: "new line please"
+O: "starts with a bullet"
+me: *tab
+O: *move the bullet in
+me: *repeats 3 times
+me: "new line please"
+O: "starts with a bullet"
+me: *tab
+O: "move the cursor leave the bullet, gotcha" +
+ +
+ + + +
+ +
+
11:16AM Nov 24, 2020
+ Teams: works on chrome desktop but it turns out not on chrome mobile. Works in Firefox including video for live events but not video for meetings
+
+#NewMicrosoft my arse +
+ + + +
+ +
+ +
+
11:12AM Dec 4, 2020
+ So, you can close the Teams app window on Mac by pressing CMD + W when you think another window has focus and not be able to get it back without restarting
+
+What I like in a tool is when there are multiple sharp edges to cut myself with when I use it +
+ + + +
+ +
+ +
+
11:13AM Dec 4, 2020
+ And, yes, it's possible in other applications.... but they also don't dump you in a dead end. +
+ + + +
+ +
+ +
+
10:42AM Dec 11, 2020
+ 1. open email
+2. save to onedrive
+3. view in onedrive
+4. download
+
+Thanks O3.65 I'm glad there's not a download attachment button +
+ +
+ +
+ +
+
10:42AM Dec 11, 2020
+ I googled yammer, got to a website, clicked start using yammer, I am logged in to O3.65 and have access to Yammer +
+ + + +
+ +
+ +
+
11:05AM Dec 14, 2020
+ Oh, forking hell. Joining a teams call UI is like the space shuttle
+
+join teams call without audio
+
+means literally without audio, it doesn't mean "join muted" which is a useful setting but instead "join without being able to hear" which doesn't seem useful to me +
+ +
+ +
+ +
+
14:23PM Jan 15, 2021
+ me: *cmd + tab
+teams: "your invisible notification window, as requested"
+me: "that's not right"
+me: *clicks "calendar" in the window menu
+teams: "your calendar, you should have said first time, here you go"
+
+(nb if I move my mouse around I get the calendar items' hover text) +
+ + + +
+ +
+ +
+
6:08PM Jan 29, 2021
+ Me: presses space
+Almost every program playing video: *toggles play/pause
+MS stream not full screen: *toggles play/pause
+MS stream in full screen: I AM EXITING FULL SCREEN AS ANYONE WOULD EXPECT +
+ +
+ + + +
+ +
+
2:57PM Feb 8, 2021
+ Me: *I wonder if @HollyDonohue01 is on this call?
+Teams: *offscreen "Calling her in to this meeting for you"
+Teams: "Hey Holly, Paul is inviting you to join these 85 people on a call"
+Me: "Argh, that's not what I meant! How do I cancel this?"
+Teams: "What is a cancel?" +
+ +
+ + + +
+ +
+
5:37PM Feb 22, 2021
+ Me: *signed into teams and using it
+Me: *clicks a button
+Teams: YOU HAVE TO SIGN IN INSIDE THE WINDOW EVEN THOUGH YOU HAVE TO SIGN IN TO SEE THE WINDOW. I AM A SECURE +
+ + + +
+ +
+ +
+
12:42PM Mar 23, 2021
+ For three weeks I've been off work and I didn't notice the feeling cos it was an absence of a thing
+
+The absence of being amazed at how bad something is... I've not noticeably waited for a computer to do something
+
+And now? "Crashing" back into using O3.65 +
+ + + +
+ +
+ +
+
12:43PM Mar 23, 2021
+ Me: please open this spreadsheet
+Teams: allow me to present an onboarding journey you don't need and can't interact with that freezes your browser +
+ + + +
+ +
+ +
+
12:53PM Mar 23, 2021
+ Oh nice, no option to mark all as read in Teams. Thank fork there are only 26
+
+As a business owner
+I want to pay my staff to have to click on every conversation
+So that I know they are engaging
+
+pro tip if you click through them quickly they don't actually get marked as read +
+ + + +
+ +
+ +
+
8:40AM Apr 6, 2021
+ I have six applications that are using the internet successfully. But not Outlook 🤬
+
+I'm lucky my calendar and mail aren't trapped in that burning building of a system, eh? +
+ + + +
+ +
+ +
+
10:59AM Apr 6, 2021
+ Thanks powerpoint +
+ + + +
+ +
+ +
+
2:43PM Apr 6, 2021
+ Office 3.64 spell check now takes my northern accent into account +
+ + + +
+ +
+ +
+
10:57AM Apr 12, 2021
+ Every Monday Outlook does a "cute" thing where it recreates a meeting series for me so that I can say I don't go to it anymore.
+
+It takes maybe 30 seconds. If it's doing that for 1% of employees that's 5 hours a week.
+
+That'd be 8 weeks FTE over the year. #HiddenCosts +
+ + + +
+ +
+ +
+
10:00AM Apr 14, 2021
+ For weeks now Teams has been "opening" a "notification" window on my Mac. It isn't viewable and only has the effect that I can no longer CMD+Tab to Teams cos that invisible window gets "shown"
+
+The "fix" is to restart Teams when I notice it
+
+(lifehack: just close it instead) +
+ + + +
+ +
+ +
+
12:22PM Apr 26, 2021
+ Sharepoint: THIS IS NOT SAVED
+Also Sharepoint: YOU SHOULD REFRESH TEH PAGE
+Me: *please save the page
+Sharepoint: HAVE A FREE STACK TRACE. I AM A WEB +
+ + + + + + + +
+ +
+ +
+
12:23PM Apr 26, 2021
+ And, yes, reader, when I refreshed the page all my edits had gone. Sharepoint has one purpose and cannot do it
+
+It's like Wordpress having a bad trip +
+ +
+ +
+ +
+
4:47PM Mar 10, 2021
+ I "like" the new "why not in a fortnight" scheduling feature
+
+here's me suggesting a meeting that the invitee is free for and Office helpfully pointing out we're both also free two weeks later than that +
+ + + +
+ +
+ +
+
3:35PM May 13, 2021
+ Teams: DONUT WORRY HUMAN, FOR THIS MEETING YOU ARNE"T ALLOWED TO ATTACH FILES IN TEH CHAT +
+ + + +
+ +
+ +
+
9:07AM May 14, 2021
+ Outlook: DO NOT WORRY HUMAN, IF I NEVER FINISH LOADING THESE SCRIPTS YOU CANNOT SEE YOUR MAIL AND GET SOME FOCUS TIME +
+ + + +
+ +
+ +
+
1:56PM May 24, 2021
+ outlook *showing me an invite: "no conflicts 👍"
+me *looking at the calendar: "do you know what a conflict is?"
+outlook *starting to sweat: "yes?" +
+ + + + + +
+ +
+ +
+
9:44AM Jul 7, 2021
+ PP: morning
+me: "open a deck, please"
+PP: here you go
+me: "open this one from the browser"
+PP: I AM FROZEN
+me: *ugh, force quit
+O3.7: DON'T WORRY WE HAVE A CUSTOM WAY OF HANDLING ERRORS
+me: is it good
+O3.7: I don't know, I've never seen it work +
+ + + + + +
+ +
+ +
+
9:45AM Jul 7, 2021
+ me: maybe if I sign out
+"Power"point: "worth a shot, guv"
+me: *sign out, please
+PP: done it
+me: *and sign back in, please
+PP: NOT A THING I CAN DO, PYSKE! +
+ + + +
+ +
+ +
+
11:58AM Jul 12, 2021
+ me: *has a 30-minute meeting with one other person
+Teams: HERE IS AN ATTENDANCE REPORT. THIS IS A HELP IF YOU DON'T KNOW IF YOU ATTENDED OR IF YOU NEED TO CHECK IF YOU JUST SPENT THRITY MINUTES ALONE OR NOT" +
+ + + +
+ +
+ +
+
4:57PM Sep 7, 2021
+ CISO: "Hello, I'd like to implement security"
+O365: "It's already done. The steps for downloading an attachment are: 1) click save to one drive 2) click view in one drive 3) download. Security!
+
+WHY CAN I NOT DOWNLOAD AN ATTACHMENT FROM AN EMAIL +
+ +
+ + + +
+ +
+
1:42PM Sep 17, 2021
+ One final disappointment from O3.6
+
+Because I insist on the unexpected browser choice of Chrome on an Android. I can't join a meeting when I find myself caught away from home.
+
+Obvs Teams isn't supported on desktop Chrome +
+ + + +
+ +
+ +
+
9:06AM Sep 18, 2021
+ And in a perfect bit of poetry, I can end this thread, let down by technology, and frozen in Teams. +
+ + + +
+ +
+ + +
+ + +
+
+ + + + + + + + + + diff --git a/2023/03/the-cloud.html b/2023/03/the-cloud.html new file mode 100644 index 000000000..64012147d --- /dev/null +++ b/2023/03/the-cloud.html @@ -0,0 +1,359 @@ + + + + + + + + + + + + + + + + + + + + + + I saved 183 million dollars by not moving to the Cloud + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sun Mar 19 2023
+

I saved 183 million dollars by not moving to the Cloud

+
+
+ + +
+
+ +
+

The average human brain has 2.5 petabytes of memory (source: random google result). 2.5 Petabytes is equal to 2,500,000 Gigabytes. Or 2,500 terabytes. The u-12tb1.112xlarge instance on AWS has 13TB of memory.

+ +

So, conclusively, 193 u-12tb1.112xlarge instances are equivalent to one brain. Or your brain could run in AWS for 15,305,472.00 USD per month. Therefore, I've saved 183 million dollars by not moving my brain to the cloud in the last year alone.

+ +

There seem to be a fashion for writing articles claiming that some company has saved hundreds of millions of dollars by not moving to the cloud.

+ +

I managed phyisical servers for more than a decade. For the UK Magistrates Courts and for the British Mountaineering Council. I was pretty good at it. But, I absolutely jumped at the chance to move to the cloud. Why?

+ + + +

I really don't miss running my own kit (colocated or directly owned).

+ +

I don't miss cycling around Manchester on a Bank Holiday weekend because I'd miscalculated how much network cabling I'd need for an upgrade.

+ +

I don't miss keeping a spreadsheet of storage so I knew when to order disks, negotiating with suppliers for cost of new disks, because I was buying a slightly smaller bulk than AWS.

+ +

I don't miss having to explain to folk in datacenter support that they could take the disks out of my failed server and put them in a new server if they had one available.

+ +

I don't miss the day the single point of failure in the rack failed and everything was offline while I waited for a new doohickey to be shipped to me because it didn't make sense to keep spares of everything on hand.

+ +

I don't miss trying to figure out if some new generation of server hardware would work for or would fit in my rack as manufacturers stopped making the kit we did use.

+ +

I don't miss hacking at a multi-thousand pound HP Proliant server with a breadknife because it was the only way to make the thing fit together due to a manufacturing error. And I couldn't wait for a replacement.

+ +

However,

+ +

The problem with all those articles isn't that they say you should or shouldn't run in the cloud. But that they make bold claims about what everyone should do.

+ +

I'm not going to say every workload should run in the cloud (cliche nod to StackOverflow) but it certainly isn't free to get all of the benefits.

+ +
+ + +
+
+ + + + + + + + + + diff --git a/2023/06/pauls-law.html b/2023/06/pauls-law.html new file mode 100644 index 000000000..86b088d04 --- /dev/null +++ b/2023/06/pauls-law.html @@ -0,0 +1,303 @@ + + + + + + + + + + + + + + + + + + + + + + Paul's Law + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Thu Jun 08 2023
+

Paul's Law

+
+
+ + +
+
+ +
+

Everyone should have their own law… This is mine:

+ +

All new build tools are better than what came before. Until they are able to solve all of the problems of the thing they replaced and then they're at least as bad. A new tool will then replace them

+ + + +

Anyone who remembers the mad rush to replace every build tool in your JavaScript projects with Grunt, to have to replace that with Gulp only days later, will know what I mean.

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/CNAME b/CNAME new file mode 100644 index 000000000..fa511c3cd --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +pauldambra.dev diff --git a/On-Page-Editing.html b/On-Page-Editing.html new file mode 100644 index 000000000..e3379b205 --- /dev/null +++ b/On-Page-Editing.html @@ -0,0 +1,612 @@ + + + + + + + + + + + + + + + + + + + + + + Websites != CMS Platform - On Page Editing + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Wed Jun 11 2014
+

Websites != CMS Platform - On Page Editing

+
+
+ +
+ +
+ + tag-icon + + learning + + + + + tag-icon + + cms + + + + + tag-icon + + design + + + + + tag-icon + + web + + + + + tag-icon + + series + + + +
+
+
+
+ +
+

This post is part of a series where I'm hoping to prove to myself that building a dynamic website with NodeJS is much more fun than using a CMS platform. See the first post for an explanation of why

+ +

The code can be found on GitHub

+ +

Previous Post

+ +

A.K.A. No More CMS-y Admin Section?

+ +

A traditional CMS framework or website has an admin section for logged in users. That section has a menu showing them which sections the user can edit and each section has a list of the pages they can edit and then the user can edit the text or upload images using a WYSIWYG editor.

+ +

Don't fix it if it aint broken but… but… HTML5 includes the contenteditable attribute which makes (the text of) almost any element editable.

+ +

If the admin section exists (in large part) to allow editing of content and editing of content can be completed in the page itself could this replace the admin section?

+ + + +

Could it?!

+ +

The benefit I can see here is that your edits are in place. They're immediately reflected on screen so the editing user can see the impact they're having. A user may not grok why a developer has put a 25 character limit on a title field. But if they only change a title and it pushes the rest of the page out then it's their call whether that's OK.

+ +

I can think of two problems with this:

+ +

1. Users expect an admin interface.

+ +

They don't expect to edit in the page +I've previously referenced "Don't Make Me Think" (shameless affiliate link) and that approach would drive the position that there's no point confusing a user only to be funky. This may be doing that…

+ +

2. Is it discoverable?

+ +

The visual affordance to indicate that a user is able to edit an element needs to be worked in to the design.

+ +

If the user can't find what to edit then this doesn't work. Also, since part of the benefit is that the edits are in the page and the page has to change to indicate where edits are possible does that water down the benefit.

+ +

I'm well out of my depth as far as design goes right now! If this was a real project I'd want to get a real designer or some actual users at this point and find out if this is a developer only idea…

+ +

so how does it work?

+ +

As part of this piece of development I switched view engine to the hbs engine. I wanted partials and handlebars and this appears to offer both with little pain.

+ +
require("./server/handlebars").init(hbs, app.locals);
+hbs.registerPartials(__dirname + "/views/partials");
+app.engine("html", hbs.__express);
+
+ +

This setup allows .hbs files stored in /views/partials to be used in {{>partialName }} handlebars blocks

+ +

The feature here is to have three columns of editable content. That's expressed in the home page layout…

+ +

+<div class="row info">
+  {{>panel panels.[0]}} {{>panel panels.[1]}} {{>panel panels.[2]}}
+</div>
+
+
+ +

Each line calls for the panel partial and passes the given element from the panels array (or undefined)

+ +

and a panel partial would be

+ +

+<div class="col-md-4">
+  <div class="panel panel-info">
+    <div class="panel-heading">
+      <h1 {{elementShouldBeEditable}}>{{safeString title}}</h1>
+    </div>
+    <div
+      class="panel-body"
+      {{elementShouldBeEditable}}
+    >
+      {{safeString body}}
+    </div>
+  </div>
+</div>
+
+
+ +

Each element that should be editable is marked with an {{elementShouldBeEditable}} handlebars helper and the content from the model is marked as safeString so that any HTML entered in the WYSIWYG editor is not escaped.

+ +

The Helpers

+ +

An editable element

+ +
handlebars.registerHelper("elementShouldBeEditable", function () {
+  if (appLocals.user) {
+    return "contenteditable=true";
+  }
+});
+
+ +

This is a standard Handlebars helper which checks if a user is set and if it is renders contenteditable=true in place.

+ +

Safe Strings

+ +

If a WYSIWYG editor saves ` some bold text ` then that is exactly what will be printed on screen as handlebars will escape the HTML to protect you from l33t haxxors.

+ +
handlebars.registerHelper("safeString", function (value) {
+  return new handlebars.handlebars.SafeString(value);
+});
+
+ +

returning a handlebars safeString instead means that handlebars will trust the content and render some bold text

+ +

The JS

+ +

This is the first JS I've added to the client. So, while I initially wrote the JS directly in the HTML, I eventually moved it into its own files and hooked up gulp to concat and uglify it.

+ +

Gulp

+ +
var concat = require("gulp-concat");
+var uglify = require("gulp-uglify");
+
+gulp.task("processJS", function () {
+  gulp
+    .src(["./public/js/*.js"])
+    .pipe(concat("app.js"))
+    .pipe(uglify())
+    .pipe(gulp.dest("./public/js/"));
+});
+
+ +

The gulp task is straightforward. On any change in a JS file in the public/js folder concat all the js files in that folder into a file called app.js, uglify that file and save it.

+ +

The main HTML page is then set to include that JS when a user is logged in

+ +
 {{#if user}}
+<script src="/js/app.js"></script>
+{{/if}} 
+
+ +

Monitor the page for changes

+ +

The first task is to watch any contenteditable elements for changes to their content and to do something when a change is detected

+ +
(function (omniclopse, $) {
+  "use strict";
+
+  //shamelessly borrowed from http://stackoverflow.com/a/14027188/222163
+  omniclopse.bindEvents = function () {
+    var before;
+    var timer;
+    $("*[contenteditable]")
+      .on("focus", function () {
+        before = $(this).html();
+      })
+      .on("keyup paste", function () {
+        if (before != $(this).html()) {
+          clearTimeout(timer);
+          timer = setTimeout(omniclopse.onContentEdited, 500);
+        }
+      });
+  };
+})((window.omniclopse = window.omniclopse || {}), $);
+
+ +

This JS watched any element with a contenteditable attribute and if an element gets focus stores the HTML content as it was on focus. On keyup or paste if the content has changed then queue a call to the onContentEdited event handler.

+ +

This has a 500 millisecond delay so that the system waits until a person has stopped editing before taking any action.

+ +

Respond to changes

+ +

When a change is detected then the page is PUT to the server to persist those changes

+ +
(function (omniclopse, $) {
+  "use strict";
+
+  //snip out addMessage for clarity of example
+
+  omniclopse.onContentEdited = function () {
+    var panels = $(".panel")
+      .map(function (index, el) {
+        var title = $(el).find("h1");
+        var body = $(el).find(".panel-body");
+        return {
+          title: title ? title.text() : "",
+          body: body ? body.html() : "",
+        };
+      })
+      .get();
+
+    var putData = { panels: panels };
+    $.ajax({
+      url: "/pages/home",
+      dataType: "json",
+      contentType: "application/json",
+      data: JSON.stringify(putData),
+      type: "PUT",
+    })
+      .fail(function (xhr, status) {
+        addMessage("could not save your changes", "alert-danger");
+      })
+      .done(function () {
+        addMessage("saved changes", "alert-success");
+      });
+  };
+})((window.omniclopse = window.omniclopse || {}), $);
+
+ +

So here the page object expected by the server is gathered from the page and PUT using $.ajax. This bit of code is bound directly to the Home page at the moment but that can be remedied when necessary.

+ +

An addMessage function shows a bootstrap alert to keep the user informed of what is happening. This is a pretty dull piece of code!

+ +
function alertTimeout(wait) {
+  setTimeout(function () {
+    $("#messageHolder").children(".alert:first-child").alert().alert("close");
+  }, wait);
+}
+
+var addMessage = function (message, bootstrapType) {
+  var outer = $("<div/>", {
+    class: "alert alert-dismissable " + bootstrapType,
+  });
+  var button = $("<button/>", {
+    type: "button",
+    class: "close",
+    "data-dismiss": "alert",
+    "aria-hidden": "true",
+  });
+  button.append("&times;");
+  outer.append(button);
+  outer.append(message);
+  $("#messageHolder").append(outer);
+  alertTimeout(3000);
+};
+
+ +

Visual Affordance

+ +

I found this a pretty hard design decision. I'm not sure I'm happy it really calls out what is happening to a user and I think I'll grab a designer the next time I'm next to one and ask their opinion but…

+ +

editable sections for anonymous users

+ +

editable sections for anonymous users

+ +
@mixin editorPencil($size) {
+  content: "\270f  ";
+  font-family: "Glyphicons Halflings";
+  font-style: normal;
+  font-size: $size;
+}
+
+div[contenteditable]:before {
+  @include editorPencil(1em);
+}
+
+h1[contenteditable]:before {
+  @include editorPencil(0.5em);
+}
+
+ +

Since the site is already using bootstrap CSS was added that uses :before to add a pencil icon to any contenteditable div or H1.

+ +

CKEditor

+ +

Another little bonus is that CKEditor is aware of contenteditable elements so including that in the page gives you WYSIWYG power directly on any contenteditable.

+ + + +

All that was necessary to hook it up was to include it in the page and to switch from using the valid <div content contenteditable/> to using <div content contenteditable=true/> a change I can live with to get the power of WYSIWYG directly on page elements

+ +

(How) does it work?

+ +

If you watch the GIF below it's clear this is a working prototype and not a finished product. But it does work!

+ +

The page content is jumping about as alert messages are added and that's not OK so a better mechanism is necessary for highlighting that changes have been persisted.

+ +

But this was really fun to add and it needed very little code to do so.

+ +

editing the page

+ +
+ + +
+
+ + + + + + + + + + diff --git a/Websites-CMS-Platform-Storing-Data.html b/Websites-CMS-Platform-Storing-Data.html new file mode 100644 index 000000000..31584ca8a --- /dev/null +++ b/Websites-CMS-Platform-Storing-Data.html @@ -0,0 +1,510 @@ + + + + + + + + + + + + + + + + + + + + + + Websites != CMS Platform - Storing Data - Part 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sat Apr 12 2014
+

Websites != CMS Platform - Storing Data - Part 1

+
+
+ +
+ +
+ + tag-icon + + nosql + + + + + tag-icon + + learning + + + + + tag-icon + + mongodb + + + + + tag-icon + + cms + + + + + tag-icon + + design + + + + + tag-icon + + web + + + + + tag-icon + + series + + + +
+
+
+
+ +
+

This post is part of a series where I'm hoping to prove to myself that building a dynamic website with NodeJS is much more fun than using a CMS platform. See the first post for an explanation of why

+ +

Previous Post

+ +

After a day writing DDL for a project that has manual schema versioning against MS SQL and is going through a lot of changes I feel honour bound to write a post about storing data in the Omniclopse site.

+ + + +

I'll be using MongoDB for two reasons.

+ +
    +
  1. The implicit schema of a NoSQL database is awesome when you're not sure of the final shape of the data.
  2. +
  3. Storing a data structure that's almost definitely going to be sent over the wire as JSON as… JSON makes a lot of sense to me.
  4. +
+ +

First Steps

+

At least for now each view will have its own document in the database (At the moment there's only one view! so why complicate things).

+ +

First it is necessary to npm install --save mongojs and then require mongojs within the server module.

+ +
var mongojs = require('mongojs');
+var db = mongojs('omniclopse', ['pages']);
+
+ +

Here the variable db connects to a MongoDB database called omniclopse and a collection called pages.

+ +

Next the call to the DB to get the home page data is added to the home route.

+ +
app.get('/', function(req, res){
+    db.pages.findOne({ name: 'home' }, function(err, doc) {
+        if (err) {
+            res.render('500', {error: err});
+        } 
+        else if (doc) {
+            res.render('home', doc);
+        } else {
+            res.render('404');
+        }
+    });
+});
+
+ +

I think this code is a bit ugly but we'll be coming back to the server later on!

+ +

Error Pages

+

Adding the 404 and 500 pages is straightforward.

+ +

For example:

+ +
<div class="container bg-danger">
+	<h1>404</h1>
+	<div>Dang! That doesn't seem to exist.</div>
+</div>
+
+ +

There are two cases where the app will need to return a 404.

+ +

First, when the URL doesn't exist an HTTP status 400 should be returned

+ +
describe('GET unknown route sends 404 status', function(){
+  it('respond with 404 html', function(done){
+    request(server)
+      .get('/never-exists')
+      .set('Accept', 'text/html')
+      .expect('Content-Type', /html/)
+      .expect(404, done);
+  });
+});
+
+ +

Second, when the database has no entry for the page then the HTTP status should be 200 but the page should be a 404.

+ +
describe('GET known route with no data sends 404 page with 200 status', function(){
+  it('respond with 404 html', function(done){
+    request(server)
+      .get('/')
+      .set('Accept', 'text/html')
+      .expect('Content-Type', /html/)
+      .expect(200, done)
+      .end(function(err, res) {
+        if (err) return done(err);
+        res.text.should.include("Dang! That doesn't seem to exist.");
+        done();
+      });
+  });
+});
+
+ +

Ah, but…

+

…the MongoDB pages collection is empty. Once this collection contains a match for name: home then this test will fail.

+ +

Run Tests against a different database instance

+

Much simpler than mocking the DB (and because I couldn't figure out how to mock it without breaking SuperTest) is running against a test copy of the DB. Very little code to write and the best code is the code you (I?) don't write.

+ +

The code to initialise the database becomes

+ +
var dbName = process.env.NODE_ENV === 'test' ? 'omnitest' : 'omniclopse';
+var db = mongojs(dbName, ['pages']);
+
+ +

and in the test spec files

+ +
var server;
+
+beforeEach(function() {
+    process.env.NODE_ENV = 'test'; 
+    server = require('../server').app;
+});
+
+ +

Now Mocha tests all pass and running the site gives

+

404 page

+ +

After adding {name:'home',carouselImages:[],panels:[]} to the pages collection using the terminal and reloading the page

+

empty page

+ +

Adding an array of carousel images to the home document:

+ +
db.pages.update({name: 'home' },
+                {$set: {
+                          carouselImages: [
+                            {
+                              url:'http://www.fillmurray.com/900/300',
+                              alt:'Bill Murray',
+                              caption:'Bill Murray'
+                            },
+                            {
+                              url:'http://www.placecage.com/900/300',
+                              alt:'Nick Cage',
+                              caption:'Nick Cage'
+                            },
+                            {
+                              url:'http://www.nicenicejpg.com/900/300',
+                              alt:'Vanilla Ice',
+                              caption:'Vanilla Ice'
+                            }
+                          ]
+                        }
+                })
+
+ +

results in:

+

partial page

+ +

Adding an array of panels to the home document results in the desired home page:

+

full page

+ +

E Voila

+

Very little code, very little effort and the page data is being loaded from the database. Hurrah!

+ +

Next

+

I'll be adding authentication so that we can then allow an admin user at Omniclopse HQ to change and add data

+ +
+ + +
+
+ + + + + + + + + + diff --git a/Websites-CMS-Platform-Storing-Data2.html b/Websites-CMS-Platform-Storing-Data2.html new file mode 100644 index 000000000..9b8ac54b7 --- /dev/null +++ b/Websites-CMS-Platform-Storing-Data2.html @@ -0,0 +1,492 @@ + + + + + + + + + + + + + + + + + + + + + + Websites != CMS Platform - Storing Data - Part 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Wed Apr 23 2014
+

Websites != CMS Platform - Storing Data - Part 2

+
+
+ +
+ +
+ + tag-icon + + learning + + + + + tag-icon + + cms + + + + + tag-icon + + design + + + + + tag-icon + + web + + + + + tag-icon + + series + + + + + tag-icon + + express + + + + + tag-icon + + mongodb + + + + + tag-icon + + nosql + + + +
+
+
+
+ +
+

This post is part of a series where I'm hoping to prove to myself that building a dynamic website with NodeJS is much more fun than using a CMS platform. See the first post for an explanation of why

+ +

The code can be found on GitHub

+ +

Previous Post

+ +

The first step is always (or at least should be) to take a step back and decide what to actually do…

+ + + +

In the last post the decision was made to store one document per page, and to have a unique index on the documents name property. This fits with a PUT request

+ +

Callers of a PUT method should anticipate the calls are idempotent and made to the URL of a given resource. That is we'll be sending data to /pages/pageName and not /pages and repeatedly sending the same document for storage means that the document should be updated not duplicated.

+ +

Tests

+

This feature requires a set of conditions are tested:

+
    +
  • you can't PUT an empty page
  • +
  • if you PUT a new page you receive a 201
  • +
  • if you PUT an existing page you receive a 200
  • +
  • the inserted or updated resource URL is in the location header of the response
  • +
+ +
describe('PUTing pages', function() {
+    it('should 400 when no body');
+
+    describe('with new name', function(){
+      it('respond with 201 status');
+    });
+
+    describe('with existing name', function(){
+      it('respond with 200 status');
+  });
+});
+
+ +

After a little backwards and forwards the tests ended up as:

+ +
var request = require('supertest');
+var should = require('should');
+
+var server;
+var db;
+
+beforeEach(function() {
+    //set environment to test and init things
+    process.env.NODE_ENV = 'test'; 
+    db = require('../server/db');
+    server = require('../server').app;
+});
+
+describe('PUTing pages', function() {
+    it('should 400 when no body', function(done) {
+        request(server)
+          .put('/pages/newPage')
+          .set('Accept', 'text/json')
+          .expect('Content-Type', /json/)
+          .expect(400, done);
+    });
+
+    describe('with a new page name', function(){
+      beforeEach(function() {
+        db.pages.remove({}, false, function(err, doc) {});
+      });
+
+      it('should respond with 201 status', function(done){
+        request(server)
+          .put('/pages/newPage')
+          .send({name:'newPage', url:'/somewhere'})
+          .set('Accept', 'text/json')
+          .expect('Content-Type', /json/)
+          .expect('location', '/somewhere')
+          .expect(201, done);
+      });
+
+    });
+
+    describe('with an existing page name', function(){
+      beforeEach(function() {
+        db.pages.remove({}, false, function(err, doc) {});
+        db.pages.insert({name:'existingPage'}, function(err, docs){});
+      });
+
+      it('should respond with 200 status', function(done){
+        request(server)
+          .put('/pages/existingPage')
+          .send({name:'existingPage', url:'/somewhereElse'})
+          .set('Accept', 'text/json')
+          .expect('Content-Type', /json/)
+          .expect('location', '/somewhereElse')
+          .expect(200, done);
+      });
+
+    });
+});
+
+ +

and an alteration to the server file to make those tests pass:

+ +
app.put('/pages/:page', function(req, res, next) {
+    var pageName = req.params.page;
+    if(!req.body || Object.getOwnPropertyNames(req.body).length === 0) {
+        return res.json(400, {});
+    }
+    db.pages.findAndModify({
+        query: { name: pageName },
+        update: { $set: req.body },
+        upsert: true,
+        new: true
+    }, function(err, doc, lastErrorObject) {
+        if(err) {
+            next(err);
+        } else {
+            res.location(doc.url || '/');
+            if(lastErrorObject.updatedExisting) {
+                res.json(200, {}); 
+            } else {
+                res.json(201,{}); 
+            }
+        }
+    });
+});
+
+ +

Again this code feels a bit ugly to me… there's a lot bunched up together - but it can be revisited as it's covered by tests. Importantly it works and allows storage of new pages and edits to existing pages

+ +

And, yes, I know that any unauthorised user can edit with this… authentication is still to come!

+ +
+ + +
+
+ + + + + + + + + + diff --git a/Websites-CMS-Platform-promises-part-2.html b/Websites-CMS-Platform-promises-part-2.html new file mode 100644 index 000000000..9571240c1 --- /dev/null +++ b/Websites-CMS-Platform-promises-part-2.html @@ -0,0 +1,583 @@ + + + + + + + + + + + + + + + + + + + + + + Websites != CMS Platform - Promises - part 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sun Jun 01 2014
+

Websites != CMS Platform - Promises - part 2

+
+
+ +
+ +
+ + tag-icon + + js + + + + + tag-icon + + learning + + + + + tag-icon + + cms + + + + + tag-icon + + design + + + + + tag-icon + + web + + + + + tag-icon + + series + + + + + tag-icon + + promises + + + +
+
+
+
+ +
+

This post is part of a series where I'm hoping to prove to myself that building a dynamic website with NodeJS is much more fun than using a CMS platform. See the first post for an explanation of why

+ +

The code can be found on GitHub

+ +

Previous Post

+ +

So, in the last post I worked on switching some callback code to using promises with Bluebird library but as I've not seen much promisified (definitely a word!) code I wasn't sure whether it was any good.

+ +

So I posted a question on the code review stackexchange asking for feedback.

+ + + +

Here's the code I had written:

+ +
//I'm using bluebird.js for promises
+var users = Promise.promisifyAll(db.users);
+var compare = Promise.promisify(bcrypt.compare);
+
+//this strategy is used by passport to handle logins
+module.exports.localStrategy = new LocalStrategy(function(username, password, done) {
+  var matchedUser;
+
+  var comparePassword = function(user){
+    if(!user) {
+      throw new NoMatchedUserError();
+    }
+
+    //memoise the loaded user so it can be returned below
+    matchedUser = user;
+    return compare(password, matchedUser.password);
+  };
+
+  users.findOneAsync({ username: username })
+    .then(comparePassword)
+    .then(function(isMatch) {
+      return isMatch
+        ? done(null, matchedUser)
+        : done(null, false, { message: 'Incorrect password.' });
+    })
+    .catch(NoMatchedUserError, function() {
+      return done(null, false, { message: 'Incorrect username.' });
+    }) 
+    .error(function(err) {
+      return done(err);
+    });
+});
+
+ +

and here's the code that was suggested

+ +
//I'm using bluebird.js for promises
+var users = Promise.promisifyAll(db.users);
+var compare = Promise.promisify(bcrypt.compare);
+
+
+// This strategy is used by passport to handle logins
+module.exports.localStrategy = new LocalStrategy(function(username, password, done) {
+  users.findOneAsync({username: username}).bind({})
+    .then(function(user) {
+        if (!user) {
+          throw new NoMatchedUserError('Incorrect username.');
+        }
+        this.user = user;
+        return compare(password, user.password);
+    })
+    .then(function(isMatch) {
+      if (isMatch) {
+        return this.user;
+      }
+      else {
+        throw { message: 'Incorrect password.' };
+      }
+    })
+    .nodeify(done);
+});
+
+ +

there are a couple of differences here that led to some great learning for me!

+ +

Bind

+ +

The first is the bind function.

+ +

In JS there is a method on the function prototype called bind. Bind returns a new function identical to the original except that the first argument to bind sets the this context for the function and any subsequent arguments are 'stored' and precede any arguments given when the new function is eventually called.

+ +
var original = function() {
+    console.log(this);
+    console.log(arguments);
+} // in a browser for example the original function logs the window object and an empty array
+
+var withNoParameters = original.bind({ada:'lovelace'});
+withNoParameters(); //logs Object {ada: "lovelace"} and an empty array
+
+var withParameters = withNoParameters.bind({ada:'lovelace'},34)
+withParameters(); //logs Object {ada: "lovelace"} and then [34]
+withParameters('Hedy Lamarr'); //logs Object {ada: "lovelace"} and then [34, "Hedy Lamarr"] 
+
+ +

The bluebird bind function doesn't allow you to add arguments but does provide the ability to bind the context. Or rather of returning a promise bound to the given context. That context follows the promise down the chain (unless a new Promise is created)

+ +

So here we can use it to simplify the code:

+
var users = Promise.promisifyAll(db.users);
+var compare = Promise.promisify(bcrypt.compare);
+
+module.exports.localStrategy = new LocalStrategy(function(username, password, done) {
+  users.findOneAsync({username: username})
+    .bind({}) //replace the findOneAsync promise with one bound to an empty object
+    .then(function(user) {
+        this.user = user; // add or update a user property on the bound object 
+        return compare(password, user.password);
+    })
+    .then(function(isMatch) {
+      if (isMatch) {
+        return this.user; //still able to refer to the same context
+      }
+    });
+});
+
+ +

Nodeify

+ +

The other fantabulous feature is nodeify. In the original code above the promisify functions convert code that expects to receive a callback into code that returns a promise. Nodeify does the reverse and returns a promise that when it is resolved will call the provided callback. Or as the bluebird docs explain it:

+ +
+

Register a node-style callback on this promise. When this promise is is either fulfilled or rejected, the node callback will be called back with the node.js convention where error reason is the first argument and success value is the second argument. The error argument will be null in case of success.

+
+ +
var users = Promise.promisifyAll(db.users);
+var compare = Promise.promisify(bcrypt.compare);
+
+module.exports.localStrategy = new LocalStrategy(function(username, password, done) {
+  users.findOneAsync({username: username}).bind({})
+    .then(function(user) {
+        this.user = user;
+        return compare(password, user.password);
+    })
+    .then(function(isMatch) {
+      if (isMatch) {
+        return this.user;
+      }
+    }).nodefiy(done); //on success calls done(null, this.user)
+});
+
+ +

So

+

These were both transformative for me. I now have a way to plug promises into my code bit by bit and to carry on using libraries that know nothing about promises.

+ +

But

+

Passport uses an optional third argument to populate the flash message so you can put a meaningful message in front of a user when they try to login and aren't successful.

+ +

I poked at nodeify with a stick and a glass of wine and couldn't make that work… because nodeify only passes on the error object or the success value.

+ +

Wonderful Community

+

After reading the code for nodeify and realising I had far less idea how JS works than than I thought I did and much, much less than the library authors I posted on StackOverflow with an example of what I wanted to achieve

+ +
module.exports.localStrategy = new LocalStrategy(function(username, password, done) {
+  users.findOneAsync({username: username}).bind({})
+    .then(function(user) {
+        if (!user) {
+          throw new NoMatchedUserError('Incorrect username.');
+          //should be equivalent to:
+          // return done(null, false, {message:'something'});
+        }
+        this.user = user;
+        return compare(password, user.password);
+    })
+    .then(function(isMatch) {
+      if (isMatch) {
+        return this.user;
+        //is equivalent to:
+        // return done(null, this.user);
+      }
+      else {
+        throw { message: 'Incorrect password.' };
+        //should be equivalent to:
+        // return done(null, false, {message:'something else'};
+      }
+    })
+    .nodeify(done);
+});
+
+ +

Apart from a message confirming that it wasn't currently possible to use nodeify that way I also got comments from one of the Bluebird project committers that they thought this was a decent use-case and could I log an issue…

+ +

I did

+ +

And they've added the feature for version 2.0

+ +

I really love it when a project is responsive! Gives me confidence that they care about what they're building and I'm safe to be using it.

+ +

(yes, I'm a massive hippy :-))

+ +

And

+

So I forked Bluebird, cloned it, switched to the 2.0 branch and ran npm build. I (relatively lazily) copied the built js files over the v1.2.4 files that npm had installed in the project and changed the code to use the new feature (with some comments added for this post)…

+ +
module.exports.localStrategy = new LocalStrategy(function(username, password, done) {
+  users.findOneAsync({ username: username })
+    .bind([]) //now the context needs to be an array
+    .then(function(user){
+      if(!user) {
+        throw new NoMatchedUserError();
+      }
+      this[0] = user; //the first item in the context should be the user
+      return compare(password, this[0].password);
+    })
+    .then(function(passwordsMatch) {
+      if (!passwordsMatch) {
+        this[0] = false; //don't return a user (as they cannot login)
+        this[1] = 'Incorrect password.'; //add a message that passport can use for a flash message
+      }
+      return this;
+    })
+    .catch(NoMatchedUserError, function() {
+      this[0] = false; // couldn't find a user so don't return one
+      this[1] = 'Incorrect username.'; //add a message that passport can use for a flash message
+      return this;
+    }) 
+    .error(function(err) {
+      return err;
+    })
+    .nodeify(done, {spread:true}); // Yay! 
+});
+
+ +

My code looks how I wanted, does what I wanted, I grok promises much more, and I've learned that the bluebird developers are lovely. Awesomeness!

+ +
+ + +
+
+ + + + + + + + + + diff --git a/Websites-CMS-Platform-promises.html b/Websites-CMS-Platform-promises.html new file mode 100644 index 000000000..f27856f37 --- /dev/null +++ b/Websites-CMS-Platform-promises.html @@ -0,0 +1,520 @@ + + + + + + + + + + + + + + + + + + + + + + Websites != CMS Platform - Promises + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sun May 18 2014
+

Websites != CMS Platform - Promises

+
+
+ +
+ +
+ + tag-icon + + learning + + + + + tag-icon + + cms + + + + + tag-icon + + design + + + + + tag-icon + + web + + + + + tag-icon + + series + + + + + tag-icon + + promises + + + + + tag-icon + + refactoring + + + +
+
+
+
+ +
+

This post is part of a series where I'm hoping to prove to myself that building a dynamic website with NodeJS is much more fun than using a CMS platform. See the first post for an explanation of why

+ +

The code can be found on GitHub

+ +

Previous Post

+ +
+

A promise represents the eventual result of an asynchronous operation.

+
+ +

The basic idea is that you can swap in a promise where you would normally pass in a callback.

+ +

The primary interaction is that you call a method which returns a promise which will eventually return a result (it can immediately return the result if it's available) and you chain a call to .then() onto that method call.

+ +

The call to then is equivalent to passing in the callback function.

+ +

Clear as mud?

+ + + +

There's (much) more at the Promises specification website.

+ +

Bluebird

+

This is JavaScript so there are a bazillion npm packages that could be used to switch the project's code to using promises. A (relatively small) bit of googling research suggested that the Bluebird library was a good bet.

+ +

In their words:

+ +
+

Bluebird is a fully featured promise library with focus on innovative features and performance

+
+ +

Before

+

Here's the code from the previous Post which shows the smelly, arrow anti-pattern

+ +
var bcrypt = require('bcrypt');
+var SALT_WORK_FACTOR = 10;
+
+module.exports = function(db) {
+  return {
+    create: function(username, password, callback) {
+        bcrypt.genSalt(SALT_WORK_FACTOR, function(err, salt) {
+            if (err) {
+                callback(err);
+                return;
+            }
+            bcrypt.hash(password, salt, function(err, hash) {
+                if (err) {
+                    callback(err);
+                    return;
+                }
+                db.users.save({
+                    username:username,
+                    password:hash
+                }, function(err, result) {
+                    if(err) {
+                        callback(err.err);
+                    } else {
+                        callback('user created');
+                    }
+                });
+            });
+        });
+    }
+  };
+};
+
+ +

This took a bit of faff to translate to promises almost entirely as a result of this being the first ever promises code I've written and I didn't RTFM.

+ +

I did have this code covered by tests so I could leave mocha running in the background and poke the code with a stick (Yay TDD!)

+ +

After

+

The first pass at implementing promises generated:

+ +
var SALT_WORK_FACTOR = 10;
+
+var bcrypt = require('bcrypt');
+var Promise = require('bluebird');
+
+var genSalt = Promise.promisify(bcrypt.genSalt);
+var genHash = Promise.promisify(bcrypt.hash);
+
+module.exports = function(db) {
+    var users = Promise.promisifyAll(db.users);
+
+  return {
+    create: function(username, password, callback) {
+        genSalt(SALT_WORK_FACTOR).then(function(salt) {
+	        return genHash(password, salt);
+	    }).then(function(hash) {
+            return users.saveAsync({
+                    username:username,
+                    password:hash
+            });
+        }).then(function() {
+            callback('user created');
+        }).error(function (e) {
+            callback(e.message);
+        });
+    }
+  };
+};
+
+ +

All tests still pass at this point and there's fewer levels of arrow to wade through but it still feels a bit pointy and not massively clear so…

+ +
var SALT_WORK_FACTOR = 10;
+
+var bcrypt = require('bcrypt');
+var Promise = require('bluebird');
+
+var genSalt = Promise.promisify(bcrypt.genSalt);
+var genHash = Promise.promisify(bcrypt.hash);
+
+module.exports = function(db) {
+    var users = Promise.promisifyAll(db.users);
+
+  return {
+    create: function(username, password, callback) {
+        var hashPassword = function() {
+            return genSalt(SALT_WORK_FACTOR).then(function(salt) {
+                return genHash(password, salt);
+            });
+        };
+
+        var persistUser = function(hashedPassword) {
+            return users.saveAsync({
+                    username:username,
+                    password:hashedPassword
+            });
+        };
+
+        hashPassword()
+        .then(persistUser)
+        .then(function() {
+            callback('user created');
+        }).error(function (e) {
+            callback(e.message);
+        });
+    }
+  };
+};
+
+ +

skipping over the setup code you can get to the meat of the module

+ +
hashPassword()
+.then(persistUser)
+.then(function() {
+    callback('user created');
+}).error(function (e) {
+    callback(e.message);
+});
+
+ +

Which is a huge amount clearer than the starting point! I do like a method to be a sentence! 'Hash password then persist user'!

+ +

A very high count of exclamation marks in this post but that was much easier and more fun than I anticipated - winner!

+ +

At this point I either need to pass through the code to implement promises more widely… or I could choose to leave everything as it is and improve each code file as it's touched.

+ +

As it is it's nearly midnight and my alarm goes off at 5:50am so…

+ +
+ + +
+
+ + + + + + + + + + diff --git a/assets/css/main.css b/assets/css/main.css new file mode 100644 index 000000000..e2840a44b --- /dev/null +++ b/assets/css/main.css @@ -0,0 +1 @@ +/*! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-feature-settings:normal;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.visible{visibility:visible}.invisible{visibility:hidden}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.isolate{isolation:isolate}.float-right{float:right}.m-1{margin:.25rem}.m-0{margin:0}.m-auto{margin:auto}.my-2{margin-bottom:.5rem;margin-top:.5rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.mb-5{margin-bottom:1.25rem}.mb-2{margin-bottom:.5rem}.mt-2{margin-top:.5rem}.mt-8{margin-top:2rem}.mr-4{margin-right:1rem}.ml-5{margin-left:1.25rem}.mb-0{margin-bottom:0}.mt-1{margin-top:.25rem}.ml-2{margin-left:.5rem}.mb-4{margin-bottom:1rem}.ml-4{margin-left:1rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-6{height:1.5rem}.h-4{height:1rem}.h-auto{height:auto}.h-full{height:100%}.h-24{height:6rem}.h-screen{height:100vh}.max-h-96{max-height:24rem}.w-6{width:1.5rem}.w-full{width:100%}.w-8{width:2rem}.w-1\/2{width:50%}.w-11\/12{width:91.666667%}.max-w-sm{max-width:24rem}.flex-auto{flex:1 1 auto}.flex-grow,.grow{flex-grow:1}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.list-none{list-style-type:none}.list-decimal{list-style-type:decimal}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.content-end{align-content:flex-end}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-center{justify-content:center}.gap-2{gap:.5rem}.gap-4{gap:1rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.5rem*var(--tw-space-x-reverse))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1rem*var(--tw-space-x-reverse))}.self-end{align-self:flex-end}.overflow-hidden,.truncate{overflow:hidden}.truncate{white-space:nowrap}.text-ellipsis,.truncate{text-overflow:ellipsis}.rounded{border-radius:.25rem}.border{border-width:1px}.border-t-2{border-top-width:2px}.border-b-2{border-bottom-width:2px}.border-l-2{border-left-width:2px}.border-t-slate-400{--tw-border-opacity:1;border-top-color:rgb(148 163 184/var(--tw-border-opacity))}.border-b-black{--tw-border-opacity:1;border-bottom-color:rgb(0 0 0/var(--tw-border-opacity))}.border-b-slate-300{--tw-border-opacity:1;border-bottom-color:rgb(203 213 225/var(--tw-border-opacity))}.border-l-sky-700{--tw-border-opacity:1;border-left-color:rgb(3 105 161/var(--tw-border-opacity))}.bg-slate-900{--tw-bg-opacity:1;background-color:rgb(15 23 42/var(--tw-bg-opacity))}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.p-2{padding:.5rem}.p-4{padding:1rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.px-1{padding-left:.25rem;padding-right:.25rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.py-8{padding-bottom:2rem;padding-top:2rem}.px-8{padding-left:2rem;padding-right:2rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-4{padding-bottom:1rem}.pt-4,.py-4{padding-top:1rem}.pt-2{padding-top:.5rem}.pl-4{padding-left:1rem}.pl-1{padding-left:.25rem}.text-right{text-align:right}.align-top{vertical-align:top}.align-middle{vertical-align:middle}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-base{font-size:1rem;line-height:1.5rem}.font-bold{font-weight:700}.uppercase{text-transform:uppercase}.leading-10{line-height:2.5rem}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-slate-500{--tw-text-opacity:1;color:rgb(100 116 139/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.underline{text-decoration-line:underline}.no-underline{text-decoration-line:none}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.drop-shadow-sm{--tw-drop-shadow:drop-shadow(0 1px 1px rgba(0,0,0,.05))}.drop-shadow-md,.drop-shadow-sm{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow-md{--tw-drop-shadow:drop-shadow(0 4px 3px rgba(0,0,0,.07)) drop-shadow(0 2px 2px rgba(0,0,0,.06))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}body{font-family:Khula,sans-serif}h1{font-size:2rem}h1,h2{margin-top:.25rem}h2{font-size:1.5rem}h3{font-size:1.2rem;margin-top:.25rem}hr{clear:both}p,ul{word-wrap:break-word;margin-bottom:1rem}li{list-style-position:inside;list-style-type:disc;margin-left:.5rem}body>header{background-image:url(/images/cardboard.jpg)}@media (max-width:600px){body>header{background-position:-400px -23px}}@media (max-width:778px){body>header{height:370px}}@media (min-width:778px){body>header{min-height:350px}}@media (min-width:984px){body>header{background-size:100%}}@media (min-width:1440px){body>header{height:600px}}@media (max-height:320px){body>header{height:250px}}img{display:block;margin:auto;max-width:1000px;width:100%}img+img{margin-top:1rem}blockquote{border-left:5px solid #eee;margin:0 0 20px;padding:5px 10px}.excerpt .series{display:none}.highlight{overflow-x:scroll}article>header{margin-bottom:.5rem}a{text-decoration-line:underline}.hover\:bg-slate-300\/75:hover{background-color:rgba(203,213,225,.75)}.hover\:bg-slate-600:hover{--tw-bg-opacity:1;background-color:rgb(71 85 105/var(--tw-bg-opacity))}.hover\:underline:hover{text-decoration-line:underline}@media (min-width:640px){.sm\:m-0{margin:0}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}}@media (min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}} \ No newline at end of file diff --git a/assets/css/syntax.css b/assets/css/syntax.css new file mode 100644 index 000000000..a295c33d3 --- /dev/null +++ b/assets/css/syntax.css @@ -0,0 +1,234 @@ +.highlight .hll { + background-color: #ffffcc; +} +.highlight { + background: #f1f5f5; +} +.highlight .c { + color: #0099ff; + font-style: italic; +} /* Comment */ +.highlight .err { + color: #aa0000; + background-color: #ffaaaa; +} /* Error */ +.highlight .k { + color: #006699; + font-weight: bold; +} /* Keyword */ +.highlight .o { + color: #555555; +} /* Operator */ +.highlight .ch { + color: #0099ff; + font-style: italic; +} /* Comment.Hashbang */ +.highlight .cm { + color: #0099ff; + font-style: italic; +} /* Comment.Multiline */ +.highlight .cp { + color: #009999; +} /* Comment.Preproc */ +.highlight .cpf { + color: #0099ff; + font-style: italic; +} /* Comment.PreprocFile */ +.highlight .c1 { + color: #0099ff; + font-style: italic; +} /* Comment.Single */ +.highlight .cs { + color: #0099ff; + font-weight: bold; + font-style: italic; +} /* Comment.Special */ +.highlight .gd { + background-color: #ffcccc; + border: 1px solid #cc0000; +} /* Generic.Deleted */ +.highlight .ge { + font-style: italic; +} /* Generic.Emph */ +.highlight .gr { + color: #ff0000; +} /* Generic.Error */ +.highlight .gh { + color: #003300; + font-weight: bold; +} /* Generic.Heading */ +.highlight .gi { + background-color: #ccffcc; + border: 1px solid #00cc00; +} /* Generic.Inserted */ +.highlight .go { + color: #aaaaaa; +} /* Generic.Output */ +.highlight .gp { + color: #000099; + font-weight: bold; +} /* Generic.Prompt */ +.highlight .gs { + font-weight: bold; +} /* Generic.Strong */ +.highlight .gu { + color: #003300; + font-weight: bold; +} /* Generic.Subheading */ +.highlight .gt { + color: #99cc66; +} /* Generic.Traceback */ +.highlight .kc { + color: #006699; + font-weight: bold; +} /* Keyword.Constant */ +.highlight .kd { + color: #006699; + font-weight: bold; +} /* Keyword.Declaration */ +.highlight .kn { + color: #006699; + font-weight: bold; +} /* Keyword.Namespace */ +.highlight .kp { + color: #006699; +} /* Keyword.Pseudo */ +.highlight .kr { + color: #006699; + font-weight: bold; +} /* Keyword.Reserved */ +.highlight .kt { + color: #007788; + font-weight: bold; +} /* Keyword.Type */ +.highlight .m { + color: #ff6600; +} /* Literal.Number */ +.highlight .s { + color: #cc3300; +} /* Literal.String */ +.highlight .na { + color: #330099; +} /* Name.Attribute */ +.highlight .nb { + color: #336666; +} /* Name.Builtin */ +.highlight .nc { + color: #00aa88; + font-weight: bold; +} /* Name.Class */ +.highlight .no { + color: #336600; +} /* Name.Constant */ +.highlight .nd { + color: #9999ff; +} /* Name.Decorator */ +.highlight .ni { + color: #999999; + font-weight: bold; +} /* Name.Entity */ +.highlight .ne { + color: #cc0000; + font-weight: bold; +} /* Name.Exception */ +.highlight .nf { + color: #cc00ff; +} /* Name.Function */ +.highlight .nl { + color: #9999ff; +} /* Name.Label */ +.highlight .nn { + color: #00ccff; + font-weight: bold; +} /* Name.Namespace */ +.highlight .nt { + color: #330099; + font-weight: bold; +} /* Name.Tag */ +.highlight .nv { + color: #003333; +} /* Name.Variable */ +.highlight .ow { + color: #000000; + font-weight: bold; +} /* Operator.Word */ +.highlight .w { + color: #bbbbbb; +} /* Text.Whitespace */ +.highlight .mb { + color: #ff6600; +} /* Literal.Number.Bin */ +.highlight .mf { + color: #ff6600; +} /* Literal.Number.Float */ +.highlight .mh { + color: #ff6600; +} /* Literal.Number.Hex */ +.highlight .mi { + color: #ff6600; +} /* Literal.Number.Integer */ +.highlight .mo { + color: #ff6600; +} /* Literal.Number.Oct */ +.highlight .sa { + color: #cc3300; +} /* Literal.String.Affix */ +.highlight .sb { + color: #cc3300; +} /* Literal.String.Backtick */ +.highlight .sc { + color: #cc3300; +} /* Literal.String.Char */ +.highlight .dl { + color: #cc3300; +} /* Literal.String.Delimiter */ +.highlight .sd { + color: #cc3300; + font-style: italic; +} /* Literal.String.Doc */ +.highlight .s2 { + color: #cc3300; +} /* Literal.String.Double */ +.highlight .se { + color: #cc3300; + font-weight: bold; +} /* Literal.String.Escape */ +.highlight .sh { + color: #cc3300; +} /* Literal.String.Heredoc */ +.highlight .si { + color: #aa0000; +} /* Literal.String.Interpol */ +.highlight .sx { + color: #cc3300; +} /* Literal.String.Other */ +.highlight .sr { + color: #33aaaa; +} /* Literal.String.Regex */ +.highlight .s1 { + color: #cc3300; +} /* Literal.String.Single */ +.highlight .ss { + color: #ffcc33; +} /* Literal.String.Symbol */ +.highlight .bp { + color: #336666; +} /* Name.Builtin.Pseudo */ +.highlight .fm { + color: #cc00ff; +} /* Name.Function.Magic */ +.highlight .vc { + color: #003333; +} /* Name.Variable.Class */ +.highlight .vg { + color: #003333; +} /* Name.Variable.Global */ +.highlight .vi { + color: #003333; +} /* Name.Variable.Instance */ +.highlight .vm { + color: #003333; +} /* Name.Variable.Magic */ +.highlight .il { + color: #ff6600; +} /* Literal.Number.Integer.Long */ diff --git a/better-affordance-js.html b/better-affordance-js.html new file mode 100644 index 000000000..b280bbb90 --- /dev/null +++ b/better-affordance-js.html @@ -0,0 +1,544 @@ + + + + + + + + + + + + + + + + + + + + + + Websites != CMS Platform - Better Editable Affordance with JS for great good + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Wed Jul 30 2014
+

Websites != CMS Platform - Better Editable Affordance with JS for great good

+
+
+ +
+ +
+ + tag-icon + + learning + + + + + tag-icon + + cms + + + + + tag-icon + + design + + + + + tag-icon + + web + + + + + tag-icon + + series + + + +
+
+
+
+ +
+

This post is part of a series where I'm hoping to prove to myself that building a dynamic website with NodeJS is much more fun than using a CMS platform. See the first post for an explanation of why

+ +

The code can be found on GitHub

+ +

Previous Post

+ + + +

In the last post a better visual affordance that a page element is editable was added. But didn't solve the problem that notifications of success or failure were obtrusive and disconnected from the edited element.

+ +

pulsing affordance

+ + + + +

The desired behaviour is that when a change is made the entire current page is persisted to the server and the user is made aware of success or failure without interrupting their workflow unnecessarily.

+ +

Editable indicator changing state

+ +

So here as the text is changed the indicator changes to the save icon. On success to a tick and after a short delay back to the editable icon.

+ +

But how?!

+ +

Changing the icon

+ +

The actual switch is

+ +
(function (omniclopse) {
+  "use strict";
+
+  omniclopse.ux = omniclopse.ux || {};
+
+  var switchIcons = function (element, oldClass, newClass) {
+    element.classList.remove(oldClass);
+    element.classList.add(newClass);
+  };
+
+  omniclopse.ux.saveContentStarted = function (element) {
+    switchIcons(element, "fa-pencil", "fa-save");
+  };
+
+  omniclopse.ux.saveContentCompleted = function (element) {
+    switchIcons(element, "fa-save", "fa-check");
+    setTimeout(function () {
+      switchIcons(element, "fa-check", "fa-pencil");
+    }, 3000);
+  };
+
+  omniclopse.ux.saveContentFailed = function (element) {
+    switchIcons(element, "fa-save", "fa-times");
+  };
+})((window.omniclopse = window.omniclopse || {}));
+
+ +

Since the site is using the well-named Font-awesome icon library all that is needed to change the icon is to alter the fa classes on the element.

+ +

As an exercise in hipsterism this is done with vanilla javascript but it would be trivial to pass JQuery into this IIFE and use the class addition and removal functions it provides instead.

+ +

So,

+ +
    +
  • when saving content has started the pencil icon is switched out for a save icon
  • +
  • when saving completes the save icon is switched for a check and timeout is set to switch that check back to the original pencil
  • +
  • when saving fails the save icon is switched for an X.
  • +
+ +

Right now this behaviour on fail is pretty rubbish as the user doesn't get an error message and there's no way to retry. Really hovering over or clicking on the X should display the error message. The icon should change to a retry symbol or clicking on it should prompt for retry and the page should use localstorage so that your edits aren't lost. But that's for another day!

+ +

Wiring it up

+ +
(function (omniclopse, $) {
+  "use strict";
+
+  var getEditableElementsForUpload = function () {
+    return $(".panel")
+      .map(function (index, el) {
+        var title = $(el).find("h1");
+        var body = $(el).find(".panel-body");
+        return {
+          title: title ? title.text() : "",
+          body: body ? body.html() : "",
+        };
+      })
+      .get();
+  };
+
+  omniclopse.onContentEdited = function (element) {
+    var icon = $(element).find("i")[0];
+
+    omniclopse.ux.saveContentStarted(icon);
+    var panels = getEditableElementsForUpload();
+
+    var putData = { panels: panels };
+    $.ajax({
+      url: "/pages/home",
+      dataType: "json",
+      contentType: "application/json",
+      data: JSON.stringify(putData),
+      type: "PUT",
+    })
+      .fail(function (xhr, status) {
+        omniclopse.ux.saveContentFailed(icon);
+      })
+      .done(function () {
+        omniclopse.ux.saveContentCompleted(icon);
+      });
+  };
+})((window.omniclopse = window.omniclopse || {}), $);
+
+ +

When the onContentEdited event is fired for an element

+ +
    +
  • the child i element which holds the editable indicator is found
  • +
  • the parts of the page that need to be persisted are gathered
  • +
  • saveContentStarted is called
  • +
  • the jquery.ajax method is used to persist the page (yes, with a hardcoded URL this is a work-in-progress after all)
  • +
  • the ajax methods fail and done promises are associated with the saveContentFailed and saveContentCompleted methods respectively
  • +
+ +

This did need a slight change to the JS that watches for changes to the page that was introduced in a previous article

+ +
(function (omniclopse, $, ckedit) {
+  "use strict";
+
+  //shamelessly borrowed from http://stackoverflow.com/a/14027188/222163
+  omniclopse.bindEvents = function () {
+    var before;
+    var timer;
+    $("*[contenteditable]")
+      .on("focus", function () {
+        before = $(this).html();
+      })
+      .on("keyup paste", function () {
+        if (before != $(this).html()) {
+          clearTimeout(timer);
+          var el = $(this)[0];
+          timer = setTimeout(function () {
+            omniclopse.onContentEdited(el);
+          }, 500);
+        }
+      });
+
+    //ckeditor replaces content when it inits against an element - yay
+    ckedit.on("instanceReady", function (e) {
+      $(e.editor.element.$).append(
+        '<i class="fa fa-pencil editable-affordance"></i>'
+      );
+    });
+  };
+})((window.omniclopse = window.omniclopse || {}), $, CKEDITOR);
+
+ +

This now adds the i child element which indicates that a particular element is editable which is necessary because of how ckeditor alters the DOM when it picks up on a contenteditable element.

+ +

And, rather than calling omniclopse.onContentEdited it now passes in the page element that triggered the event so its editable indicator can be updated.

+ +

The result

+ +

is a pretty, funky, pulsing indicator that shows an element is editable and changes state with the element to keep the user informed of what is happening in the background.

+ +

editable indicator changing state after typing finishes

+ +

Doh-stscript

+ +

a postscript but also doh

+ +

The eagle-eyed will notice a difference between the first example gif of the end result and this one. Which is the result of a bug I introduced.

+ + + +

The code above which actually fires the onContentEdited event uses a timeout so that the event doesn't fire until after content has finished changing.

+ +

In the original version it looked like timer = setTimeout(omniclopse.onContentEdited, 500); which says call the omniclopse.onContentEdited event after 500 milliseconds.

+ +

When I had to pass in the element so its state could be updated I made the simplest (and stupidest) change possible so that the line of code now read timer = setTimeout(omniclopse.onContentEdited($(this)[0]), 500);

+ +

Even without viewing these side-by-side JS ninjas might see what I did…

+ +
timer = setTimeout(omniclopse.onContentEdited, 500);
+timer = setTimeout(omniclopse.onContentEdited($(this)[0]), 500);
+
+ +

Because the second version has brackets against the function name JS evaluates the function as soon as it parses it which isn't what we want to happen.

+ +

This is definitely what qualifies as an ID-10T problem.

+ +

What this meant was as soon as the HTML changed and even while the user is still typing the system starts to update. That wasn't the desired behaviour!

+ +

This code should read (as it does above)…

+ +
var el = $(this)[0];
+timer = setTimeout(function () {
+  omniclopse.onContentEdited(el);
+}, 500);
+
+ +

This now captures the element that is being edited in the el variable and then passes a function to setTimeout which when SetTimeout actually runs calls onContentEdited.

+ +

The even more eagle-eyed will notice I've stopped bothering to write tests for these little bits of JS and now I'm introducing bugs by changing old bits of code. Who could have guessed?!

+ +
+ + +
+
+ + + + + + + + + + diff --git a/better-affordance.html b/better-affordance.html new file mode 100644 index 000000000..c9c23873c --- /dev/null +++ b/better-affordance.html @@ -0,0 +1,469 @@ + + + + + + + + + + + + + + + + + + + + + + Websites != CMS Platform - Better Editable Affordance + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sun Jul 20 2014
+

Websites != CMS Platform - Better Editable Affordance

+
+
+ +
+ +
+ + tag-icon + + learning + + + + + tag-icon + + cms + + + + + tag-icon + + design + + + + + tag-icon + + web + + + + + tag-icon + + series + + + +
+
+
+
+ +
+

This post is part of a series where I'm hoping to prove to myself that building a dynamic website with NodeJS is much more fun than using a CMS platform. See the first post for an explanation of why

+ +

The code can be found on GitHub

+ +

Previous Post

+ +

In the last post I wasn't happy with the visual affordance that a page element is editable.

+ +

editable sections for anonymous users

+ +

editable sections for anonymous users

+ + + +

I also wasn't happy that the page elements shifted around as alerts were added to the screen.

+ +

editing the page

+ +

So…

+ +

That's what a proof of concept is for, right?

+ +

I still don't have a better idea of how to indicate that an element is editable but we can make it nicer!

+ +

And…

+ +

There are two steps

+ +
    +
  • Make the affordance more betterer
  • +
  • Make the affordance give more info
  • +
+ +

A more affordable affordance

+ +

ouch! what a pun

+ +

The indicator that an element is editable has to be on the element itself otherwise how is a user to know what they can edit - but what we had didn't draw the eye.

+ +

By using CSS3 keyframes we can cock-a-snoot at older browsers (without breaking them) and get the desired behaviour.

+ +

Pulsing editor indicator

+ +
@mixin editorAffordance($size, $pos, $glow) {
+  position: absolute;
+  left: $pos;
+  top: $pos;
+  z-index: 900;
+  color: $glow;
+  font-size: $size;
+  border-radius: 15px;
+  padding: 5px;
+}
+
+[contenteditable] {
+  -moz-user-select: none;
+  -khtml-user-select: none;
+  -webkit-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+  outline: none;
+  position: relative;
+}
+
+div[contenteditable] {
+  .editable-affordance {
+    @include editorAffordance(1em, 0, $complementaryDarkTwo);
+    @include animation("darkpulse 2.5s infinite ease-in");
+  }
+}
+
+h1[contenteditable] {
+  .editable-affordance {
+    @include editorAffordance(0.5em, -0.5em, gold);
+    @include animation("goldpulse 2.5s infinite ease-in");
+  }
+}
+
+ +

The important bits here are:

+ +

The positioning

+ +

Using the [contenteditable] rule to set position:relative on the editable elements means we can add an element as a child with .editable-afforance as one of its classes. That class has a rule that sets position:absolute and some positioning to put the element top left (but those positions are passed in so don't need to be top left).

+ +

Positioning something absolutely inside something that is positioned relatively positions the child in relation to the parent (see - CSS is straight-forward +).

+ +

Giving an element that indicates something is editable but doesn't push that editable content out of its way.

+ +

No blue outline

+ +

Adding user-select:none means that when the editable element is selected the browser doesn't (shouldn't?) add its default outline that indicates the item is selected.

+ +

The magic

+ +

The @include animation('dark pulse... is where the magic happens.

+ +

The animation.scss file has some scss goodness that pumps out browser specific versions of the rules required for the pulse effect. That complexity also hides what's going on somewhat.

+ +

As always the Mozilla Developer Network documentation is awesome. In (very) short the animation rule is passed the name of a keyframes rule. The keyframes rule tells the browser what CSS to apply at known points in the animation. Those known points are calculated using the animation duration.

+ +

So, if 2s is set as the animation-duration then a keyframe rule for 50% applies after 1 second.

+ +

Here there are three rules that set a cycling box shadow inside and outside of the element

+ +
@include keyframes(darkpulse) {
+  0% {
+    box-shadow: inset 0px 0px 5px rgb(61, 28, 79), 0px 0px 15px rgb(61, 28, 79);
+  }
+  50% {
+    box-shadow: inset 0px 0px 15px rgb(61, 28, 79), 0px 0px 35px rgb(61, 28, 79);
+  }
+  100% {
+    box-shadow: inset 0px 0px 5px rgb(61, 28, 79), 0px 0px 15px rgb(61, 28, 79);
+  }
+}
+
+ +

And here is a codepen so you can play with the CSS that generates the effect

+ +

See the Pen gIseG by Paul D'Ambra (@pauldambra) on CodePen.

+ + +

The next post will cover the second objective of making the editable affordance give information about whether saving edits was successful.

+ +
+ + +
+
+ + + + + + + + + + diff --git a/categories.html b/categories.html new file mode 100644 index 000000000..c9be9c72d --- /dev/null +++ b/categories.html @@ -0,0 +1,1075 @@ + + + + + + + + + + + + + + + + + + + + + + Categories + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +

Categories

+
+ + + tag-icon + AMP + + + + tag-icon + CI + + + + tag-icon + HTTP + + + + tag-icon + Structured Data + + + + tag-icon + Tortured Metaphors + + + + tag-icon + agile + + + + tag-icon + c# + + + + tag-icon + cms + + + + tag-icon + continuous delivery + + + + tag-icon + continuous-integration + + + + tag-icon + cooking + + + + tag-icon + databases + + + + tag-icon + design + + + + tag-icon + discussion + + + + tag-icon + energy + + + + tag-icon + hardware + + + + tag-icon + kata + + + + tag-icon + life events + + + + tag-icon + metaphors + + + + tag-icon + month-notes + + + + tag-icon + naming + + + + tag-icon + nosql + + + + tag-icon + programming + + + + tag-icon + rant + + + + tag-icon + react + + + + tag-icon + refactoring + + + + tag-icon + semantics + + + + tag-icon + serverless + + + + tag-icon + snark + + + + tag-icon + software-engineering + + + + tag-icon + ssh + + + + tag-icon + testing + + + + tag-icon + ux + + + + tag-icon + visualisation + + + + tag-icon + windows-mobile + + + + tag-icon + year-notes + + +
+
+ +

AMP

+ + +

CI

+ + +

HTTP

+ + +

Structured Data

+ + +

Tortured Metaphors

+ + +

agile

+ + +

c#

+ + +

cms

+ + +

continuous delivery

+ + +

continuous-integration

+ + +

cooking

+ + +

databases

+ + +

design

+ + +

discussion

+ + +

energy

+ + +

hardware

+ + +

kata

+ + +

life events

+ + +

metaphors

+ + +

month-notes

+ + +

naming

+ + +

nosql

+ + +

programming

+ + +

rant

+ + +

react

+ + +

refactoring

+ + +

semantics

+ + +

serverless

+ + +

snark

+ + +

software-engineering

+ + +

ssh

+ + +

testing

+ + +

ux

+ + +

visualisation

+ + +

windows-mobile

+ + +

year-notes

+ + + +
+ + + + + + + + diff --git a/dear-diary-year-one.html b/dear-diary-year-one.html new file mode 100644 index 000000000..5f6a60e1d --- /dev/null +++ b/dear-diary-year-one.html @@ -0,0 +1,410 @@ + + + + + + + + + + + + + + + + + + + + + + Dear Diary Year One + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Dear Diary

+ +

+I've fallen into a habit of ending each working week by tweeting a diary entry of my week. And have realised I've kept it up for a year now. +

+

+It's not very accessible because the entries are images so that I can fit them into a tweet. But it's been really useful for keping track of the positives I (and mostly the people I work with) achieve. +

+

+ But mostly it's helpful for reflecting on whether I'm doing valuable things and whether they are the things I meant to do each week. +

+

+ You can follow the entries on twitter but that doesn't feel permanent enough so I've gathered them here. +

+ + + + +

2017week1

+ an image from my diary from before I switched to text based entries served as images + + + +

2017week2

+ an image from my diary from before I switched to text based entries served as images + + + +

2017week3

+ an image from my diary from before I switched to text based entries served as images + + + +

2017week4

+ an image from my diary from before I switched to text based entries served as images + + + +

2017week5and6

+ an image from my diary from before I switched to text based entries served as images + + + +

2017week7

+ an image from my diary from before I switched to text based entries served as images + + + +

2017week8

+ an image from my diary from before I switched to text based entries served as images + + + +

2017week9

+ an image from my diary from before I switched to text based entries served as images + + + +

2017week10

+ an image from my diary from before I switched to text based entries served as images + + + +

2017week11

+ an image from my diary from before I switched to text based entries served as images + + + +

2017week12

+ an image from my diary from before I switched to text based entries served as images + + + +

2017week13

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week1

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week2

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week3

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week4

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week5

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week6

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week7

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week8

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week9

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week10

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week11

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week12

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week13

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week14

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week15

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week16

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week17

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week18

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week19

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week20and21

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week22

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week23

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week24

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week25

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week26

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week27

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week28

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week29

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week30

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week31

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week32

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week33

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week34

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week35

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week36

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week37

+ an image from my diary from before I switched to text based entries served as images + + + +

2018week38

+ an image from my diary from before I switched to text based entries served as images + + + + +
+ + + + + + + + diff --git a/facebook-instant-feed.xml b/facebook-instant-feed.xml new file mode 100644 index 000000000..bd6b7448b --- /dev/null +++ b/facebook-instant-feed.xml @@ -0,0 +1,742 @@ + + + Mindless Rambling Nonsense + My thoughts are mindless and rambling so the best place for them is the internet + https://pauldambra.dev/ + 2023-07-24T18:48:55+00:00 + en-gb + + + Zucchini focaccia + My thoughts are mindless and rambling so the best place for them is the internet + + + https://pauldambra.dev/recipes/2023/07/zucchini-focaccia.html + https://pauldambra.dev/recipes/2023/07/zucchini-focaccia.html + + 2023-07-24T07:00:00+00:00 + paul.dambra+fb-instant@gmail.com + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +
A kanban board
+
+ + +

post.title

+ + +

+

This year we've grown way too many zucchinis.

+ +

the plants

+ +

One way I've been trying to use them up is putting them in dough.

+ +

The kids love it!

+ + + +

chopped garlic and zucchini +blitzed cooked zucchini +dough +ready for the oven +ready for the oven two +cooked-one +cooked-two

+ +

+ + +
+ Paul D'Ambra +
+ + + + +
+
+ + +
+
+ + + Paul's Law + My thoughts are mindless and rambling so the best place for them is the internet + + + https://pauldambra.dev/2023/06/pauls-law.html + https://pauldambra.dev/2023/06/pauls-law.html + + 2023-06-08T08:00:00+00:00 + paul.dambra+fb-instant@gmail.com + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +
A kanban board
+
+ + +

post.title

+ + +

+

Everyone should have their own law… This is mine:

+ +

All new build tools are better than what came before. Until they are able to solve all of the problems of the thing they replaced and then they're at least as bad. A new tool will then replace them

+ + + + + +
+ Paul D'Ambra +
+ + + + +
+
+ + +
+
+ + + March 2023 Month Notes + My thoughts are mindless and rambling so the best place for them is the internet + + + https://pauldambra.dev/2023/03/mar-month-notes.html + https://pauldambra.dev/2023/03/mar-month-notes.html + + 2023-04-02T18:00:00+00:00 + paul.dambra+fb-instant@gmail.com + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +
A kanban board
+
+ + +

post.title

+ + +

+

Year Goals for 2023

+ +
    +
  • continue becoming a better engineer and team-mate
  • +
  • practice Italian every day
  • +
  • train at the gym at least twice a week every week
  • +
  • 8 leisurely cycle rides
  • +
  • visit Italy at least twice
  • +
+ +

How did March go?

+ + +

+ + +
+ Paul D'Ambra +
+ + + + +
+
+ + +
+
+ + + I saved 183 million dollars by not moving to the Cloud + My thoughts are mindless and rambling so the best place for them is the internet + + + https://pauldambra.dev/2023/03/the-cloud.html + https://pauldambra.dev/2023/03/the-cloud.html + + 2023-03-19T08:00:00+00:00 + paul.dambra+fb-instant@gmail.com + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +
A kanban board
+
+ + +

post.title

+ + +

+

The average human brain has 2.5 petabytes of memory (source: random google result). 2.5 Petabytes is equal to 2,500,000 Gigabytes. Or 2,500 terabytes. The u-12tb1.112xlarge instance on AWS has 13TB of memory.

+ +

So, conclusively, 193 u-12tb1.112xlarge instances are equivalent to one brain. Or your brain could run in AWS for 15,305,472.00 USD per month. Therefore, I've saved 183 million dollars by not moving my brain to the cloud in the last year alone.

+ +

There seem to be a fashion for writing articles claiming that some company has saved hundreds of millions of dollars by not moving to the cloud.

+ +

I managed phyisical servers for more than a decade. For the UK Magistrates Courts and for the British Mountaineering Council. I was pretty good at it. But, I absolutely jumped at the chance to move to the cloud. Why?

+ + +

+ + +
+ Paul D'Ambra +
+ + + + +
+
+ + +
+
+ + + Office 365 mega-thread + My thoughts are mindless and rambling so the best place for them is the internet + + + https://pauldambra.dev/2023/03/office-365-mega-thread.html + https://pauldambra.dev/2023/03/office-365-mega-thread.html + + 2023-03-05T08:00:00+00:00 + paul.dambra+fb-instant@gmail.com + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +
A kanban board
+
+ + +

post.title

+ + +

+

Between the end of 2019 and when I left the Co-op on Sep 18th 2021 I used Office 365 and never found a single redeeming feature.

+ +

Well, maybe one, a small set of Co-op employees had access to slack and g-suite in Co-op Digital. But in the rest of Co-op they were using installed (i.e. local only) old versions of Office (without video-conferencing and chat). For them, maybe Office 365 was an improvement - and certainly it made remote work during the pandemic possible.

+ +

But for me, it was a constant source of frustration.

+ +

I'm sure that there are great people working on Office with care and attention but I didn't experience that. It was like being haunted and losing your mind all in one go. I had a habit of tooting my frustrations. I'm aware of them having been submitted as evidence in one procurement process. I don't think they swung the decision.

+ +

If the tooter-web dissappeared they'd be the one thing I missed and so I've copied them here.

+ + +

+ + +
+ Paul D'Ambra +
+ + + + +
+
+ + +
+
+ + + Feb 2023 Month Notes + My thoughts are mindless and rambling so the best place for them is the internet + + + https://pauldambra.dev/2023/03/feb-month-notes.html + https://pauldambra.dev/2023/03/feb-month-notes.html + + 2023-03-01T08:00:00+00:00 + paul.dambra+fb-instant@gmail.com + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +
A kanban board
+
+ + +

post.title

+ + +

+

Year Goals for 2023

+ +
    +
  • continue becoming a better engineer and team-mate
  • +
  • practice Italian every day
  • +
  • train at the gym at least twice a week every week
  • +
  • 8 leisurely cycle rides
  • +
  • visit Italy at least twice
  • +
+ +

How did February go?

+ + +

+ + +
+ Paul D'Ambra +
+ + + + +
+
+ + +
+
+ + + Jan 2023 Month Notes + My thoughts are mindless and rambling so the best place for them is the internet + + + https://pauldambra.dev/2023/02/jan-month-notes.html + https://pauldambra.dev/2023/02/jan-month-notes.html + + 2023-02-03T08:00:00+00:00 + paul.dambra+fb-instant@gmail.com + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +
A kanban board
+
+ + +

post.title

+ + +

+

Year Goals for 2023

+ +
    +
  • continue becoming a better engineer and team-mate
  • +
  • practice Italian every day
  • +
  • train at the gym at least twice a week every week
  • +
  • 8 leisurely cycle rides
  • +
  • visit Italy at least twice
  • +
+ +

How did January go?

+ + +

+ + +
+ Paul D'Ambra +
+ + + + +
+
+ + +
+
+ + + 2022 Year Notes + My thoughts are mindless and rambling so the best place for them is the internet + + + https://pauldambra.dev/2023/01/year-notes.html + https://pauldambra.dev/2023/01/year-notes.html + + 2023-01-15T08:00:00+00:00 + paul.dambra+fb-instant@gmail.com + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +
A kanban board
+
+ + +

post.title

+ + +

+

Forgive me, for I have sinned, it's been 2 years since my last year notes 👼

+ +

I wrote year notes for 2019, and 2020. I've been super un-inspired to write for the last few years. Which is a shame - because it's a a great way to learn.

+ +

So much happened last year that it feels way longer than a year. So, discipline over motiviation - here are my 2022 year notes. Or at least as much as I can write while the house is empty of other people.

+ + +

+ + +
+ Paul D'Ambra +
+ + + + +
+
+ + +
+
+ + + Pasta e fasule + My thoughts are mindless and rambling so the best place for them is the internet + + + https://pauldambra.dev/recipes/2023/01/pasta-e-fasule.html + https://pauldambra.dev/recipes/2023/01/pasta-e-fasule.html + + 2023-01-08T07:00:00+00:00 + paul.dambra+fb-instant@gmail.com + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +
A kanban board
+
+ + +

post.title

+ + +

+

Pasta e Fasule, as made by my Neapolitan Nonna Luisa

+ +

Pasta e Fasule is Neapolitan for Pasta e Fagiole which is Italian for Pasta and Beans

+ +

Pasta and bean soup reminds me of being looked after when recovering from an illness as a kid. I like it so thick the spoon will stand up.

+ + + +

You can use odds and ends of pasta or break spaghetti in. When my dad was little he'd be sent to the pasta shop to get their broken odds and ends. They were cheaper.

+ +

steps in making pasta fasule as a gif

+ +

+ + +
+ Paul D'Ambra +
+ + + + +
+
+ + +
+
+ + + Pizza dough + My thoughts are mindless and rambling so the best place for them is the internet + + + https://pauldambra.dev/recipes/2022/09/pizza-dough.html + https://pauldambra.dev/recipes/2022/09/pizza-dough.html + + 2022-09-04T07:00:00+00:00 + paul.dambra+fb-instant@gmail.com + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +
A kanban board
+
+ + +

post.title

+ + +

+

It turns out that Neapolitan pizza dough is a strictly described thing. The UK version of the EU rules are here. Those instructions use 1.8 kilograms of flour and 1 litre of water. But (if you're not going to a wholesaler) flour comes in 1 kilogram bags. Ingredients have been adjusted to 1kg for this recipe.

+ +

I sometimes use a biga starter. Instructions for that are here. But it takes more work, time, and nuance.

+ +

This recipe can be completed in a single day and makes a very consistently tasty dough

+ +

(Yes! That much salt)

+ +

the dough balls resting on a wooden surface

+ +

+ + +
+ Paul D'Ambra +
+ + + + +
+
+ + +
+
+ +
+
\ No newline at end of file diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 000000000..7d4d00279 Binary files /dev/null and b/favicon.ico differ diff --git a/feed.xml b/feed.xml new file mode 100644 index 000000000..1cae8e968 --- /dev/null +++ b/feed.xml @@ -0,0 +1,1810 @@ + + + + Mindless Rambling Nonsense + My thoughts are mindless and rambling so the best place for them is the internet + https://pauldambra.dev + + 2023-07-24T18:48:55+00:00 + + + Zucchini focaccia + <p>This year we've grown way too many zucchinis.</p> + +<p><img src="/images/2023/07/the plants.jpg" alt="the plants" /></p> + +<p>One way I've been trying to use them up is putting them in dough.</p> + +<p>The kids love it!</p> + +<!--alex disable he-her dad-mom--> + +<p><img src="/images/2023/07/chopped.jpg" alt="chopped garlic and zucchini" loading="lazy" /> +<img src="/images/2023/07/blitzed.jpg" alt="blitzed cooked zucchini" loading="lazy" /> +<img src="/images/2023/07/dough.jpg" alt="dough" loading="lazy" /> +<img src="/images/2023/07/ready for the oven.jpg" alt="ready for the oven" loading="lazy" /> +<img src="/images/2023/07/ready for the oven two.jpg" alt="ready for the oven two" loading="lazy" /> +<img src="/images/2023/07/cooked-one.jpg" alt="cooked-one" loading="lazy" /> +<img src="/images/2023/07/cooked-two.jpg" alt="cooked-two" loading="lazy" /></p> + + Mon, 24 Jul 2023 07:00:00 +0000 + + + https://pauldambra.dev/recipes/2023/07/zucchini-focaccia.html + + + https://pauldambra.dev/recipes/2023/07/zucchini-focaccia.html + + + + + Paul's Law + <p>Everyone should have their own law… This is mine:</p> + +<h1 id="all-new-build-tools-are-better-than-what-came-before-until-they-are-able-to-solve-all-of-the-problems-of-the-thing-they-replaced-and-then-theyre-at-least-as-bad-a-new-tool-will-then-replace-them">All new build tools are better than what came before. Until they are able to solve all of the problems of the thing they replaced and then they're at least as bad. A new tool will then replace them</h1> + +<!--more--> + +<p>Anyone who remembers the mad rush to replace every build tool in your JavaScript projects with Grunt, to have to replace that with Gulp only days later, will know what I mean.</p> + + Thu, 08 Jun 2023 08:00:00 +0000 + + + https://pauldambra.dev/2023/06/pauls-law.html + + + https://pauldambra.dev/2023/06/pauls-law.html + + + + + March 2023 Month Notes + <p>Year Goals for 2023</p> + +<ul> + <li>continue becoming a better engineer and team-mate</li> + <li>practice Italian every day</li> + <li>train at the gym at least twice a week every week</li> + <li>8 leisurely cycle rides</li> + <li>visit Italy at least twice</li> +</ul> + +<p>How did March go?</p> + +<!--more--> + +<p>In reverse order</p> + +<h2 id="visit-italy-at-least-twice">Visit Italy at least twice</h2> + +<p>Excitement building this month because "Italy Trip Number One" is in April…</p> + +<h2 id="8-leisurely-cycle-rides">8 leisurely cycle rides</h2> + +<p>0 cycle rides in Mar.</p> + +<h2 id="train-at-the-gym-at-least-twice-a-week">train at the gym at least twice a week</h2> + +<p>✅ Three times a week even while in the Caribbean with work. Still enjoying it.</p> + +<h2 id="practice-italian-every-day">Practice Italian every day</h2> + +<p>✅ only a few minutes at a time, but I did it every day.</p> + +<h2 id="continue-becoming-a-better-engineer-and-team-mate">Continue becoming a better engineer and team-mate</h2> + +<p>This was my last month on <a href="https://posthog.com/handbook/small-teams/product-analytics">team product analytics</a>. I'm moving to <a href="https://posthog.com/handbook/small-teams/session-recording">team session replay</a>. Ben and I built network performance monitoring in December and had a great work vibe - exciting to be building more monitoring tools.</p> + +<p>It makes sense to move teams now because there's a natural gap… 1 week in Aruba with work, and then 2 weeks in Italy. So, I can start fresh when I get back.</p> + +<h3 id="yep-thats-right---aruba">Yep, that's right - Aruba</h3> + +<p>We had our annual offsite in Aruba this month. It was a ridiculously beautiful place.</p> + +<p><img src="/images/2023/04/02/pool.jpg" alt="a pina colada by the pool" loading="lazy" /></p> + +<p>The highlight is always <a href="https://posthog.com/handbook/company/offsites#all-company-offsite-hackathon">the hackathon</a>. This year I worked on a team building issue tracking into PostHog.</p> + +<p><img src="/images/2023/04/02/issue-tracking.gif" alt="a gif of the issue tracking page we built" loading="lazy" /></p> + +<p>Hackathon always reminds me of how powerful it is to start work together and excited.</p> + +<p>My new favourite planning method is "post-it notes on a table with food and drink".</p> + +<p><img src="/images/2023/04/02/planning.jpg" alt="a table with post-it notes on the surface" loading="lazy" /></p> + + Sun, 02 Apr 2023 18:00:00 +0000 + + + https://pauldambra.dev/2023/03/mar-month-notes.html + + + https://pauldambra.dev/2023/03/mar-month-notes.html + + + + + I saved 183 million dollars by not moving to the Cloud + <p>The average human brain has 2.5 petabytes of memory (source: random google result). 2.5 Petabytes is equal to 2,500,000 Gigabytes. Or 2,500 terabytes. The u-12tb1.112xlarge instance on AWS has 13TB of memory.</p> + +<p>So, conclusively, 193 u-12tb1.112xlarge instances are equivalent to one brain. Or your brain could run in AWS for 15,305,472.00 USD per month. Therefore, I've saved 183 million dollars by not moving my brain to the cloud in the last year alone.</p> + +<p>There seem to be a fashion for writing <a href="https://world.hey.com/dhh/why-we-re-leaving-the-cloud-654b47e0">articles</a> <a href="https://tech.ahrefs.com/how-ahrefs-saved-us-400m-in-3-years-by-not-going-to-the-cloud-8939dd930af8">claiming</a> that some company has saved hundreds of millions of dollars by not moving to the cloud.</p> + +<p>I managed phyisical servers for more than a decade. For the UK Magistrates Courts and for the British Mountaineering Council. I was pretty good at it. But, I absolutely jumped at the chance to move to the cloud. Why?</p> + +<!--more--> + +<h2 id="i-really-dont-miss-running-my-own-kit-colocated-or-directly-owned">I <em>really</em> don't miss running my own kit (colocated or directly owned).</h2> + +<p>I don't miss cycling around Manchester on a Bank Holiday weekend because I'd miscalculated how much network cabling I'd need for an upgrade.</p> + +<p>I don't miss keeping a spreadsheet of storage so I knew when to order disks, negotiating with suppliers for cost of new disks, because I was buying a slightly smaller bulk than AWS.</p> + +<p>I don't miss having to explain to folk in datacenter support that they could take the disks out of my failed server and put them in a new server if they had one available.</p> + +<p>I don't miss the day the single point of failure in the rack failed and everything was offline while I waited for a new doohickey to be shipped to me because it didn't make sense to keep spares of everything on hand.</p> + +<p>I don't miss trying to figure out if some new generation of server hardware would work for or would fit in my rack as manufacturers stopped making the kit we did use.</p> + +<p>I don't miss hacking at a multi-thousand pound HP Proliant server with a breadknife because it was the only way to make the thing fit together due to a manufacturing error. And I couldn't wait for a replacement.</p> + +<h2 id="however">However,</h2> + +<p>The problem with all those articles isn't that they say you should or shouldn't run in the cloud. But that they make bold claims about what <em>everyone</em> should do.</p> + +<p>I'm not going to say every workload should run in the cloud (cliche nod to StackOverflow) but it certainly isn't free to get all of the benefits.</p> + + Sun, 19 Mar 2023 08:00:00 +0000 + + + https://pauldambra.dev/2023/03/the-cloud.html + + + https://pauldambra.dev/2023/03/the-cloud.html + + + + + Office 365 mega-thread + <p>Between the end of 2019 and when I left the Co-op on Sep 18th 2021 I used Office 365 and never found a single redeeming feature.</p> + +<p>Well, maybe one, a small set of Co-op employees had access to slack and g-suite in Co-op Digital. But in the rest of Co-op they were using installed (i.e. local only) old versions of Office (<strong>without video-conferencing and chat</strong>). For them, maybe Office 365 was an improvement - and certainly it made remote work during the pandemic possible.</p> + +<p>But for me, it was a constant source of frustration.</p> + +<p>I'm sure that there are great people working on Office with care and attention but I didn't <em>experience</em> that. It was like being haunted and losing your mind all in one go. I had a habit of <a href="https://twitter.com/pauldambra/status/1185848202249023488">tooting my frustrations</a>. I'm aware of them having been submitted as evidence in one procurement process. I don't think they swung the decision.</p> + +<p>If the tooter-web dissappeared they'd be the one thing I missed and so I've copied them here.</p> + +<!--more--> + +<p>I didn't start the thread until after I had already made several of the toots. So the initial few dates might appear out of order. I've kept the order that I added them to the thread instead of listing them in date order.</p> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">10:20AM Oct 20, 2019</div> + My life with office 365 is not richer + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">3:52 PM Oct 8, 2019</div> + Me (a person at work): I'd like to import* a calendar<br /> +Office 365: "YOU MUST WANT SPORTS!"<br /> +<br /> +This should not be the default behaviour of anything<br /> +<br /> +* I want to view a calendar + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/1.jpeg" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">7:46PM Sep 26, 2019</div> + Interesting to learn that all one time password apps should behave the same because it's described in an RFC...<br /> +<br /> +And yet Office 365 can only use Microsoft authenticator<br /> +<br /> +#NewMicrosoft<br /> +<br /> +#VeryStaringFace + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">12:01PM Oct 18, 2019</div> + Hey @office365 can you opensource? It would be quicker for me to contribute code to fix Word than to figure out how to amend a numbered list in this garbage fire + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + + <video autoplay="" muted="" loop="" playsinline="" class="rounded drop-shadow-md"> + <source src="/images/office365/2.mp4" type="video/mp4" /> + </video> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">8:33PM Oct 9, 2019</div> + In word you click "Give feedback to Microsoft".<br /> +<br /> + 3 apps, 3 different feedback mechanisms.<br /> +<br /> +that gives me a sad + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">7:41PM Oct 9, 2019</div> + Where's the approprate place to report that the visual affordance for giving feedback is different in Outlook, Word, and Powerpoint (in the browser)? + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">4:48PM Oct 8, 2019</div> + Hey @office365 what is going on with pasting into lists in Word in Chrome?!<br /> +<br /> +(gif: MS Word in the browser being very odd about lists) + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + + <video autoplay="" muted="" loop="" playsinline="" class="rounded drop-shadow-md"> + <source src="/images/office365/3.mp4" type="video/mp4" /> + </video> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">4:29PM Oct 4, 2019</div> + Me: "a new Word document just look many times already in the last couple of days"<br /> +O365: "Please just this time but not the others could you mindlessly click this box accepting a certificate"<br /> +Me: *just wanting to write some text *clicks OutlookO365: "H! Psyche! Nothing happened . Lol" + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">6:03PM Oct 9, 2019</div> + I just selected a time for an invite in outlook calendar in a browser on my phone. <br /> +<br /> +Great example of why you should use native inputs instead of building your own<br /> +<br /> +(Spoler that was not a native input and it was too hard) + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">10:20AM Oct 20, 2019</div> + And today, I found a uservoice entry for snoozing email (blimey do I miss @inboxbygmail)<br /> +<br /> +I can sign in using google or facebook but not office 365<br /> +<br /> +I have twice given consent for storage of PII but my vote hasn't registered + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/4.jpeg" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">10:24AM Oct 20, 2019</div> + I've just realised that I can "like" an email in Outlook<br /> +<br /> +What am I supposed to imagine happens when I do that?!<br /> +<br /> +Does the other person get an email saying that I've liked a different email? What is it for? + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">10:39AM Oct 20, 2019</div> + Outlook: "try the new focussed inbox. we'll stop moving messages to the 'Clutter' folder"<br /> +Me: "What's the clutter folder?" Where is that?! Ugh I guess I'll learn more"<br /> +Outlook: "lol video is unavailable. psyche!" + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/5.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + <img src="/images/office365/6.jpeg" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">10:41AM Oct 20, 2019</div> + Nope, no clutter folder :/ + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/7.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">1:39PM Oct 21, 2019</div> + Listening to someone use Outlook for the first time<br /> +<br /> +"this isn't nice"<br /> +"that's unexpected"<br /> +<br /> +#UX + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">8:00PM Oct 21, 2019</div> + Insert a picture with the cursor in a cell in excel. Adds image at full size.<br /> +<br /> +Insert a picture in a powerpoint slide. Adds image as small as it feels it can get away with.<br /> +<br /> +As a user<br /> +I want adding images to be as frustrating as possible<br /> +So that I close my laptop and go outside + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">11:10AM Oct 24, 2019</div> + Me: *I wonder if I can book a meeting with someone<br /> +Calendar: I WILL SHOW YOU MYSELF HORZONTL SO ALL TEXT IS HIDDEN. I HALP YOU MAKE A DECIDE + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/8.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">8:35PM Oct 29, 2019</div> + Calendar: "Don't worry some of what you need to click on is off th ebottom of the screen but despite it being lierally the default behaviour of a web page you can't scroll to it"<br /> +Calendar: *holds up hand for high five + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + + <video autoplay="" muted="" loop="" playsinline="" class="rounded drop-shadow-md"> + <source src="/images/office365/9.mp4" type="video/mp4" /> + </video> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">1:00PM Nov 6, 2019</div> + Presented without comment #calendar #search + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/10.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">9:58AM Nov 25, 2019</div> + The two states of opening an email in Outlook on poor signal<br /> +<br /> +(NB I have gmail open in another window in the same browser. Guess whether it can open my mail) + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/11.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + <img src="/images/office365/12.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">11:42AM Dec 3, 2019</div> + Me: find this document<br /> +OneDrive: here are some results... including the folder they are in...<br /> +Me: oh, useful can I open that folder from here<br /> +OneDrive: No! + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + + <video autoplay="" muted="" loop="" playsinline="" class="rounded drop-shadow-md"> + <source src="/images/office365/13.mp4" type="video/mp4" /> + </video> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">11:44AM Dec 3, 2019</div> + And just randomly someone else's name is the title of the left hand column of the OneDrive page.<br /> +<br /> +Really is the least discoverable UI I've worked with for quite some time. + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/14.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">12:56PM Dec 3, 2019</div> + When the browser tab has this red thing it's Outlook making it look like you've new mail when actually you haven't + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/15.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">9:28AM Dec 12, 2019</div> + Me: navigates to a week in the calendar<br /> +Me: "yep, that's the one" *clicks new event<br /> +Office 364.5: Ah, you must want to default to today not the week you're looking actually + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + + <video autoplay="" muted="" loop="" playsinline="" class="rounded drop-shadow-md"> + <source src="/images/office365/16.mp4" type="video/mp4" /> + </video> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">6:58PM Dec 19, 2019</div> + My office 420 session just expired *while* I was typing into a Word document.<br /> +<br /> +It told me to refresh the page.<br /> +<br /> +I did.<br /> +<br /> +3 paragraphs of text gone.<br /> +<br /> +I have literally never lost a character of text in over a decade of using Google. + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">12:23PM Jan 28, 2020</div> + Today in my-life-editing-word-documents-in-chrome the cursor moves around while I'm typing and so every sentence is a battle + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + + <video autoplay="" muted="" loop="" playsinline="" class="rounded drop-shadow-md"> + <source src="/images/office365/17.mp4" type="video/mp4" /> + </video> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">10:17AM Feb 11, 2020</div> + Me: fuck it, ok, I'll open Outlook as a native app<br /> +Outlook: you have to quit word first, Lol" + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + + <video autoplay="" muted="" loop="" playsinline="" class="rounded drop-shadow-md"> + <source src="/images/office365/18.mp4" type="video/mp4" /> + </video> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">10:21AM Feb 11, 2020</div> + Outlook (native app): log in twice now please.<br /> +Outlook (native app): click allow or deny on this meaningless tech message.<br /> +Outlook (native app): and now here are two appointments in the past that nobody has asked about<br /> +<br /> +#FuckingHell + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">10:55AM Feb 11, 2020</div> + Outlook (native app): here're 1664 reminders for the past, human I am halp + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">9:05AM Feb 12, 2020</div> + Even though the cursor is in it I can't type in the box to thell them what I don't like...<br /> +<br /> +I don't feel like my feedback is valued. + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/19.jpeg" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">6:36pM Mar 2, 2020</div> + Me: "please save this spreadsheet with this password to open it"<br /> +Me: *pastes password into box<br /> +Excel: "please confirm the password into this new box"<br /> +Me: *pastes password into box<br /> +Excel: "they are not the same"<br /> +Me: "I feel like you have secret password format restrictions" + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + + <video autoplay="" muted="" loop="" playsinline="" class="rounded drop-shadow-md"> + <source src="/images/office365/20.mp4" type="video/mp4" /> + </video> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">2:27PM Mar 24, 2020</div> + Searching in GMail: "did you mean this email that doesn't actually have the words you typed into search but we had a feeling you might actually want?"<br /> +<br /> +Searching in Outlook: + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/21.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">9:21AM Jun 7, 2020</div> + The mail icon on the left has no little notification. That means I don't have mail. If it had a little notification it would mean I had mail<br /> +<br /> +The mail icon on the right has a little notification. That rarely means I have mail. How is even that little detail so badly implemented?! + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/22.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">9:30AM Jun 7, 2020</div> + I checked. There was no mail. Now, there's a notification on twitter. I checked. There was something new.<br /> +<br /> +Twitter can get it right and they've made showing a list of snippets of text complicated. + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/23.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">7:50PM Jun 15, 2020</div> + Me: *signs in to Word desktop app<br /> +Word: you have to sign in<br /> +Me: *clicks sign in<br /> +Word: *with no feedback "you have to sign in"<br /> +Me: *clicks sign in<br /> +Word: *with no feedback "you have to sign in"<br /> +...<br /> +Me: *clicks sign in<br /> +Word: "seventh times the charm" *saves changes + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">7:54PM Jun 15, 2020</div> + Me: *highlights line of text<br /> +Me: *paste<br /> +Word: "Don't worry, I've stuck this pasted text as the start of the next nearest heading in the document." + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + + <video autoplay="" muted="" loop="" playsinline="" class="rounded drop-shadow-md"> + <source src="/images/office365/24.mp4" type="video/mp4" /> + </video> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">12:18PM Jun 16, 2020</div> + Me: *clicks a calendar appointment in O365 web calendar<br /> +O342: "here's your little white diamond"<br /> +Me: "no, that's not what should happen" *clicks again<br /> +O213: "yep, little white diamond, as requested"<br /> +Me: *waits a minute and clicks again<br /> +O420: "your diamond, good sir" + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/25.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">10:18AM Jun 19, 2020</div> + wait, so teams works in the app or in chrome?<br /> +<br /> +sorry, I was late for the meeting. I foolishly thought having two browsers on my computer would be enough + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">12:02PM Jun 26, 2020</div> + me: scroll down please<br /> +word in the browser: I DoNT sCroLl An1m0r<br /> +me: it's just a browser window<br /> +word: I DoNT sCroLl An1m0r<br /> +me: refreshes window<br /> +word: NO SCROLL ONLY RENDER + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + + <video autoplay="" muted="" loop="" playsinline="" class="rounded drop-shadow-md"> + <source src="/images/office365/26.mp4" type="video/mp4" /> + </video> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">12:08PM Jun 26, 2020</div> + Ha, forking hall MS, One instance of firefox, all tabs scroll except for MS Word tabs. even newly opened ones. <br /> +<br /> +This has happened to multiple open tabs (and now any new tabs) at the same time.<br /> +<br /> +Office 365 must be a burning nightmare of a code base. + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">12:10PM Jun 26, 2020</div> + Oh, my bad, it was Chrome not firefox. Teams can't do video in Firefox so I have to run two different browsers. + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + + <video autoplay="" muted="" loop="" playsinline="" class="rounded drop-shadow-md"> + <source src="/images/office365/27.mp4" type="video/mp4" /> + </video> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">1:04PM Jun 26, 2020</div> + I have the outlook calendar integration in Slack now. It's great except...<br /> +<br /> +slack: you have a meeting!<br /> +me: * clicks link<br /> +link: * opens in my default browser<br /> +teams: I can't a video in this browser<br /> +me: * copies link from browser URL bar<br /> +me: * pastes into Chrome<br /> +<br /> +1/2 + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">1:04PM Jun 26, 2020</div> + teams: You want to open in app or browser?<br /> +me: IN THE BROWSER<br /> +teams: would you like to join this call<br /> +me: * clicks join now<br /> +<br /> +in another timeline<br /> +<br /> +me: * clicks link<br /> +zoom: * want to join call?<br /> +me: * clicks join now<br /> +<br /> +2/2 + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">11:52AM Jul 15, 2020</div> + Me: highlights text at the beginning of a bullet point and presses delete<br /> +Outlook: I R DELETE THE BULLET POINT AND NOT THE WORDIES AS PER YOUR RECENT REQUEST + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">11:37AM Jul 16, 2020</div> + ORGANISE WHAT MESSAGES, OUTLOOK?! YOU ARE LITERALLY TELLING ME THERE ARE NO MESSAGES AND THAT I SHOULD ORGANISE THEM + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/28.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">8:16PM Jul 16, 2020</div> + How I make a line chart in excel (web version) where the first column should be the Y axis values.<br /> +<br /> +1) copy data to google sheets + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">8:57AM Jul 29, 2020</div> + me: open the next email, please<br /> +outlook: here you go<br /> +me: close email<br /> +me: oh, actually, open email again<br /> +outlook: I can't display that email<br /> +me: but... but... + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">10:31AM Aug 6, 2020</div> + powerpoint: "Here's how text selection works"<br /> +me: "Yes, that is what I expected"<br /> +narrator: "It was not what he expected"<br /> +<br /> +(look at the difference in the same key commands when the cursor is on "predictable" vs. when it is on "Not") + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + + <video autoplay="" muted="" loop="" playsinline="" class="rounded drop-shadow-md"> + <source src="/images/office365/29.mp4" type="video/mp4" /> + </video> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">10:36AM Aug 6, 2020</div> + After I stopped recording the screen I deleted the text by holding down backspace. Sometimes the cursor moved left without actually removing the character to its left.<br /> +<br /> +This is fine becuase text editing is new<br /> +<br /> +NB it is not new and not fine + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + + <video autoplay="" muted="" loop="" playsinline="" class="rounded drop-shadow-md"> + <source src="/images/office365/30.mp4" type="video/mp4" /> + </video> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">8:48AM Aug 21, 2020</div> + me: *typing in box<br /> +teams: wHy NoT rEfErsH teH paJ + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/31.jpeg" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">8:50AM Aug 21, 2020</div> + Let's not worry about the fact that while Teams wants me to refresh the page cos I'm not connected to the internet. I can toot from the same computer.<br /> +<br /> +TIL: https://Twitter.com runs on my laptop<br /> +<br /> +Turns out it's called teams cos it's teeming with bugs + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/32.jpeg" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">2:53PM Sep 2, 2020</div> + Me: ...<br /> +Me: ...<br /> +TeAmS: I HAVE ANTICIPATED YOUR NEEDS AND MUTED THE LIVE STREAM AGAIN FOR YOU HUMAN. NO NEED TO THANK ME + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">5:16PM Sep 2, 2020</div> + This isn't just an Office 365 complaint. But look at all that unused space... What are all of those buttons?! <br /> +<br /> +Can we just all agree that icons need to have text alongside them? + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/33.jpeg" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">10:37AM Sep 9, 2020</div> + I don't have the energy for snark today<br /> +<br /> +fucking teams! + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">11:18AM Sep 15, 2020</div> + I've edited text in probably 10 applications already today. In all bar one of them the only thing that has caused mistakes is my fat fingers<br /> +<br /> +I'm on my fourth attempt trying to edit a line of text in powerpoint and whole blocks of the text keep disappearing + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + + <video autoplay="" muted="" loop="" playsinline="" class="rounded drop-shadow-md"> + <source src="/images/office365/34.mp4" type="video/mp4" /> + </video> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">9:00PM Sep 15, 2020</div> + Award for the most value-less interruption ever goes to...<br /> +<br /> +*opens golden envelope<br /> +<br /> +message to say that you clicked on a link for content you have access to and would you like to go to that content? + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/35.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">3:54PM Nov 9, 2020</div> + Kid in the same room playing an online game, I'm watching a video, and using Slack. My house couldn't currently have more internet.<br /> +<br /> +Me: * clicks a link in Teams<br /> +Teams: "I don't feel too well" + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/36.jpeg" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">7:42AM Nov 9, 2020</div> + me: "I'd like to edit this bulleted list"<br /> +outlook: "gotcha"<br /> +me: "new line please"<br /> +O: "starts with a bullet"<br /> +me: *tab<br /> +O: *move the bullet in<br /> +me: *repeats 3 times<br /> +me: "new line please"<br /> +O: "starts with a bullet"<br /> +me: *tab<br /> +O: "move the cursor leave the bullet, gotcha" + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + + <video autoplay="" muted="" loop="" playsinline="" class="rounded drop-shadow-md"> + <source src="/images/office365/37.mp4" type="video/mp4" /> + </video> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">11:16AM Nov 24, 2020</div> + Teams: works on chrome desktop but it turns out not on chrome mobile. Works in Firefox including video for live events but not video for meetings<br /> +<br /> +#NewMicrosoft my arse + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/38.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">11:12AM Dec 4, 2020</div> + So, you can close the Teams app window on Mac by pressing CMD + W when you think another window has focus and not be able to get it back without restarting<br /> +<br /> +What I like in a tool is when there are multiple sharp edges to cut myself with when I use it + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/39.jpeg" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">11:13AM Dec 4, 2020</div> + And, yes, it's possible in other applications.... but they also don't dump you in a dead end. + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/40.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">10:42AM Dec 11, 2020</div> + 1. open email<br /> +2. save to onedrive<br /> +3. view in onedrive<br /> +4. download<br /> +<br /> +Thanks O3.65 I'm glad there's not a download attachment button + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">10:42AM Dec 11, 2020</div> + I googled yammer, got to a website, clicked start using yammer, I am logged in to O3.65 and have access to Yammer + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/41.jpeg" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">11:05AM Dec 14, 2020</div> + Oh, forking hell. Joining a teams call UI is like the space shuttle<br /> +<br /> +join teams call without audio<br /> +<br /> +means literally without audio, it doesn't mean "join muted" which is a useful setting but instead "join without being able to hear" which doesn't seem useful to me + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">14:23PM Jan 15, 2021</div> + me: *cmd + tab<br /> +teams: "your invisible notification window, as requested"<br /> +me: "that's not right"<br /> +me: *clicks "calendar" in the window menu<br /> +teams: "your calendar, you should have said first time, here you go"<br /> +<br /> +(nb if I move my mouse around I get the calendar items' hover text) + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/42.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">6:08PM Jan 29, 2021</div> + Me: presses space<br /> +Almost every program playing video: *toggles play/pause<br /> +MS stream not full screen: *toggles play/pause<br /> +MS stream in full screen: I AM EXITING FULL SCREEN AS ANYONE WOULD EXPECT + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + + <video autoplay="" muted="" loop="" playsinline="" class="rounded drop-shadow-md"> + <source src="/images/office365/43.mp4" type="video/mp4" /> + </video> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">2:57PM Feb 8, 2021</div> + Me: *I wonder if @HollyDonohue01 is on this call?<br /> +Teams: *offscreen "Calling her in to this meeting for you"<br /> +Teams: "Hey Holly, Paul is inviting you to join these 85 people on a call"<br /> +Me: "Argh, that's not what I meant! How do I cancel this?"<br /> +Teams: "What is a cancel?" + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + + <video autoplay="" muted="" loop="" playsinline="" class="rounded drop-shadow-md"> + <source src="/images/office365/44.mp4" type="video/mp4" /> + </video> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">5:37PM Feb 22, 2021</div> + Me: *signed into teams and using it<br /> +Me: *clicks a button<br /> +Teams: YOU HAVE TO SIGN IN INSIDE THE WINDOW EVEN THOUGH YOU HAVE TO SIGN IN TO SEE THE WINDOW. I AM A SECURE + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/45.jpeg" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">12:42PM Mar 23, 2021</div> + For three weeks I've been off work and I didn't notice the feeling cos it was an absence of a thing<br /> +<br /> +The absence of being amazed at how bad something is... I've not noticeably waited for a computer to do something<br /> +<br /> +And now? "Crashing" back into using O3.65 + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/46.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">12:43PM Mar 23, 2021</div> + Me: please open this spreadsheet<br /> +Teams: allow me to present an onboarding journey you don't need and can't interact with that freezes your browser + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/47.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">12:53PM Mar 23, 2021</div> + Oh nice, no option to mark all as read in Teams. Thank fork there are only 26<br /> +<br /> +As a business owner<br /> +I want to pay my staff to have to click on every conversation <br /> +So that I know they are engaging<br /> +<br /> +pro tip if you click through them quickly they don't actually get marked as read + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/48.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">8:40AM Apr 6, 2021</div> + I have six applications that are using the internet successfully. But not Outlook 🤬<br /> +<br /> +I'm lucky my calendar and mail aren't trapped in that burning building of a system, eh? + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/49.jpeg" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">10:59AM Apr 6, 2021</div> + Thanks powerpoint + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/50.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">2:43PM Apr 6, 2021</div> + Office 3.64 spell check now takes my northern accent into account + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/51.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">10:57AM Apr 12, 2021</div> + Every Monday Outlook does a "cute" thing where it recreates a meeting series for me so that I can say I don't go to it anymore.<br /> +<br /> +It takes maybe 30 seconds. If it's doing that for 1% of employees that's 5 hours a week.<br /> +<br /> +That'd be 8 weeks FTE over the year. #HiddenCosts + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/52.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">10:00AM Apr 14, 2021</div> + For weeks now Teams has been "opening" a "notification" window on my Mac. It isn't viewable and only has the effect that I can no longer CMD+Tab to Teams cos that invisible window gets "shown"<br /> +<br /> +The "fix" is to restart Teams when I notice it<br /> +<br /> +(lifehack: just close it instead) + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/53.jpeg" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">12:22PM Apr 26, 2021</div> + Sharepoint: THIS IS NOT SAVED<br /> +Also Sharepoint: YOU SHOULD REFRESH TEH PAGE<br /> +Me: *please save the page<br /> +Sharepoint: HAVE A FREE STACK TRACE. I AM A WEB + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/54.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + <img src="/images/office365/55.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + <img src="/images/office365/56.jpeg" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">12:23PM Apr 26, 2021</div> + And, yes, reader, when I refreshed the page all my edits had gone. Sharepoint has one purpose and cannot do it<br /> +<br /> +It's like Wordpress having a bad trip + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">4:47PM Mar 10, 2021</div> + I "like" the new "why not in a fortnight" scheduling feature<br /> +<br /> +here's me suggesting a meeting that the invitee is free for and Office helpfully pointing out we're both also free two weeks later than that + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/57.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">3:35PM May 13, 2021</div> + Teams: DONUT WORRY HUMAN, FOR THIS MEETING YOU ARNE"T ALLOWED TO ATTACH FILES IN TEH CHAT + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/58.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">9:07AM May 14, 2021</div> + Outlook: DO NOT WORRY HUMAN, IF I NEVER FINISH LOADING THESE SCRIPTS YOU CANNOT SEE YOUR MAIL AND GET SOME FOCUS TIME + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/59.jpeg" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">1:56PM May 24, 2021</div> + outlook *showing me an invite: "no conflicts 👍"<br /> +me *looking at the calendar: "do you know what a conflict is?"<br /> +outlook *starting to sweat: "yes?" + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/60.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + <img src="/images/office365/61.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">9:44AM Jul 7, 2021</div> + PP: morning<br /> +me: "open a deck, please"<br /> +PP: here you go<br /> +me: "open this one from the browser"<br /> +PP: I AM FROZEN<br /> +me: *ugh, force quit<br /> +O3.7: DON'T WORRY WE HAVE A CUSTOM WAY OF HANDLING ERRORS<br /> +me: is it good<br /> +O3.7: I don't know, I've never seen it work + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/62.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + <img src="/images/office365/63.jpeg" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">9:45AM Jul 7, 2021</div> + me: maybe if I sign out<br /> +"Power"point: "worth a shot, guv"<br /> +me: *sign out, please<br /> +PP: done it<br /> +me: *and sign back in, please<br /> +PP: NOT A THING I CAN DO, PYSKE! + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/64.jpeg" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">11:58AM Jul 12, 2021</div> + me: *has a 30-minute meeting with one other person<br /> +Teams: HERE IS AN ATTENDANCE REPORT. THIS IS A HELP IF YOU DON'T KNOW IF YOU ATTENDED OR IF YOU NEED TO CHECK IF YOU JUST SPENT THRITY MINUTES ALONE OR NOT" + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/65.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">4:57PM Sep 7, 2021</div> + CISO: "Hello, I'd like to implement security"<br /> +O365: "It's already done. The steps for downloading an attachment are: 1) click save to one drive 2) click view in one drive 3) download. Security!<br /> +<br /> +WHY CAN I NOT DOWNLOAD AN ATTACHMENT FROM AN EMAIL + <div class="flex flex-row flex-wrap justify-start gap-4"> + + </div> + + <video autoplay="" muted="" loop="" playsinline="" class="rounded drop-shadow-md"> + <source src="/images/office365/66.mp4" type="video/mp4" /> + </video> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">1:42PM Sep 17, 2021</div> + One final disappointment from O3.6<br /> +<br /> +Because I insist on the unexpected browser choice of Chrome on an Android. I can't join a meeting when I find myself caught away from home.<br /> +<br /> +Obvs Teams isn't supported on desktop Chrome + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/67.jpeg" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + +<div class="border rounded p-4 my-2 drop-shadow-sm"> + <div class="ml-2 float-right text-gray-500">9:06AM Sep 18, 2021</div> + And in a perfect bit of poetry, I can end this thread, let down by technology, and frozen in Teams. + <div class="flex flex-row flex-wrap justify-start gap-4"> + + <img src="/images/office365/68.png" class="max-w-sm w-1/2 h-auto m-0 rounded drop-shadow-md" loading="lazy" /> + + </div> + +</div> + + + Sun, 05 Mar 2023 08:00:00 +0000 + + + https://pauldambra.dev/2023/03/office-365-mega-thread.html + + + https://pauldambra.dev/2023/03/office-365-mega-thread.html + + + + + Feb 2023 Month Notes + <p>Year Goals for 2023</p> + +<ul> + <li>continue becoming a better engineer and team-mate</li> + <li>practice Italian every day</li> + <li>train at the gym at least twice a week every week</li> + <li>8 leisurely cycle rides</li> + <li>visit Italy at least twice</li> +</ul> + +<p>How did February go?</p> + +<!--more--> + +<p>In reverse order</p> + +<h2 id="visit-italy-at-least-twice">Visit Italy at least twice</h2> + +<p>I'm still figuring out when Italy #2 will be. I'd love to take daughter #1 to Sicily and then catch the train back up through the country. I can work, she can study, and we can both enjoy the food and culture. Maybe a pipe-dream…</p> + +<h2 id="8-leisurely-cycle-rides">8 leisurely cycle rides</h2> + +<p>0 cycle rides in Feb.</p> + +<p>Lots of parenting and responsibilities this month, so very little time for myself. And the bags under my eyes are proof of that 🙈</p> + +<h2 id="train-at-the-gym-at-least-twice-a-week">train at the gym at least twice a week</h2> + +<p>We were away in Cornwall for half term and so I managed 3 times a week while I was at home. And 2 times a week the 2 weeks that the trip overlapped with. But the dog and I went for a long run while in Cornwall and I accidentally went for a very long walk there. Turns out South West and South East are not the same direction 🤣</p> + +<p>Here I am just after turning around, trying to figure out how to get back on track. At this point in time I thought I was all the way on the right-hand edge of that map segment 😅</p> + +<p><img src="/images/2023/03/01/cornwall-map.png" alt="an OS map showing my position in Cornwall" loading="lazy" /></p> + +<p>I don't ache everywhere all the time anymore. And, actually, feel pretty good. Plus time at the gym is uninterrupted pod-cast time. So, I'm happy with that.</p> + +<h2 id="practice-italian-every-day">Practice Italian every day</h2> + +<p>✅ only a few minutes at a time, but I did it every day.</p> + +<!--alex ignore dad-mom--> +<p>Still trying to get my Dad to talk to me in Italian habitually. 🇮🇹</p> + +<h2 id="continue-becoming-a-better-engineer-and-team-mate">Continue becoming a better engineer and team-mate</h2> + +<p>I've been concentrating on this more this month. We joke a lot about being a group of lone wolves and in sprint planning I was described as "wolfing with everyone". I guess that's a good thing 😅</p> + +<p>My GCSE physics teacher told us to always start solving a problem with a diagram. This month's work was tricky, slow, and frustrating. But when I took the time to draw a diagram or two, and then go for an accidentally long walk, my brain was prepared, and my subconscious figured out how to make the complicated thing much, much less complicated.</p> + +<p><img src="/images/2023/03/01/drawing.png" alt="a diagram of the problem I was trying to solve" loading="lazy" /></p> + +<p>One of my favourite engineering aphorisms is from Kent Beck: <a href="https://twitter.com/KentBeck/status/250733358307500032">"for each desired change, make the change easy (warning: this may be hard), then make the easy change"</a>.</p> + +<p>This particular piece of work, I had three attempts at that had to be abandoned because of side-effects to the change. By the time I'd simplified it, it was a less than 1-day change. <a href="https://github.com/PostHog/posthog/pull/14348">Here's an example of one of the pieces of simplication https://github.com/PostHog/posthog/pull/14348</a>.</p> + + Wed, 01 Mar 2023 08:00:00 +0000 + + + https://pauldambra.dev/2023/03/feb-month-notes.html + + + https://pauldambra.dev/2023/03/feb-month-notes.html + + + + + Jan 2023 Month Notes + <p>Year Goals for 2023</p> + +<ul> + <li>continue becoming a better engineer and team-mate</li> + <li>practice Italian every day</li> + <li>train at the gym at least twice a week every week</li> + <li>8 leisurely cycle rides</li> + <li>visit Italy at least twice</li> +</ul> + +<p>How did January go?</p> + +<!--more--> + +<p>In reverse order</p> + +<h2 id="visit-italy-at-least-twice">Visit Italy at least twice</h2> + +<p>Cancelled a trip to Genoa because our next offsite is in Aruba the same month (hard life).</p> + +<p>But added a trip to Rome with the kids.</p> + +<p>So, still need to figure out what my second trip will be</p> + +<h2 id="8-leisurely-cycle-rides">8 leisurely cycle rides</h2> + +<p>0 cycle rides in Jan.</p> + +<p>The weather in the peak district and leisurely have not overlapped this month 🤣</p> + +<h2 id="train-at-the-gym-at-least-twice-a-week">train at the gym at least twice a week</h2> + +<p>Managed 3 times a week. I ache everywhere all the time 🥵</p> + +<p>But I went for a short jog with the dog yesterday. It felt light and effort-free. First time both achilles have been pain free in a long time. So, I'm hopeful that I'll be able to get back to running soon. Although I need to not do my common failure mode and ramp up to 3 hour runs with the dog too soon and injure myself again 🤣</p> + +<h2 id="practice-italian-every-day">Practice Italian every day</h2> + +<p>✅ only a few minutes at a time, but I did it every day.</p> + +<!--alex ignore dad-mom--> +<p>Still trying to get my Dad to talk to me in Italian habitually. 🇮🇹</p> + +<h2 id="continue-becoming-a-better-engineer-and-team-mate">Continue becoming a better engineer and team-mate</h2> + +<p>I didn't think about how I'd measure this 🙈</p> + +<p>I've done a lot of solo-work this month. So arguably not being a great team mate. But it has been soaking up bugs and customer issues so the others on the team can focus.</p> + +<p>Have also managed to stick to small PRs. Despite working on a bunch of tricky frustrating things that lend themselves to sprawling PRs that never get merged… So, I'm pleased with that discipline.</p> + + Fri, 03 Feb 2023 08:00:00 +0000 + + + https://pauldambra.dev/2023/02/jan-month-notes.html + + + https://pauldambra.dev/2023/02/jan-month-notes.html + + + + + 2022 Year Notes + <p>Forgive me, for I have sinned, it's been 2 years since my last year notes 👼</p> + +<p>I wrote <a href="/2020/01/year-notes.html">year notes for 2019</a>, <a href="/2021/01/year-notes.html">and 2020</a>. I've been super un-inspired to write for the last few years. Which is a shame - because it's a a great way to learn.</p> + +<p>So much happened last year that it feels way longer than a year. So, discipline over motiviation - here are my 2022 year notes. Or at least as much as I can write while the house is empty of other people.</p> + +<!--more--> + +<h1 id="goals-from-2021-year-notes">Goals from 2021 year notes</h1> + +<p><em>and whether I achieved them or not</em></p> + +<p><em>(slightly reogranised since I see themes with hindsight that I didn't at the time)</em></p> + +<h2 id="leadership">leadership</h2> + +<ul> + <li>❓ read about leadership and get over myself</li> + <li>❓ by March understand what business and team goals I'm contributing to</li> + <li>😅 meet one-on-one with everyone on my team at least once</li> + <li>😅 keep those meetings going with some of them</li> +</ul> + +<p>In retrospect I was struggling with a role that was poltical and not technical. 2021 saw me move teams, see that the grass wasn't greener, realise that the garden was (for me) poisoned, and move jobs to a technical role.</p> + +<h2 id="writing-software">writing software</h2> + +<ul> + <li>🙃 record a video of event sourcing from scratch + <ul> + <li>started but not completed</li> + <li><a href="https://youtube.com/playlist?list=PLosfMbzQO-j0C-IvdhzQ0G2qwtH6bnxqd">three videos I recorded</a></li> + </ul> + </li> + <li>✅ make time to write code <em>for days at a time</em></li> +</ul> + +<p>And the move (back) to a technical role was harder than I anticipated. New stack, new org, new culture. But I wouldn't change the decision for all the money in the world (well, maybe <em>all</em> the money)</p> + +<h2 id="myself">myself</h2> + +<ul> + <li>❌ start weeknotes again</li> + <li>✅ practice Italian every day</li> + <li>❌ 15km running on average over 40 weeks of the year + <ul> + <li>43 runs totalling 168km = 3.9km per run</li> + <li>I ended up struggling with achilles pain in 2021 and hardly running at all in 2022</li> + <li>but physio is helping</li> + </ul> + </li> + <li>✅ 4 leisurely cycle rides + <ul> + <li>not in 2021, but I did in 2022</li> + </ul> + </li> +</ul> + +<p>I'm more who I want to be, but there's more to do</p> + +<h2 id="my-world">my world</h2> + +<!--alex ignore white--> + +<ul> + <li>👀 use the unfair super power of being a white, middle-class, middle-aged, straight man to lift others up</li> +</ul> + +<p>This is the least I could do. I should take it for granted and figure out the answer to "Great, you want to help others, so what?"</p> + +<p>So, 2022</p> + +<h1 id="travel">Travel</h1> + +<p>Working at <a href="https://posthog.com">PostHog</a> comes with a number of benefits. Freedom to travel because the work is remote, and travel <em>for</em> the work, is one that I've been loving!</p> + +<p><img src="/images/2022-travel.png" alt="2022 travel map from Google Maps showing the countries and places I visited" loading="lazy" /></p> + +<p>In 2022 I visited 6 countries</p> + +<ul> + <li>Barcelona, Spain + <ul> + <li>for the engineering offsite</li> + </ul> + </li> + <li>Reykjavik, Iceland + <ul> + <li>for the all company offsite</li> + </ul> + </li> + <li>Forte Di Marme, Italy + <ul> + <li>because I wanted to take the kids to Italy with their Nonno</li> + </ul> + </li> + <li>Pescara, Italy + <ul> + <li>for ten days because my family are wonderful and I wanted to spend time in Italy</li> + </ul> + </li> + <li>Rome, Italy + <ul> + <li>for the product analytics team offsite</li> + </ul> + </li> + <li>Paris, France + <ul> + <li>because I wanted to take the kids to Paris</li> + </ul> + </li> + <li>Lisbon, Portugal + <ul> + <li>because we have a budget to meet each other,</li> + <li>my colleague let us use their house for free,</li> + <li>so Ben/Paul super-fun-time could happen to make real user monitoring.</li> + <li>I've never worked anywhere where we are as free to choose our own goals</li> + </ul> + </li> +</ul> + +<p>I spent nearly 20 days in Italy in 2022. More than I've spent there for 20 years. The older I get, the more I value my heritage (#SoCliche). My spoken Italian has progressed from "Nouns and pointing" to "Hangry three year old".</p> + +<p>Travelling for work has been an incredible thing. Lisbon was superb. It was fun to have a goal, work hard all day, and then have food and chat in the evenings. Barcelona, Rome, and Reykjavik were amazing. I'm convinced that intermittently coming together is one of the things that makes remote work, erm, work. Not only that though. Having <a href="https://posthog.com/handbook/people/spending-money#budget-for-socializing">a budget to meet and socialise</a> is a super power.</p> + +<p>Pescara was like breathing out. It's only the second time in my life I've travelled overseas by myself. And it's the longest I've spent not needing to parent for a decade and a half. I'm incredibly lucky that my family put up with me being away.</p> + +<h2 id="work">Work</h2> + +<p><img src="/images/2022-contributions.png" alt="2022 contributions graph from GitHub showing a step-change around June" loading="lazy" /></p> + +<p>Interestingly, there's a step change in my GitHub contributions around the time I went to Pescara too. And, without wanting to seem big-headed, I think, a step change in my performance at work too.</p> + +<p>Changing back from a leadership role to a typey-typey-software role, at the end of 2021, was way more of a change than I anticipated. Alongside going all-remote and discovering which habits that helped in an office job that don't help anymore. It's amazing how much engineering you can forget in four years of talking to people about engineering.</p> + +<p>What do I think has helped 🤔</p> + +<ul> + <li>cadence + <ul> + <li>I like to have a large(r) spike PR where I can experiment and gather feedback</li> + <li>and then splitting smaller pieces of work from that</li> + <li>the smaller pieces are easier to engineer well</li> + <li>and way more safe to release</li> + <li>aiming for merging more than one PR a day</li> + </ul> + </li> + <li>stopping and thinking + <ul> + <li>decide a goal, figure out how to get there, figure out how much of your time to give it</li> + <li>and then do that</li> + </ul> + </li> + <li>extreme ownershp and turn the ship around + <ul> + <li>a very incredible colleague bought me <a href="https://www.goodreads.com/book/show/16158601-turn-the-ship-around">"turn the ship around"</a></li> + <li>another incredible colleague suggested <a href="https://www.goodreads.com/book/show/23848190-extreme-ownership">"extreme ownership"</a></li> + <li>they're both great books + <ul> + <li>although I struggled with the "yee-haw shoot people" presentation of extreme ownsership</li> + </ul> + </li> + <li>"This isn't going to happen until I make it happen! -&gt; How do I make it happen? -&gt; How do I remove things stopping it happening?"</li> + </ul> + </li> + <li>improvements accrue if you let them + <ul> + <li>this is maybe a corollary of "stopping and thinking"</li> + <li>I had a great pairing session where a colleague made (to them) a throw away comment about how React works</li> + <li>it changed the model of how I think about it.</li> + <li>Spotting that, I asked myself how that should change how I approach work</li> + <li>the last three months I've been working with another colleague on application performance (the back-end of the back-end)</li> + <li>they're incredible, if I can learn 1% of their skill I'll consider it a success</li> + <li>but now I want to <a href="https://github.com/PostHog/posthog/pull/13700">find other work</a> I can prioritise to practice what I think I've learned so I can trick my brain into storing the knowledge</li> + </ul> + </li> + <li>talking to users + <ul> + <li>I've spent time supporting users</li> + <li>joining video calls to help them</li> + <li>running user interviews</li> + <li>understanding the users and seeing the struggles they have is 💯</li> + <li>I've been working a lot on our dashboards, not because I thought it was important but because <em>they did</em>.</li> + </ul> + </li> +</ul> + +<p>Annoyingly, I not sure I know what made the difference. I really want to figure it out so I can take advantage of it well. I'm surrounded by amazing people and finding that wonderfully motivating.</p> + +<p>### Open Source</p> + +<p>A brief aside about working on open source software. An unexpected (for me) side-effect has been how incredible it is to be able to share exactly what I mean when talking to people about software. "I think it is good to do X" becomes "Here's a PR (or set of them) that I think demonstrate a way to do X well".</p> + +<p>I think that's awesome.</p> + +<p>Also, sometimes in remote work I miss the power of someone looking over your shoulder while you work. It's way harder to cut corners when someone is watching. Remembering that anyone can watch my work helps remind me to take the step from <a href="https://wiki.c2.com/?MakeItWorkMakeItRightMakeItFast">"make it work" to then "make it right"</a></p> + +<!--alex ignore simple --> + +<p>For example, I fixed a bunch of bugs in our dashboards product (I think more than I introduced 😅). In doing that we learned about what made it easier to introduce those bugs than to avoid them. I could have moved back on to my main priority… but the world is watching, so I figured out a way to make it harder to introduce the bugs than to avoid them (<a href="https://wiki.c2.com/?XpSimplicityRules">"four rules of simple design"</a> for the win) <a href="https://github.com/PostHog/posthog/pull/13630">https://github.com/PostHog/posthog/pull/13630</a></p> + +<h1 id="kids">Kids</h1> + +<p><img src="/images/2022-family.jpg" alt="toddle and dog sitting on a path" loading="lazy" /></p> + +<p>Since last year notes I've graduated from three kids to four kids. It's still incredible. I'm still always very tired. So amazingly worth it. They have said they don't want to be on social media so I won't mention much here.</p> + +<p>And now they're home… so I'm going to publish without editing and procrastinating.</p> + +<h1 id="what-writing-this-taught-me-i-want-to-do-in-2023">What writing this taught me I want to do in 2023</h1> + +<ul> + <li>continue becoming a better engineer and team-mate</li> + <li>practice Italian every day</li> + <li>train at the gym at least twice a week every week</li> + <li>8 leisurely cycle rides</li> + <li>visit Italy at least twice</li> +</ul> + + Sun, 15 Jan 2023 08:00:00 +0000 + + + https://pauldambra.dev/2023/01/year-notes.html + + + https://pauldambra.dev/2023/01/year-notes.html + + + + + Pasta e fasule + <p>Pasta e Fasule, as made by my Neapolitan Nonna Luisa</p> + +<p>Pasta e Fasule is Neapolitan for Pasta e Fagiole which is Italian for Pasta and Beans</p> + +<p>Pasta and bean soup reminds me of being looked after when recovering from an illness as a kid. I like it so thick the spoon will stand up.</p> + +<!--alex disable he-her dad-mom--> + +<p>You can use odds and ends of pasta or break spaghetti in. When my dad was little he'd be sent to the pasta shop to get their broken odds and ends. They were cheaper.</p> + +<p><img src="/images/pasta-fasule.gif" alt="steps in making pasta fasule as a gif" loading="lazy" /></p> + + Sun, 08 Jan 2023 07:00:00 +0000 + + + https://pauldambra.dev/recipes/2023/01/pasta-e-fasule.html + + + https://pauldambra.dev/recipes/2023/01/pasta-e-fasule.html + + + + + Pizza dough + <p>It turns out that Neapolitan pizza dough is a strictly described thing. The <a href="https://www.legislation.gov.uk/eur/2010/97/annexes">UK version of the EU rules are here</a>. Those instructions use 1.8 kilograms of flour and 1 litre of water. But (if you're not going to a wholesaler) flour comes in 1 kilogram bags. Ingredients have been adjusted to 1kg for this recipe.</p> + +<p>I sometimes use a biga starter. <a href="https://pauldambra.dev/biga-calculator/">Instructions for that are here</a>. But it takes more work, time, and nuance.</p> + +<p>This recipe can be completed in a single day and makes a very consistently tasty dough</p> + +<p>(Yes! <em>That</em> much salt)</p> + +<p><img src="/images/dough-balls.jpg" alt="the dough balls resting on a wooden surface" loading="lazy" /></p> + + Sun, 04 Sep 2022 07:00:00 +0000 + + + https://pauldambra.dev/recipes/2022/09/pizza-dough.html + + + https://pauldambra.dev/recipes/2022/09/pizza-dough.html + + + + + \ No newline at end of file diff --git a/fun-with-structs.html b/fun-with-structs.html new file mode 100644 index 000000000..b6135c574 --- /dev/null +++ b/fun-with-structs.html @@ -0,0 +1,602 @@ + + + + + + + + + + + + + + + + + + + + + + Fun With Structs + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sun Feb 01 2015
+

Fun With Structs

+
+ +
+ +
+

We had a brief conversation at work the other day about extending a type to make our code clearer…

+ +
public class MeaningfulName : MathsName
+{
+    public MeaningfulName(double w, double x, double y, double z) : base(w, x, y, z)
+    {
+    }
+}
+
+ + + +

We're porting some code written by our maths wizards and there's lots of convention in their domain specific language which we were trying to capture (because we're not maths wizards and (maybe only I) get easily lost).

+ +

In one case the prospective base class wasn't a class but a struct so we couldn't do this. And we briefly discussed whether we should convert it to a class.

+ +

In that conversation I stated that structs weren't value objects. You see, I've checked in the past. After a job interview where I stated that they were value objects and wrote some tests afterwards to check… I wish I still had those tests because…

+ +

Luckily, I have colleagues whose opinions I trust and I try for my opinions to be 'strong opinions weakly held' (which might be a Greg Young-ism). So while I'm having a lazy day I wrote some tests and blew my mind!

+ +

TLDR;

+ +
    +
  • Don't trust my memory
  • +
  • Do test your assumptions! Do test your code! Maybe use structs!
  • +
  • Clear names are likely to be much more important than C# performance.
  • +
+ +

A struct - What is it?

+ +

Value Semantics

+ +

Quoting from MSDN "A variable of a struct type directly contains the data of the struct, whereas a variable of a class type contains a reference to the data, the latter known as an object."

+ +

+public class NotValueSemantics
+{
+	public int X { get; private set; }
+
+	public NotValueSemantics(int x) 
+	{
+		X = x;
+	}
+}
+
+var first = new NotValueSemantics(10);
+var second = first;
+second.X = 100;
+Console.WriteLine(first.X); //writes 100
+
+
+ +

in the example above the line var second = first adds another reference to the memory that holds the first Object so operations on second affect first. The assignment of 100 to second.X is reflected in first.X.

+ +

+public struct ValueSemantics
+{
+	public int X { get; private set; }
+
+	public ValueSemantics(int x) : this()
+	{
+		X = x;
+	}
+}
+
+var first = new ValueSemantics(10);
+var second = first;
+second.X = 100;
+Console.WriteLine(first.X); //writes *10*
+
+
+ +

Instead if we declare it as a struct (other than requiring a call through to the base parameterless constructor) the difference is that var second = first copies the data not a reference to it. As a result operations on second don't affect first so the assignment second.X = 100; is not reflected in first.X.

+ +

As an aside, it is possible to "break" this difference and this excellent article by Jon Skeet shows how.

+ +

Value Object

+ +

The key fact about a value object is that its properties determine what it is, so two value objects are equal when their properties are equal. Eric Evans in Domain-driven Design: Tackling Complexity in the Heart of Software says "you don't care which '4' you have or which 'Q'".

+ +

So here are the tests I wrote to check my understanding of structs…

+ +
[Test] //this test passes!
+public void structs_with_equal_properties_are_equal()
+{
+    var a = new ValueObject(1, 2);
+    var b = new ValueObject(1, 2);
+    Assert.AreEqual(a,b);
+}
+
+[Test] //this test passes!
+public void structs_with_different_properties_are_not_equal()
+{
+    var a = new ValueObject(1, 1);
+    var b = new ValueObject(1, 2);
+    Assert.AreNotEqual(a, b);
+}
+
+private struct ValueObject
+{
+    public int X {get; private set;}
+    public int Y {get; private set;}
+
+    public ValueObject(int x, int y)
+        : this()
+    {
+        X = x;
+        Y = y;
+    }
+}
+
+ +

Both of those tests pass. I'd have confidently put money on those tests failing. As above I'm certain I've written similar tests in the past and seen them fail - I should trust my memory even less than I currently do!

+ +

So a struct in C# appears to be a pretty cheap way (in terms of typing) to define a value object.

+ +

The equivalent… i.e. a class which acts as a value object.

+ +
public class ValueObject : IEquatable<ValueObject>
+{
+    public int X { get; private set; }
+    public int Y { get; private set; }
+
+    public ValueObject(int x, int y)
+    {
+        X = x;
+        Y = y;
+    }
+
+    public bool Equals(ValueObject other)
+    {
+        if (ReferenceEquals(null, other)) return false;
+        if (ReferenceEquals(this, other)) return true;
+        return X == other.X && Y == other.Y;
+    }
+
+    public override bool Equals(object obj)
+    {
+        if (ReferenceEquals(null, obj)) return false;
+        if (ReferenceEquals(this, obj)) return true;
+        if (obj.GetType() != this.GetType()) return false;
+        return Equals((ValueObject) obj);
+    }
+
+    public override int GetHashCode()
+    {
+        unchecked
+        {
+            return (X*397) ^ Y;
+        }
+    }
+
+    public static bool operator ==(ValueObject left, ValueObject right)
+    {
+        return Equals(left, right);
+    }
+
+    public static bool operator !=(ValueObject left, ValueObject right)
+    {
+        return !Equals(left, right);
+    }
+}
+
+ +

Much more code to read than the equivalent struct! Although I used resharper's code generation to add the equality methods so actually not that much more of the typing.

+ +

The section on differences between structs and classes on MSDN is unusually clear for MS documentation and worth a read.

+ +

So…

+

I've learned something and that has value. However, the team still has the problem of wanting to improve clarity. What solutions might there be.

+ +

I can think of three.

+ +

1) Don't do anything

+ +

Maybe leave it as structs and use variable names and some method & class extractiion refactorings to make what is happening clearer instead of leaning on the types to do that.

+ +

This would be fine. My feeling is that the code in question leads itself to errors because it has multiple things with the same type but different semantics being used close together. It feels like it will be easier to introduce bugs as it is - but we should always be careful so this is an option.

+ +

2) Use a wrapper class

+ +

This can be a really useful way of extending a sealed object or struct.

+ +
public class MeaningfulName 
+{
+	private MathsName _mathsThing;
+
+	public MeaningfulName(double w, double x, double y, double z )	
+	{
+		_mathsThing = new MathsName(w, x, y, z)
+	}
+
+	public double DoAMathsThing(double a) 
+	{
+		return _mathsThing.DoAMathsThing(a);
+	}
+}
+
+ +

If there was a requirement to extend the functionality of MathsName and we didn't own that object then this would be a good solution but this wrapper would be calling the wrapped method with no alterations. And MathsName has a lot of methods :(

+ +

So lots of the typing to implement but even more importantly it's possible that someone can alter MathsName without realising that they need to alter MeaningfulName too so there's the potential for bugs. It's better to help people fall into the pit of success and this solution doesn't do that.

+ +

3) Change it to a class

+ +

There'd not be very much of the typing. Only the addition of the equality methods (and probably some equality tests for safety). The struct in question is relatively well covered anyway so the change would be safe.

+ +

But we create tens of thousands of these structs and pass them around. What would the impact of converting this to a class be. + +Class vs. Struct - the big fight +———-

+ +

I created a console application that creates a bunch of objects and structs and then calls a method on them. You can see the full program here.

+ +
public class ValueClass : IEquatable<ValueClass>
+{
+    public double Z { get; private set; }
+    public double Y { get; private set; }
+    public double X { get; private set; }
+    public double W { get; private set; }
+
+    public ValueClass(double w, double x, double y, double z)
+    {
+        W = w; X = x; Y = y; Z = z;
+    }
+
+    public static ValueClass operator +(ValueClass left, ValueClass right)
+    {
+        return new ValueClass(left.W + right.W, left.X + right.X, left.Y + right.Y, left.Z + right.Z);
+    }
+
+    //snip equality members
+}
+
+public struct ValueStruct
+{
+    public double Z { get; private set; }
+    public double Y { get; private set; }
+    public double X { get; private set; }
+    public double W { get; private set; }
+
+    public ValueStruct(double w, double x, double y, double z) : this()
+    {
+        W = w; X = x; Y = y; Z = z;
+    }
+
+    public static ValueStruct operator +(ValueStruct left, ValueStruct right)
+    {
+        return new ValueStruct(left.W + right.W, left.X + right.X, left.Y + right.Y, left.Z + right.Z);
+    }
+}
+
+ +

and then doing something like

+ +
private static void AddObjectsTogether()
+{
+    var endValues = new List<ValueClass>(Capacity);
+
+    foreach (var value in CreateABunchOfObjects())
+    {
+        endValues.Add(value + MakeRandomValueClass());
+    }
+}
+
+ +

Profiling

+ +

First I profiled the application running with a Capacity of 25 million. The code spent a little less than 10% of its time dealing with the structs and a little more than 12% of its time dealing with classes. Running with a capacity of 250,000 showed the reverse.

+ +

What this told me was I don't really understand profiling… Doh!

+ +

And you can create seventy five million structs in 2.8 seconds and seventy five million object in 2.3 seconds. That's orders of magnitude higher than I care about.

+ +

Memory

+ +

When running the program to generate the structs it ran for about five seconds, preallocated around 790MB memory and kept that amount of memory in use until the end +just generating structs

+ +

When running the program to generate objects it ran for about thirty seconds, used slightly more memory, but didn't preallocate the memory. +just generating classes

+ +

When running the program to generate both it ran for about thirty seconds, used slightly more memory, but didn't preallocate the memory. +just generating classes

+ +

What this told me was I've no idea about memory profiling… Doh!

+ +

But that there's some tradeoff to be made where if you are creating primarily structs the compiler can preallocate memory to speed things up but if you aren't then it cannot - maybe?! That needs some research…

+ +

Performance

+ +

I definitely need to learn how to profile programs.

+ +

The speed-up when dealing primarily with structs is intriguing but:

+ +
    +
  • it came with more constant memory usage - easier to budget for but possible requires more overall
  • +
  • I don't think the real code that led to this is primarily structs so this might be a red herring
  • +
+ +

Overall

+ +

A much longer and more rambling post than anyone deserved to read!

+ +

Do test your assumptions! Do test your code! Maybe use structs!

+ + +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/happy-numbers-kata.html b/happy-numbers-kata.html new file mode 100644 index 000000000..7b82b80ad --- /dev/null +++ b/happy-numbers-kata.html @@ -0,0 +1,600 @@ + + + + + + + + + + + + + + + + + + + + + + Happy Numbers + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sat Nov 22 2014
+

Happy Numbers

+
+
+ +
+ +
+ + tag-icon + + c# + + + + + tag-icon + + kata + + + + + tag-icon + + tdd + + + + + tag-icon + + learning + + + +
+
+
+
+ +
+

I love C# but while we're trying to beat our deployment process into submission at work I'm only really writing Ruby and Powershell. So when a few, different articles about the Happy Numbers kata turned up on my twitter feed and I found myself with a large whisky and a sleeping family I thought I'd have a go.

+ +

The Happy Numbers kata is defined as

+ +
+

Choose a two-digit number (eg. 23), square each digit and add them together. +Keep repeating this until you reach 1 +or the cycle carries on in a continuous loop.

+ +

If you reach 1 then the number you started with is a “happy number”.

+ +

Can you find all the happy numbers between 1 and 100?

+
+ + + +

There is more info on what a Happy Number is on wikipedia.

+ +

The second link above has some spoilers in. Particularly that the order of the numbers doesn't matter, and that zeroes don't matter. I did previously rediscover that the order that you add numbers doesn't matter when working on some insurance software a few years back (although it took me a while :annoyedwithselfemoticon:) so let's be kind to me and assume I'd have got there on my own if I hadn't read the article first (although it was a large whisky).

+ +

After writing a couple of tests:

+ +
[Test]
+[TestCase(31, true)]
+[TestCase(4, false)]
+[TestCase(2, false)]
+public void CanIdentifyHappyNumber(int i, bool expected)
+{
+    Assert.AreEqual(expected, i.IsAHappyNumber());
+}
+
+ +

I realised that I really like Ruby's having a question mark on methods that return a boolean and miss that feature in C#.

+ +

And that the sensible public API was a call to IsAHappyNumber directly on the integer so I could crank out a large test.

+ +
// Taken from http://oeis.org/A007770
+private static readonly int[] HappyNumbersUpTo1000 =
+{
+    1, 7, 10, 13, 19, 23, 28, 31, 32, 44, 49, 68, 70, 79, 82, 86, 91, 94, 97, 100, 103, 109, 129, 130, 133, 139,
+    167, 176, 188, 190, 192, 193, 203, 208, 219, 226, 230, 236, 239, 262, 263, 280, 291, 293, 301, 302, 310, 313,
+    319, 320, 326, 329, 331, 338, 356, 362, 365, 367, 368, 376, 379, 383, 386, 391, 392, 397, 404, 409, 440, 446,
+    464, 469, 478, 487, 490, 496, 536, 556, 563, 565, 566, 608, 617, 622, 623, 632, 635, 637, 638, 644, 649, 653,
+    655, 656, 665, 671, 673, 680, 683, 694, 700, 709, 716, 736, 739, 748, 761, 763, 784, 790, 793, 802, 806, 818,
+    820, 833, 836, 847, 860, 863, 874, 881, 888, 899, 901, 904, 907, 910, 912, 913, 921, 923, 931, 932, 937, 940,
+    946, 964, 970, 973, 989, 998, 1000
+};
+
+[Test]
+public void CanTestAThousandNumbers()
+{
+    for (var i = 0; i < 1000; i++)
+    {
+        Assert.AreEqual(HappyNumbersUpTo1000.Contains(i), i.IsAHappyNumber());
+    }
+}
+
+ +

So, with a 1000 failing tests I could run through a few naive implementations to get to a reasonable one.

+ +
using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+
+namespace HappyNumbers
+{
+    // Choose a two-digit number (eg. 23), square each digit and add them together. 
+    // Keep repeating this until you reach 1 or the cycle carries on in a continuous loop. 
+    // If you reach 1 then the number you started with is a “happy number”.
+    public static class HappyNumbers
+    {
+        private static readonly Dictionary<int, HashSet<int>> NumberChain = new Dictionary<int, HashSet<int>>(); 
+        private static readonly Dictionary<string, bool> HappyNumberResults = new Dictionary<string, bool>();
+
+        public static bool IsAHappyNumber(this int startingNumber)
+        {
+            var digitsToTest = startingNumber.GetDigits()
+                                             .StripZeroes()
+                                             .OrderedByDigit();
+
+            var happyNumberKey = string.Join(",", digitsToTest);
+
+            if (AlreadyKnowThatThisNumberIsHappy(happyNumberKey))
+            {
+                return HappyNumberResults[happyNumberKey];
+            }
+
+            GuardAgainstStrangeness(startingNumber);
+            NumberChain.Add(startingNumber, new HashSet<int>{startingNumber});
+            TestForHappiness(startingNumber, happyNumberKey);
+            return HappyNumberResults[happyNumberKey];
+        }
+
+        private static void GuardAgainstStrangeness(int startingNumber)
+        {
+            if (NumberChain.ContainsKey(startingNumber))
+            {
+                throw new Exception(
+                    string.Format(
+                        "I didn't think we could have a number ({0}) without a result that was in the number chain at this point",
+                        startingNumber));
+            }
+        }
+
+        private static bool AlreadyKnowThatThisNumberIsHappy(string happyNumberKey)
+        {
+            return HappyNumberResults.ContainsKey(happyNumberKey);
+        }
+
+        private static void TestForHappiness(int startingNumber, string happyNumberKey)
+        {
+            var nextInChain = startingNumber;
+            while (!HappyNumberCalculationIsCompleteFor(happyNumberKey))
+            {
+                nextInChain = nextInChain.GetSumOfSquaredDigits();
+                if (nextInChain == 1)
+                {
+                    HappyNumberResults.Add(happyNumberKey, true);
+                    break;
+                }
+                if (TheCalculationChainHasLoopedAround(startingNumber, nextInChain))
+                {
+                    HappyNumberResults.Add(happyNumberKey, false);
+                    break;
+                }
+            }
+        }
+
+        /// <summary>
+        /// If the last calculated sum of the squares of the digits is already in the set of calculated numbers
+        /// then this number chain has looped around
+        /// </summary>
+        private static bool TheCalculationChainHasLoopedAround(int startingNumber, int sumOfSquaredDigits)
+        {
+            var canAddToTheNumbersInThisChain = NumberChain[startingNumber].Add(sumOfSquaredDigits);
+            return !canAddToTheNumbersInThisChain;
+        }
+
+        private static bool HappyNumberCalculationIsCompleteFor(string happyNumberKey)
+        {
+            return HappyNumberResults.ContainsKey(happyNumberKey);
+        }
+
+        private static int GetSumOfSquaredDigits(this int number)
+        {
+            return number.GetDigits()
+                         .Sum(digit => digit*digit);
+        }
+
+        private static IEnumerable<int> GetDigits(this int number)
+        {
+            return number.ToString(CultureInfo.InvariantCulture)
+                         .Select(digit => int.Parse(digit.ToString(CultureInfo.InvariantCulture)));
+        }
+
+        private static IEnumerable<int> StripZeroes(this IEnumerable<int> numbers)
+        {
+            return numbers.Where(digit => digit != 0);
+        }
+
+        private static IEnumerable<int> OrderedByDigit(this IEnumerable<int> numbers)
+        {
+            return numbers.OrderBy(i=>i);
+        }
+    }
+}
+
+
+ +

OK, there's a lot going on there. First there are two dictionaries: one which keeps track of the numbers generated while processing each number processed; the other which keeps track of the results for a key describing each number (not the number be being processed).

+ +
private static readonly Dictionary<int, HashSet<int>> NumberChain = new Dictionary<int, HashSet<int>>(); 
+private static readonly Dictionary<string, bool> HappyNumberResults = new Dictionary<string, bool>();
+
+ +

The first dictionary is used for deciding when a number is sad - if a number is seen for a second time then either it was the start number or the process has started looping. The second dictionary is for shortcut results. That is since 123, 213, 321, 1203, 2130, 3021, etc all have the same result once we've seen 123 we can immediately return a result for all other numbers that have a single 1, 2, and 3 and any number of zeroes.

+ +

Then var happyNumberKey = string.Join(",", digitsToTest); because two instances of int[] aren't equal based on their contents it is necessary to generate a key that from the arrays so that they can be compared when adding to the HappyNumberResults dictionary.

+ +

There are a bunch of methods to help reveal intent - mainly for this method that does the meat of the work:

+
private static void TestForHappiness(int startingNumber, string happyNumberKey)
+{
+    var nextInChain = startingNumber;
+    while (!HappyNumberCalculationIsCompleteFor(happyNumberKey))
+    {
+        nextInChain = nextInChain.GetSumOfSquaredDigits();
+        if (nextInChain == 1)
+        {
+            HappyNumberResults.Add(happyNumberKey, true);
+            break;
+        }
+        if (TheCalculationChainHasLoopedAround(startingNumber, nextInChain))
+        {
+            HappyNumberResults.Add(happyNumberKey, false);
+            break;
+        }
+    }
+}
+
+ +

This expresses the algorithm: take a number, get the sum of the square of its digits, test for a result, stop if you have one or do the same to that sum. I don't like the triple check of HappyNumberIsCompleteFor, nextInChain==1, and TheCalculationChainHasLoopedAround but I can't immediately see how to split that up without it being too meh.

+ +

Results

+

My first naive implementation didn't order digits or strip zeroes and reached around 320,000 in five seconds. I added ordering of digits but (since I was drinking) I used an integer array as the key on the HappyNumbersResults dictionary - doh! At least when performance didn't improve I realised what I'd done.

+ +

Switching to a string key for the short-cut dictionary had, as could be expected, no impact for unordered digits but pushes the maximum reached up to around 2,000,000 for ordered digits.

+ +

Removing zeroes from the digits array didn't have much impact - presumably because calculating the square of zero isn't very expensive.

+ +

Can this be improved?

+

Processing two million numbers in five seconds is pretty good but I wondered if this could be improved on with some of the fangling available in the TPL library.

+ +

First lesson here was that I don't get the TPL at all… at all

+ +

My first attempt at parallel processing of the list meant adding the cost of starting a thread for every number (as the Happy Numbers are processed one at a time) so I added an extension method to call AreHappyNumbers on a list of integers.

+ +
public static Dictionary<int, bool> AreHappyNumbers(
+    this IEnumerable<int> numbersToProcess, 
+    CancellationToken cancellationToken)
+{
+    var numbers = numbersToProcess as int[] ?? numbersToProcess.ToArray();
+    var results = new Dictionary<int, bool>(numbers.Count());
+    foreach (var number in numbers)
+    {
+        if (cancellationToken.IsCancellationRequested)
+        {
+            Debug.WriteLine("returning before processing {0} on cancel", number);
+            return results;
+        }
+        results.Add(number, number.IsAHappyNumber());
+    }
+    return results;
+} 
+
+ +

This method takes a range of numbers and a cancellation token. It then loops over the numbers calculating if they are happy and testing for cancellation before each number.

+ +

After quite a few false starts and confusions with the TPL I ended up with the following:

+ +
[Test]
+public void ParallelRunForFiveSeconds()
+{
+    var watch = Stopwatch.StartNew();
+    var cancellationTokenSource = new CancellationTokenSource();
+    var tasks = new List<Task<Dictionary<int, bool>>>();
+    var count = 0;
+
+    //without this line the whole thing runs to completion
+    cancellationTokenSource.CancelAfter(5000);
+
+    try
+    {
+        foreach (var index in Enumerable.Range(0, 10))
+        {
+            var cancellationToken = cancellationTokenSource.Token;
+            tasks.Add(Task.Factory.StartNew(
+                () => Enumerable.Range(index * 1000000, 1000000)
+                                .AreHappyNumbers(cancellationToken)));
+        }
+
+        //without timeout the stopwatch measures around 6.5 seconds
+       Task.WaitAll(tasks.Cast<Task>().ToArray(), timeout: TimeSpan.FromSeconds(5));
+       count = tasks.Select(t => t.Result).Sum(result => result.Count);
+    }
+    catch (TaskCanceledException)
+    {
+        Debug.WriteLine("task was cancelled and that's ok");
+    }
+
+    Debug.WriteLine("watch has been running for {0} seconds", watch.Elapsed.TotalSeconds);
+
+    Debug.WriteLine("In five seconds the number of numbers was {0}", count);
+}
+
+ +

So, yes that's not a test and it is probably awful TPL code but I'm pretty damn sure it doesn't run for more than 5 seconds and it calculates…

+ +

<drumroll/>

+ +

around SEVEN AND A HALF MILLION NUMBERS in those five seconds. Yes, they're not consecutive - but that's a pretty good improvement from two million. Such a good improvement that I'm doubting myself (although I can't see the error if there was one!)

+ +

So…

+ +

I thought the Happy Numbers kata would be a little diversion for an evening but the addition of a five second limit suggested in Kevin Rutherford's post made for a really interesting challenge. + +I don't tend to work on problems that lead me to need to optimise as heavily as I have done here. That meant I had to think very differently about what I was doing and that's always a good thing (I think). Although it has lead to some pretty ugly code. Maybe after a little rest I'll see if I can keep the performance and make it smell less - it's definitely ended up as a ball of mud!

+ +

If anyone does grok the TPL and can point out what I've done badly or could be improved the code is on GitHub and I'd appreciate any pointers.

+ + +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/images/1-no-event.jpg b/images/1-no-event.jpg new file mode 100644 index 000000000..68c0ab5e7 Binary files /dev/null and b/images/1-no-event.jpg differ diff --git a/images/1.jpg b/images/1.jpg new file mode 100644 index 000000000..72064afed Binary files /dev/null and b/images/1.jpg differ diff --git a/images/2-events-written.png b/images/2-events-written.png new file mode 100644 index 000000000..a58f7038a Binary files /dev/null and b/images/2-events-written.png differ diff --git a/images/2-one-event.jpg b/images/2-one-event.jpg new file mode 100644 index 000000000..6c71d558f Binary files /dev/null and b/images/2-one-event.jpg differ diff --git a/images/2.jpg b/images/2.jpg new file mode 100644 index 000000000..909384c69 Binary files /dev/null and b/images/2.jpg differ diff --git a/images/2022-contributions.png b/images/2022-contributions.png new file mode 100644 index 000000000..4f9db605b Binary files /dev/null and b/images/2022-contributions.png differ diff --git a/images/2022-family.jpg b/images/2022-family.jpg new file mode 100644 index 000000000..c9f7fde19 Binary files /dev/null and b/images/2022-family.jpg differ diff --git a/images/2022-travel.png b/images/2022-travel.png new file mode 100644 index 000000000..395fcc45f Binary files /dev/null and b/images/2022-travel.png differ diff --git a/images/2023/03/01/cornwall-map.png b/images/2023/03/01/cornwall-map.png new file mode 100644 index 000000000..e09a22e50 Binary files /dev/null and b/images/2023/03/01/cornwall-map.png differ diff --git a/images/2023/03/01/drawing.png b/images/2023/03/01/drawing.png new file mode 100644 index 000000000..dbb7c93b8 Binary files /dev/null and b/images/2023/03/01/drawing.png differ diff --git a/images/2023/04/02/issue-tracking.gif b/images/2023/04/02/issue-tracking.gif new file mode 100644 index 000000000..a5a77c252 Binary files /dev/null and b/images/2023/04/02/issue-tracking.gif differ diff --git a/images/2023/04/02/planning.jpg b/images/2023/04/02/planning.jpg new file mode 100644 index 000000000..c3b1b5c3e Binary files /dev/null and b/images/2023/04/02/planning.jpg differ diff --git a/images/2023/04/02/pool.jpg b/images/2023/04/02/pool.jpg new file mode 100644 index 000000000..a0008ce61 Binary files /dev/null and b/images/2023/04/02/pool.jpg differ diff --git a/images/2023/07/blitzed.jpg b/images/2023/07/blitzed.jpg new file mode 100644 index 000000000..80df04755 Binary files /dev/null and b/images/2023/07/blitzed.jpg differ diff --git a/images/2023/07/chopped.jpg b/images/2023/07/chopped.jpg new file mode 100644 index 000000000..2576eb43d Binary files /dev/null and b/images/2023/07/chopped.jpg differ diff --git a/images/2023/07/cooked-one.jpg b/images/2023/07/cooked-one.jpg new file mode 100644 index 000000000..76eea9552 Binary files /dev/null and b/images/2023/07/cooked-one.jpg differ diff --git a/images/2023/07/cooked-two.jpg b/images/2023/07/cooked-two.jpg new file mode 100644 index 000000000..f0900ead8 Binary files /dev/null and b/images/2023/07/cooked-two.jpg differ diff --git a/images/2023/07/dough.jpg b/images/2023/07/dough.jpg new file mode 100644 index 000000000..88d8bc24d Binary files /dev/null and b/images/2023/07/dough.jpg differ diff --git a/images/2023/07/ready for the oven two.jpg b/images/2023/07/ready for the oven two.jpg new file mode 100644 index 000000000..d2237ac87 Binary files /dev/null and b/images/2023/07/ready for the oven two.jpg differ diff --git a/images/2023/07/ready for the oven.jpg b/images/2023/07/ready for the oven.jpg new file mode 100644 index 000000000..03c1046e0 Binary files /dev/null and b/images/2023/07/ready for the oven.jpg differ diff --git a/images/2023/07/the plants.jpg b/images/2023/07/the plants.jpg new file mode 100644 index 000000000..24e627041 Binary files /dev/null and b/images/2023/07/the plants.jpg differ diff --git a/images/2023/07/zucchini-focaccia-og.jpg b/images/2023/07/zucchini-focaccia-og.jpg new file mode 100644 index 000000000..efe52980e Binary files /dev/null and b/images/2023/07/zucchini-focaccia-og.jpg differ diff --git a/images/3-many-events.jpg b/images/3-many-events.jpg new file mode 100644 index 000000000..3b71e729e Binary files /dev/null and b/images/3-many-events.jpg differ diff --git a/images/3.jpg b/images/3.jpg new file mode 100644 index 000000000..246bed0e8 Binary files /dev/null and b/images/3.jpg differ diff --git a/images/300/1-no-event.jpg b/images/300/1-no-event.jpg new file mode 100644 index 000000000..3a98a17c4 Binary files /dev/null and b/images/300/1-no-event.jpg differ diff --git a/images/300/1.jpg b/images/300/1.jpg new file mode 100644 index 000000000..7e2f880a2 Binary files /dev/null and b/images/300/1.jpg differ diff --git a/images/300/2-events-written.png b/images/300/2-events-written.png new file mode 100644 index 000000000..fd163c424 Binary files /dev/null and b/images/300/2-events-written.png differ diff --git a/images/300/2-one-event.jpg b/images/300/2-one-event.jpg new file mode 100644 index 000000000..9c72d7178 Binary files /dev/null and b/images/300/2-one-event.jpg differ diff --git a/images/300/2.jpg b/images/300/2.jpg new file mode 100644 index 000000000..c77d89c4e Binary files /dev/null and b/images/300/2.jpg differ diff --git a/images/300/2022-contributions.png b/images/300/2022-contributions.png new file mode 100644 index 000000000..41ab08e32 Binary files /dev/null and b/images/300/2022-contributions.png differ diff --git a/images/300/2022-family.jpg b/images/300/2022-family.jpg new file mode 100644 index 000000000..d65a9971c Binary files /dev/null and b/images/300/2022-family.jpg differ diff --git a/images/300/2022-travel.png b/images/300/2022-travel.png new file mode 100644 index 000000000..2c988ab5a Binary files /dev/null and b/images/300/2022-travel.png differ diff --git a/images/300/3-many-events.jpg b/images/300/3-many-events.jpg new file mode 100644 index 000000000..90ac89d13 Binary files /dev/null and b/images/300/3-many-events.jpg differ diff --git a/images/300/3.jpg b/images/300/3.jpg new file mode 100644 index 000000000..94750690d Binary files /dev/null and b/images/300/3.jpg differ diff --git a/images/300/4-new-readmodel.jpg b/images/300/4-new-readmodel.jpg new file mode 100644 index 000000000..7d463bdf3 Binary files /dev/null and b/images/300/4-new-readmodel.jpg differ diff --git a/images/300/5-caught-up.jpg b/images/300/5-caught-up.jpg new file mode 100644 index 000000000..83cda205f Binary files /dev/null and b/images/300/5-caught-up.jpg differ diff --git a/images/300/6-graph.jpg b/images/300/6-graph.jpg new file mode 100644 index 000000000..416046cc1 Binary files /dev/null and b/images/300/6-graph.jpg differ diff --git a/images/300/ABC.png b/images/300/ABC.png new file mode 100644 index 000000000..1039521d5 Binary files /dev/null and b/images/300/ABC.png differ diff --git a/images/300/AMP-webmaster.png b/images/300/AMP-webmaster.png new file mode 100644 index 000000000..9731dcf86 Binary files /dev/null and b/images/300/AMP-webmaster.png differ diff --git a/images/300/GitHub-Mark-Light-32px.png b/images/300/GitHub-Mark-Light-32px.png new file mode 100644 index 000000000..3ba833fbb Binary files /dev/null and b/images/300/GitHub-Mark-Light-32px.png differ diff --git a/images/300/actual_different_styles_meme_by_zeurel-d38c306.png b/images/300/actual_different_styles_meme_by_zeurel-d38c306.png new file mode 100644 index 000000000..5a5648bb9 Binary files /dev/null and b/images/300/actual_different_styles_meme_by_zeurel-d38c306.png differ diff --git a/images/300/affordance-loggedin.png b/images/300/affordance-loggedin.png new file mode 100644 index 000000000..b051bc4bb Binary files /dev/null and b/images/300/affordance-loggedin.png differ diff --git a/images/300/affordance-loggedout.png b/images/300/affordance-loggedout.png new file mode 100644 index 000000000..d6c1df9c2 Binary files /dev/null and b/images/300/affordance-loggedout.png differ diff --git a/images/300/agilecam.jpg b/images/300/agilecam.jpg new file mode 100644 index 000000000..21ba9ec7e Binary files /dev/null and b/images/300/agilecam.jpg differ diff --git a/images/300/api-console-output.png b/images/300/api-console-output.png new file mode 100644 index 000000000..00c55f5d6 Binary files /dev/null and b/images/300/api-console-output.png differ diff --git a/images/300/api-gateway.png b/images/300/api-gateway.png new file mode 100644 index 000000000..b87d29164 Binary files /dev/null and b/images/300/api-gateway.png differ diff --git a/images/300/beach.jpg b/images/300/beach.jpg new file mode 100644 index 000000000..1740c3105 Binary files /dev/null and b/images/300/beach.jpg differ diff --git a/images/300/bill.png b/images/300/bill.png new file mode 100644 index 000000000..e6c21509c Binary files /dev/null and b/images/300/bill.png differ diff --git a/images/300/both.png b/images/300/both.png new file mode 100644 index 000000000..ddede6bfa Binary files /dev/null and b/images/300/both.png differ diff --git a/images/300/cardboard.jpg b/images/300/cardboard.jpg new file mode 100644 index 000000000..39a3963e1 Binary files /dev/null and b/images/300/cardboard.jpg differ diff --git a/images/300/chickens.jpg b/images/300/chickens.jpg new file mode 100644 index 000000000..f3ba4e49e Binary files /dev/null and b/images/300/chickens.jpg differ diff --git a/images/300/code.png b/images/300/code.png new file mode 100644 index 000000000..3b5ec61ce Binary files /dev/null and b/images/300/code.png differ diff --git a/images/300/common-language.jpg b/images/300/common-language.jpg new file mode 100644 index 000000000..389bad028 Binary files /dev/null and b/images/300/common-language.jpg differ diff --git a/images/300/cqrs.jpg b/images/300/cqrs.jpg new file mode 100644 index 000000000..211aaf797 Binary files /dev/null and b/images/300/cqrs.jpg differ diff --git a/images/300/crafts.jpg b/images/300/crafts.jpg new file mode 100644 index 000000000..1d213a4c3 Binary files /dev/null and b/images/300/crafts.jpg differ diff --git a/images/300/current-sequence.jpg b/images/300/current-sequence.jpg new file mode 100644 index 000000000..9ab83fe6c Binary files /dev/null and b/images/300/current-sequence.jpg differ diff --git a/images/300/dog-2019-01-01.jpg b/images/300/dog-2019-01-01.jpg new file mode 100644 index 000000000..a29857af8 Binary files /dev/null and b/images/300/dog-2019-01-01.jpg differ diff --git a/images/300/dog-2020-01-01.jpg b/images/300/dog-2020-01-01.jpg new file mode 100644 index 000000000..d048f264c Binary files /dev/null and b/images/300/dog-2020-01-01.jpg differ diff --git a/images/300/dog-2021-01-01.jpg b/images/300/dog-2021-01-01.jpg new file mode 100644 index 000000000..bc4a3b347 Binary files /dev/null and b/images/300/dog-2021-01-01.jpg differ diff --git a/images/300/dough-balls.jpg b/images/300/dough-balls.jpg new file mode 100644 index 000000000..90ddec060 Binary files /dev/null and b/images/300/dough-balls.jpg differ diff --git a/images/300/dough-wide.jpg b/images/300/dough-wide.jpg new file mode 100644 index 000000000..76922070a Binary files /dev/null and b/images/300/dough-wide.jpg differ diff --git a/images/300/ducks.jpg b/images/300/ducks.jpg new file mode 100644 index 000000000..e36b69cd4 Binary files /dev/null and b/images/300/ducks.jpg differ diff --git a/images/300/dynamo-cheap-perf.png b/images/300/dynamo-cheap-perf.png new file mode 100644 index 000000000..9a58fc4ea Binary files /dev/null and b/images/300/dynamo-cheap-perf.png differ diff --git a/images/300/dynamo-console.png b/images/300/dynamo-console.png new file mode 100644 index 000000000..a8c42802d Binary files /dev/null and b/images/300/dynamo-console.png differ diff --git a/images/300/east.jpg b/images/300/east.jpg new file mode 100644 index 000000000..ceb0618a8 Binary files /dev/null and b/images/300/east.jpg differ diff --git a/images/300/event-composed.jpg b/images/300/event-composed.jpg new file mode 100644 index 000000000..78b04ceb8 Binary files /dev/null and b/images/300/event-composed.jpg differ diff --git a/images/300/event-notification.jpg b/images/300/event-notification.jpg new file mode 100644 index 000000000..27bf6e939 Binary files /dev/null and b/images/300/event-notification.jpg differ diff --git a/images/300/event-sourced.jpg b/images/300/event-sourced.jpg new file mode 100644 index 000000000..a97a9b71c Binary files /dev/null and b/images/300/event-sourced.jpg differ diff --git a/images/300/facebook-black-32.png b/images/300/facebook-black-32.png new file mode 100644 index 000000000..4c78423ba Binary files /dev/null and b/images/300/facebook-black-32.png differ diff --git a/images/300/fifteen-dependencies.png b/images/300/fifteen-dependencies.png new file mode 100644 index 000000000..55701c1fe Binary files /dev/null and b/images/300/fifteen-dependencies.png differ diff --git a/images/300/first-slice-2.jpg b/images/300/first-slice-2.jpg new file mode 100644 index 000000000..7421f6d23 Binary files /dev/null and b/images/300/first-slice-2.jpg differ diff --git a/images/300/foggy-day.jpg b/images/300/foggy-day.jpg new file mode 100644 index 000000000..e06b7a920 Binary files /dev/null and b/images/300/foggy-day.jpg differ diff --git a/images/300/forecast.png b/images/300/forecast.png new file mode 100644 index 000000000..7c3737ae4 Binary files /dev/null and b/images/300/forecast.png differ diff --git a/images/300/god-of-death.png b/images/300/god-of-death.png new file mode 100644 index 000000000..f3d564e10 Binary files /dev/null and b/images/300/god-of-death.png differ diff --git a/images/300/helpful-advice.png b/images/300/helpful-advice.png new file mode 100644 index 000000000..ca8119299 Binary files /dev/null and b/images/300/helpful-advice.png differ diff --git a/images/300/home404.png b/images/300/home404.png new file mode 100644 index 000000000..3214b44fb Binary files /dev/null and b/images/300/home404.png differ diff --git a/images/300/homeBare.png b/images/300/homeBare.png new file mode 100644 index 000000000..f954b2bbd Binary files /dev/null and b/images/300/homeBare.png differ diff --git a/images/300/homeCarousel.png b/images/300/homeCarousel.png new file mode 100644 index 000000000..8d5e0dfaf Binary files /dev/null and b/images/300/homeCarousel.png differ diff --git a/images/300/homeFull.png b/images/300/homeFull.png new file mode 100644 index 000000000..baf7f5ac5 Binary files /dev/null and b/images/300/homeFull.png differ diff --git a/images/300/ideal-board.jpg b/images/300/ideal-board.jpg new file mode 100644 index 000000000..6c8756c12 Binary files /dev/null and b/images/300/ideal-board.jpg differ diff --git a/images/300/initial-commit-log.png b/images/300/initial-commit-log.png new file mode 100644 index 000000000..7dc3c378c Binary files /dev/null and b/images/300/initial-commit-log.png differ diff --git a/images/300/integrates-with-github.png b/images/300/integrates-with-github.png new file mode 100644 index 000000000..86d857065 Binary files /dev/null and b/images/300/integrates-with-github.png differ diff --git a/images/300/interactive-spelling.png b/images/300/interactive-spelling.png new file mode 100644 index 000000000..776c99aca Binary files /dev/null and b/images/300/interactive-spelling.png differ diff --git a/images/300/lambda-console.png b/images/300/lambda-console.png new file mode 100644 index 000000000..8aae968bc Binary files /dev/null and b/images/300/lambda-console.png differ diff --git a/images/300/logo.png b/images/300/logo.png new file mode 100644 index 000000000..5c98c3d3a Binary files /dev/null and b/images/300/logo.png differ diff --git a/images/300/lost.jpg b/images/300/lost.jpg new file mode 100644 index 000000000..6efd17cc6 Binary files /dev/null and b/images/300/lost.jpg differ diff --git a/images/300/mockup_5.png b/images/300/mockup_5.png new file mode 100644 index 000000000..90bda62b0 Binary files /dev/null and b/images/300/mockup_5.png differ diff --git a/images/300/new-sequence.jpg b/images/300/new-sequence.jpg new file mode 100644 index 000000000..9db9cfd7e Binary files /dev/null and b/images/300/new-sequence.jpg differ diff --git a/images/300/nonewline.png b/images/300/nonewline.png new file mode 100644 index 000000000..dbcca2c2b Binary files /dev/null and b/images/300/nonewline.png differ diff --git a/images/300/npm-run.png b/images/300/npm-run.png new file mode 100644 index 000000000..3070f1b2e Binary files /dev/null and b/images/300/npm-run.png differ diff --git a/images/300/objects.png b/images/300/objects.png new file mode 100644 index 000000000..710a82c51 Binary files /dev/null and b/images/300/objects.png differ diff --git a/images/300/one-board.jpg b/images/300/one-board.jpg new file mode 100644 index 000000000..8e9990ae6 Binary files /dev/null and b/images/300/one-board.jpg differ diff --git a/images/300/part-four-flow.jpg b/images/300/part-four-flow.jpg new file mode 100644 index 000000000..ab13999f1 Binary files /dev/null and b/images/300/part-four-flow.jpg differ diff --git a/images/300/pasta-fasule.jpg b/images/300/pasta-fasule.jpg new file mode 100644 index 000000000..9ca951a19 Binary files /dev/null and b/images/300/pasta-fasule.jpg differ diff --git a/images/300/pepper-1.jpg b/images/300/pepper-1.jpg new file mode 100644 index 000000000..76f019ffe Binary files /dev/null and b/images/300/pepper-1.jpg differ diff --git a/images/300/pepper-2.jpg b/images/300/pepper-2.jpg new file mode 100644 index 000000000..3c4819aa5 Binary files /dev/null and b/images/300/pepper-2.jpg differ diff --git a/images/300/pepper-wide.jpg b/images/300/pepper-wide.jpg new file mode 100644 index 000000000..5a495078c Binary files /dev/null and b/images/300/pepper-wide.jpg differ diff --git a/images/300/personal-access-tokens.png b/images/300/personal-access-tokens.png new file mode 100644 index 000000000..3ed39e4a2 Binary files /dev/null and b/images/300/personal-access-tokens.png differ diff --git a/images/300/radar.jpg b/images/300/radar.jpg new file mode 100644 index 000000000..14cc8846a Binary files /dev/null and b/images/300/radar.jpg differ diff --git a/images/300/reactotype_screenshot.png b/images/300/reactotype_screenshot.png new file mode 100644 index 000000000..f5b94ac98 Binary files /dev/null and b/images/300/reactotype_screenshot.png differ diff --git a/images/300/run-nightwatch.png b/images/300/run-nightwatch.png new file mode 100644 index 000000000..241dd0aac Binary files /dev/null and b/images/300/run-nightwatch.png differ diff --git a/images/300/run2.0.jpg b/images/300/run2.0.jpg new file mode 100644 index 000000000..8982a7254 Binary files /dev/null and b/images/300/run2.0.jpg differ diff --git a/images/300/second-slice-4.jpg b/images/300/second-slice-4.jpg new file mode 100644 index 000000000..4248dcbe8 Binary files /dev/null and b/images/300/second-slice-4.jpg differ diff --git a/images/300/serverless-maintenance.png b/images/300/serverless-maintenance.png new file mode 100644 index 000000000..6d2471eea Binary files /dev/null and b/images/300/serverless-maintenance.png differ diff --git a/images/300/serverless.jpg b/images/300/serverless.jpg new file mode 100644 index 000000000..69e1ff236 Binary files /dev/null and b/images/300/serverless.jpg differ diff --git a/images/300/stack.png b/images/300/stack.png new file mode 100644 index 000000000..494fa15e4 Binary files /dev/null and b/images/300/stack.png differ diff --git a/images/300/start-api.png b/images/300/start-api.png new file mode 100644 index 000000000..ee6cab0fd Binary files /dev/null and b/images/300/start-api.png differ diff --git a/images/300/structs.png b/images/300/structs.png new file mode 100644 index 000000000..c39c29248 Binary files /dev/null and b/images/300/structs.png differ diff --git a/images/300/structured-data-crawled.png b/images/300/structured-data-crawled.png new file mode 100644 index 000000000..fefafd98d Binary files /dev/null and b/images/300/structured-data-crawled.png differ diff --git a/images/300/sunny-day.jpg b/images/300/sunny-day.jpg new file mode 100644 index 000000000..077441e8c Binary files /dev/null and b/images/300/sunny-day.jpg differ diff --git a/images/300/tada.png b/images/300/tada.png new file mode 100644 index 000000000..76b1d8f84 Binary files /dev/null and b/images/300/tada.png differ diff --git a/images/300/testing-api-1.png b/images/300/testing-api-1.png new file mode 100644 index 000000000..c59e40c1a Binary files /dev/null and b/images/300/testing-api-1.png differ diff --git a/images/300/testing-api-result-2.png b/images/300/testing-api-result-2.png new file mode 100644 index 000000000..44c134fd0 Binary files /dev/null and b/images/300/testing-api-result-2.png differ diff --git a/images/300/the-chart-1.png b/images/300/the-chart-1.png new file mode 100644 index 000000000..750c30e27 Binary files /dev/null and b/images/300/the-chart-1.png differ diff --git a/images/300/the-discussion.png b/images/300/the-discussion.png new file mode 100644 index 000000000..5bdb744e1 Binary files /dev/null and b/images/300/the-discussion.png differ diff --git a/images/300/the-graph.png b/images/300/the-graph.png new file mode 100644 index 000000000..608d3e06b Binary files /dev/null and b/images/300/the-graph.png differ diff --git a/images/300/the-quadrants.png b/images/300/the-quadrants.png new file mode 100644 index 000000000..56c0c5ded Binary files /dev/null and b/images/300/the-quadrants.png differ diff --git a/images/300/toot.png b/images/300/toot.png new file mode 100644 index 000000000..3335421c2 Binary files /dev/null and b/images/300/toot.png differ diff --git a/images/300/travis.png b/images/300/travis.png new file mode 100644 index 000000000..5c8483ddb Binary files /dev/null and b/images/300/travis.png differ diff --git a/images/300/tree.jpg b/images/300/tree.jpg new file mode 100644 index 000000000..5cd835c06 Binary files /dev/null and b/images/300/tree.jpg differ diff --git a/images/300/twitter-32.png b/images/300/twitter-32.png new file mode 100644 index 000000000..62289815d Binary files /dev/null and b/images/300/twitter-32.png differ diff --git a/images/300/twitter-black-32.png b/images/300/twitter-black-32.png new file mode 100644 index 000000000..3be729d4b Binary files /dev/null and b/images/300/twitter-black-32.png differ diff --git a/images/300/unhappiness.png b/images/300/unhappiness.png new file mode 100644 index 000000000..dbc1432eb Binary files /dev/null and b/images/300/unhappiness.png differ diff --git a/images/300/votes.png b/images/300/votes.png new file mode 100644 index 000000000..f750a7754 Binary files /dev/null and b/images/300/votes.png differ diff --git a/images/300/weeknotes-autoload-graph.png b/images/300/weeknotes-autoload-graph.png new file mode 100644 index 000000000..f0f5ea63b Binary files /dev/null and b/images/300/weeknotes-autoload-graph.png differ diff --git a/images/300/weeknotes.png b/images/300/weeknotes.png new file mode 100644 index 000000000..13f28faaa Binary files /dev/null and b/images/300/weeknotes.png differ diff --git a/images/300/yarn-desc.png b/images/300/yarn-desc.png new file mode 100644 index 000000000..30abe2945 Binary files /dev/null and b/images/300/yarn-desc.png differ diff --git a/images/300/yarn-run.png b/images/300/yarn-run.png new file mode 100644 index 000000000..5bc8835f2 Binary files /dev/null and b/images/300/yarn-run.png differ diff --git a/images/300/zero-velocity.png b/images/300/zero-velocity.png new file mode 100644 index 000000000..e7f5578c2 Binary files /dev/null and b/images/300/zero-velocity.png differ diff --git a/images/4-new-readmodel.jpg b/images/4-new-readmodel.jpg new file mode 100644 index 000000000..3ebb62b5f Binary files /dev/null and b/images/4-new-readmodel.jpg differ diff --git a/images/480/1-no-event.jpg b/images/480/1-no-event.jpg new file mode 100644 index 000000000..abb0da234 Binary files /dev/null and b/images/480/1-no-event.jpg differ diff --git a/images/480/1.jpg b/images/480/1.jpg new file mode 100644 index 000000000..ac8d20deb Binary files /dev/null and b/images/480/1.jpg differ diff --git a/images/480/2-events-written.png b/images/480/2-events-written.png new file mode 100644 index 000000000..112f3fa5b Binary files /dev/null and b/images/480/2-events-written.png differ diff --git a/images/480/2-one-event.jpg b/images/480/2-one-event.jpg new file mode 100644 index 000000000..ad6c6b6c6 Binary files /dev/null and b/images/480/2-one-event.jpg differ diff --git a/images/480/2.jpg b/images/480/2.jpg new file mode 100644 index 000000000..424b4a9dd Binary files /dev/null and b/images/480/2.jpg differ diff --git a/images/480/2022-contributions.png b/images/480/2022-contributions.png new file mode 100644 index 000000000..89fb19af0 Binary files /dev/null and b/images/480/2022-contributions.png differ diff --git a/images/480/2022-family.jpg b/images/480/2022-family.jpg new file mode 100644 index 000000000..c731d54d4 Binary files /dev/null and b/images/480/2022-family.jpg differ diff --git a/images/480/2022-travel.png b/images/480/2022-travel.png new file mode 100644 index 000000000..0ecddcf1e Binary files /dev/null and b/images/480/2022-travel.png differ diff --git a/images/480/3-many-events.jpg b/images/480/3-many-events.jpg new file mode 100644 index 000000000..e27d69cec Binary files /dev/null and b/images/480/3-many-events.jpg differ diff --git a/images/480/3.jpg b/images/480/3.jpg new file mode 100644 index 000000000..f7958eccc Binary files /dev/null and b/images/480/3.jpg differ diff --git a/images/480/4-new-readmodel.jpg b/images/480/4-new-readmodel.jpg new file mode 100644 index 000000000..9c6d6db42 Binary files /dev/null and b/images/480/4-new-readmodel.jpg differ diff --git a/images/480/5-caught-up.jpg b/images/480/5-caught-up.jpg new file mode 100644 index 000000000..886c264ad Binary files /dev/null and b/images/480/5-caught-up.jpg differ diff --git a/images/480/6-graph.jpg b/images/480/6-graph.jpg new file mode 100644 index 000000000..85d86049d Binary files /dev/null and b/images/480/6-graph.jpg differ diff --git a/images/480/ABC.png b/images/480/ABC.png new file mode 100644 index 000000000..2fd20b47b Binary files /dev/null and b/images/480/ABC.png differ diff --git a/images/480/AMP-webmaster.png b/images/480/AMP-webmaster.png new file mode 100644 index 000000000..bd108315f Binary files /dev/null and b/images/480/AMP-webmaster.png differ diff --git a/images/480/GitHub-Mark-Light-32px.png b/images/480/GitHub-Mark-Light-32px.png new file mode 100644 index 000000000..3ba833fbb Binary files /dev/null and b/images/480/GitHub-Mark-Light-32px.png differ diff --git a/images/480/actual_different_styles_meme_by_zeurel-d38c306.png b/images/480/actual_different_styles_meme_by_zeurel-d38c306.png new file mode 100644 index 000000000..13b9296f2 Binary files /dev/null and b/images/480/actual_different_styles_meme_by_zeurel-d38c306.png differ diff --git a/images/480/affordance-loggedin.png b/images/480/affordance-loggedin.png new file mode 100644 index 000000000..e48971094 Binary files /dev/null and b/images/480/affordance-loggedin.png differ diff --git a/images/480/affordance-loggedout.png b/images/480/affordance-loggedout.png new file mode 100644 index 000000000..89e288e7d Binary files /dev/null and b/images/480/affordance-loggedout.png differ diff --git a/images/480/agilecam.jpg b/images/480/agilecam.jpg new file mode 100644 index 000000000..d267443a9 Binary files /dev/null and b/images/480/agilecam.jpg differ diff --git a/images/480/api-console-output.png b/images/480/api-console-output.png new file mode 100644 index 000000000..2e2068cbb Binary files /dev/null and b/images/480/api-console-output.png differ diff --git a/images/480/api-gateway.png b/images/480/api-gateway.png new file mode 100644 index 000000000..0fa6eb0e9 Binary files /dev/null and b/images/480/api-gateway.png differ diff --git a/images/480/beach.jpg b/images/480/beach.jpg new file mode 100644 index 000000000..ec5f575e5 Binary files /dev/null and b/images/480/beach.jpg differ diff --git a/images/480/bill.png b/images/480/bill.png new file mode 100644 index 000000000..01ed86db1 Binary files /dev/null and b/images/480/bill.png differ diff --git a/images/480/both.png b/images/480/both.png new file mode 100644 index 000000000..07e8875c0 Binary files /dev/null and b/images/480/both.png differ diff --git a/images/480/cardboard.jpg b/images/480/cardboard.jpg new file mode 100644 index 000000000..7949a697b Binary files /dev/null and b/images/480/cardboard.jpg differ diff --git a/images/480/chickens.jpg b/images/480/chickens.jpg new file mode 100644 index 000000000..450d5ff6b Binary files /dev/null and b/images/480/chickens.jpg differ diff --git a/images/480/code.png b/images/480/code.png new file mode 100644 index 000000000..03d29b4a7 Binary files /dev/null and b/images/480/code.png differ diff --git a/images/480/common-language.jpg b/images/480/common-language.jpg new file mode 100644 index 000000000..2f0a5f57a Binary files /dev/null and b/images/480/common-language.jpg differ diff --git a/images/480/cqrs.jpg b/images/480/cqrs.jpg new file mode 100644 index 000000000..0539c0979 Binary files /dev/null and b/images/480/cqrs.jpg differ diff --git a/images/480/crafts.jpg b/images/480/crafts.jpg new file mode 100644 index 000000000..7a230f311 Binary files /dev/null and b/images/480/crafts.jpg differ diff --git a/images/480/current-sequence.jpg b/images/480/current-sequence.jpg new file mode 100644 index 000000000..fb11dcf3e Binary files /dev/null and b/images/480/current-sequence.jpg differ diff --git a/images/480/dog-2019-01-01.jpg b/images/480/dog-2019-01-01.jpg new file mode 100644 index 000000000..d3a8265da Binary files /dev/null and b/images/480/dog-2019-01-01.jpg differ diff --git a/images/480/dog-2020-01-01.jpg b/images/480/dog-2020-01-01.jpg new file mode 100644 index 000000000..35a1a7102 Binary files /dev/null and b/images/480/dog-2020-01-01.jpg differ diff --git a/images/480/dog-2021-01-01.jpg b/images/480/dog-2021-01-01.jpg new file mode 100644 index 000000000..4041a1544 Binary files /dev/null and b/images/480/dog-2021-01-01.jpg differ diff --git a/images/480/dough-balls.jpg b/images/480/dough-balls.jpg new file mode 100644 index 000000000..91975468a Binary files /dev/null and b/images/480/dough-balls.jpg differ diff --git a/images/480/dough-wide.jpg b/images/480/dough-wide.jpg new file mode 100644 index 000000000..54ee27d0b Binary files /dev/null and b/images/480/dough-wide.jpg differ diff --git a/images/480/ducks.jpg b/images/480/ducks.jpg new file mode 100644 index 000000000..de6425db2 Binary files /dev/null and b/images/480/ducks.jpg differ diff --git a/images/480/dynamo-cheap-perf.png b/images/480/dynamo-cheap-perf.png new file mode 100644 index 000000000..7bd5c13a4 Binary files /dev/null and b/images/480/dynamo-cheap-perf.png differ diff --git a/images/480/dynamo-console.png b/images/480/dynamo-console.png new file mode 100644 index 000000000..7695bf765 Binary files /dev/null and b/images/480/dynamo-console.png differ diff --git a/images/480/east.jpg b/images/480/east.jpg new file mode 100644 index 000000000..e423254cc Binary files /dev/null and b/images/480/east.jpg differ diff --git a/images/480/event-composed.jpg b/images/480/event-composed.jpg new file mode 100644 index 000000000..0569d537b Binary files /dev/null and b/images/480/event-composed.jpg differ diff --git a/images/480/event-notification.jpg b/images/480/event-notification.jpg new file mode 100644 index 000000000..7dca0feaa Binary files /dev/null and b/images/480/event-notification.jpg differ diff --git a/images/480/event-sourced.jpg b/images/480/event-sourced.jpg new file mode 100644 index 000000000..5a98ebcef Binary files /dev/null and b/images/480/event-sourced.jpg differ diff --git a/images/480/facebook-black-32.png b/images/480/facebook-black-32.png new file mode 100644 index 000000000..4c78423ba Binary files /dev/null and b/images/480/facebook-black-32.png differ diff --git a/images/480/fifteen-dependencies.png b/images/480/fifteen-dependencies.png new file mode 100644 index 000000000..81bde9549 Binary files /dev/null and b/images/480/fifteen-dependencies.png differ diff --git a/images/480/first-slice-2.jpg b/images/480/first-slice-2.jpg new file mode 100644 index 000000000..45a4ad2c1 Binary files /dev/null and b/images/480/first-slice-2.jpg differ diff --git a/images/480/foggy-day.jpg b/images/480/foggy-day.jpg new file mode 100644 index 000000000..1fb0d5421 Binary files /dev/null and b/images/480/foggy-day.jpg differ diff --git a/images/480/forecast.png b/images/480/forecast.png new file mode 100644 index 000000000..5f5c09f0f Binary files /dev/null and b/images/480/forecast.png differ diff --git a/images/480/god-of-death.png b/images/480/god-of-death.png new file mode 100644 index 000000000..0540bbfb6 Binary files /dev/null and b/images/480/god-of-death.png differ diff --git a/images/480/helpful-advice.png b/images/480/helpful-advice.png new file mode 100644 index 000000000..cf2cf76d7 Binary files /dev/null and b/images/480/helpful-advice.png differ diff --git a/images/480/home404.png b/images/480/home404.png new file mode 100644 index 000000000..2ae47a3d4 Binary files /dev/null and b/images/480/home404.png differ diff --git a/images/480/homeBare.png b/images/480/homeBare.png new file mode 100644 index 000000000..035206e6b Binary files /dev/null and b/images/480/homeBare.png differ diff --git a/images/480/homeCarousel.png b/images/480/homeCarousel.png new file mode 100644 index 000000000..e865671ed Binary files /dev/null and b/images/480/homeCarousel.png differ diff --git a/images/480/homeFull.png b/images/480/homeFull.png new file mode 100644 index 000000000..cf0cfadf9 Binary files /dev/null and b/images/480/homeFull.png differ diff --git a/images/480/ideal-board.jpg b/images/480/ideal-board.jpg new file mode 100644 index 000000000..1c62a6cae Binary files /dev/null and b/images/480/ideal-board.jpg differ diff --git a/images/480/initial-commit-log.png b/images/480/initial-commit-log.png new file mode 100644 index 000000000..e46e00dd2 Binary files /dev/null and b/images/480/initial-commit-log.png differ diff --git a/images/480/integrates-with-github.png b/images/480/integrates-with-github.png new file mode 100644 index 000000000..07b9df7fd Binary files /dev/null and b/images/480/integrates-with-github.png differ diff --git a/images/480/interactive-spelling.png b/images/480/interactive-spelling.png new file mode 100644 index 000000000..ba91b072c Binary files /dev/null and b/images/480/interactive-spelling.png differ diff --git a/images/480/lambda-console.png b/images/480/lambda-console.png new file mode 100644 index 000000000..ac1cdafaa Binary files /dev/null and b/images/480/lambda-console.png differ diff --git a/images/480/logo.png b/images/480/logo.png new file mode 100644 index 000000000..5c98c3d3a Binary files /dev/null and b/images/480/logo.png differ diff --git a/images/480/lost.jpg b/images/480/lost.jpg new file mode 100644 index 000000000..e348cee04 Binary files /dev/null and b/images/480/lost.jpg differ diff --git a/images/480/mockup_5.png b/images/480/mockup_5.png new file mode 100644 index 000000000..b9c8b12b8 Binary files /dev/null and b/images/480/mockup_5.png differ diff --git a/images/480/new-sequence.jpg b/images/480/new-sequence.jpg new file mode 100644 index 000000000..18457b7a7 Binary files /dev/null and b/images/480/new-sequence.jpg differ diff --git a/images/480/nonewline.png b/images/480/nonewline.png new file mode 100644 index 000000000..ba0324553 Binary files /dev/null and b/images/480/nonewline.png differ diff --git a/images/480/npm-run.png b/images/480/npm-run.png new file mode 100644 index 000000000..7079f2cc9 Binary files /dev/null and b/images/480/npm-run.png differ diff --git a/images/480/objects.png b/images/480/objects.png new file mode 100644 index 000000000..afe2d7ef5 Binary files /dev/null and b/images/480/objects.png differ diff --git a/images/480/one-board.jpg b/images/480/one-board.jpg new file mode 100644 index 000000000..0a5cc9f41 Binary files /dev/null and b/images/480/one-board.jpg differ diff --git a/images/480/part-four-flow.jpg b/images/480/part-four-flow.jpg new file mode 100644 index 000000000..f5fad764b Binary files /dev/null and b/images/480/part-four-flow.jpg differ diff --git a/images/480/pasta-fasule.jpg b/images/480/pasta-fasule.jpg new file mode 100644 index 000000000..47e848626 Binary files /dev/null and b/images/480/pasta-fasule.jpg differ diff --git a/images/480/pepper-1.jpg b/images/480/pepper-1.jpg new file mode 100644 index 000000000..da6ca0215 Binary files /dev/null and b/images/480/pepper-1.jpg differ diff --git a/images/480/pepper-2.jpg b/images/480/pepper-2.jpg new file mode 100644 index 000000000..aebd174e0 Binary files /dev/null and b/images/480/pepper-2.jpg differ diff --git a/images/480/pepper-wide.jpg b/images/480/pepper-wide.jpg new file mode 100644 index 000000000..5dbb17a06 Binary files /dev/null and b/images/480/pepper-wide.jpg differ diff --git a/images/480/personal-access-tokens.png b/images/480/personal-access-tokens.png new file mode 100644 index 000000000..5fabea234 Binary files /dev/null and b/images/480/personal-access-tokens.png differ diff --git a/images/480/radar.jpg b/images/480/radar.jpg new file mode 100644 index 000000000..0a7c2ae19 Binary files /dev/null and b/images/480/radar.jpg differ diff --git a/images/480/reactotype_screenshot.png b/images/480/reactotype_screenshot.png new file mode 100644 index 000000000..a005ef570 Binary files /dev/null and b/images/480/reactotype_screenshot.png differ diff --git a/images/480/run-nightwatch.png b/images/480/run-nightwatch.png new file mode 100644 index 000000000..0693bcca9 Binary files /dev/null and b/images/480/run-nightwatch.png differ diff --git a/images/480/run2.0.jpg b/images/480/run2.0.jpg new file mode 100644 index 000000000..da21dd374 Binary files /dev/null and b/images/480/run2.0.jpg differ diff --git a/images/480/second-slice-4.jpg b/images/480/second-slice-4.jpg new file mode 100644 index 000000000..b22e16fe2 Binary files /dev/null and b/images/480/second-slice-4.jpg differ diff --git a/images/480/serverless-maintenance.png b/images/480/serverless-maintenance.png new file mode 100644 index 000000000..26ef2398d Binary files /dev/null and b/images/480/serverless-maintenance.png differ diff --git a/images/480/serverless.jpg b/images/480/serverless.jpg new file mode 100644 index 000000000..afe910f44 Binary files /dev/null and b/images/480/serverless.jpg differ diff --git a/images/480/stack.png b/images/480/stack.png new file mode 100644 index 000000000..88fb1ea9b Binary files /dev/null and b/images/480/stack.png differ diff --git a/images/480/start-api.png b/images/480/start-api.png new file mode 100644 index 000000000..31bfae355 Binary files /dev/null and b/images/480/start-api.png differ diff --git a/images/480/structs.png b/images/480/structs.png new file mode 100644 index 000000000..e5ea2ea48 Binary files /dev/null and b/images/480/structs.png differ diff --git a/images/480/structured-data-crawled.png b/images/480/structured-data-crawled.png new file mode 100644 index 000000000..e464888dc Binary files /dev/null and b/images/480/structured-data-crawled.png differ diff --git a/images/480/sunny-day.jpg b/images/480/sunny-day.jpg new file mode 100644 index 000000000..8ea61e532 Binary files /dev/null and b/images/480/sunny-day.jpg differ diff --git a/images/480/tada.png b/images/480/tada.png new file mode 100644 index 000000000..f39b4b511 Binary files /dev/null and b/images/480/tada.png differ diff --git a/images/480/testing-api-1.png b/images/480/testing-api-1.png new file mode 100644 index 000000000..4cb3a578e Binary files /dev/null and b/images/480/testing-api-1.png differ diff --git a/images/480/testing-api-result-2.png b/images/480/testing-api-result-2.png new file mode 100644 index 000000000..9b88e2bf9 Binary files /dev/null and b/images/480/testing-api-result-2.png differ diff --git a/images/480/the-chart-1.png b/images/480/the-chart-1.png new file mode 100644 index 000000000..6ed65bc6d Binary files /dev/null and b/images/480/the-chart-1.png differ diff --git a/images/480/the-discussion.png b/images/480/the-discussion.png new file mode 100644 index 000000000..6065f29dd Binary files /dev/null and b/images/480/the-discussion.png differ diff --git a/images/480/the-graph.png b/images/480/the-graph.png new file mode 100644 index 000000000..2e1dcc525 Binary files /dev/null and b/images/480/the-graph.png differ diff --git a/images/480/the-quadrants.png b/images/480/the-quadrants.png new file mode 100644 index 000000000..8ab367e01 Binary files /dev/null and b/images/480/the-quadrants.png differ diff --git a/images/480/toot.png b/images/480/toot.png new file mode 100644 index 000000000..520882a7a Binary files /dev/null and b/images/480/toot.png differ diff --git a/images/480/travis.png b/images/480/travis.png new file mode 100644 index 000000000..7829f6171 Binary files /dev/null and b/images/480/travis.png differ diff --git a/images/480/tree.jpg b/images/480/tree.jpg new file mode 100644 index 000000000..8387a08e0 Binary files /dev/null and b/images/480/tree.jpg differ diff --git a/images/480/twitter-32.png b/images/480/twitter-32.png new file mode 100644 index 000000000..62289815d Binary files /dev/null and b/images/480/twitter-32.png differ diff --git a/images/480/twitter-black-32.png b/images/480/twitter-black-32.png new file mode 100644 index 000000000..3be729d4b Binary files /dev/null and b/images/480/twitter-black-32.png differ diff --git a/images/480/unhappiness.png b/images/480/unhappiness.png new file mode 100644 index 000000000..8e694d9c2 Binary files /dev/null and b/images/480/unhappiness.png differ diff --git a/images/480/votes.png b/images/480/votes.png new file mode 100644 index 000000000..d8e7a90bf Binary files /dev/null and b/images/480/votes.png differ diff --git a/images/480/weeknotes-autoload-graph.png b/images/480/weeknotes-autoload-graph.png new file mode 100644 index 000000000..3722058b2 Binary files /dev/null and b/images/480/weeknotes-autoload-graph.png differ diff --git a/images/480/weeknotes.png b/images/480/weeknotes.png new file mode 100644 index 000000000..f8cb5b7e7 Binary files /dev/null and b/images/480/weeknotes.png differ diff --git a/images/480/yarn-desc.png b/images/480/yarn-desc.png new file mode 100644 index 000000000..40c3701c4 Binary files /dev/null and b/images/480/yarn-desc.png differ diff --git a/images/480/yarn-run.png b/images/480/yarn-run.png new file mode 100644 index 000000000..c928cc63d Binary files /dev/null and b/images/480/yarn-run.png differ diff --git a/images/480/zero-velocity.png b/images/480/zero-velocity.png new file mode 100644 index 000000000..82653849a Binary files /dev/null and b/images/480/zero-velocity.png differ diff --git a/images/5-caught-up.jpg b/images/5-caught-up.jpg new file mode 100644 index 000000000..78bd24603 Binary files /dev/null and b/images/5-caught-up.jpg differ diff --git a/images/6-graph.jpg b/images/6-graph.jpg new file mode 100644 index 000000000..f6ef16af7 Binary files /dev/null and b/images/6-graph.jpg differ diff --git a/images/ABC.png b/images/ABC.png new file mode 100644 index 000000000..2fd20b47b Binary files /dev/null and b/images/ABC.png differ diff --git a/images/AMP-webmaster.png b/images/AMP-webmaster.png new file mode 100644 index 000000000..ac1471805 Binary files /dev/null and b/images/AMP-webmaster.png differ diff --git a/images/GitHub-Mark-Light-32px.png b/images/GitHub-Mark-Light-32px.png new file mode 100644 index 000000000..3ba833fbb Binary files /dev/null and b/images/GitHub-Mark-Light-32px.png differ diff --git a/images/Hoarding_living_room.jpeg b/images/Hoarding_living_room.jpeg new file mode 100644 index 000000000..1b4ca8212 Binary files /dev/null and b/images/Hoarding_living_room.jpeg differ diff --git a/images/actual_different_styles_meme_by_zeurel-d38c306.png b/images/actual_different_styles_meme_by_zeurel-d38c306.png new file mode 100644 index 000000000..2d26acf35 Binary files /dev/null and b/images/actual_different_styles_meme_by_zeurel-d38c306.png differ diff --git a/images/affordance-loggedin.png b/images/affordance-loggedin.png new file mode 100644 index 000000000..e48971094 Binary files /dev/null and b/images/affordance-loggedin.png differ diff --git a/images/affordance-loggedout.png b/images/affordance-loggedout.png new file mode 100644 index 000000000..89e288e7d Binary files /dev/null and b/images/affordance-loggedout.png differ diff --git a/images/affordance-with-delay.gif b/images/affordance-with-delay.gif new file mode 100644 index 000000000..922fe6119 Binary files /dev/null and b/images/affordance-with-delay.gif differ diff --git a/images/affordance-with-state.gif b/images/affordance-with-state.gif new file mode 100644 index 000000000..d0f2f5056 Binary files /dev/null and b/images/affordance-with-state.gif differ diff --git a/images/agilecam.jpg b/images/agilecam.jpg new file mode 100644 index 000000000..3c8ba0dda Binary files /dev/null and b/images/agilecam.jpg differ diff --git a/images/api-console-output.png b/images/api-console-output.png new file mode 100644 index 000000000..cfa08ea28 Binary files /dev/null and b/images/api-console-output.png differ diff --git a/images/api-gateway.png b/images/api-gateway.png new file mode 100644 index 000000000..0a888fe57 Binary files /dev/null and b/images/api-gateway.png differ diff --git a/images/bad_history.jpeg b/images/bad_history.jpeg new file mode 100644 index 000000000..2bad6d927 Binary files /dev/null and b/images/bad_history.jpeg differ diff --git a/images/beach.jpg b/images/beach.jpg new file mode 100644 index 000000000..721b980a3 Binary files /dev/null and b/images/beach.jpg differ diff --git a/images/bill.png b/images/bill.png new file mode 100644 index 000000000..a4536585b Binary files /dev/null and b/images/bill.png differ diff --git a/images/both.png b/images/both.png new file mode 100644 index 000000000..2b12051f6 Binary files /dev/null and b/images/both.png differ diff --git a/images/cardboard.jpg b/images/cardboard.jpg new file mode 100644 index 000000000..b610b16a1 Binary files /dev/null and b/images/cardboard.jpg differ diff --git a/images/chickens.jpg b/images/chickens.jpg new file mode 100644 index 000000000..02f62ec37 Binary files /dev/null and b/images/chickens.jpg differ diff --git a/images/code.png b/images/code.png new file mode 100644 index 000000000..60c862441 Binary files /dev/null and b/images/code.png differ diff --git a/images/common-language.jpg b/images/common-language.jpg new file mode 100644 index 000000000..0b4e32657 Binary files /dev/null and b/images/common-language.jpg differ diff --git a/images/coop.gif b/images/coop.gif new file mode 100644 index 000000000..1b0ff74e7 Binary files /dev/null and b/images/coop.gif differ diff --git a/images/cqrs.jpg b/images/cqrs.jpg new file mode 100644 index 000000000..1f11f7f40 Binary files /dev/null and b/images/cqrs.jpg differ diff --git a/images/crafts.jpg b/images/crafts.jpg new file mode 100644 index 000000000..4ceb3e355 Binary files /dev/null and b/images/crafts.jpg differ diff --git a/images/credit-card-1104961_1280.webp b/images/credit-card-1104961_1280.webp new file mode 100644 index 000000000..d84accf53 Binary files /dev/null and b/images/credit-card-1104961_1280.webp differ diff --git a/images/current-sequence.jpg b/images/current-sequence.jpg new file mode 100644 index 000000000..0118023f1 Binary files /dev/null and b/images/current-sequence.jpg differ diff --git a/images/dear_diary_year_one/1.2017week1.png b/images/dear_diary_year_one/1.2017week1.png new file mode 100644 index 000000000..fd72c9aaf Binary files /dev/null and b/images/dear_diary_year_one/1.2017week1.png differ diff --git a/images/dear_diary_year_one/10.2017week11.png b/images/dear_diary_year_one/10.2017week11.png new file mode 100644 index 000000000..76df5761c Binary files /dev/null and b/images/dear_diary_year_one/10.2017week11.png differ diff --git a/images/dear_diary_year_one/11.2017week12.png b/images/dear_diary_year_one/11.2017week12.png new file mode 100644 index 000000000..ac8850b26 Binary files /dev/null and b/images/dear_diary_year_one/11.2017week12.png differ diff --git a/images/dear_diary_year_one/12.2017week13.png b/images/dear_diary_year_one/12.2017week13.png new file mode 100644 index 000000000..f3d39d7a2 Binary files /dev/null and b/images/dear_diary_year_one/12.2017week13.png differ diff --git a/images/dear_diary_year_one/13.2018week1.png b/images/dear_diary_year_one/13.2018week1.png new file mode 100644 index 000000000..5dbcc77ec Binary files /dev/null and b/images/dear_diary_year_one/13.2018week1.png differ diff --git a/images/dear_diary_year_one/14.2018week2.png b/images/dear_diary_year_one/14.2018week2.png new file mode 100644 index 000000000..2f6bca551 Binary files /dev/null and b/images/dear_diary_year_one/14.2018week2.png differ diff --git a/images/dear_diary_year_one/15.2018week3.png b/images/dear_diary_year_one/15.2018week3.png new file mode 100644 index 000000000..51aaa1979 Binary files /dev/null and b/images/dear_diary_year_one/15.2018week3.png differ diff --git a/images/dear_diary_year_one/16.2018week4.png b/images/dear_diary_year_one/16.2018week4.png new file mode 100644 index 000000000..c2b5da171 Binary files /dev/null and b/images/dear_diary_year_one/16.2018week4.png differ diff --git a/images/dear_diary_year_one/17.2018week5.png b/images/dear_diary_year_one/17.2018week5.png new file mode 100644 index 000000000..2b07c9686 Binary files /dev/null and b/images/dear_diary_year_one/17.2018week5.png differ diff --git a/images/dear_diary_year_one/18.2018week6.png b/images/dear_diary_year_one/18.2018week6.png new file mode 100644 index 000000000..11d33778b Binary files /dev/null and b/images/dear_diary_year_one/18.2018week6.png differ diff --git a/images/dear_diary_year_one/19.2018week7.png b/images/dear_diary_year_one/19.2018week7.png new file mode 100644 index 000000000..bc778fd7e Binary files /dev/null and b/images/dear_diary_year_one/19.2018week7.png differ diff --git a/images/dear_diary_year_one/2.2017week2.png b/images/dear_diary_year_one/2.2017week2.png new file mode 100644 index 000000000..ae13cdb7c Binary files /dev/null and b/images/dear_diary_year_one/2.2017week2.png differ diff --git a/images/dear_diary_year_one/20.2018week8.png b/images/dear_diary_year_one/20.2018week8.png new file mode 100644 index 000000000..f6b447957 Binary files /dev/null and b/images/dear_diary_year_one/20.2018week8.png differ diff --git a/images/dear_diary_year_one/21.2018week9.png b/images/dear_diary_year_one/21.2018week9.png new file mode 100644 index 000000000..d8a4b8637 Binary files /dev/null and b/images/dear_diary_year_one/21.2018week9.png differ diff --git a/images/dear_diary_year_one/22.2018week10.png b/images/dear_diary_year_one/22.2018week10.png new file mode 100644 index 000000000..c2ad8249f Binary files /dev/null and b/images/dear_diary_year_one/22.2018week10.png differ diff --git a/images/dear_diary_year_one/23.2018week11.png b/images/dear_diary_year_one/23.2018week11.png new file mode 100644 index 000000000..1161ed780 Binary files /dev/null and b/images/dear_diary_year_one/23.2018week11.png differ diff --git a/images/dear_diary_year_one/24.2018week12.png b/images/dear_diary_year_one/24.2018week12.png new file mode 100644 index 000000000..91215ad4f Binary files /dev/null and b/images/dear_diary_year_one/24.2018week12.png differ diff --git a/images/dear_diary_year_one/25.2018week13.png b/images/dear_diary_year_one/25.2018week13.png new file mode 100644 index 000000000..8035aca1b Binary files /dev/null and b/images/dear_diary_year_one/25.2018week13.png differ diff --git a/images/dear_diary_year_one/26.2018week14.png b/images/dear_diary_year_one/26.2018week14.png new file mode 100644 index 000000000..798b9af55 Binary files /dev/null and b/images/dear_diary_year_one/26.2018week14.png differ diff --git a/images/dear_diary_year_one/27.2018week15.png b/images/dear_diary_year_one/27.2018week15.png new file mode 100644 index 000000000..4c7578bde Binary files /dev/null and b/images/dear_diary_year_one/27.2018week15.png differ diff --git a/images/dear_diary_year_one/28.2018week16.png b/images/dear_diary_year_one/28.2018week16.png new file mode 100644 index 000000000..b6c51cfa8 Binary files /dev/null and b/images/dear_diary_year_one/28.2018week16.png differ diff --git a/images/dear_diary_year_one/29.2018week17.png b/images/dear_diary_year_one/29.2018week17.png new file mode 100644 index 000000000..11d397e11 Binary files /dev/null and b/images/dear_diary_year_one/29.2018week17.png differ diff --git a/images/dear_diary_year_one/3.2017week3.png b/images/dear_diary_year_one/3.2017week3.png new file mode 100644 index 000000000..463f18487 Binary files /dev/null and b/images/dear_diary_year_one/3.2017week3.png differ diff --git a/images/dear_diary_year_one/30.2018week18.png b/images/dear_diary_year_one/30.2018week18.png new file mode 100644 index 000000000..2a2f85229 Binary files /dev/null and b/images/dear_diary_year_one/30.2018week18.png differ diff --git a/images/dear_diary_year_one/31.2018week19.png b/images/dear_diary_year_one/31.2018week19.png new file mode 100644 index 000000000..40a8ee7d4 Binary files /dev/null and b/images/dear_diary_year_one/31.2018week19.png differ diff --git a/images/dear_diary_year_one/32.2018week20and21.png b/images/dear_diary_year_one/32.2018week20and21.png new file mode 100644 index 000000000..a494f302d Binary files /dev/null and b/images/dear_diary_year_one/32.2018week20and21.png differ diff --git a/images/dear_diary_year_one/33.2018week22.png b/images/dear_diary_year_one/33.2018week22.png new file mode 100644 index 000000000..df0af0d51 Binary files /dev/null and b/images/dear_diary_year_one/33.2018week22.png differ diff --git a/images/dear_diary_year_one/34.2018week23.png b/images/dear_diary_year_one/34.2018week23.png new file mode 100644 index 000000000..814441fe5 Binary files /dev/null and b/images/dear_diary_year_one/34.2018week23.png differ diff --git a/images/dear_diary_year_one/35.2018week24.png b/images/dear_diary_year_one/35.2018week24.png new file mode 100644 index 000000000..9cee8da3b Binary files /dev/null and b/images/dear_diary_year_one/35.2018week24.png differ diff --git a/images/dear_diary_year_one/36.2018week25.png b/images/dear_diary_year_one/36.2018week25.png new file mode 100644 index 000000000..674f0ec38 Binary files /dev/null and b/images/dear_diary_year_one/36.2018week25.png differ diff --git a/images/dear_diary_year_one/37.2018week26.png b/images/dear_diary_year_one/37.2018week26.png new file mode 100644 index 000000000..e592ffe64 Binary files /dev/null and b/images/dear_diary_year_one/37.2018week26.png differ diff --git a/images/dear_diary_year_one/38.2018week27.png b/images/dear_diary_year_one/38.2018week27.png new file mode 100644 index 000000000..066e3e7b1 Binary files /dev/null and b/images/dear_diary_year_one/38.2018week27.png differ diff --git a/images/dear_diary_year_one/39.2018week28.png b/images/dear_diary_year_one/39.2018week28.png new file mode 100644 index 000000000..a1c678482 Binary files /dev/null and b/images/dear_diary_year_one/39.2018week28.png differ diff --git a/images/dear_diary_year_one/4.2017week4.png b/images/dear_diary_year_one/4.2017week4.png new file mode 100644 index 000000000..5dbd74239 Binary files /dev/null and b/images/dear_diary_year_one/4.2017week4.png differ diff --git a/images/dear_diary_year_one/40.2018week29.png b/images/dear_diary_year_one/40.2018week29.png new file mode 100644 index 000000000..382749f1a Binary files /dev/null and b/images/dear_diary_year_one/40.2018week29.png differ diff --git a/images/dear_diary_year_one/41.2018week30.png b/images/dear_diary_year_one/41.2018week30.png new file mode 100644 index 000000000..61a6ffb92 Binary files /dev/null and b/images/dear_diary_year_one/41.2018week30.png differ diff --git a/images/dear_diary_year_one/42.2018week31.png b/images/dear_diary_year_one/42.2018week31.png new file mode 100644 index 000000000..4e58b05a4 Binary files /dev/null and b/images/dear_diary_year_one/42.2018week31.png differ diff --git a/images/dear_diary_year_one/43.2018week32.png b/images/dear_diary_year_one/43.2018week32.png new file mode 100644 index 000000000..07211f625 Binary files /dev/null and b/images/dear_diary_year_one/43.2018week32.png differ diff --git a/images/dear_diary_year_one/44.2018week33.png b/images/dear_diary_year_one/44.2018week33.png new file mode 100644 index 000000000..f373b3d60 Binary files /dev/null and b/images/dear_diary_year_one/44.2018week33.png differ diff --git a/images/dear_diary_year_one/45.2018week34.png b/images/dear_diary_year_one/45.2018week34.png new file mode 100644 index 000000000..f6c59bb2e Binary files /dev/null and b/images/dear_diary_year_one/45.2018week34.png differ diff --git a/images/dear_diary_year_one/46.2018week35.png b/images/dear_diary_year_one/46.2018week35.png new file mode 100644 index 000000000..a82ceb7cd Binary files /dev/null and b/images/dear_diary_year_one/46.2018week35.png differ diff --git a/images/dear_diary_year_one/47.2018week36.png b/images/dear_diary_year_one/47.2018week36.png new file mode 100644 index 000000000..1dd6b2a71 Binary files /dev/null and b/images/dear_diary_year_one/47.2018week36.png differ diff --git a/images/dear_diary_year_one/48.2018week37.png b/images/dear_diary_year_one/48.2018week37.png new file mode 100644 index 000000000..3864df02b Binary files /dev/null and b/images/dear_diary_year_one/48.2018week37.png differ diff --git a/images/dear_diary_year_one/49.2018week38.png b/images/dear_diary_year_one/49.2018week38.png new file mode 100644 index 000000000..402a32d69 Binary files /dev/null and b/images/dear_diary_year_one/49.2018week38.png differ diff --git a/images/dear_diary_year_one/5.2017week5and6.png b/images/dear_diary_year_one/5.2017week5and6.png new file mode 100644 index 000000000..96fa58b18 Binary files /dev/null and b/images/dear_diary_year_one/5.2017week5and6.png differ diff --git a/images/dear_diary_year_one/6.2017week7.png b/images/dear_diary_year_one/6.2017week7.png new file mode 100644 index 000000000..96a63c370 Binary files /dev/null and b/images/dear_diary_year_one/6.2017week7.png differ diff --git a/images/dear_diary_year_one/7.2017week8.png b/images/dear_diary_year_one/7.2017week8.png new file mode 100644 index 000000000..25f885eb5 Binary files /dev/null and b/images/dear_diary_year_one/7.2017week8.png differ diff --git a/images/dear_diary_year_one/8.2017week9.png b/images/dear_diary_year_one/8.2017week9.png new file mode 100644 index 000000000..ae295491a Binary files /dev/null and b/images/dear_diary_year_one/8.2017week9.png differ diff --git a/images/dear_diary_year_one/9.2017week10.png b/images/dear_diary_year_one/9.2017week10.png new file mode 100644 index 000000000..054758b31 Binary files /dev/null and b/images/dear_diary_year_one/9.2017week10.png differ diff --git a/images/dog-2019-01-01.jpg b/images/dog-2019-01-01.jpg new file mode 100644 index 000000000..9a3497c40 Binary files /dev/null and b/images/dog-2019-01-01.jpg differ diff --git a/images/dog-2020-01-01.jpg b/images/dog-2020-01-01.jpg new file mode 100644 index 000000000..977f38fff Binary files /dev/null and b/images/dog-2020-01-01.jpg differ diff --git a/images/dog-2021-01-01.jpg b/images/dog-2021-01-01.jpg new file mode 100644 index 000000000..8e33a208d Binary files /dev/null and b/images/dog-2021-01-01.jpg differ diff --git a/images/dough-balls.jpg b/images/dough-balls.jpg new file mode 100644 index 000000000..813228a83 Binary files /dev/null and b/images/dough-balls.jpg differ diff --git a/images/dough-wide.jpg b/images/dough-wide.jpg new file mode 100644 index 000000000..245a33845 Binary files /dev/null and b/images/dough-wide.jpg differ diff --git a/images/ducks.jpg b/images/ducks.jpg new file mode 100644 index 000000000..b2b144e92 Binary files /dev/null and b/images/ducks.jpg differ diff --git a/images/dynamo-cheap-perf.png b/images/dynamo-cheap-perf.png new file mode 100644 index 000000000..26e492c1c Binary files /dev/null and b/images/dynamo-cheap-perf.png differ diff --git a/images/dynamo-console.png b/images/dynamo-console.png new file mode 100644 index 000000000..2132e0328 Binary files /dev/null and b/images/dynamo-console.png differ diff --git a/images/east.jpg b/images/east.jpg new file mode 100644 index 000000000..b2f132438 Binary files /dev/null and b/images/east.jpg differ diff --git a/images/editing.gif b/images/editing.gif new file mode 100644 index 000000000..894ab96a3 Binary files /dev/null and b/images/editing.gif differ diff --git a/images/event-composed.jpg b/images/event-composed.jpg new file mode 100644 index 000000000..287a35362 Binary files /dev/null and b/images/event-composed.jpg differ diff --git a/images/event-notification.jpg b/images/event-notification.jpg new file mode 100644 index 000000000..f4ca40d70 Binary files /dev/null and b/images/event-notification.jpg differ diff --git a/images/event-sourced.jpg b/images/event-sourced.jpg new file mode 100644 index 000000000..169fd3774 Binary files /dev/null and b/images/event-sourced.jpg differ diff --git a/images/events/2-events-written.png b/images/events/2-events-written.png new file mode 100644 index 000000000..a58f7038a Binary files /dev/null and b/images/events/2-events-written.png differ diff --git a/images/events/5/1-no-event.jpg b/images/events/5/1-no-event.jpg new file mode 100644 index 000000000..5bc350c7b Binary files /dev/null and b/images/events/5/1-no-event.jpg differ diff --git a/images/events/5/2-one-event.jpg b/images/events/5/2-one-event.jpg new file mode 100644 index 000000000..b78e7f74f Binary files /dev/null and b/images/events/5/2-one-event.jpg differ diff --git a/images/events/5/3-many-events.jpg b/images/events/5/3-many-events.jpg new file mode 100644 index 000000000..394d6e59c Binary files /dev/null and b/images/events/5/3-many-events.jpg differ diff --git a/images/events/5/4-new-readmodel.jpg b/images/events/5/4-new-readmodel.jpg new file mode 100644 index 000000000..604904a2d Binary files /dev/null and b/images/events/5/4-new-readmodel.jpg differ diff --git a/images/events/5/5-caught-up.jpg b/images/events/5/5-caught-up.jpg new file mode 100644 index 000000000..3208aea83 Binary files /dev/null and b/images/events/5/5-caught-up.jpg differ diff --git a/images/events/5/6-graph.jpg b/images/events/5/6-graph.jpg new file mode 100644 index 000000000..37ab5dd2f Binary files /dev/null and b/images/events/5/6-graph.jpg differ diff --git a/images/events/6/bill.png b/images/events/6/bill.png new file mode 100644 index 000000000..a4536585b Binary files /dev/null and b/images/events/6/bill.png differ diff --git a/images/events/6/current-sequence.jpg b/images/events/6/current-sequence.jpg new file mode 100644 index 000000000..d4891b661 Binary files /dev/null and b/images/events/6/current-sequence.jpg differ diff --git a/images/events/6/helpful-advice.png b/images/events/6/helpful-advice.png new file mode 100644 index 000000000..ff5078fff Binary files /dev/null and b/images/events/6/helpful-advice.png differ diff --git a/images/events/6/live-demo.gif b/images/events/6/live-demo.gif new file mode 100644 index 000000000..bd94a5776 Binary files /dev/null and b/images/events/6/live-demo.gif differ diff --git a/images/events/6/new-sequence.jpg b/images/events/6/new-sequence.jpg new file mode 100644 index 000000000..7ec2a34a7 Binary files /dev/null and b/images/events/6/new-sequence.jpg differ diff --git a/images/events/api-console-output.png b/images/events/api-console-output.png new file mode 100644 index 000000000..cfa08ea28 Binary files /dev/null and b/images/events/api-console-output.png differ diff --git a/images/events/api-gateway.png b/images/events/api-gateway.png new file mode 100644 index 000000000..0a888fe57 Binary files /dev/null and b/images/events/api-gateway.png differ diff --git a/images/events/c4/1.jpg b/images/events/c4/1.jpg new file mode 100644 index 000000000..68b6d38f2 Binary files /dev/null and b/images/events/c4/1.jpg differ diff --git a/images/events/c4/2.jpg b/images/events/c4/2.jpg new file mode 100644 index 000000000..2f343ddce Binary files /dev/null and b/images/events/c4/2.jpg differ diff --git a/images/events/c4/3.jpg b/images/events/c4/3.jpg new file mode 100644 index 000000000..7e0d707c2 Binary files /dev/null and b/images/events/c4/3.jpg differ diff --git a/images/events/c4/first-slice-2.jpg b/images/events/c4/first-slice-2.jpg new file mode 100644 index 000000000..48d003e88 Binary files /dev/null and b/images/events/c4/first-slice-2.jpg differ diff --git a/images/events/c4/second-slice-4.jpg b/images/events/c4/second-slice-4.jpg new file mode 100644 index 000000000..26f5b5cc2 Binary files /dev/null and b/images/events/c4/second-slice-4.jpg differ diff --git a/images/events/cqrs.jpg b/images/events/cqrs.jpg new file mode 100644 index 000000000..f2b82d6ed Binary files /dev/null and b/images/events/cqrs.jpg differ diff --git a/images/events/dynamo-console.png b/images/events/dynamo-console.png new file mode 100644 index 000000000..2132e0328 Binary files /dev/null and b/images/events/dynamo-console.png differ diff --git a/images/events/east.jpg b/images/events/east.jpg new file mode 100644 index 000000000..abffc99ab Binary files /dev/null and b/images/events/east.jpg differ diff --git a/images/events/event-composed.jpg b/images/events/event-composed.jpg new file mode 100644 index 000000000..264f3e0e6 Binary files /dev/null and b/images/events/event-composed.jpg differ diff --git a/images/events/event-notification.jpg b/images/events/event-notification.jpg new file mode 100644 index 000000000..253cd97bb Binary files /dev/null and b/images/events/event-notification.jpg differ diff --git a/images/events/event-sourced.jpg b/images/events/event-sourced.jpg new file mode 100644 index 000000000..888e9a5e7 Binary files /dev/null and b/images/events/event-sourced.jpg differ diff --git a/images/events/lambda-console.png b/images/events/lambda-console.png new file mode 100644 index 000000000..33f096b3b Binary files /dev/null and b/images/events/lambda-console.png differ diff --git a/images/events/part-four-flow.jpg b/images/events/part-four-flow.jpg new file mode 100644 index 000000000..adb2a58bd Binary files /dev/null and b/images/events/part-four-flow.jpg differ diff --git a/images/events/serverless.jpg b/images/events/serverless.jpg new file mode 100644 index 000000000..f2593caee Binary files /dev/null and b/images/events/serverless.jpg differ diff --git a/images/events/stack.png b/images/events/stack.png new file mode 100644 index 000000000..0bb5ad75d Binary files /dev/null and b/images/events/stack.png differ diff --git a/images/events/start-api.png b/images/events/start-api.png new file mode 100644 index 000000000..f1bef1504 Binary files /dev/null and b/images/events/start-api.png differ diff --git a/images/events/testing-api-1.png b/images/events/testing-api-1.png new file mode 100644 index 000000000..3bce02f9d Binary files /dev/null and b/images/events/testing-api-1.png differ diff --git a/images/events/testing-api-result-2.png b/images/events/testing-api-result-2.png new file mode 100644 index 000000000..2fd05a318 Binary files /dev/null and b/images/events/testing-api-result-2.png differ diff --git a/images/facebook-black-32.png b/images/facebook-black-32.png new file mode 100644 index 000000000..4c78423ba Binary files /dev/null and b/images/facebook-black-32.png differ diff --git a/images/fifteen-dependencies.png b/images/fifteen-dependencies.png new file mode 100644 index 000000000..95a39c888 Binary files /dev/null and b/images/fifteen-dependencies.png differ diff --git a/images/first-slice-2.jpg b/images/first-slice-2.jpg new file mode 100644 index 000000000..e5f54ec24 Binary files /dev/null and b/images/first-slice-2.jpg differ diff --git a/images/foggy-day.jpg b/images/foggy-day.jpg new file mode 100644 index 000000000..c5371e97a Binary files /dev/null and b/images/foggy-day.jpg differ diff --git a/images/forecast.png b/images/forecast.png new file mode 100644 index 000000000..0b8895e80 Binary files /dev/null and b/images/forecast.png differ diff --git a/images/god-of-death.png b/images/god-of-death.png new file mode 100644 index 000000000..72e0590d8 Binary files /dev/null and b/images/god-of-death.png differ diff --git a/images/helpful-advice.png b/images/helpful-advice.png new file mode 100644 index 000000000..ff5078fff Binary files /dev/null and b/images/helpful-advice.png differ diff --git a/images/home404.png b/images/home404.png new file mode 100644 index 000000000..b205418c8 Binary files /dev/null and b/images/home404.png differ diff --git a/images/homeBare.png b/images/homeBare.png new file mode 100644 index 000000000..1f01d83b2 Binary files /dev/null and b/images/homeBare.png differ diff --git a/images/homeCarousel.png b/images/homeCarousel.png new file mode 100644 index 000000000..ac0cccc70 Binary files /dev/null and b/images/homeCarousel.png differ diff --git a/images/homeFull.png b/images/homeFull.png new file mode 100644 index 000000000..abf7f1028 Binary files /dev/null and b/images/homeFull.png differ diff --git a/images/icons/icon-128x128.png b/images/icons/icon-128x128.png new file mode 100644 index 000000000..9cb09c0d3 Binary files /dev/null and b/images/icons/icon-128x128.png differ diff --git a/images/icons/icon-144x144.png b/images/icons/icon-144x144.png new file mode 100644 index 000000000..ec0f0e2eb Binary files /dev/null and b/images/icons/icon-144x144.png differ diff --git a/images/icons/icon-152x152.png b/images/icons/icon-152x152.png new file mode 100644 index 000000000..89dfb3288 Binary files /dev/null and b/images/icons/icon-152x152.png differ diff --git a/images/icons/icon-192x192.png b/images/icons/icon-192x192.png new file mode 100644 index 000000000..437b40980 Binary files /dev/null and b/images/icons/icon-192x192.png differ diff --git a/images/icons/icon-384x384.png b/images/icons/icon-384x384.png new file mode 100644 index 000000000..5a2a4ca3a Binary files /dev/null and b/images/icons/icon-384x384.png differ diff --git a/images/icons/icon-512x512.png b/images/icons/icon-512x512.png new file mode 100644 index 000000000..ee3f38b14 Binary files /dev/null and b/images/icons/icon-512x512.png differ diff --git a/images/icons/icon-72x72.png b/images/icons/icon-72x72.png new file mode 100644 index 000000000..8c9dce31a Binary files /dev/null and b/images/icons/icon-72x72.png differ diff --git a/images/icons/icon-96x96.png b/images/icons/icon-96x96.png new file mode 100644 index 000000000..8c1afaf73 Binary files /dev/null and b/images/icons/icon-96x96.png differ diff --git a/images/ideal-board.jpg b/images/ideal-board.jpg new file mode 100644 index 000000000..0bd93caca Binary files /dev/null and b/images/ideal-board.jpg differ diff --git a/images/initial-commit-log.png b/images/initial-commit-log.png new file mode 100644 index 000000000..a68e35725 Binary files /dev/null and b/images/initial-commit-log.png differ diff --git a/images/integrates-with-github.png b/images/integrates-with-github.png new file mode 100644 index 000000000..6d33cd9eb Binary files /dev/null and b/images/integrates-with-github.png differ diff --git a/images/interactive-spelling.png b/images/interactive-spelling.png new file mode 100644 index 000000000..3bf6f10c9 Binary files /dev/null and b/images/interactive-spelling.png differ diff --git a/images/kids-games/clocky-mc-face.gif b/images/kids-games/clocky-mc-face.gif new file mode 100644 index 000000000..2430615ef Binary files /dev/null and b/images/kids-games/clocky-mc-face.gif differ diff --git a/images/kids-games/cry-me-a-colour.gif b/images/kids-games/cry-me-a-colour.gif new file mode 100644 index 000000000..135d81d58 Binary files /dev/null and b/images/kids-games/cry-me-a-colour.gif differ diff --git a/images/kids-games/get-to-1000.gif b/images/kids-games/get-to-1000.gif new file mode 100644 index 000000000..640b22370 Binary files /dev/null and b/images/kids-games/get-to-1000.gif differ diff --git a/images/kids-games/namey-numbers.gif b/images/kids-games/namey-numbers.gif new file mode 100644 index 000000000..d94ba66b6 Binary files /dev/null and b/images/kids-games/namey-numbers.gif differ diff --git a/images/kids-games/readerer.gif b/images/kids-games/readerer.gif new file mode 100644 index 000000000..e453f0398 Binary files /dev/null and b/images/kids-games/readerer.gif differ diff --git a/images/kids-games/round-it.gif b/images/kids-games/round-it.gif new file mode 100644 index 000000000..66576a6a1 Binary files /dev/null and b/images/kids-games/round-it.gif differ diff --git a/images/kids.gif b/images/kids.gif new file mode 100644 index 000000000..13e244d76 Binary files /dev/null and b/images/kids.gif differ diff --git a/images/lambda-console.png b/images/lambda-console.png new file mode 100644 index 000000000..33f096b3b Binary files /dev/null and b/images/lambda-console.png differ diff --git a/images/live-demo.gif b/images/live-demo.gif new file mode 100644 index 000000000..bd94a5776 Binary files /dev/null and b/images/live-demo.gif differ diff --git a/images/logo.png b/images/logo.png new file mode 100644 index 000000000..5c98c3d3a Binary files /dev/null and b/images/logo.png differ diff --git a/images/lost.jpg b/images/lost.jpg new file mode 100644 index 000000000..9e7fbc138 Binary files /dev/null and b/images/lost.jpg differ diff --git a/images/mockup_5.png b/images/mockup_5.png new file mode 100644 index 000000000..ca984c1a5 Binary files /dev/null and b/images/mockup_5.png differ diff --git a/images/myface.jpeg b/images/myface.jpeg new file mode 100644 index 000000000..dc8789962 Binary files /dev/null and b/images/myface.jpeg differ diff --git a/images/new-sequence.jpg b/images/new-sequence.jpg new file mode 100644 index 000000000..d0a6859f9 Binary files /dev/null and b/images/new-sequence.jpg differ diff --git a/images/nonewline.png b/images/nonewline.png new file mode 100644 index 000000000..643f45a4a Binary files /dev/null and b/images/nonewline.png differ diff --git a/images/npm-run.png b/images/npm-run.png new file mode 100644 index 000000000..ce1e89f65 Binary files /dev/null and b/images/npm-run.png differ diff --git a/images/objects.png b/images/objects.png new file mode 100644 index 000000000..ef7429112 Binary files /dev/null and b/images/objects.png differ diff --git a/images/office365/1.jpeg b/images/office365/1.jpeg new file mode 100644 index 000000000..b2bf3148e Binary files /dev/null and b/images/office365/1.jpeg differ diff --git a/images/office365/10.png b/images/office365/10.png new file mode 100644 index 000000000..3cd632ce1 Binary files /dev/null and b/images/office365/10.png differ diff --git a/images/office365/11.png b/images/office365/11.png new file mode 100644 index 000000000..770d0e08b Binary files /dev/null and b/images/office365/11.png differ diff --git a/images/office365/12.png b/images/office365/12.png new file mode 100644 index 000000000..6eadfde1f Binary files /dev/null and b/images/office365/12.png differ diff --git a/images/office365/13.mp4 b/images/office365/13.mp4 new file mode 100644 index 000000000..8e33a33c2 Binary files /dev/null and b/images/office365/13.mp4 differ diff --git a/images/office365/14.png b/images/office365/14.png new file mode 100644 index 000000000..14f949e56 Binary files /dev/null and b/images/office365/14.png differ diff --git a/images/office365/15.png b/images/office365/15.png new file mode 100644 index 000000000..2a6aa9b89 Binary files /dev/null and b/images/office365/15.png differ diff --git a/images/office365/16.mp4 b/images/office365/16.mp4 new file mode 100644 index 000000000..aea4860cd Binary files /dev/null and b/images/office365/16.mp4 differ diff --git a/images/office365/17.mp4 b/images/office365/17.mp4 new file mode 100644 index 000000000..e75487807 Binary files /dev/null and b/images/office365/17.mp4 differ diff --git a/images/office365/18.mp4 b/images/office365/18.mp4 new file mode 100644 index 000000000..e4c4046bc Binary files /dev/null and b/images/office365/18.mp4 differ diff --git a/images/office365/19.jpeg b/images/office365/19.jpeg new file mode 100644 index 000000000..e1724f047 Binary files /dev/null and b/images/office365/19.jpeg differ diff --git a/images/office365/2.mp4 b/images/office365/2.mp4 new file mode 100644 index 000000000..87134f224 Binary files /dev/null and b/images/office365/2.mp4 differ diff --git a/images/office365/20.mp4 b/images/office365/20.mp4 new file mode 100644 index 000000000..b35c3a1c8 Binary files /dev/null and b/images/office365/20.mp4 differ diff --git a/images/office365/21.png b/images/office365/21.png new file mode 100644 index 000000000..793679f12 Binary files /dev/null and b/images/office365/21.png differ diff --git a/images/office365/22.png b/images/office365/22.png new file mode 100644 index 000000000..4dc753f5e Binary files /dev/null and b/images/office365/22.png differ diff --git a/images/office365/23.png b/images/office365/23.png new file mode 100644 index 000000000..c57655ea7 Binary files /dev/null and b/images/office365/23.png differ diff --git a/images/office365/24.mp4 b/images/office365/24.mp4 new file mode 100644 index 000000000..0f51c436c Binary files /dev/null and b/images/office365/24.mp4 differ diff --git a/images/office365/25.png b/images/office365/25.png new file mode 100644 index 000000000..8486bbd3d Binary files /dev/null and b/images/office365/25.png differ diff --git a/images/office365/26.mp4 b/images/office365/26.mp4 new file mode 100644 index 000000000..5e0607a19 Binary files /dev/null and b/images/office365/26.mp4 differ diff --git a/images/office365/27.mp4 b/images/office365/27.mp4 new file mode 100644 index 000000000..898a3fd1c Binary files /dev/null and b/images/office365/27.mp4 differ diff --git a/images/office365/28.png b/images/office365/28.png new file mode 100644 index 000000000..0d40a5909 Binary files /dev/null and b/images/office365/28.png differ diff --git a/images/office365/29.mp4 b/images/office365/29.mp4 new file mode 100644 index 000000000..24c4616a8 Binary files /dev/null and b/images/office365/29.mp4 differ diff --git a/images/office365/3.mp4 b/images/office365/3.mp4 new file mode 100644 index 000000000..732013a10 Binary files /dev/null and b/images/office365/3.mp4 differ diff --git a/images/office365/30.mp4 b/images/office365/30.mp4 new file mode 100644 index 000000000..b9579ae03 Binary files /dev/null and b/images/office365/30.mp4 differ diff --git a/images/office365/31.jpeg b/images/office365/31.jpeg new file mode 100644 index 000000000..82767e775 Binary files /dev/null and b/images/office365/31.jpeg differ diff --git a/images/office365/32.jpeg b/images/office365/32.jpeg new file mode 100644 index 000000000..82767e775 Binary files /dev/null and b/images/office365/32.jpeg differ diff --git a/images/office365/33.jpeg b/images/office365/33.jpeg new file mode 100644 index 000000000..7e98a7daa Binary files /dev/null and b/images/office365/33.jpeg differ diff --git a/images/office365/34.mp4 b/images/office365/34.mp4 new file mode 100644 index 000000000..ae5d04439 Binary files /dev/null and b/images/office365/34.mp4 differ diff --git a/images/office365/35.png b/images/office365/35.png new file mode 100644 index 000000000..9bd42703a Binary files /dev/null and b/images/office365/35.png differ diff --git a/images/office365/36.jpeg b/images/office365/36.jpeg new file mode 100644 index 000000000..535e8c09c Binary files /dev/null and b/images/office365/36.jpeg differ diff --git a/images/office365/37.mp4 b/images/office365/37.mp4 new file mode 100644 index 000000000..06dae4f98 Binary files /dev/null and b/images/office365/37.mp4 differ diff --git a/images/office365/38.jpeg b/images/office365/38.jpeg new file mode 100644 index 000000000..f28455f38 Binary files /dev/null and b/images/office365/38.jpeg differ diff --git a/images/office365/39.jpeg b/images/office365/39.jpeg new file mode 100644 index 000000000..bbb3b460c Binary files /dev/null and b/images/office365/39.jpeg differ diff --git a/images/office365/4.jpeg b/images/office365/4.jpeg new file mode 100644 index 000000000..1cb59e379 Binary files /dev/null and b/images/office365/4.jpeg differ diff --git a/images/office365/40.png b/images/office365/40.png new file mode 100644 index 000000000..16d038053 Binary files /dev/null and b/images/office365/40.png differ diff --git a/images/office365/41.jpeg b/images/office365/41.jpeg new file mode 100644 index 000000000..6cec0c1b8 Binary files /dev/null and b/images/office365/41.jpeg differ diff --git a/images/office365/42.png b/images/office365/42.png new file mode 100644 index 000000000..51f6a5fcf Binary files /dev/null and b/images/office365/42.png differ diff --git a/images/office365/43.mp4 b/images/office365/43.mp4 new file mode 100644 index 000000000..cb3cd37fb Binary files /dev/null and b/images/office365/43.mp4 differ diff --git a/images/office365/44.mp4 b/images/office365/44.mp4 new file mode 100644 index 000000000..7ce70a383 Binary files /dev/null and b/images/office365/44.mp4 differ diff --git a/images/office365/45.jpeg b/images/office365/45.jpeg new file mode 100644 index 000000000..7d8a83b97 Binary files /dev/null and b/images/office365/45.jpeg differ diff --git a/images/office365/46.png b/images/office365/46.png new file mode 100644 index 000000000..9efa2c9a6 Binary files /dev/null and b/images/office365/46.png differ diff --git a/images/office365/47.png b/images/office365/47.png new file mode 100644 index 000000000..86122eec5 Binary files /dev/null and b/images/office365/47.png differ diff --git a/images/office365/48.png b/images/office365/48.png new file mode 100644 index 000000000..adb57faaf Binary files /dev/null and b/images/office365/48.png differ diff --git a/images/office365/49.jpeg b/images/office365/49.jpeg new file mode 100644 index 000000000..cc01338d0 Binary files /dev/null and b/images/office365/49.jpeg differ diff --git a/images/office365/5.png b/images/office365/5.png new file mode 100644 index 000000000..756eee79c Binary files /dev/null and b/images/office365/5.png differ diff --git a/images/office365/50.png b/images/office365/50.png new file mode 100644 index 000000000..a1c45cb50 Binary files /dev/null and b/images/office365/50.png differ diff --git a/images/office365/51.png b/images/office365/51.png new file mode 100644 index 000000000..3623518e7 Binary files /dev/null and b/images/office365/51.png differ diff --git a/images/office365/52.png b/images/office365/52.png new file mode 100644 index 000000000..fd83233a4 Binary files /dev/null and b/images/office365/52.png differ diff --git a/images/office365/53.jpeg b/images/office365/53.jpeg new file mode 100644 index 000000000..2d6dc426a Binary files /dev/null and b/images/office365/53.jpeg differ diff --git a/images/office365/54.png b/images/office365/54.png new file mode 100644 index 000000000..a2fcf1b60 Binary files /dev/null and b/images/office365/54.png differ diff --git a/images/office365/55.png b/images/office365/55.png new file mode 100644 index 000000000..74c97a055 Binary files /dev/null and b/images/office365/55.png differ diff --git a/images/office365/56.jpeg b/images/office365/56.jpeg new file mode 100644 index 000000000..41ed76d05 Binary files /dev/null and b/images/office365/56.jpeg differ diff --git a/images/office365/57.png b/images/office365/57.png new file mode 100644 index 000000000..c605b875a Binary files /dev/null and b/images/office365/57.png differ diff --git a/images/office365/58.png b/images/office365/58.png new file mode 100644 index 000000000..707ef49c3 Binary files /dev/null and b/images/office365/58.png differ diff --git a/images/office365/59.jpeg b/images/office365/59.jpeg new file mode 100644 index 000000000..ad06cfe28 Binary files /dev/null and b/images/office365/59.jpeg differ diff --git a/images/office365/6.jpeg b/images/office365/6.jpeg new file mode 100644 index 000000000..35bb2ed6a Binary files /dev/null and b/images/office365/6.jpeg differ diff --git a/images/office365/60.png b/images/office365/60.png new file mode 100644 index 000000000..508c115eb Binary files /dev/null and b/images/office365/60.png differ diff --git a/images/office365/61.png b/images/office365/61.png new file mode 100644 index 000000000..ad226c188 Binary files /dev/null and b/images/office365/61.png differ diff --git a/images/office365/62.png b/images/office365/62.png new file mode 100644 index 000000000..6808fda26 Binary files /dev/null and b/images/office365/62.png differ diff --git a/images/office365/63.jpeg b/images/office365/63.jpeg new file mode 100644 index 000000000..fe355657d Binary files /dev/null and b/images/office365/63.jpeg differ diff --git a/images/office365/64.jpeg b/images/office365/64.jpeg new file mode 100644 index 000000000..def7ac5d8 Binary files /dev/null and b/images/office365/64.jpeg differ diff --git a/images/office365/65.png b/images/office365/65.png new file mode 100644 index 000000000..591831639 Binary files /dev/null and b/images/office365/65.png differ diff --git a/images/office365/66.mp4 b/images/office365/66.mp4 new file mode 100644 index 000000000..bf8058dee Binary files /dev/null and b/images/office365/66.mp4 differ diff --git a/images/office365/67.jpeg b/images/office365/67.jpeg new file mode 100644 index 000000000..1f1122ac2 Binary files /dev/null and b/images/office365/67.jpeg differ diff --git a/images/office365/68.png b/images/office365/68.png new file mode 100644 index 000000000..6524924ce Binary files /dev/null and b/images/office365/68.png differ diff --git a/images/office365/7.png b/images/office365/7.png new file mode 100644 index 000000000..d0fce54a7 Binary files /dev/null and b/images/office365/7.png differ diff --git a/images/office365/8.png b/images/office365/8.png new file mode 100644 index 000000000..3d691b8bc Binary files /dev/null and b/images/office365/8.png differ diff --git a/images/office365/9.mp4 b/images/office365/9.mp4 new file mode 100644 index 000000000..67403cd0f Binary files /dev/null and b/images/office365/9.mp4 differ diff --git a/images/one-board.jpg b/images/one-board.jpg new file mode 100644 index 000000000..9549397a3 Binary files /dev/null and b/images/one-board.jpg differ diff --git a/images/part-four-flow.jpg b/images/part-four-flow.jpg new file mode 100644 index 000000000..344d554ea Binary files /dev/null and b/images/part-four-flow.jpg differ diff --git a/images/pasta-fasule.gif b/images/pasta-fasule.gif new file mode 100644 index 000000000..57335eb8a Binary files /dev/null and b/images/pasta-fasule.gif differ diff --git a/images/pasta-fasule.jpg b/images/pasta-fasule.jpg new file mode 100644 index 000000000..380fc4273 Binary files /dev/null and b/images/pasta-fasule.jpg differ diff --git a/images/pepper-1.jpg b/images/pepper-1.jpg new file mode 100644 index 000000000..c0fbee221 Binary files /dev/null and b/images/pepper-1.jpg differ diff --git a/images/pepper-2.jpg b/images/pepper-2.jpg new file mode 100644 index 000000000..fdc633299 Binary files /dev/null and b/images/pepper-2.jpg differ diff --git a/images/pepper-wide.jpg b/images/pepper-wide.jpg new file mode 100644 index 000000000..229d696fe Binary files /dev/null and b/images/pepper-wide.jpg differ diff --git a/images/personal-access-tokens.png b/images/personal-access-tokens.png new file mode 100644 index 000000000..fdede75e3 Binary files /dev/null and b/images/personal-access-tokens.png differ diff --git a/images/pulse.gif b/images/pulse.gif new file mode 100644 index 000000000..d06da0c10 Binary files /dev/null and b/images/pulse.gif differ diff --git a/images/puppy.gif b/images/puppy.gif new file mode 100644 index 000000000..a83ca94fb Binary files /dev/null and b/images/puppy.gif differ diff --git a/images/puppy.mp4 b/images/puppy.mp4 new file mode 100644 index 000000000..802a12419 Binary files /dev/null and b/images/puppy.mp4 differ diff --git a/images/puppy.webm b/images/puppy.webm new file mode 100644 index 000000000..751daae6c Binary files /dev/null and b/images/puppy.webm differ diff --git a/images/radar.jpg b/images/radar.jpg new file mode 100644 index 000000000..4fc0d59e3 Binary files /dev/null and b/images/radar.jpg differ diff --git a/images/reactotype.gif b/images/reactotype.gif new file mode 100644 index 000000000..a8fc6f1f6 Binary files /dev/null and b/images/reactotype.gif differ diff --git a/images/reactotype_screenshot.png b/images/reactotype_screenshot.png new file mode 100644 index 000000000..f0e3f0626 Binary files /dev/null and b/images/reactotype_screenshot.png differ diff --git a/images/rss.svg b/images/rss.svg new file mode 100644 index 000000000..e7abef12b --- /dev/null +++ b/images/rss.svg @@ -0,0 +1,59 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/images/run-nightwatch.png b/images/run-nightwatch.png new file mode 100644 index 000000000..546ba7bc4 Binary files /dev/null and b/images/run-nightwatch.png differ diff --git a/images/run2.0.jpg b/images/run2.0.jpg new file mode 100644 index 000000000..155d6d749 Binary files /dev/null and b/images/run2.0.jpg differ diff --git a/images/second-slice-4.jpg b/images/second-slice-4.jpg new file mode 100644 index 000000000..8c009f130 Binary files /dev/null and b/images/second-slice-4.jpg differ diff --git a/images/serverless-maintenance.png b/images/serverless-maintenance.png new file mode 100644 index 000000000..53ba2bf4c Binary files /dev/null and b/images/serverless-maintenance.png differ diff --git a/images/serverless.jpg b/images/serverless.jpg new file mode 100644 index 000000000..391bd7a84 Binary files /dev/null and b/images/serverless.jpg differ diff --git a/images/solar/feed-in-tariff-graph.png b/images/solar/feed-in-tariff-graph.png new file mode 100644 index 000000000..016f09990 Binary files /dev/null and b/images/solar/feed-in-tariff-graph.png differ diff --git a/images/solar/panels-east.png b/images/solar/panels-east.png new file mode 100644 index 000000000..3b8cd5b08 Binary files /dev/null and b/images/solar/panels-east.png differ diff --git a/images/solar/panels-south.png b/images/solar/panels-south.png new file mode 100644 index 000000000..8db8803bb Binary files /dev/null and b/images/solar/panels-south.png differ diff --git a/images/solar/production.png b/images/solar/production.png new file mode 100644 index 000000000..60aa4bf35 Binary files /dev/null and b/images/solar/production.png differ diff --git a/images/stack.png b/images/stack.png new file mode 100644 index 000000000..0bb5ad75d Binary files /dev/null and b/images/stack.png differ diff --git a/images/start-api.png b/images/start-api.png new file mode 100644 index 000000000..f1bef1504 Binary files /dev/null and b/images/start-api.png differ diff --git a/images/structs.png b/images/structs.png new file mode 100644 index 000000000..53da22832 Binary files /dev/null and b/images/structs.png differ diff --git a/images/structured-data-crawled.png b/images/structured-data-crawled.png new file mode 100644 index 000000000..03551c695 Binary files /dev/null and b/images/structured-data-crawled.png differ diff --git a/images/sunny-day.jpg b/images/sunny-day.jpg new file mode 100644 index 000000000..e95d9e720 Binary files /dev/null and b/images/sunny-day.jpg differ diff --git a/images/tada.png b/images/tada.png new file mode 100644 index 000000000..87d01a37a Binary files /dev/null and b/images/tada.png differ diff --git a/images/tag.svg b/images/tag.svg new file mode 100644 index 000000000..1567f261b --- /dev/null +++ b/images/tag.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/images/tech-debts/Hoarding_living_room.jpeg b/images/tech-debts/Hoarding_living_room.jpeg new file mode 100644 index 000000000..1b4ca8212 Binary files /dev/null and b/images/tech-debts/Hoarding_living_room.jpeg differ diff --git a/images/tech-debts/actual_different_styles_meme_by_zeurel-d38c306.png b/images/tech-debts/actual_different_styles_meme_by_zeurel-d38c306.png new file mode 100644 index 000000000..2d26acf35 Binary files /dev/null and b/images/tech-debts/actual_different_styles_meme_by_zeurel-d38c306.png differ diff --git a/images/tech-debts/bad_history.jpeg b/images/tech-debts/bad_history.jpeg new file mode 100644 index 000000000..2bad6d927 Binary files /dev/null and b/images/tech-debts/bad_history.jpeg differ diff --git a/images/tech-debts/credit-card-1104961_1280.webp b/images/tech-debts/credit-card-1104961_1280.webp new file mode 100644 index 000000000..d84accf53 Binary files /dev/null and b/images/tech-debts/credit-card-1104961_1280.webp differ diff --git a/images/tech-debts/tree.jpg b/images/tech-debts/tree.jpg new file mode 100644 index 000000000..a91dad18e Binary files /dev/null and b/images/tech-debts/tree.jpg differ diff --git a/images/testing-api-1.png b/images/testing-api-1.png new file mode 100644 index 000000000..3bce02f9d Binary files /dev/null and b/images/testing-api-1.png differ diff --git a/images/testing-api-result-2.png b/images/testing-api-result-2.png new file mode 100644 index 000000000..2fd05a318 Binary files /dev/null and b/images/testing-api-result-2.png differ diff --git a/images/the-chart-1.png b/images/the-chart-1.png new file mode 100644 index 000000000..29c553171 Binary files /dev/null and b/images/the-chart-1.png differ diff --git a/images/the-discussion.png b/images/the-discussion.png new file mode 100644 index 000000000..110f0698a Binary files /dev/null and b/images/the-discussion.png differ diff --git a/images/the-graph.png b/images/the-graph.png new file mode 100644 index 000000000..1359aea4a Binary files /dev/null and b/images/the-graph.png differ diff --git a/images/the-quadrants.png b/images/the-quadrants.png new file mode 100644 index 000000000..20eb8dcbf Binary files /dev/null and b/images/the-quadrants.png differ diff --git a/images/toot.jpeg b/images/toot.jpeg new file mode 100644 index 000000000..31ebb9bb6 Binary files /dev/null and b/images/toot.jpeg differ diff --git a/images/toot.png b/images/toot.png new file mode 100644 index 000000000..81e63214d Binary files /dev/null and b/images/toot.png differ diff --git a/images/travis.png b/images/travis.png new file mode 100644 index 000000000..606275ce7 Binary files /dev/null and b/images/travis.png differ diff --git a/images/tree.jpg b/images/tree.jpg new file mode 100644 index 000000000..e4fcaedeb Binary files /dev/null and b/images/tree.jpg differ diff --git a/images/twitter-32.png b/images/twitter-32.png new file mode 100644 index 000000000..62289815d Binary files /dev/null and b/images/twitter-32.png differ diff --git a/images/twitter-black-32.png b/images/twitter-black-32.png new file mode 100644 index 000000000..3be729d4b Binary files /dev/null and b/images/twitter-black-32.png differ diff --git a/images/typing.gif b/images/typing.gif new file mode 100644 index 000000000..0471f8372 Binary files /dev/null and b/images/typing.gif differ diff --git a/images/unhappiness.png b/images/unhappiness.png new file mode 100644 index 000000000..b96ec1503 Binary files /dev/null and b/images/unhappiness.png differ diff --git a/images/votes.png b/images/votes.png new file mode 100644 index 000000000..9b98dee83 Binary files /dev/null and b/images/votes.png differ diff --git a/images/weeknotes-autoload-graph.png b/images/weeknotes-autoload-graph.png new file mode 100644 index 000000000..c71f01f97 Binary files /dev/null and b/images/weeknotes-autoload-graph.png differ diff --git a/images/weeknotes.png b/images/weeknotes.png new file mode 100644 index 000000000..7d0a3bdb3 Binary files /dev/null and b/images/weeknotes.png differ diff --git a/images/yarn-desc.png b/images/yarn-desc.png new file mode 100644 index 000000000..73713ce51 Binary files /dev/null and b/images/yarn-desc.png differ diff --git a/images/yarn-run.png b/images/yarn-run.png new file mode 100644 index 000000000..488b3da49 Binary files /dev/null and b/images/yarn-run.png differ diff --git a/images/zero-velocity.png b/images/zero-velocity.png new file mode 100644 index 000000000..78b9d72d2 Binary files /dev/null and b/images/zero-velocity.png differ diff --git a/index.html b/index.html new file mode 100644 index 000000000..16412c4e9 --- /dev/null +++ b/index.html @@ -0,0 +1,753 @@ + + + + + + + + + + + + + + + + + + + + + + Mindless Rambling Nonsense + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ +

Zucchini focaccia

+
+

This year we've grown way too many zucchinis.

+ +

the plants

+ +

One way I've been trying to use them up is putting them in dough.

+ +

The kids love it!

+ + + +

chopped garlic and zucchini +blitzed cooked zucchini +dough +ready for the oven +ready for the oven two +cooked-one +cooked-two

+ +
+
+
posted on: 24 Jul 2023
+
+ in: cooking +
+ + tag-icon + + recipe + + + + + tag-icon + + zucchini + + + + + tag-icon + + focaccia + + + + + tag-icon + + glut + + + +
+
+
+
+ +
+
+ +

Paul's Law

+
+

Everyone should have their own law… This is mine:

+ +

All new build tools are better than what came before. Until they are able to solve all of the problems of the thing they replaced and then they're at least as bad. A new tool will then replace them

+ + +
+
+
posted on: 08 Jun 2023
+
+ in: snark +
+ + tag-icon + + laws + + + + + tag-icon + + silliness + + + +
+
+
+
+ +
+
+ +

March 2023 Month Notes

+
+

Year Goals for 2023

+ +
    +
  • continue becoming a better engineer and team-mate
  • +
  • practice Italian every day
  • +
  • train at the gym at least twice a week every week
  • +
  • 8 leisurely cycle rides
  • +
  • visit Italy at least twice
  • +
+ +

How did March go?

+ + +
+
+
posted on: 02 Apr 2023
+ +
+
+ +
+
+ +

I saved 183 million dollars by not moving to the Cloud

+
+

The average human brain has 2.5 petabytes of memory (source: random google result). 2.5 Petabytes is equal to 2,500,000 Gigabytes. Or 2,500 terabytes. The u-12tb1.112xlarge instance on AWS has 13TB of memory.

+ +

So, conclusively, 193 u-12tb1.112xlarge instances are equivalent to one brain. Or your brain could run in AWS for 15,305,472.00 USD per month. Therefore, I've saved 183 million dollars by not moving my brain to the cloud in the last year alone.

+ +

There seem to be a fashion for writing articles claiming that some company has saved hundreds of millions of dollars by not moving to the cloud.

+ +

I managed phyisical servers for more than a decade. For the UK Magistrates Courts and for the British Mountaineering Council. I was pretty good at it. But, I absolutely jumped at the chance to move to the cloud. Why?

+ + +
+
+
posted on: 19 Mar 2023
+
+ in: rant +
+ + tag-icon + + cloud + + + + + tag-icon + + experience + + + + + tag-icon + + context + + + +
+
+
+
+ +
+
+ +

Office 365 mega-thread

+
+

Between the end of 2019 and when I left the Co-op on Sep 18th 2021 I used Office 365 and never found a single redeeming feature.

+ +

Well, maybe one, a small set of Co-op employees had access to slack and g-suite in Co-op Digital. But in the rest of Co-op they were using installed (i.e. local only) old versions of Office (without video-conferencing and chat). For them, maybe Office 365 was an improvement - and certainly it made remote work during the pandemic possible.

+ +

But for me, it was a constant source of frustration.

+ +

I'm sure that there are great people working on Office with care and attention but I didn't experience that. It was like being haunted and losing your mind all in one go. I had a habit of tooting my frustrations. I'm aware of them having been submitted as evidence in one procurement process. I don't think they swung the decision.

+ +

If the tooter-web dissappeared they'd be the one thing I missed and so I've copied them here.

+ + +
+
+
posted on: 05 Mar 2023
+
+ in: rant + +
+
+
+ +
+
+ +

Feb 2023 Month Notes

+
+

Year Goals for 2023

+ +
    +
  • continue becoming a better engineer and team-mate
  • +
  • practice Italian every day
  • +
  • train at the gym at least twice a week every week
  • +
  • 8 leisurely cycle rides
  • +
  • visit Italy at least twice
  • +
+ +

How did February go?

+ + +
+
+
posted on: 01 Mar 2023
+ +
+
+ +
+
+ +

Jan 2023 Month Notes

+
+

Year Goals for 2023

+ +
    +
  • continue becoming a better engineer and team-mate
  • +
  • practice Italian every day
  • +
  • train at the gym at least twice a week every week
  • +
  • 8 leisurely cycle rides
  • +
  • visit Italy at least twice
  • +
+ +

How did January go?

+ + +
+
+
posted on: 03 Feb 2023
+ +
+
+ +
+
+ +

2022 Year Notes

+
+

Forgive me, for I have sinned, it's been 2 years since my last year notes 👼

+ +

I wrote year notes for 2019, and 2020. I've been super un-inspired to write for the last few years. Which is a shame - because it's a a great way to learn.

+ +

So much happened last year that it feels way longer than a year. So, discipline over motiviation - here are my 2022 year notes. Or at least as much as I can write while the house is empty of other people.

+ + +
+
+
posted on: 15 Jan 2023
+ +
+
+ +
+
+ +

Pasta e fasule

+
+

Pasta e Fasule, as made by my Neapolitan Nonna Luisa

+ +

Pasta e Fasule is Neapolitan for Pasta e Fagiole which is Italian for Pasta and Beans

+ +

Pasta and bean soup reminds me of being looked after when recovering from an illness as a kid. I like it so thick the spoon will stand up.

+ + + +

You can use odds and ends of pasta or break spaghetti in. When my dad was little he'd be sent to the pasta shop to get their broken odds and ends. They were cheaper.

+ +

steps in making pasta fasule as a gif

+ +
+
+
posted on: 08 Jan 2023
+
+ in: cooking +
+ + tag-icon + + recipe + + + + + tag-icon + + pasta + + + +
+
+
+
+ +
+
+ +

Pizza dough

+
+

It turns out that Neapolitan pizza dough is a strictly described thing. The UK version of the EU rules are here. Those instructions use 1.8 kilograms of flour and 1 litre of water. But (if you're not going to a wholesaler) flour comes in 1 kilogram bags. Ingredients have been adjusted to 1kg for this recipe.

+ +

I sometimes use a biga starter. Instructions for that are here. But it takes more work, time, and nuance.

+ +

This recipe can be completed in a single day and makes a very consistently tasty dough

+ +

(Yes! That much salt)

+ +

the dough balls resting on a wooden surface

+ +
+
+
posted on: 04 Sep 2022
+
+ in: cooking +
+ + tag-icon + + recipe + + + + + tag-icon + + salad + + + +
+
+
+
+ +
+ + « Prev + + + + + + 1 + + + + + + 2 + + + + + + 3 + + + + + + 4 + + + + + + 5 + + + + + + 6 + + + + + + 7 + + + + + + 8 + + + + + + + Next » + + +
+ + +
+ + + + + + + + diff --git a/keybase.txt b/keybase.txt new file mode 100644 index 000000000..a2b786506 --- /dev/null +++ b/keybase.txt @@ -0,0 +1,75 @@ +================================================================== +https://keybase.io/pauldambra +-------------------------------------------------------------------- + +I hereby claim: + + * I am an admin of https://pauldambra.dev + * I am pauldambra (https://keybase.io/pauldambra) on keybase. + * I have a public key with fingerprint 0D4B 9EA7 24DA 468E C242 A843 56C8 5550 9D09 8C8A + +To do so, I am signing this object: + +{ + "body": { + "key": { + "eldest_kid": "01201847a7911fb7a5af1e26e7f33c633cc9050e1617964b7a2b32176b41c73f113e0a", + "fingerprint": "0d4b9ea724da468ec242a84356c855509d098c8a", + "host": "keybase.io", + "key_id": "56c855509d098c8a", + "kid": "0101a862800bcf0a9be0a37e0067fab5e2fc02dc92c30751580c87b0c4f1dcb204f00a", + "uid": "1d159232b9d2ed2f1b0722d6831da619", + "username": "pauldambra" + }, + "service": { + "hostname": "pauldambra.dev", + "protocol": "https:" + }, + "type": "web_service_binding", + "version": 1 + }, + "ctime": 1568281978, + "expire_in": 157680000, + "prev": "5193adb477fcc0f8e2dcba8604815345ebccdd83f4e222a481264b0fcdda1917", + "seqno": 9, + "tag": "signature" +} + +which yields the signature: + +-----BEGIN PGP MESSAGE----- +Version: Keybase OpenPGP v2.1.3 +Comment: https://keybase.io/crypto + +yMNuAnicbVJrUFVVGAUCSgIFAoc7ochhIJRHe5/3uZoiRCBOZYWAgl7PYx84gPde +7j0XeQyGaOEgw1jmNLwcAsosNGhCKqoRhhGIdFDAHkoCM6GMwJAwMpBm5zD5q37s +2bO/vdb61rf27vF6ysnDOaVv9676VZUVzgNdtQ6nfUX+RcWYYJEKMWMxloNWNpQr +IbtqylEkzIgBiAPIkgzPcBDKAsNTvAwRTiNGJgiR1pbIAQogSEOGo0kNgAsEDhla +IKHIEDKEBAI8FonJijkT2aw2xazqshIpcIhncFLiSZpFIk7iPEsSFC2yFEUBTgIc +K7I6Mcti1xmaOYG3o2jFotW0g2nF3v/gn/gGkGdpnAVAEGXAc4Jmg2AQADQj8wKF +cFkEuCRyuEgAhoIUC0SWEYBIylASBRyQMljx7ViRgxKkOJzABU7CkYTLUAAMjks0 +S0CJpyGnA+3IZuYPIg1t5R25En9QsPFYSSSm1fMVEenR6rP8BxMtoXyNb7VZVIto +ydXuslTVajfqXLXQqoMPIcH0r4xJUMySlqXGyEc2u2IxY0aoIUVV0XUhRbM4CzmG +jcRQgVWxIZOiIyiG1qIAQO+jtdOSgxzBSwLJMLIoAplFWhiClhggWUgRJIUEUZQk +lpBJhOM4r1Vx7XmBrBV5yEEG0wfLM1swI6fZ5DM1SbuSaeZVhw1hJd1dGa5Ozh5O +7m4u+h9z8ljl/eTnPapd81gN8btaERWTJgTd7XEUDyV1YJTh1966yeZg4pPFl1KS +XxFaQ6zj96SRs0EzBcZ7G12o9oKXdx29gF23Hvtw7524pua1GQafiZ3fbvtxb9T+ +9afPLKy/6L6nN7j60RvetuKE6vsNMeGhL0bURF0yv7up8Zf4op314xGxJek3e0DQ +kZjP25u7GWXDRIPrluSha2k7DvsYspzzk2u7Xn+mZOZ+eukB3x4lsP/o4zSvhCtf +52VP1K+73HQhNbHb9wO/iZqbSYOGFGNrxgNPQ8aDqdTnA78sVqeXRTmLKJM+q4w1 +3l5qOLFmW5zzqYEfcjqcv+v8qT3w3MngxpKw38vCVk/fiAiInm4Maa+Y6/q+NOJP +9viJ2fnFjcfHyzf95tfWsa8zcPKju7HP9Rs6Ww3g/YdDVdRCv2fdW4vnZ1vedtma +UG7JktbGe8WHzfyV3cxUnS29tjCfnPkxXVU9YWnafP6rGz5T22caRg487XWLaUod +r+PG6JYxfnj/ybmg9i3hZ8KvXFw4Xd/GXt0auiG+kpmiva8X/fHqe5fJxS/KFH9i +sJHuu52YCFb7xr2ZVxngGH1tMvSF0cJn3fo/bVoKDnnn8I655Rr/v2vK5i5ljyy1 +insCkjarkXduDbq6DceOeqjnTL0JLbOZkvcpt3kwkPjwyKFl9zFqXWKbienzTIv+ +pnxmd/rPjvHtOcPMP9ZvvGY= +=dHZ0 +-----END PGP MESSAGE----- + +And finally, I am proving ownership of this host by posting or +appending to this document. + +View my publicly-auditable identity here: https://keybase.io/pauldambra + +================================================================== diff --git a/kids-games.html b/kids-games.html new file mode 100644 index 000000000..21080d188 --- /dev/null +++ b/kids-games.html @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + + + + + Mindless Rambling Nonsense - kids games + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Games I've made for my kids over the years

+

It turns out they are motivated by being rewarded with a random cat gif

+ + +
+ +
+

namey-numbers

+ +
+
+ gif of the game: namey-numbers +
+
+ +
+

clocky-mc-face

+ +
+
+ gif of the game: clocky-mc-face +
+
+ +
+

get-to-1000

+ +
+
+ gif of the game: get-to-1000 +
+
+ +
+

round-it

+ +
+
+ gif of the game: round-it +
+
+ +
+

readerer

+ +
+
+ gif of the game: readerer +
+
+ +
+

cry-me-a-colour

+ +
+
+ gif of the game: cry-me-a-colour +
+
+ +
+ +
+ + + + + + + + diff --git a/kill-if-with-objects.html b/kill-if-with-objects.html new file mode 100644 index 000000000..c490b1c1e --- /dev/null +++ b/kill-if-with-objects.html @@ -0,0 +1,525 @@ + + + + + + + + + + + + + + + + + + + + + + Remove If With Objects + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Wed Sep 07 2016
+

Remove If With Objects

+
+ +
+ +
+

Today I had super-fun spotting the opportunity for a refactoring and figuring out how to apply it. I wanted to think it through while it was fresh in my mind to try to cement any learning opportunity

+ +

The refactoring in question is "Replace Conditional Dispatcher with Command".

+ +

Quoting that source the opportunity for this refactoring is when: +

+
+

Conditional logic is used to dispatch requests and execute actions.

+
+ +

And the solution is: +

+
+

Create a Command for each action. Store the Commands in a collection and +replace the conditional logic with code to fetch and execute Commands.

+
+ +

It's one of those subtle changes that has real power to tidy up and add to the expressiveness of your code.

+ +

If (pun intended) you aren't familiar with it I'd definitely recommend trying it on for size by looking for an opportunity to apply it in your systems.

+ + + +

Some Context

+ +

In this instance we have a seam in our system that will over time become an anti-corruption (or defuckulation) layer. However, we're not building the full ACL as we'd have to solve lots of the other problems in the system first to know what should go into it.

+ +

When one system "publishes" some data this class will be responsible for applying any translation and saving it in a second.

+ +

The initial implementation looked like this

+ +
  public class ThingsPublisher : MagicSystemHook
+  {
+    private readonly IThingRepository _thingRepository;
+
+    public ThingsPublisher(IThingRepository thingRepository)
+    {
+      _thingRepository = thingRepository;
+    }
+
+    public override void OnPublish(IEnumerable<Thing> publishedThings)
+    {
+      foreach (var thing in publishedThings
+                            .Where(pt => pt.Type == "WhatWeDidFirst"))
+      {
+        var convertedThing = thing.Convert();
+        _thingRepository.Upsert(convertedThing);
+      }
+    }
+  }
+
+ +

Then over a few weeks we added a few more types that we needed to handle and uncovered some more detail in the requirements and ended up with something more like this

+ +
  public class ThingsPublisher : MagicSystemHook
+  {
+    private readonly IThingRepository _thingRepository;
+
+    private static readonly string[] _typesWeCareAbout =
+    {
+      "WhatWeDidFirst",
+      "WhatWeDidSecond",
+      "ATypeThatComesAlongWithWhatWeDidSecond",
+      "AModificationToWhatWeDidSecond",
+      "WhatWeDidThird"
+    };
+
+    private readonly KeyValuePair<string, string>[] _replacements =
+    {
+      new KeyValuePair<string, string>(@"/some-pattern/", "/its-pair/"),
+      new KeyValuePair<string, string>(@"/another-pattern/", "/and-its-pair/"),
+      new KeyValuePair<string, string>(@"/fiddly-detailed-pattern/", "/with-its-pair/"),
+    };
+
+    public ThingsPublisher(IThingRepository thingRepository)
+    {
+      _thingRepository = thingRepository;
+    }
+
+    public override void OnPublish(IEnumerable<Thing> publishedThings)
+    {
+      foreach (var thing in publishedThings.Where(WeCareAboutIt))
+      {
+        var convertedThing = ConvertThing(thing);
+
+        if (thing.Replaceable.StartsWith("one-expectation") || thing.Replaceable.StartsWith("another-expectation"))
+        {
+          _thingRepository.Upsert(convertedThing);
+        }
+      }
+    }
+
+    private ConvertedThing ConvertThing(Thing thing)
+    {
+      // omitted for brevity
+      // uses _replacements
+      return new ConvertedThing();
+    }
+
+    private static bool WeCareAboutIt(Thing t) => _typesWeCareAbout.Contains(t.Type);
+  }
+
+ +

At this point

+ +

It's worth being clear (and not only because I was partially responsible for it) that I'm not saying that this code is wrong.

+ +

We were learning about and from the system as we went so trying to write the "right" code would have definitely been wasteful previously.

+ +

When we hit this class to modify it today, however, there were a few signals that set off my spidey-senses: +

+
    +
  • we were making the fourth change
  • +
  • it didn't pass the squint test
  • +
  • say what you see +
      +
    • when we were talking about it neither my colleague nor I could clearly express what we thought it did
    • +
    • when we were expressing what it did we were talking about things implicit in the code not things explicit
    • +
    +
  • +
+ +

Fourth Change

+ +

I like to have several examples within a system before I start to look for an abstraction. Or put another way "A little duplication is better than the wrong abstraction".

+ +

The Squint Test

+ +

Sandi Metz proposed the squint test (see this talk) as a quick way to see if the shape of your code or the grouping of colours in your editor suggests any problems with your code. Amusingly there's a package for Atom to save you having to actually squint.

+ +

Say what you see

+ +

If you are talking about the code and you aren't using the words on the screen. Or if you can't succinctly explain what the conditionals are. Then you should be looking at whether there's information in your brain or elsewhere in the system that would help clarify what is happening. It's really easy to not be aware of what you have to know to understand some code - it's why i <3 code reviews.

+ +

The Refactored Code

+ +

As we talked about it we both realised that what we had was difficult to express because we'd accidentally discovered a new concept. Something that was unconsciously in our brains when we wrote it and hadn't made its way into the computer.

+ +
  public class ThingsPublisher : MagicSystemHook
+  {
+    private readonly IThingRepository _thingRepository;
+
+    private interface IMightSaveThings
+    {
+      void MaybeSave(ConvertedThing convertedThing, IThingRepository thingRepository);
+    }
+
+    private class SaveUnchangedThing : IMightSaveThings
+    {
+      public void MaybeSave(ConvertedThing convertedThing, IThingRepository thingRepository)
+      {
+        thingRepository.Upsert(convertedThing);
+      }
+    }
+
+    private class FilterAmendAndSaveThings : IMightSaveThings
+    {
+      private readonly Regex _thirdPartyPattern;
+      private readonly string _replacementPattern;
+
+      public FilterAmendAndSaveThings(string thirdPartyPattern, string replacementPattern)
+      {
+        _thirdPartyPattern = new Regex(thirdPartyPattern);
+        _replacementPattern = replacementPattern;
+      }
+
+      public void MaybeSave(ConvertedThing convertedThing, IThingRepository thingRepository)
+      {
+        if (!_thirdPartyPattern.IsMatch(convertedThing.Replaceable)) return;
+
+        convertedThing.Replaceable = _thirdPartyPattern.Replace(convertedThing.Replaceable, _replacementPattern);
+        thingRepository.Upsert(convertedThing);
+      }
+    }
+
+    private static readonly Dictionary<string, IMightSaveThings> _typeRules = new Dictionary<string, IMightSaveThings>
+    {
+      {"WhatWeDidFirst", new SaveUnchangedThing()},
+      {"WhatWeDidSecond", new FilterAmendAndSaveThings(@"/some-pattern/", "/its-pair/")},
+      {"ATypeThatComesAlongWithWhatWeDidSecond",new FilterAmendAndSaveThings(@"/another-pattern/", "/and-its-pair/")},
+      {"AModificationToWhatWeDidSecond",new FilterAmendAndSaveThings(@"/fiddly-detailed-pattern/", "/with-its-pair/")},
+      {"WhatWeDidThird", new SaveUnchangedThing()}
+    };
+
+    public ThingsPublisher(IThingRepository thingRepository)
+    {
+      _thingRepository = thingRepository;
+    }
+
+    public override void OnPublish(IEnumerable<Thing> publishedThings)
+    {
+      foreach (var thing in publishedThings)
+      {
+        if (!_typeRules.ContainsKey(thing.Type))
+          continue;
+
+        var typeRule = _typeRules[thing.Type];
+        var convertedThing = ConvertThing(thing);
+        typeRule.MaybeSave(convertedThing, _thingRepository);
+      }
+    }
+
+    private static ConvertedThing ConvertThing(Thing thing)
+    {
+      // omitted for brevity
+      //much simpler factory method
+      return new ConvertedThing();
+    }
+  }
+
+ +

So now

+ +

there are concepts explicitly in the code that were hidden or accidental beforehand

+ +
    +
  • we only have rules for some types and now those rules are listed alongside the types in the _typeRules dictionary
  • +
  • generally speaking the rule is that a thing might be saved. I.e. the rule is of type IMightSaveThings
  • +
  • for some types we save it without changing it +
      +
    • these we always save so maybe the name could be even better
    • +
    +
  • +
  • other types we don't always save, and when we do save them we change them first
  • +
+ +

The beauty in this code is that not only should it be easier to grok for somebody new to it (or us in a few weeks) now if we need to add additional rules that don't break the MaybeSave contract we can do that in one place. And changes that break the contract do so visibly and prompt us to think about what the change means for the code.

+ +

And we didn't only improve the code… we had a lot of fun (within context) realising it and fixing it.

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/lighthouse-budget.json b/lighthouse-budget.json new file mode 100644 index 000000000..d5d03b966 --- /dev/null +++ b/lighthouse-budget.json @@ -0,0 +1,15 @@ +[ + { + "path": "/*", + "resourceSizes": [ + { + "resourceType": "document", + "budget": 18 + }, + { + "resourceType": "total", + "budget": 550 + } + ] + } +] diff --git a/manifest.json b/manifest.json new file mode 100755 index 000000000..f181cc08b --- /dev/null +++ b/manifest.json @@ -0,0 +1,51 @@ +{ + "name": "Mindless Rambling Nonsense", + "short_name": "MiRaNo", + "theme_color": "#ffffff", + "background_color": "#000000", + "display": "standalone", + "start_url": "./?utm_source=web_app_manifest", + "icons": [ + { + "src": "images/icons/icon-72x72.png", + "sizes": "72x72", + "type": "image/png" + }, + { + "src": "images/icons/icon-96x96.png", + "sizes": "96x96", + "type": "image/png" + }, + { + "src": "images/icons/icon-128x128.png", + "sizes": "128x128", + "type": "image/png" + }, + { + "src": "images/icons/icon-144x144.png", + "sizes": "144x144", + "type": "image/png" + }, + { + "src": "images/icons/icon-152x152.png", + "sizes": "152x152", + "type": "image/png" + }, + { + "src": "images/icons/icon-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "images/icons/icon-384x384.png", + "sizes": "384x384", + "type": "image/png" + }, + { + "src": "images/icons/icon-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "splash_pages": null +} \ No newline at end of file diff --git a/page2/index.html b/page2/index.html new file mode 100644 index 000000000..c06715163 --- /dev/null +++ b/page2/index.html @@ -0,0 +1,779 @@ + + + + + + + + + + + + + + + + + + + + + + Mindless Rambling Nonsense - page 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ +

Roast Pepper Salad

+
+

Be careful to have lots of good bread available to soak up the tasty oil when you're finished! 👩‍🍳👌

+ +

the pepper salad in a bowl +the pepper salad on friselle

+ +
+
+
posted on: 04 Sep 2022
+
+ in: cooking +
+ + tag-icon + + recipe + + + + + tag-icon + + salad + + + +
+
+
+
+ +
+
+ +

Five years of Solar Panels

+
+

We've had photovoltaic (PV) solar panels generating electricity on our roof for exactly 5 years. I've explained the impact a few times privately or on the tooter website. I'm writing it down here so that I don't have to re-remember all the details each time. And since electricity prices are in the news at the moment and it might be useful to some folks.

+ +

We had 14 panels installed on 25th August 2017. It cost £4,793.25 which included 5% VAT. Our house is south facing at the rear. They've generated 17.06MWh of electricity in the last five years.

+ +

That's 17,060Kwh or a little over 17,000 "units".

+ +

And that represents around 4 tonnes of CO2 "saved".

+ + +
+
+
posted on: 22 Aug 2022
+
+ in: energy +
+ + tag-icon + + solar + + + + + tag-icon + + energy + + + +
+
+
+
+ +
+
+ +

Are you a weather vane or a sign post

+
+

Tony Benn once said: "I have divided politicians into two categories: the Signposts and the Weathercocks. The Signpost says: 'This is the way we should go.' And you don't have to follow them but if you come back in ten years time the Signpost is still there. The Weathercock hasn’t got an opinion until they've looked at the polls, talked to the focus groups, discussed it with the +spin doctors."

+ +

I heard this quote recently and it has really struck me…

+ +

Having changed problem domain, work environment, stack, and programming language this last year I'm wondering what signpost I want to be.

+ + +
+
+
posted on: 17 Sep 2021
+ +
+
+ +
+
+ +

Advice given on ending four years at Co-op

+
+

I've finished at the Co-op after four years. I was feeling emotional and wrote some "wise words". I thought I'd record them here. In the future, when I'm reminiscing, they can transport me back to this feeling.

+ + +
+
+
posted on: 17 Sep 2021
+ +
+
+ +
+
+ +

Tech Debts

+
+

Here is something I had to write down at work. Something I've tried to say (with varying success) more than once. That I'm publishing here so I can refer back to it in future and in case it is useful for someone else

+ +

When I am thinking about this, I am thinking about three things

+ +

Two of the agile principles

+ +
    +
  • +
    +

    Continuous attention to technical excellence and good design enhances agility.

    +
    +
  • +
  • +
    +

    Simplicity–the art of maximising the amount of work not done–is essential.

    +
    +
  • +
+ +

And the XP Rule

+ + + + +
+
+
posted on: 21 Jul 2021
+
+ in: semantics +
+ + tag-icon + + meaning + + + + + tag-icon + + pedantry + + + +
+
+
+
+ +
+
+ +

2020 Year Notes

+
+

Since October 2017 I've been keeping week notes. I've found them a fantastic tool to track my focus and to remember to reflect on success (or lack of it). I didn't write them for most of 2020. When pandemic hit it seemed too self-centered. I wish I'd kept them up now.

+ +

I wrote year notes last year. Surprisingly that was the only blog post I wrote last year 😱

+ +

Here are mine for 2020… It feels egotistical… but it's intended to remind me to reflect. Hopefully it's useful to me in the future even if it isn't to anyone else.

+ + +
+
+
posted on: 03 Jan 2021
+ +
+
+ +
+
+ +

2019 Year Notes

+
+

Since October 2017 I've been keeping week notes. I've found them a fantastic tool to track my focus and to remember to reflect on success (or lack of it).

+ +

Some colleagues and some tooters have written year notes as 2019 ends. And here are mine… It feels egotistical… but hopefully it's useful to me in the future even if it isn't to anyone else.

+ + +
+
+
posted on: 07 Jan 2020
+ +
+
+ +
+
+ +

Serverless - Lessons learned

+
+

At the 2019 Manchester Java Unconference I attended a discussion on "Cloud Native Functions". It turned out nobody in the group had used "cloud native" but I've been working with teams using serviceful systems.

+ +

I have a bad habit of talking more than I should but, despite my best efforts, the group expressed interest in hearing what teams at Co-op Digital had learned in the last ten months or so of working with serviceful systems in AWS.

+ +

We defined some terms, covered some pitfalls and gotchas, some successes, and most of all our key learning: that once you can deploy one serviceful system into production you can move faster than you ever have before.

+ +

Let's spend a little while defining our terms…

+ + +
+
+
posted on: 30 Nov 2019
+ +
+
+ +
+
+ +

Serverless - Part Six - Making a view

+
+

Part One - describing event-driven and serverless systems

+ +

Part Two - Infrastructure as code walking skeleton

+ +

Part Three - SAM Local and the first event producer

+ +

Part Four - Making streams of events

+ +

Part Five - Making a read model

+ +

In part 5 the code was written to make sure that whenever a destination changes the recent destinations read model will update. Now that read model can be used to realise a view that a human can use. We'll add code to create a HTML view behind AWS cloudfront. This will demonstrate how event driven systems can be created by adding new code instead of changing existing code.

+ + +
+
+
posted on: 01 Jul 2018
+ +
+
+ +
+
+ +

Serverless - Part Five - Read Models

+
+

Part One - describing event-driven and serverless systems

+ +

Part Two - Infrastructure as code walking skeleton

+ +

Part Three - SAM Local and the first event producer

+ +

Part Four - Making streams of events

+ +

OK, four months since part four. I got a puppy and have written the code for this part of the series in 2 minute blocks after sleepless nights. Not a productive way to do things!

+ + + +

Getting ready to make some HTML

+ +

Now that the API lets clients propose destinations to the visit plannr the home page for the service can be built. It's going to show the most recently updated destinations.

+ +

In a CRUD SQL system the application would have been maintaining the most up-to-date state of each destination in SQL and you'd read them when the HTML is requested. But this application isn't storing the state of the destinations but the facts that it has been told about the destinations.

+ +
+

As an aside a lot of people don't realise that CRUD SQL stands for C an we R eally not U se SQL D atabases they may S eem familiar but all the ORM stuff is well over our Q uota for comp L icated dependencies.

+
+ +

In an event driven system applications subscribe to be notified when new events occur. They can create read models as the events arrive. Those read models are what the application uses to, erm, read data. So they're used in places many applications make SQL queries. Now this visit plannr application needs a read model for recently updated destinations.

+ + +
+
+
posted on: 10 Jun 2018
+ +
+
+ +
+ + + « Prev + + + + + + + 1 + + + + + + 2 + + + + + + 3 + + + + + + 4 + + + + + + 5 + + + + + + 6 + + + + + + 7 + + + + + + 8 + + + + + + + Next » + + +
+ + +
+ + + + + + + + diff --git a/page3/index.html b/page3/index.html new file mode 100644 index 000000000..e4fd375ae --- /dev/null +++ b/page3/index.html @@ -0,0 +1,871 @@ + + + + + + + + + + + + + + + + + + + + + + Mindless Rambling Nonsense - page 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ +

DRY - considered harmful

+
+

DRY or WET?

+ +

DRY, in software development, stands for Don't Repeat Yourself. This is often taken to mean remove any duplication of lines of code. See the anti-example in the wiki page comparing to WET code - which stands for Write Everything Twice. This reinforces the idea that this is about the amount you type.

+ +

Below we're going to look at what the impact of removing duplication of lines of code does to some software, hopefully demonstrate that it isn't desirable as an absolute rule, and show what the better way might be.

+ + +
+
+
posted on: 30 Mar 2018
+
+ in: programming +
+ + tag-icon + + dry + + + + + tag-icon + + solid + + + + + tag-icon + + refactoring + + + + + tag-icon + + kotlin + + + +
+
+
+
+ +
+
+ +

Serverless - Part Four

+
+

Part One - describing event-driven and serverless systems

+ +

Part Two - Infrastructure as code walking skeleton

+ +

Part Three - SAM Local and the first event producer

+ +

In this post we start to see how we can build a stream of events that lets us create state. We'll do this by adding an event subscrber that waits until a user proposes a destination to visit and validates the location they've provided.

+ +

the slice being built in this article

+ + +
+
+
posted on: 15 Mar 2018
+ +
+
+ +
+
+ +

Serverless - Part Three

+
+

Part One - describing event-driven and serverless systems

+ +

Part Two - Infrastructure as code walking skeleton

+ +

In this post we will look at how SAM local let's you develop locally and write the first lambda function. To take a ProposeDestination command and write a DestinationProposed event to the eventstream.

+ +

"SAM Local can be used to test functions locally, start a local API Gateway from a SAM template, validate a SAM template, and generate sample payloads for various event sources."

+ + +
+
+
posted on: 05 Feb 2018
+ +
+
+ +
+
+ +

Serverless - Part Two

+
+

After describing event-driven and serverless systems in part one it is time to write some code. Well, almost. The first task is a walking skeleton: some code that runs on production infrastructure to prove we could have a CI pipeline.

+ +

I think I'll roll my AWS credentials pretty frequently now - since I can't imagine I'll get through this series without leaking my keys somehow

+ +

¯\_(ツ)_/¯

+ +

Putting authentication and authorisation to one side, because the chunk is too big otherwise, this task is to write a command channel to allow editors to propose destinations on the visitplannr system.

+ +

This requires the set up of API Gateway, AWS Lambda, and DynamoDB infrastructure and showing some code running. But doesn't require DynamoDB table streams or more than one lambda.

+ +

That feels like a meaningful slice.

+ + +
+
+
posted on: 04 Feb 2018
+ +
+
+ +
+
+ +

Serverless - Part One

+
+

Anyone who knows me knows that I like to talk about Event-driven systems. And that I'm very excited about serverless systems in utility computing.

+ +

I started my career in I.T. having to order network cables, care about fuses, and plan storage and compute capacity. It was slow, frustrating, and if you got it wrong it could take (best case scenario!) days to correct.

+ +

Over a few articles I hope to communicate what serverless is, why you should find it exciting, and how to start using it.

+ +

Let's start by defining our terms…

+ + +
+
+
posted on: 03 Feb 2018
+ +
+
+ +
+
+ +

Is where we're going where we're going

+
+

Velocity is…

+ +

A way of measuring the progress being made by a software team. Not all teams use velocity. I've been on quite a few that do. So at least some teams still use it as a measure.

+ + +
+
+
posted on: 29 Jan 2018
+
+ in: agile +
+ + tag-icon + + physics + + + + + tag-icon + + software + + + + + tag-icon + + agile + + + + + tag-icon + + cargo-cult + + + + + tag-icon + + rant + + + +
+
+
+
+ +
+
+ +

Constructiphor

+
+

On Twitter I…

+ +

…made a toot-storm about using construction as a metaphor for software engineering.

+ +
+

I've never really got on with construction metaphors for software. The cost of mistakes and rework is high in construction

+
+ +

the toot itself

+ +

This isn't saying that Software isn't putting things together but rather I've seen people justify not 'being agile' by using construction metaphors.

+ + +
+
+
posted on: 15 Oct 2017
+
+ in: metaphors +
+ + tag-icon + + metaphors + + + + + tag-icon + + software + + + +
+
+
+
+ +
+
+ +

Testing Meaning in HTML!

+
+ + +

One of the benefits of generating a site as a static artefact (here using Jekyll but there are a gazillion tools) is that the finished product is a known quantity. Anything that's a known quantity can be tested!

+ +

A previous post in this series looked at testing the generated HTML for technical correctness… Things like if the HTML is well-formed or that links go to real destinations.

+ +

This post describes testing the meaning of the text in the generated HTML. Checking spelling, and keeping myself honest in my attempt to use more inclusive language.

+ + +
+
+
posted on: 17 Aug 2017
+ +
+
+ +
+
+ +

Retrosperiment

+
+

(originally posted on the code computerlove blog. At the now unreachable link: https://lean.codecomputerlove.com/a-retrosperiment/)

+ +

Experimenting with a "new" retro format

+ +

For our team's most recent retro we decided to try a new format to see how it affected our discussion. We thought we'd share it here in case it has value for other teams.

+ +

What is it?

+ +

A retrospective is a practice from XP described on the c2.wiki as

+ +
+

A practice which has an XP team asking itself, at the end of each iteration : What went well ? +What could be improved ? +What could we experiment with ?

+
+ +

We've recently had several discussions trying to focus on the real and perceived progress of our work and thought it would be beneficial to run the retro with a focus on the impact of our team's principles and practices. Specifically how they relate to delivery of value and speed of delivery.

+ + +
+
+
posted on: 06 Jul 2017
+
+ in: agile +
+ + tag-icon + + agile + + + + + tag-icon + + xp + + + + + tag-icon + + retrospective + + + +
+
+
+
+ +
+
+ +

Where we're going we don't need columns

+
+

A few years ago while waiting for a user group to start at the Manchester ThoughtWorks office I bothered a couple of the devs there about their board. That conversation, after a bit of fangling, led to my convincing the team I was on at the time to use a radar board to represent our backlog.

+ +

It allowed us to combine a fluid representation of the business's priorities with a physical representation of the cost of reorganising those priorities. But also, in a way you don't get with a columnar board, gave an immediate feedback mechanism when too much work had been proposed or accepted.

+ +

Apologies to the two ThoughtWorks devs if I misrepresent any of their good ideas as mine or my bad ideas as theirs.

+ + +
+
+
posted on: 24 Jun 2017
+ +
+
+ +
+ + + « Prev + + + + + + + 1 + + + + + + 2 + + + + + + 3 + + + + + + 4 + + + + + + 5 + + + + + + 6 + + + + + + 7 + + + + + + 8 + + + + + + + Next » + + +
+ + +
+ + + + + + + + diff --git a/page4/index.html b/page4/index.html new file mode 100644 index 000000000..41cd94b48 --- /dev/null +++ b/page4/index.html @@ -0,0 +1,898 @@ + + + + + + + + + + + + + + + + + + + + + + Mindless Rambling Nonsense - page 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ +

Big Pile of Soil

+
+

During Kevin Rutherford's guided discussion on clean code at Agile Manchester 2017 we talked briefly about whether there was a difference between 'cleaning code' and 'clean code'.

+ +

I suggested that I might expect to have to make code dirtier on the road to making it cleaner. Being of the opinion that sometimes you need to add duplication in order to see your way to removing it.

+ +

As I am a creature of bad habit I jumped immediately into tortuous metaphor.

+ + +
+
+
posted on: 14 May 2017
+ +
+
+ +
+
+ +

Ubictionary

+
+ + +

I've spent a great first day at Agile Manchester 2017. One of the slides at a talk from Anna Dick was the stand-out point of the day for me.

+ +

"Find a common language, don't rely on agile jargon"

+ + +
+
+
posted on: 10 May 2017
+
+ in: naming + +
+
+
+ +
+
+ +

Generating static AMP pages

+
+ + +

AMP or Accelerated Mobile Pages is a Google-backed project that allows you to use restricted HTML to delivery static content quickly. Since AMP HTML is restricted it isn't a fit for every site.

+ +

Since this blog is published as static HTML articles it is a good candidate for publishing an AMP version. An open source AMP jekyll plugin was amended to add AMP versions of pages.

+ +

The major discovery was that the validation tooling around AMP is awesome. Compare that to Facebook Instant Articles where there is almost no validation tooling (that I could discover at least)…

+ +

This didn't feel like a topic that justified several posts so to avoid taking too long this is a bit of a whistle-stop tour of adding AMP pages to this blog.

+ + +
+
+
posted on: 22 Mar 2017
+
+ in: AMP +
+ + tag-icon + + series + + + + + tag-icon + + blog + + + + + tag-icon + + recursion + + + + + tag-icon + + AMP + + + + + tag-icon + + jekyll + + + +
+
+
+
+ +
+
+ +

Testing Static HTML!

+
+ + +

One of the benefits of generating a site as a static artefact (here using Jekyll but there are a gazillion tools) is that the finished product is a known quantity. Anything that's a known quantity can be tested!

+ + +
+
+
posted on: 19 Mar 2017
+ +
+
+ +
+
+ +

Yarn!

+
+

Yarn is a new JS package manager that promises to be fast, secure, and reliable. My initial experience is that it is fast. I'm excited about making time to use it for real at work. Kudos to the developers!

+ + +
+
+
posted on: 18 Oct 2016
+
+ in: CI +
+ + tag-icon + + yarn + + + + + tag-icon + + npm + + + + + tag-icon + + js + + + + + tag-icon + + CI + + + +
+
+
+
+ +
+
+ +

Adding Structured Data to a Jekyll site

+
+ + +

Structured Data is a way of adding context to files served on the web so that computers (primarily but not only search engines) can respond to what your content means.

+ +

Google, for example, will alter and improve how your site appears in search results based on the context you give your data. And if your site is considered authoritative can use the data to build the knowledge cards it sits alongside other search results.

+ +

This blog is only authoritative for being unread but I've not worked with structured data and thought I'd investigate.

+ + +
+
+
posted on: 20 Sep 2016
+
+ in: Structured Data +
+ + tag-icon + + jekyll + + + + + tag-icon + + recursion + + + + + tag-icon + + seo + + + + + tag-icon + + json+ld + + + +
+
+
+
+ +
+
+ +

Using Travis CI to build a Jekyll site

+
+

<aside class=""mb-2 ml-4 border-l-2 border-l-sky-700 pl-1""> + <h1 class="text-base">This post is part of a series on improving this blog #recursion</h1> + <div class="flex flex-row"> + <div class="flex-grow"></div> + <div class="flex-grow content-end"> + Next Post + </div> + </div> +</aside>

+ +

I recently had a conversation where I said that I couldn't build an AMP version of my blog because I use Github Pages to build and serve it. Github don't allow any Jekyll plugins to run.

+ +

Later that day my subconscious prompted me to realise that, since Github pages will serve static HTML quite happily, I could use Travis CI to build a Github repository that held the source for the blog and push the static output to a second repository that Github would publish as is.

+ + +
+
+
posted on: 18 Sep 2016
+ +
+
+ +
+
+ +

Remove If With Objects

+
+

Today I had super-fun spotting the opportunity for a refactoring and figuring out how to apply it. I wanted to think it through while it was fresh in my mind to try to cement any learning opportunity

+ +

The refactoring in question is "Replace Conditional Dispatcher with Command".

+ +

Quoting that source the opportunity for this refactoring is when: +

+
+

Conditional logic is used to dispatch requests and execute actions.

+
+ +

And the solution is: +

+
+

Create a Command for each action. Store the Commands in a collection and +replace the conditional logic with code to fetch and execute Commands.

+
+ +

It's one of those subtle changes that has real power to tidy up and add to the expressiveness of your code.

+ +

If (pun intended) you aren't familiar with it I'd definitely recommend trying it on for size by looking for an opportunity to apply it in your systems.

+ + +
+
+
posted on: 07 Sep 2016
+ +
+
+ +
+
+ +

Powershell on Linux

+
+

MS have open-sourced powershell and made it work on many platforms. Kudos to them - I'm loving the "new MS".

+ +

I've never really got powershell. Although it's definitely an improvement on vbscript so I have used it when I've needed to automate windows.

+ +

But as a task approaches some ill-defined level of complexity I switch to C#, Ruby, or Node rather than writing a script. Not that those are the only options but I don't know Perl, or Python, or $yourFavouriteTool.

+ +

As a result I have barely written any Powershell on Windows and, as I've done more work on Linux over the last few years, I've also barely written any bash.

+ +

So, while I think it's a good thing that MS are opening up and releasing cross platform tools I was underwhelmed. But…

+ + +
+
+
posted on: 28 Aug 2016
+ +
+
+ +
+
+ +

Real vs. Software Engineering

+
+

I recently had some "fun".

+ +

Earlier this year we got a great deal on two ducks, three chickens, a coop, a run, and Gary the Rescue Cockerel

+ + +
+
+
posted on: 29 Nov 2015
+
+ in: agile +
+ + tag-icon + + agile + + + + + tag-icon + + bad-metaphors + + + +
+
+
+
+ +
+ + + « Prev + + + + + + + 1 + + + + + + 2 + + + + + + 3 + + + + + + 4 + + + + + + 5 + + + + + + 6 + + + + + + 7 + + + + + + 8 + + + + + + + Next » + + +
+ + +
+ + + + + + + + diff --git a/page5/index.html b/page5/index.html new file mode 100644 index 000000000..59c024918 --- /dev/null +++ b/page5/index.html @@ -0,0 +1,861 @@ + + + + + + + + + + + + + + + + + + + + + + Mindless Rambling Nonsense - page 5 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ +

Reactotype Part 3

+
+

At the end of the last post I realised I'd sacrificed some good practice in the rush to make it work (i.e. worked normally like all those other guilty software engineers everywhere everyday.)

+ + + +

So earlier today I played with the kids to tire them out enough that I could distract them with television and write some #holidaycode because I am a good(-ish) parent.

+ +

I managed to

+ +
    +
  • switch from using magic strings in the messagebus channel and topic identifiers
  • +
  • remove some duplication
  • +
  • and get some tests around ReactJS
  • +
+ + +
+
+
posted on: 20 Feb 2015
+
+ in: react +
+ + tag-icon + + learning + + + + + tag-icon + + react + + + + + tag-icon + + js + + + + + tag-icon + + series + + + +
+
+
+
+ +
+
+ +

Reactotype Part 2

+
+ + +

I posted about my impressions of working with React slowly building an HTML table and banging on about it. I ended that post with one of the more memorable cliff-hangers in recent time.

+ +
+

Sorting and Filtering the Table

+ +

That we will leave till part two… because I introduced a relatively artifical constraint that I didn't want the filtering control to be a part of the table.

+ +

Imagine that there will be many tables with the same filter. I don't want to bind the filter to any one table or insist that every table has it.

+ +

At first I expected that it would force me to understand React's components and how to compose them… instead I stumbled on something really cool #cliffhanger

+
+ +

Exciting! Right?

+ +

I want to add a filter control and I don't want it to be bound to a particular table so that it can be re-used.

+ + +
+
+
posted on: 19 Feb 2015
+
+ in: react +
+ + tag-icon + + learning + + + + + tag-icon + + react + + + + + tag-icon + + js + + + + + tag-icon + + series + + + +
+
+
+
+ +
+
+ +

Reactotype Part 1

+
+

part one because I've got a feeling this is a topic about which I'll be able to bang on.

+ +

React JS was made by Facebook to be the V in MVC. In other words, it only deals with the UI. It's sold as being fast - both for performance and development. A contentious part of React is that it mushes JS and HTML together… More specifically, you put HTML inside the JS and not vice versa.

+ + +
+
+
posted on: 17 Feb 2015
+
+ in: react +
+ + tag-icon + + learning + + + + + tag-icon + + react + + + + + tag-icon + + js + + + + + tag-icon + + series + + + +
+
+
+
+ +
+
+ +

Fun With Structs

+
+

We had a brief conversation at work the other day about extending a type to make our code clearer…

+ +
public class MeaningfulName : MathsName
+{
+    public MeaningfulName(double w, double x, double y, double z) : base(w, x, y, z)
+    {
+    }
+}
+
+ + +
+
+
posted on: 01 Feb 2015
+ +
+
+ +
+
+ +

Happy Numbers

+
+

I love C# but while we're trying to beat our deployment process into submission at work I'm only really writing Ruby and Powershell. So when a few, different articles about the Happy Numbers kata turned up on my twitter feed and I found myself with a large whisky and a sleeping family I thought I'd have a go.

+ +

The Happy Numbers kata is defined as

+ +
+

Choose a two-digit number (eg. 23), square each digit and add them together. +Keep repeating this until you reach 1 +or the cycle carries on in a continuous loop.

+ +

If you reach 1 then the number you started with is a “happy number”.

+ +

Can you find all the happy numbers between 1 and 100?

+
+ + +
+
+
posted on: 22 Nov 2014
+
+ in: kata +
+ + tag-icon + + c# + + + + + tag-icon + + kata + + + + + tag-icon + + tdd + + + + + tag-icon + + learning + + + +
+
+
+
+ +
+
+ +

Transforming web.config values with Rake

+
+

I've really been enjoying using Albacore Rake instead of MSBuild at work. It's enabled us to get everyone involved (because ugh, msbuild xml) and to improve our CI/CD pipeline.

+ +

Today we were talking about reducing the number of build configurations we have… which we only have in order to support config transforms.

+ + +
+
+
posted on: 06 Nov 2014
+
+ in: continuous-integration +
+ + tag-icon + + rake + + + + + tag-icon + + CI + + + + + tag-icon + + ruby + + + +
+
+
+
+ +
+
+ +

Static Factory Methods FTW

+
+

It is relatively common to find (or write) a line of code like this

+ +
	var thingy = new Thingy(_someDependency, false);
+
+ +

Reading this line a person can know this is initialising a Thingy which takes a dependency on something… and something else is false. + +I'm really lazy and easily distracted so I don't like to have to think about anything except the one task I'm trying to not get distracted from. Having to think about what it means that something is false provides an opportunity for me to get distracted.

+ + +
+
+
posted on: 13 Sep 2014
+ +
+
+ +
+
+ +

Websites != CMS Platform - Wrapping Up

+
+

This post is part of a series where I'm hoping to prove to myself that building a dynamic website with NodeJS is much more fun than using a CMS platform. See the first post for an explanation of why

+ +

The code can be found on GitHub

+ +

Previous Post + +I've been banging this drum into the internet's echo chamber for five months now and enough is enough!

+ + +
+
+
posted on: 03 Aug 2014
+
+ in: cms +
+ + tag-icon + + learning + + + + + tag-icon + + cms + + + + + tag-icon + + design + + + + + tag-icon + + web + + + + + tag-icon + + series + + + +
+
+
+
+ +
+
+ +

Websites != CMS Platform - Better Editable Affordance with JS for great good

+
+

This post is part of a series where I'm hoping to prove to myself that building a dynamic website with NodeJS is much more fun than using a CMS platform. See the first post for an explanation of why

+ +

The code can be found on GitHub

+ +

Previous Post

+ + + +

In the last post a better visual affordance that a page element is editable was added. But didn't solve the problem that notifications of success or failure were obtrusive and disconnected from the edited element.

+ +

pulsing affordance

+ + +
+
+
posted on: 30 Jul 2014
+
+ in: cms +
+ + tag-icon + + learning + + + + + tag-icon + + cms + + + + + tag-icon + + design + + + + + tag-icon + + web + + + + + tag-icon + + series + + + +
+
+
+
+ +
+
+ +

Websites != CMS Platform - Better Editable Affordance

+
+

This post is part of a series where I'm hoping to prove to myself that building a dynamic website with NodeJS is much more fun than using a CMS platform. See the first post for an explanation of why

+ +

The code can be found on GitHub

+ +

Previous Post

+ +

In the last post I wasn't happy with the visual affordance that a page element is editable.

+ +

editable sections for anonymous users

+ +

editable sections for anonymous users

+ + +
+
+
posted on: 20 Jul 2014
+
+ in: cms +
+ + tag-icon + + learning + + + + + tag-icon + + cms + + + + + tag-icon + + design + + + + + tag-icon + + web + + + + + tag-icon + + series + + + +
+
+
+
+ +
+ + + « Prev + + + + + + + 1 + + + + + + 2 + + + + + + 3 + + + + + + 4 + + + + + + 5 + + + + + + 6 + + + + + + 7 + + + + + + 8 + + + + + + + Next » + + +
+ + +
+ + + + + + + + diff --git a/page6/index.html b/page6/index.html new file mode 100644 index 000000000..edc43158e --- /dev/null +++ b/page6/index.html @@ -0,0 +1,1007 @@ + + + + + + + + + + + + + + + + + + + + + + Mindless Rambling Nonsense - page 6 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ +

Websites != CMS Platform - On Page Editing

+
+

This post is part of a series where I'm hoping to prove to myself that building a dynamic website with NodeJS is much more fun than using a CMS platform. See the first post for an explanation of why

+ +

The code can be found on GitHub

+ +

Previous Post

+ +

A.K.A. No More CMS-y Admin Section?

+ +

A traditional CMS framework or website has an admin section for logged in users. That section has a menu showing them which sections the user can edit and each section has a list of the pages they can edit and then the user can edit the text or upload images using a WYSIWYG editor.

+ +

Don't fix it if it aint broken but… but… HTML5 includes the contenteditable attribute which makes (the text of) almost any element editable.

+ +

If the admin section exists (in large part) to allow editing of content and editing of content can be completed in the page itself could this replace the admin section?

+ + +
+
+
posted on: 11 Jun 2014
+
+ in: cms +
+ + tag-icon + + learning + + + + + tag-icon + + cms + + + + + tag-icon + + design + + + + + tag-icon + + web + + + + + tag-icon + + series + + + +
+
+
+
+ +
+
+ +

Websites != CMS Platform - Promises - part 2

+
+

This post is part of a series where I'm hoping to prove to myself that building a dynamic website with NodeJS is much more fun than using a CMS platform. See the first post for an explanation of why

+ +

The code can be found on GitHub

+ +

Previous Post

+ +

So, in the last post I worked on switching some callback code to using promises with Bluebird library but as I've not seen much promisified (definitely a word!) code I wasn't sure whether it was any good.

+ +

So I posted a question on the code review stackexchange asking for feedback.

+ + +
+
+
posted on: 01 Jun 2014
+
+ in: cms +
+ + tag-icon + + js + + + + + tag-icon + + learning + + + + + tag-icon + + cms + + + + + tag-icon + + design + + + + + tag-icon + + web + + + + + tag-icon + + series + + + + + tag-icon + + promises + + + +
+
+
+
+ +
+
+ +

Websites != CMS Platform - Promises

+
+

This post is part of a series where I'm hoping to prove to myself that building a dynamic website with NodeJS is much more fun than using a CMS platform. See the first post for an explanation of why

+ +

The code can be found on GitHub

+ +

Previous Post

+ +
+

A promise represents the eventual result of an asynchronous operation.

+
+ +

The basic idea is that you can swap in a promise where you would normally pass in a callback.

+ +

The primary interaction is that you call a method which returns a promise which will eventually return a result (it can immediately return the result if it's available) and you chain a call to .then() onto that method call.

+ +

The call to then is equivalent to passing in the callback function.

+ +

Clear as mud?

+ + +
+
+
posted on: 18 May 2014
+
+ in: cms +
+ + tag-icon + + learning + + + + + tag-icon + + cms + + + + + tag-icon + + design + + + + + tag-icon + + web + + + + + tag-icon + + series + + + + + tag-icon + + promises + + + + + tag-icon + + refactoring + + + +
+
+
+
+ +
+
+ +

Websites != CMS Platform - Logging in to the site

+
+

This post is part of a series where I'm hoping to prove to myself that building a dynamic website with NodeJS is much more fun than using a CMS platform. See the first post for an explanation of why

+ +

The code can be found on GitHub

+ +

Previous Post

+ +

This was the first part of the process which felt 'hard' so where I've felt the absence of a CMS platform but it's also only the second time I've ever implemented authentication using NodeJS. And still only boiled down to a few hours work.

+ + +
+
+
posted on: 27 Apr 2014
+
+ in: cms +
+ + tag-icon + + learning + + + + + tag-icon + + cms + + + + + tag-icon + + design + + + + + tag-icon + + web + + + + + tag-icon + + series + + + + + tag-icon + + mongodb + + + + + tag-icon + + nosql + + + + + tag-icon + + express + + + +
+
+
+
+ +
+
+ +

Websites != CMS Platform - Storing Data - Part 2

+
+

This post is part of a series where I'm hoping to prove to myself that building a dynamic website with NodeJS is much more fun than using a CMS platform. See the first post for an explanation of why

+ +

The code can be found on GitHub

+ +

Previous Post

+ +

The first step is always (or at least should be) to take a step back and decide what to actually do…

+ + +
+
+
posted on: 23 Apr 2014
+
+ in: cms +
+ + tag-icon + + learning + + + + + tag-icon + + cms + + + + + tag-icon + + design + + + + + tag-icon + + web + + + + + tag-icon + + series + + + + + tag-icon + + express + + + + + tag-icon + + mongodb + + + + + tag-icon + + nosql + + + +
+
+
+
+ +
+
+ +

Websites != CMS Platform - Storing Data - Part 1

+
+

This post is part of a series where I'm hoping to prove to myself that building a dynamic website with NodeJS is much more fun than using a CMS platform. See the first post for an explanation of why

+ +

Previous Post

+ +

After a day writing DDL for a project that has manual schema versioning against MS SQL and is going through a lot of changes I feel honour bound to write a post about storing data in the Omniclopse site.

+ + +
+
+
posted on: 12 Apr 2014
+
+ in: cms +
+ + tag-icon + + nosql + + + + + tag-icon + + learning + + + + + tag-icon + + mongodb + + + + + tag-icon + + cms + + + + + tag-icon + + design + + + + + tag-icon + + web + + + + + tag-icon + + series + + + +
+
+
+
+ +
+
+ +

A DTO by any other name would implement ISweetSmellEquality

+
+

I've been thinking about what people call the objects they pass around and whether they are the right names and why… and when… and I feel like the dog running behind the television to see where the onscreen dog went - on the verge of a paradigm shifting change in perspective but not quite getting it (and possibly a bit smelly)

+ + +
+
+
posted on: 01 Apr 2014
+ +
+
+ +
+
+ +

Testing With Browserstack and Selenium

+
+

This post is part of a series where I'm hoping to prove to myself that building a dynamic website with NodeJS is much more fun than using a CMS platform. See the first post for an explanation of why

+ +

Previous Post

+ +

Browserstack

+ +

I love Browserstack's awesome service. It allows you to test your websites on different browsers and operating systems. Helping reduce the need to have access to physical devices for testing and reproducing bugs.

+ +

Selenium WebDriver

+ +

BrowserStack allow automation using a Selenium web driver. You can access this with Python, Ruby, Java, C#, Perl, PHP, or Node.js. It is also possible to test publicly or locally available sites using BrowserStack.

+ + +
+
+
posted on: 25 Mar 2014
+
+ in: testing +
+ + tag-icon + + browserstack + + + + + tag-icon + + selenium + + + + + tag-icon + + testing + + + + + tag-icon + + js + + + +
+
+
+
+ +
+
+ +

Website != CMS Platform - Displaying pages - part 2

+
+

This post is part of a series where I'm hoping to prove to myself that building a dynamic website without a CMS is comparable to building one with a known CMS. See the first post for an explanation of why

+ +

Previous Post

+ + + +

In his awesome book, "Don't Make Me Think" (shameless affiliate link), Steve Krug drives home the message that time spent figuring out how your site is supposed to work is not time spent deciding to engage with your site. So, we're not going to do any ground-breaking design work for this company web page.

+ + +
+
+
posted on: 23 Mar 2014
+
+ in: cms +
+ + tag-icon + + learning + + + + + tag-icon + + cms + + + + + tag-icon + + design + + + + + tag-icon + + web + + + + + tag-icon + + series + + + +
+
+
+
+ +
+
+ +

Websites != CMS Platform - Displaying pages

+
+

This post is part of a series where I'm hoping to prove to myself that building a dynamic website without a CMS is comparable to building one with a known CMS. See the first post for an explanation of why

+ +

Previous Post +Next Post

+ +

Setup

+ + + +

So, it's relatively easy to get an Hello World page displaying…

+ + +
+
+
posted on: 17 Mar 2014
+
+ in: cms +
+ + tag-icon + + learning + + + + + tag-icon + + cms + + + + + tag-icon + + design + + + + + tag-icon + + web + + + + + tag-icon + + series + + + +
+
+
+
+ +
+ + + « Prev + + + + + + + 1 + + + + + + 2 + + + + + + 3 + + + + + + 4 + + + + + + 5 + + + + + + 6 + + + + + + 7 + + + + + + 8 + + + + + + + Next » + + +
+ + +
+ + + + + + + + diff --git a/page7/index.html b/page7/index.html new file mode 100644 index 000000000..3a8f26564 --- /dev/null +++ b/page7/index.html @@ -0,0 +1,772 @@ + + + + + + + + + + + + + + + + + + + + + + Mindless Rambling Nonsense - page 7 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ +

Websites != CMS Platform

+
+

I was once complaining about having difficulty setting up a very slightly unusual feature in a Drupal site that was taking forever to achieve. The framework made so many assumptions about what I should do that it wouldn't let me do what I wanted to.

+ + + +

A freelancer commented that if he was quoting on a project that had a requirement that it use a given CMS he didn't quote any less than building from scratch. He had found it didn't make enough difference to the effort he'd spend…

+ +

This stuck with me and matches my experience so far. (yeah, yeah, confirmation bias. I know)

+ + +
+
+
posted on: 22 Feb 2014
+
+ in: cms +
+ + tag-icon + + learning + + + + + tag-icon + + cms + + + + + tag-icon + + design + + + + + tag-icon + + web + + + + + tag-icon + + series + + + +
+
+
+
+ +
+
+ +

Comparing MongoDb and TokuMX

+
+

TokuMX is an

+ +
+

"open source, high-performance distribution of MongoDB".

+
+ +

On a current project we're using MongoDB and, as the system is likely to scale fairly heavily, worrying (primarily) about storage. So, I picked up a task to compare MongoDB and TokuMX.

+ + +
+
+
posted on: 23 Jan 2014
+
+ in: nosql +
+ + tag-icon + + mongodb + + + + + tag-icon + + tokumx + + + + + tag-icon + + comparison + + + + + tag-icon + + benchmark + + + +
+
+
+
+ +
+
+ +

Astronomical Database Identfier

+
+

I dealt with an unusual requirement over the last few days. And wish I'd understood some of the more unusual ways that big numbers are handled in C#, Entity Framework, MS SQL and Oracle

+ + +
+
+
posted on: 22 Nov 2013
+
+ in: databases +
+ + tag-icon + + entity-framework + + + + + tag-icon + + oracle + + + + + tag-icon + + wat + + + + + tag-icon + + rant + + + +
+
+
+
+ +
+
+ +

Automagical search UX

+
+

So I'm building a page in a mobile app to find "things".

+ +

Some assumptions:

+ +
    +
  • If you're using the app you are already familiar with the "things"
  • +
  • You've clicked "Find Things" and so you're expecting, as a minimum, to type something into a box (to tell the app what things you want to find)
  • +
  • You're a busy person and you don't want to have to think
  • +
+ + +
+
+
posted on: 20 Mar 2013
+
+ in: ux +
+ + tag-icon + + design + + + + + tag-icon + + analysis + + + + + tag-icon + + search + + + +
+
+
+
+ +
+
+ +

Obligatory iOS6 maps post

+
+

For years now I've not bothered buying a satnav because maps on my iPhone has been good enough… sometimes a bit dodgy (once taking a route more fitted for a mountain bike) but generally serviceable.

+ +

Taking a trip from Manchester to Kettering this weekend with only my iPhone on iOS6 and the missus' on iOS5 was eye opening. Also, bleedin' awful… - 'drive around a roundabout twice in confusion' awful.

+ +

I really did give it a good go but this image sums up the difficulty faced using iOS6 maps.

+ + +
+
+
posted on: 23 Sep 2012
+
+ in: design +
+ + tag-icon + + iOS6 + + + +
+
+
+
+ +
+
+ +

Y U NO SELL DOWNLOADS HOLLYWOOD

+
+ + +

So it occurred to me that my kids might enjoy The Lion King (they like roaring). Our TV is really a computer and is hooked up to the internets allowing all kinds of iPlayer and similar streaming goodness.

+ +

I guess I'm not unusual in that when I want to find something I google it…

+ + +
+
+
posted on: 25 Jul 2012
+
+ in: rant +
+ + tag-icon + + netflix + + + + + tag-icon + + streaming + + + + + tag-icon + + hollywood + + + +
+
+
+
+ +
+
+ +

Is there really just an iPad market?

+
+

Disclaimer: I use and love an iPad (1). I've got an iPhone, mac mini, a MBP and an iMac. But I'm not an out and out fanboy - I'm a windows admin and nascent C# developer. I try to use Linux where it fits and find more places it fits all the time. And I've been developing an Android application.

+ +

TL;DR The transformer prime is a beautiful computer but it might be true there's an iPad market and not a tablet market.

+ +

UPDATE

+ +

And then today Google release Chrome for Ice Cream Sandwich. BEST. TABLET. BROWSER. EVAR.

+ + +
+
+
posted on: 07 Feb 2012
+
+ in: rant +
+ + tag-icon + + iOS + + + + + tag-icon + + android + + + +
+
+
+
+ +
+
+ +

Setting up an MVC3 website using built-in membership provider

+
+

Oh wait… this is awful. AWRUCHKA. Right dry heaving done with.

+ +

It's a good job so few websites want to authenticate users and collect data on them otherwise we'd constantly have to write the same code ove… what's that? Oh my! Everyone is going through this.

+ + +
+
+
posted on: 12 Jan 2012
+ +
+
+ +
+
+ +

An unusbscribey follow up

+
+

So recently I blogged a bloggy thing here about unsubscribe links.

+ +

I know a lot of people are of the opinion that an unsubscribe link should unsubscribe you and require no further action and that the whole idempotency thing is software design flim-flam and I was tempted to agree until I was introduced to the concept of pre-fetching…

+ + +
+
+
posted on: 20 Sep 2011
+
+ in: HTTP +
+ + tag-icon + + GET + + + + + tag-icon + + idempotence + + + + + tag-icon + + detail + + + + + tag-icon + + follow-up + + + +
+
+
+
+ +
+
+ +

How to design an unsubscribe link?!?

+
+

We send out mail to 70,000+ members of our organisation. In theory they know they're getting it cos they're advised when they join the organisation that we'll send the email… yes, I know that implicit opt-ins aren't best practice… I want to polish up our email unsubscribe flow since the amount of mail we send out is steadily climbing as we move from paper to email for more things.

+ + +
+
+
posted on: 05 Aug 2011
+
+ in: HTTP +
+ + tag-icon + + GET + + + + + tag-icon + + idempotence + + + + + tag-icon + + detail + + + +
+
+
+
+ +
+ + + « Prev + + + + + + + 1 + + + + + + 2 + + + + + + 3 + + + + + + 4 + + + + + + 5 + + + + + + 6 + + + + + + 7 + + + + + + 8 + + + + + + + Next » + + +
+ + +
+ + + + + + + + diff --git a/page8/index.html b/page8/index.html new file mode 100644 index 000000000..b9b942b05 --- /dev/null +++ b/page8/index.html @@ -0,0 +1,747 @@ + + + + + + + + + + + + + + + + + + + + + + Mindless Rambling Nonsense - page 8 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ +

Get with the program(ming)!

+
+ + +

Twice recently I've hit the same problem with two different mobile phone vendor's websites. Vodafone (displayed here) and 3. When I type a phone number I split it into three sections using white-space. "nnnn nnn nnnn" that's how I remember numbers. That's not uncommon I don't think…

+ + +
+
+
posted on: 10 Jun 2011
+
+ in: rant +
+ + tag-icon + + design + + + + + tag-icon + + ux + + + +
+
+
+
+ +
+
+ +

SSH without password

+
+ + +

I've resolved to learn more about linux and have been slowly boggling at how easy I find some tasks are in comparison to the MS world…

+ +

Recently I've been working on what was intended to be a small and straight-forward website that has rapidly grown to be a large behemoth that will take credit card payments.

+ + +
+
+
posted on: 11 Apr 2011
+
+ in: ssh +
+ + tag-icon + + linux + + + + + tag-icon + + windows + + + + + tag-icon + + git + + + + + tag-icon + + ssh + + + + + tag-icon + + learning + + + +
+
+
+
+ +
+
+ +

Refactor ==fun

+
+

I've been using JetBrains Resharper for a while after a recommendation along the lines of "I can't stand to write code without it now" and…

+ +

I can't stand to write code without it now!

+ + +
+
+
posted on: 20 Oct 2010
+ +
+
+ +
+
+ +

Odd, odd, odd login behaviour

+
+

I've got a mini 5101. A little HP netbook that I lurve. It runs Windows 7 and Ubuntu 10.04 with aplomb.

+ + +
+
+
posted on: 07 Aug 2010
+ +
+
+ +
+
+ +

There's more in them thar hills...

+
+

…than the Factory pattern.

+ +

So anyway I learn about design patterns and begin to use the factory pattern. And much like many other people I settle into a world where there are no other patterns. All is comfortable and fluffy and instantiated from calling code much as it was in days gone by.

+ + +
+
+
posted on: 08 May 2010
+ +
+
+ +
+
+ +

Quack Quack Says The Duck now with added threading

+
+

In a previous post I advertised an application I'd made for WinMo to entertain my toddler.

+ + +

Having watched her play with it and having been reminded to K.I.S.S. I've fixed a bug that highlighted the difference in expectations between myself and a toddler.

+ + +
+
+
posted on: 29 Mar 2010
+
+ in: windows-mobile +
+ + tag-icon + + parenting + + + + + tag-icon + + microsoft + + + + + tag-icon + + vb.net + + + + + tag-icon + + ux + + + +
+
+
+
+ +
+
+ +

c# Background Worker

+
+

I've been meaning to get around to writing a good tutorial on c# background workers. Mainly because I use them to separate the GUI from all the heavy lifting and I always forget how to update things.

+ +

In case I never get around to it. This is about the clearest introduction I've ever found. Well worth a read…

+ +
+
+
posted on: 01 Dec 2009
+
+ in: c# + +
+
+
+ +
+
+ +

Bing is not a search engine

+
+

Over the last two days I've been researching using Windows Deployment Services with BDD. I've got 4 workstations to build so I may as well investigate it right?

+ + +
+
+
posted on: 30 Oct 2009
+
+ in: rant +
+ + tag-icon + + bing + + + + + tag-icon + + rant + + + + + tag-icon + + search + + + + + tag-icon + + microsoft + + + +
+
+
+
+ +
+
+ +

Quack Quack says the Duck

+
+ +

I've released a piece of software that I made for my 18 month old daughter on Codeplex http://qqstd.codeplex.com/.

+ +

It's a small dotNet app for Windows Mobile that creates sound-image pairs by scanning a resource folder and then randomly displays one of the images. When the image is touched the sound associated with the image is played. + +I developed it to occupy my daughter and teach her animal noises but the app doesn't care what if finds so you could use pictures of family and friends and their names said out-loud; Vehicles and their engine noises or anything that enters your transom.

+ + +
+
+
posted on: 04 Oct 2009
+
+ in: windows-mobile +
+ + tag-icon + + vb.net + + + + + tag-icon + + code + + + + + tag-icon + + parenting + + + +
+
+
+
+ +
+
+ +

Anonymous methods when invoking in VB net

+
+

Well maybe not but you can get close in some circumstances.

+ + +
+
+
posted on: 28 May 2009
+
+ in: programming +
+ + tag-icon + + vb.net + + + + + tag-icon + + code + + + +
+
+
+
+ +
+ + + « Prev + + + + + + + 1 + + + + + + 2 + + + + + + 3 + + + + + + 4 + + + + + + 5 + + + + + + 6 + + + + + + 7 + + + + + + 8 + + + + + + Next » + +
+ + +
+ + + + + + + + diff --git a/powershell-on-linux.html b/powershell-on-linux.html new file mode 100644 index 000000000..832d040ba --- /dev/null +++ b/powershell-on-linux.html @@ -0,0 +1,666 @@ + + + + + + + + + + + + + + + + + + + + + + Powershell on Linux + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sun Aug 28 2016
+

Powershell on Linux

+
+ +
+ +
+

MS have open-sourced powershell and made it work on many platforms. Kudos to them - I'm loving the "new MS".

+ +

I've never really got powershell. Although it's definitely an improvement on vbscript so I have used it when I've needed to automate windows.

+ +

But as a task approaches some ill-defined level of complexity I switch to C#, Ruby, or Node rather than writing a script. Not that those are the only options but I don't know Perl, or Python, or $yourFavouriteTool.

+ +

As a result I have barely written any Powershell on Windows and, as I've done more work on Linux over the last few years, I've also barely written any bash.

+ +

So, while I think it's a good thing that MS are opening up and releasing cross platform tools I was underwhelmed. But…

+ + + +

…some colleagues were excited and people I think of as being *nix people too

+ +
+ + +
+ +

I thought I'd spend some time to compare the two.

+ +

The Test

+ +

I've been watching Gary Bernhardt's Destroy All Software screencasts (which I heartily recommend). I enjoyed his use of looping over git revisions to carry out tasks to show either some git tool or bash itself.

+ +

I came up with a fake scenario of having a Big-Data problem. The amount of data was suddenly, and unexpectedly increased as a result of human error. And that’s caused all our systems to explode (something I’ve seen happen too so not that fake). My task was to investigate and find out when the error occurred.

+ +

the initial commit log

+ +

If you read that commit log you might be able to guess where the error occurred. Let’s pretend that the log is more complex and not so useful.

+ +

Aside

+ +

If you like that Git log output run:

+ +
git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"
+
+ +

NB scroll to the right to copy that text it's really long.

+ +

And then use git lg to run it

+ +

Carrying on

+ +

This particular big data company use a venerable, and well-trusted database technology - a file. I decided on this approach

+ +
    +
  • get a list of commits
  • +
  • loop over them checking each one out
  • +
  • while checkout out count the lines in each file
  • +
  • compare each linecount to the last
  • +
  • break when the change in linecount breaches some arbitrary threshold
  • +
  • clean up
  • +
+ +

This turns out to be pretty trivial in both bash:

+ +
#! /bin/bash
+
+set -e
+
+LASTCOMMIT=0
+
+for commit in `git rev-list master --reverse`
+do
+	`git checkout $commit &> /dev/null`
+    lines=$(wc -l < big-data.txt)
+    linediff=`expr $lines - $LASTCOMMIT`
+    if [[ $linediff -gt 10 ]]; then
+    	echo "this is the bad commit $commit"
+    	break
+    fi
+
+    LASTCOMMIT=$lines
+done
+
+`git checkout master &> /dev/null`
+
+ +

and PowerShell:

+ +
$lastcommit = 0
+
+foreach ($commit in iex "git rev-list master --reverse")
+{ 
+    iex "git checkout $commit" *> $null
+
+    $lines=(iex "wc -l big-data.txt").Split(" ", [System.StringSplitOptions]::RemoveEmptyEntries)
+    $linediff = $lines[0] - $lastcommit
+    
+    if ($linediff -gt 10) 
+    {
+        write-host "this is the bad commit $($commit)"
+        break
+    }
+
+    $lastcommit = $lines[0]
+}
+
+iex "git checkout master" *> $null
+
+ +

There were a couple of differences because PowerShell insisted on evaluating the command strings. And it turns out ‘&’ and ‘<’ are "reserved for future use".

+ + +

Both were relatively easy to write, relatively easy to read, run correctly, and give the correct output.

+ +

More Complex Example

+ +

I downloaded the stack overflow vote archive. At time of writing this is a 10 Gigabyte xml file. I want to know how many of the votes are from 2010 and a breakdown of the types of votes in that year.

+ +

the stackoverflow votes file

+ +

Bash

+ +
#! /bin/bash
+
+set -e
+
+non_data_lines_count=3
+lines=$(( $(wc -l < $1) - $non_data_lines_count ))
+
+twenty_ten_regex="CreationDate=\"2010"
+vote_type_regex="VoteTypeId=\"([0-9]+)\""
+
+declare -a vote_types
+
+while read line ; do
+    if [[ $line =~ $vote_type_regex ]]; then 
+        vote_type_id="${BASH_REMATCH[1]}"
+        ((vote_types[vote_type_id]++))
+    fi
+done < <(grep $twenty_ten_regex $1)
+
+twenty_ten_count=0
+for k in "${!vote_types[@]}"
+do
+  twenty_ten_count=$(($twenty_ten_count + vote_types[$k]))
+  echo "VoteTypeId: $k had ${vote_types[$k]} votes"
+done
+
+echo "there were $lines total votes in the file"
+echo "there were $twenty_ten_count total votes in 2010"
+
+ +

This script:

+ +
    +
  • counts the lines in the file +
      +
    • subtracting three because there are three lines that aren't vote data in the file
    • +
    +
  • +
  • it also reads the lines in the file from 2010 one by one
  • +
  • then increments a count in an associative array if it can capture a VoteTypeId from the line
  • +
  • when finished it sums those VoteTypeIds to count the 2010 v0tes
  • +
  • finally it prints out some output
  • +
+ +

running time ./process-votes.sh ~/Downloads/Votes.xml gives the following:

+ +
 VoteTypeId: 1 had 467439 votes
+ VoteTypeId: 2 had 4198861 votes
+ VoteTypeId: 3 had 256759 votes
+ VoteTypeId: 4 had 200 votes
+ VoteTypeId: 5 had 380887 votes
+ VoteTypeId: 8 had 8632 votes
+ VoteTypeId: 9 had 8578 votes
+ VoteTypeId: 10 had 199344 votes
+ VoteTypeId: 11 had 13231 votes
+ VoteTypeId: 12 had 535 votes
+ VoteTypeId: 15 had 965 votes
+ there were 105301744 total votes in the file
+ there were 5535431 total votes in 2010
+ ./process-votes.sh ~/Downloads/Votes.xml  581.57s user 408.41s system 105% cpu 15:36.35 total
+
+ +

PowerShell

+ +
Param ([string] $filePath = $null)
+
+$reader = [System.IO.File]::OpenText($filePath)
+
+$voteTypes = @{}
+$lines=-3
+$twentyTenLines=0
+
+while($null -ne ($line = $reader.ReadLine())) 
+{
+    $lines++
+    if ($line -match "CreationDate=""2010")
+    {
+        $xml = [xml]"$line"
+        $twentyTenLines++
+        $voteTypes[$xml.row.VoteTypeId]++
+    }
+}
+
+foreach ($voteTypeCount in $voteTypes.GetEnumerator()) {
+    Write-Host "VoteTypeId: $($voteTypeCount.Name) had $($voteTypeCount.Value) votes"
+}
+
+write-host "there were $($lines) total votes in the file"
+write-host "there were $($twentyTenLines) total votes in 2010"
+
+ +

The process followed here is almost the same. The script:

+ +
    +
  • reads each line of the file one at a time +
      +
    • as it does so it counts them
    • +
    • starting from minus three because there are three lines that aren't vote data in the file
    • +
    +
  • +
  • it checks each line to see if it is from 2010 +
      +
    • if it is it parses that line as XML
    • +
    • increments a count of lines in 2010
    • +
    • and increments a count of the VoteTypeId
    • +
    +
  • +
  • finally it prints out some output
  • +
+ +

running time powershell ./process-votes.ps1 ~/Downloads/Votes.xml gives the output:

+ +
VoteTypeId: 8 had 8632 votes
+VoteTypeId: 4 had 200 votes
+VoteTypeId: 12 had 535 votes
+VoteTypeId: 1 had 467439 votes
+VoteTypeId: 10 had 199344 votes
+VoteTypeId: 3 had 256759 votes
+VoteTypeId: 9 had 8578 votes
+VoteTypeId: 11 had 13231 votes
+VoteTypeId: 2 had 4198861 votes
+VoteTypeId: 5 had 380887 votes
+VoteTypeId: 15 had 965 votes
+there were 105301745 total votes in the file
+there were 5535431 total votes in 2010
+powershell ./process-votes.ps1 ~/Downloads/Votes.xml  279.78s user 7.00s system 99% cpu 4:49.45 total
+
+ +

At first I parsed every line as xml so I could access the CreationDate but that took nearly ten times as long.

+ +

I also tried out the same approach of reading and counting every line instead of grepping only 2010 lines in bash. That slowed the bash implementation down even further.

+ +

Confirmation Bias!

+ +

Is my tendency to move away from scripts justified by these results? I know little Python so it seemed a fair comparison to try a Python version.

+ +
import sys
+import re
+
+class Counter(dict):
+    def __missing__(self, key):
+        return 0
+
+line_counter = 0
+vote_types = Counter()
+
+creation_date_regex = re.compile('CreationDate="2010')
+vote_type_regex = re.compile('VoteTypeId=\"(\d+)\"')
+
+with open(sys.argv[1], "r") as file:
+    for line in file:
+        line_counter += 1
+        if creation_date_regex.findall(line):
+            vote_type_id = vote_type_regex.findall(line)[0]
+            vote_types[vote_type_id] += 1
+
+for key, value in vote_types.iteritems():
+    print "VoteTypeId: %s had %s votes" % (key, value)
+
+print "there were %s total votes in the file" % line_counter
+print "there were %s total votes in 2010" % sum(vote_types.values())
+
+ +

Running this was much faster

+ +
VoteTypeId: 11 had 13231 votes
+VoteTypeId: 10 had 199344 votes
+VoteTypeId: 12 had 535 votes
+VoteTypeId: 15 had 965 votes
+VoteTypeId: 1 had 467439 votes
+VoteTypeId: 3 had 256759 votes
+VoteTypeId: 2 had 4198861 votes
+VoteTypeId: 5 had 380887 votes
+VoteTypeId: 4 had 200 votes
+VoteTypeId: 9 had 8578 votes
+VoteTypeId: 8 had 8632 votes
+there were 105301745 total votes in the file
+there were 5535431 total votes in 2010
+python ./stack/process-votes.py ~/Downloads/Votes.xml  59.06s user 4.65s system 98% cpu 1:04.41 total
+
+
+ +

This supports switching away from scripting languages for complex tasks.

+ +

Aside

+ +

If you're observant you'll have noticed that the bash implementation reports one fewer line than the others. It turns out this is because the Votes.xml file doesn't have a newline character at the end.

+ +

tailing the votes file

+ +

The percent symbol at the end of this output demonstrates that.

+ +

The POSIX definition states that a file ends with an empty line (see this awesome StackOverflow answer for a breakdown) and since the Votes.xml file is non-compliant wc counts it incorrectly.

+ +

So What?

+ +

You can see the code I ran on github. Both powershell and bash are pretty new for me (for anything beyond trivial tasks). Also, I can count on the fingers of one hand the number of times I've had to directly process a large file like this so I might be doing something naive. As a result I'd really welcome feedback!

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
LanguageRun Time (s)Lines per Second
bash581.57181
PowerShell279.78376,373
Python59.061,784,775
+ +

PowerShell runs stably on Mac OS. And while it is slower than Python (for the given task) it runs significantly faster than bash.

+ +

This was actually pretty good fun. Both bash and powershell are more complex than I imagined but not as hard to write as I thought. That said I really missed being able to write tests - especially when the runs to prove the scripts against the full dataset took tens of minutes.

+ +

If you're at a windows shop and already have PowerShell chops this could be a useful way to begin to introduce linux into the environment without having to learn a whole new toolchain all at once.

+ +

And being able to dip into the .Net framework from PowerShell and pipe structured data between commands is an intriguing opportunity. I still <3 the new Microsoft.

+ +

That said… looking at the Python result and being mindful that Python has run on Windows and *nix for years maybe we should all learn Python instead of Bash or PowerShell

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/rake-transforms.html b/rake-transforms.html new file mode 100644 index 000000000..6d1e436af --- /dev/null +++ b/rake-transforms.html @@ -0,0 +1,433 @@ + + + + + + + + + + + + + + + + + + + + + + Transforming web.config values with Rake + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Thu Nov 06 2014
+

Transforming web.config values with Rake

+
+ +
+ +
+

I've really been enjoying using Albacore Rake instead of MSBuild at work. It's enabled us to get everyone involved (because ugh, msbuild xml) and to improve our CI/CD pipeline.

+ +

Today we were talking about reducing the number of build configurations we have… which we only have in order to support config transforms.

+ + + +

NB: this is all with Albacore Rake version one.

+ +

Here's a rakefile that will clean and build a .Net solution

+ +
require 'albacore'
+
+SOLUTION_FILE = "../SolutionName.sln"
+BUILD_CONFIG = ENV['Configuration'] || "Release"
+
+MSBUILD_PROPERTIES = {
+  :configuration => BUILD_CONFIG,
+  :VisualStudioVersion => 12.0 # or you have to edit csproj for this to work
+}
+
+task :default => [:build]
+
+msbuild :clean do |msb|
+    msb.properties = MSBUILD_PROPERTIES
+    msb.targets :Clean
+    msb.solution = SOLUTION_FILE
+    msb.verbosity = :normal
+end
+
+msbuild :build=>[:clean] do |msb|
+    msb.properties = MSBUILD_PROPERTIES
+    msb.targets :Build
+    msb.solution = SOLUTION_FILE
+    msb.verbosity = :normal
+end
+
+ +

In this scenario I want to replace the value of an app setting key. The section in the web.config in question:

+ +
  <appSettings>
+    <add key="TheSetting" value="value that needs to change"/>
+  </appSettings>
+
+ +

aside: don't always do this!

+ +

The web.config transform to replace one or two values is pretty straightforward so if you don't have many different configurations or many things to change then you can probably stick with that.

+ +

But if you want to be able to apply the transform outside of packaging/deploying or if things are getting gnarly then I definitely recommend exploring albacore

+ +

A template web.config

+

I keep rake files in a folder at the root of the solution named Build (but they don't have to be there). And so I added a folder named templates to that. Then added a copy of the web.config to that folder.

+ +

This annoys and worries me as it will need to be kept in sync as the web.config changes (say someone adds a Nuget package to the project that alters the web.config). And so this feels like a potential source of error. But…

+ +

In this file any value that needs changing can be replaced with a ruby string interpolation format placeholder thingy (hmmm, did I give away that I don't ruby programming language much?)

+ +

So…

+ +
  <appSettings>
+    <add key="TheSetting" value="%{the_setting}"/>
+  </appSettings>
+
+ +

YAML

+ +

YAML or YAML Ain't Markup Language is a "human friendly data serialization + standard for all programming languages.". There's more to learn about yaml in ruby here

+ +

For each build config add a yaml file. In this example case I added a release.yaml file to the Build/templates folder:

+ +
:the_setting: altered-value
+
+ +

Complex, right? Wrong! If anything there's too little text in here for my tastes (although I'm unfamiliar with yaml so it may be because of the effort I have to expend to parse it)

+ +

The important thing here is that the yaml key begins with a colon to support the string replacement method used below.

+ +

The rake task

+ +

The rakefile should look like this:

+ +
require 'albacore'
+require 'yaml'
+
+SOLUTION_FILE = "../SolutionName.sln"
+BUILD_CONFIG = ENV['Configuration'] || "Release"
+WEBCONFIG_PATH = "../ProjectName/Web.config"
+
+MSBUILD_PROPERTIES = {
+  :configuration => BUILD_CONFIG,
+  :VisualStudioVersion => 12.0
+}
+
+task :default => [:build]
+
+msbuild :clean do |msb|
+    msb.properties = MSBUILD_PROPERTIES
+    msb.targets :Clean
+    msb.solution = SOLUTION_FILE
+    msb.verbosity = :normal
+end
+
+msbuild :build=>[:clean] do |msb|
+    msb.properties = MSBUILD_PROPERTIES
+    msb.targets :Build
+    msb.solution = SOLUTION_FILE
+    msb.verbosity = :normal
+end
+
+task :transform_config do 
+  configHash = YAML.load_file("templates/#{BUILD_CONFIG}.yaml")
+  templateConfig = File.read("templates/web.config") 
+  newConfig = templateConfig % configHash
+  File.write(WEBCONFIG_PATH, newConfig)
+end
+
+ +

This file has a new task which loads the settings from the yaml file into a hash. And then loads the contents of the web.config template as a string.

+ +

It then uses the string % hash method of string interpolation that has been available since Ruby 1.9.2

+ +

This mechanism requires that the hash keys are symbols and not strings which is why the keys in the yaml file have to begin with a colon.

+ +

Does this solve my problems?

+ +

More config yaml files can be added. Whole sections can be excluded from configs by being replaced with empty strings. And more importantly the project and solution files don't need to know about these configurations to support different values in different deployments.

+ +

There might be pain points here I haven't discovered in this example but I like what I see so far!

+ + +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/reactotype/part-one.html b/reactotype/part-one.html new file mode 100644 index 000000000..4b55272e1 --- /dev/null +++ b/reactotype/part-one.html @@ -0,0 +1,632 @@ + + + + + + + + + + + + + + + + + + + + + + Reactotype Part 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Tue Feb 17 2015
+

Reactotype Part 1

+
+
+ +
+ +
+ + tag-icon + + learning + + + + + tag-icon + + react + + + + + tag-icon + + js + + + + + tag-icon + + series + + + +
+
+
+
+ +
+

part one because I've got a feeling this is a topic about which I'll be able to bang on.

+ +

React JS was made by Facebook to be the V in MVC. In other words, it only deals with the UI. It's sold as being fast - both for performance and development. A contentious part of React is that it mushes JS and HTML together… More specifically, you put HTML inside the JS and not vice versa.

+ + + +

I've played with it very briefly while prototyping an Imgur comment generator and saw a talk on it recently at MancJS.

+ +

So I already had a few thoughts:

+ +
    +
  • I found I grokked things quicker with React than Angular when I built the commenturion examples. And I had zero React experience and reasonable Angular when I built those.
  • +
  • (It might be that) you have to do things the React way +
      +
    • I don't think this distinguishes it from other things
    • +
    +
  • +
  • JS & Code & HTML all mushed up together… +
      +
    • I'm not sure if it feels wrong because it's unfamiliar or because it is wrong
    • +
    +
  • +
+ +

Why

+ +

We're using Angular in an application at work and, it's allowed us to move fast but, I do feel like there's a lot more of the typing to make things happen. And I still don't get directives even after several half-hearted attempts to understand them.

+ +

It seems like the cool-kids hold the position we should ditch everything for React. And both my toe-dipped-in-the-water and the talk I saw suggested there was something worth investigating. So I thought I'd use my #holidaycode time to investigate.

+ +

What

+ +

Most of my front-end work for the last eight months and most for the foreseeable future is data-visualisation heavy so, that's what I wanted to riff on. + +I grabbed some data on the gender pay gap in the UK from the Annual Survey of Hours and Earnings, 2014 Provisional Results. And decided to use a simplified version of the source for Figure 8. Showing the gender pay gap for median gross hourly earnings (excluding overtime), UK, April 1997 to 2014 + +Now to represent as a table and a chart so a good starting point. Plus I've got three daughters so, it's a subject close to my heart.

+ +

How

+ +

As I sat down and thought about what this would mean (writing a build script to mush the jsx into js, grabbing bootstrap et al from the interweb, etc) I was overcome with ennui (which is a lot to cope with for something that should be #holidaycode levels of fun). I've done that set up so many times it seemed like wasted time.

+ +

Setup

+ +

I remembered Yeoman.io and a quick search found a ReactJS with Gulp yeoman generator was available so I grabbed them:

+ +
mkdir reactotype && cd reactotype
+npm install -g yo
+npm install -g generator-react-gulp-browserify
+yo react-gulp-browserify
+
+ +

Fast, simple, easy, and…

+ +

The gulp setup was sooooo much better than I could have managed myself. Browserify & reactify so that I can use CommonJS modules and it all ends up in the browser correctly.

+ +

And gulp webserver! I love finding new (to me), awesome (to me) things

+ +
gulp.task('serve', function () {
+    gulp.src('./dist')
+        .pipe($.webserver({
+            livereload: true,
+            port: 9000
+        }));
+});
+
+ +

Seems like overkill when I can do python -m SimpleHTTPServer since I'm only making static HTML with some JS but… I have to remember to type that in and it means I need a terminal window for running that. This way I can gulp watch and it re-smooshes file changes and serves them up for me. + +Simple! And simple is my favourite thing (that I find hard to achieve).

+ +

I like Gulp for the same reason that I prefer Rake to MSBuild. I can read & write code so I can figure out what it's doing (or force it do something undesirable if I don't know the sensible thing). Whereas with Grunt or MSBuild unless you know the JSON/XML magic incantation you're stuck.

+ +

Approach

+

I started with a static HTML file and bit-by-bit added features using React. First rendering the table, then sorting it, then allowing sorting and filtering by year.

+ +

All the code is up on Github.

+ +

So I created a module which gives out a list of PayYears.

+ +
{'year': '1997', 'all': 27.5, 'fulltime': 17.4, 'parttime': 0.6 } 
+
+ +

A PayYear has a year, the percentage pay gap between genders for all employment, for fulltime and for parttime.

+ +

Rendering the table

+ +
/** @jsx React.DOM */
+'use strict';
+
+var React = window.React = require('react');
+
+var PayRow = React.createClass({
+    render: function() {
+        return (
+            <tr>
+                <td>{this.props.payYear.year}</td>
+                <td>{this.props.payYear.all}</td>
+                <td>{this.props.payYear.fulltime}</td>
+                <td>{this.props.payYear.parttime}</td>
+            </tr>
+        );
+    }
+});
+
+var PayTable = React.createClass({
+    render: function() {
+        return (
+            <table className="table table-striped">
+                <thead>
+                    <tr>
+                        <th>Year</th>
+                        <th>All</th>
+                        <th>Full-time</th>
+                        <th>Part-time</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    {this.props.payYears.map(function(payYear) {
+                      return <PayRow key={payYear.year} payYear={payYear} />;
+                    })}
+                </tbody>
+            </table>
+        );
+    }
+});
+
+module.exports = PayTable;
+
+ +

So… +/** @jsx React.DOM */ +This is so that the JSX magic can turn this into real Javascript. JSX is the thing that let's you mix HTML into your JS.

+ +

Looking at the code that creates a table row.

+ +
var PayRow = React.createClass({
+    render: function() {
+        return (
+            <tr>
+                <td>{this.props.payYear.year}</td>
+                <td>{this.props.payYear.all}</td>
+                <td>{this.props.payYear.fulltime}</td>
+                <td>{this.props.payYear.parttime}</td>
+            </tr>
+        );
+    }
+});
+
+ +

createClass is a helper to construct an instance of a component class for you. That class has a render function which returns HTML.

+ +

Call JS, get HTML. Looks funky but is straightforward. + +It's saying that in order to have a PayRow you have to have something providing this.props.payYear, which has to have a minimum set of properties, and you spit out a table row as HTML.

+ +

That's pretty straightforward.

+ +

Having said what a row of the table is we can say what the table is

+ +
var PayTable = React.createClass({
+    render: function() {
+        return (
+            <table className="table table-striped">
+                <thead>
+                    <tr>
+                        <th>Year</th>
+                        <th>All</th>
+                        <th>Full-time</th>
+                        <th>Part-time</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    {this.props.payYears.map(function(payYear) {
+                      return <PayRow key={payYear.year} payYear={payYear} />;
+                    })}
+                </tbody>
+            </table>
+        );
+    }
+});
+
+ +

So this says that to render a table you need a bunch of HTML and then take this.props.payYears and map it into a set of PayRows that make up the table body.

+ +

Oh, and cause I only skimmed the docs, at first I missed that you have to add classes to HTML in JSX using className (since class is a reserved word in JS). + +Elsewhere in the code we call React.render(<PayTable payYears={data} />, document.getElementById('idOfContainer')); and this spits the generated HTML into the page.

+ +

That's not weird… really. If you can HTML & JS then you can follow what is happening. Other than glossing over the magic JSX incantation, and what React.createClass does there's nothing new or complex here. It's only mashing one or more JS data structures into HTML templates.

+ +

Sorting

+ +

This was probably the hardest bit for me to wrap my head around. Because this isn't a framework this isn't provided for me. So, the horror, I had to write the code.

+ +

The only part to change was the PayTable code.

+ +
function sortAscending(a,b) {
+  if (a.year < b.year)
+     return -1;
+  if (a.year > b.year)
+    return 1;
+  return 0;
+};
+
+function sortDescending(a,b) {
+  if (a.year > b.year)
+     return -1;
+  if (a.year < b.year)
+    return 1;
+  return 0;
+};
+
+var PayTable = React.createClass({
+    getInitialState: function() {
+        return {
+            sortDirection: 'descending',
+            data: this.props.payYears.sort(sortDescending)
+        };
+    },
+    sortData: function() {
+        if(this.state.sortDirection==='descending') {
+            this.setState({ 
+                sortDirection: 'ascending',
+                data: this.props.payYears.sort(sortAscending)
+            });
+        } else {
+            this.setState({ 
+                sortDirection: 'descending',
+                data: this.props.payYears.sort(sortDescending)
+            });
+        }
+    },
+    render: function() {
+        return (
+            <table className="table table-striped">
+                <thead>
+                    <tr>
+                        <th onClick={this.sortData}
+                            className={this.state.sortDirection}
+                        >
+                            Year
+                        </th>
+                        <th>All</th>
+                        <th>Full-time</th>
+                        <th>Part-time</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    {this.state.data.map(function(payYear) {
+                      return <PayRow key={payYear.year} payYear={payYear} />;
+                    })}
+                </tbody>
+            </table>
+        );
+    }
+});
+
+ +

First I had to come to terms with properties and state. So…

+ +

Properties

+ +

Passed in to the component when you create it. Should be immutable (this is JS so they aren't immutable but you're supposed to treat them as immutable).

+ +

State

+ +

Not passed in. Not immutable. It's the, erm, well, the state.

+ +

So in addition to a render function we add a getInitialState function. This provides the initial state of the component. It should be idempotent - i.e. no matter how many times the component is created, all other things being equal, the initial state is the same.

+ +

I've spotted a "problem" with this… In the docs it is described as an antipattern to assign a foo property to foo state. You can do it but instead you should have an intialFoo property that is assigned to the foo state. That change of calling out that the link between the two is that the property is the initial value and shouldn't be mutated within the component's state seems important to being in the React mindset.

+ +

Anyhoo, having decided that a user would be able to click on the Year column header and that interaction would sort the column and that the Year header would have a visual affordance to show the data is sortable and in which direction it currently is. This means that the initial state is the direction of the sort and the sorted data.

+ +

On the table header we assign an onClick handler that calls a sortData function. That function checks the current direction of sorting and uses that to pick the new direction and resort the data. Those two new items are assigned to the component's state.

+ +

In order to sort the data we call sort with a comparator function. So I wrote two comparators sortAscending and sortDescending. +

+

OnClick is the work of the devil, though

+ +

Well, in a world where the JS that the onclick handler calls could be anywhere so maintaining the code is made harder, I totally agree. But here… the sortData function is six lines away. And will always be in the same file, nearby.

+ +

Unless someone takes the time to move that behaviour out into some pure JS (which could well be a good thing) and then the JSX is coupled to the JS object rather than the JS object being coupled to the HTML (by needing to know the identifier of the element whose events it reacts to.

+ +

And, why should <th onClick={this.sortData}> be different from <th ng-click="sortData">?! Except, I suppose, that in the Angularified example I have to know where to go to find the sortData function because it's in a controller/directive somewhere whereas in the React version it's nearby or explicitly called.

+ +

I don't know, right now, where I stand on this.

+ +

But

+ +

Other than the dance to get how to coordinate state so that the table would update this felt pretty easy and almost none of the typing. I could grok enough of React to do this in one sitting.

+ +

As a result I (think I) could explain it to people and have a good expectation that they would be able to work with it, extend it, use the idea elsewhere.

+ +

That's the thing I've really enjoyed about working with React (on this admittedly small example). It seems like the application is going to be simpler and that stuff that acts together has to live together.

+ +

Sorting and Filtering the Table

+ +

That we will leave till part two… because I introduced a relatively artificial constraint that I didn't want the filtering control to be a part of the table.

+ +

Imagine that there will be many tables with the same filter. I don't want to bind the filter to any one table or insist that every table has it.

+ +

At first I expected that it would force me to understand React's components and how to compose them… instead I stumbled on something really cool #cliffhanger

+ +
+ + +
+
+ + + + + + + + + + diff --git a/reactotype/part-three.html b/reactotype/part-three.html new file mode 100644 index 000000000..0c80f1937 --- /dev/null +++ b/reactotype/part-three.html @@ -0,0 +1,670 @@ + + + + + + + + + + + + + + + + + + + + + + Reactotype Part 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Fri Feb 20 2015
+

Reactotype Part 3

+
+
+ +
+ +
+ + tag-icon + + learning + + + + + tag-icon + + react + + + + + tag-icon + + js + + + + + tag-icon + + series + + + +
+
+
+
+ +
+

At the end of the last post I realised I'd sacrificed some good practice in the rush to make it work (i.e. worked normally like all those other guilty software engineers everywhere everyday.)

+ + + +

So earlier today I played with the kids to tire them out enough that I could distract them with television and write some #holidaycode because I am a good(-ish) parent.

+ +

I managed to

+ +
    +
  • switch from using magic strings in the messagebus channel and topic identifiers
  • +
  • remove some duplication
  • +
  • and get some tests around ReactJS
  • +
+ + + +

Messagebus channels and topics

+ +

In the last post we (I) used magic strings to identify the channel and topic that messages were being published to.

+ +

I'm not completely sold on this particular structure:

+ +
var messageBusStructure = {
+	channels: {
+		filters: 'filters'
+	},
+	topics: {
+		filters: {
+			yearBoundsChange: 'year.bounds.change'
+		}
+	}
+};
+
+ +

but the general idea holds since it should mean that the postal publish/subscribe code is less prone to typing errors.

+ +
postal.subscribe({
+	channel: bus.channels.filters,
+	topic : bus.topics.filters.yearBoundsChange,
+	callback: function(d, e) {
+		this.filterData(d);
+	}
+}).context(this);
+
+//is better than
+
+postal.subscribe({
+	channel: "filters",
+	topic : "year.bounds.change",
+	callback: function(d, e) {
+    	this.filterData(d);
+  	}
+}).context(this);
+
+ +

Remove duplication with fire

+ +

Then I componentised (ugh, is that a word?!) the input controls being used in the FilterBox so I could remove the duplication of handling their events.

+ +
var YearFilterInput = React.createClass({
+	publishOnChange: function(event) {
+		var eventData = {};
+		eventData[this.props.name.toLowerCase()] =
+			parseInt(event.target.value, 10);
+
+		postal.publish({
+			channel: bus.channels.filters,
+			topic : bus.topics.filters.yearBoundsChange,
+			data: eventData
+		});
+	},
+	render: function() {
+		return (
+				<div className="form-group">
+					<label htmlFor={this.props.name}>{this.props.name}</label>
+					<input type="number"
+						   name={this.props.name}
+						   className="form-control"
+						   defaultValue={this.props.default}
+						   min={this.props.initialEarliest}
+						   max={this.props.initialLatest}
+						   onChange={this.publishOnChange}/>
+				</div>
+		);
+	}
+});
+
+var FilterBox = React.createClass({
+	render: function() {
+		return (
+			<div className="col-xs-12">
+				<YearFilterInput name="Earliest"
+								 default={this.props.initialEarliest}
+								 initialEarliest={this.props.initialEarliest}
+								 initialLatest={this.props.initialLatest} />
+				<YearFilterInput name="Latest"
+								 default={this.props.initialLatest}
+								 initialEarliest={this.props.initialEarliest}
+								 initialLatest={this.props.initialLatest} />
+			</div>
+		);
+	}
+});
+
+ +

This changed the structure of the data that forms the message.

+ +
publishOnChange: function(event) {
+	var eventData = {};
+	eventData[this.props.name.toLowerCase()] =
+		parseInt(event.target.value, 10);
+
+	postal.publish({
+		channel: bus.channels.filters,
+		topic : bus.topics.filters.yearBoundsChange,
+		data: eventData
+	});
+}
+
+ +

Now, since the component only knows about itself, the eventdata is an object with this component's identifier as a property and the new integer value of the input control as the value for the property.

+ +

I didn't want to read both Year filter values in order to send the (currently) two filter inputs as the last version of the code did. There's little point it being a component if it has to know about other components on the page to work.

+ +

That does mean that the PayTable subscriber to this message had to change how it handled the message.

+ +

React Add-ons (which it turns out is an awesome thing) has an update helper which is used to merge the newly received filterBounds into the existing state.

+ +
//the paytable now does
+var newState = React.addons.update(this.state, {$merge: filterBounds});
+
+ +

Where previously the code replaced the state with the message body because we'd coupled everything up and implicitly the message body was known to match the state.

+ +

This should be better since, if I go on to fangle this much, it should handle change better as the PayTable makes fewer assumptions about the message payload.

+ +

Testing

+ +

It was already getting to be a pain going to the site and changing values in the boxes to check that things were working the way I expected… which means we need tests!

+ +

Facebook have created Jest which is (I think) a wrapper around Jasmine. I tend to use Mocha… and to be honest I didn't want to learn another new thing right now if I could avoid it. So I wondered if anyone else had solved the problem (and eventually realised they had.)

+ +

The steps I ended up taking were:

+ +

1. Install Mocha

+ +
npm install --save-dev mocha
+npm install --save-dev gulp-mocha
+npm install --save-dev should
+
+ +

2. Add a gulp task to run tests

+ +
gulp.task('test', function() {
+    //this require line took me a while to figure out!
+    //more below!
+	require('./tests/compiler.js');
+	return gulp.src(['tests/*Spec.js'], { read: false })
+	.pipe(mocha({
+		reporter: 'spec',
+		globals: {
+			should: require('should')
+		}
+	}));
+});
+
+ +

If you don't Gulp this says grab all of the javascript files in the tests folder whose names end with Spec and pass them into Mocha.

+ +

3. Actually have some tests

+ +

I had to npm install jsdom since React has to have a DOM to work against. And then figure out (read largely copy from other people on the Google) how to have React render into that DOM. The secret-sauce was in the React.addons.TestUtils which continues the React.addons reign of awesome.

+ +

The end result (snipped a little for clarity) was:

+ +
'use strict';
+
+var jsdom = require('jsdom');
+var React = require('react/addons');
+var postal = require('postal');
+var bus = require('../app/scripts/messageBus');
+var FilterBox = require('../app/scripts/filterBox');
+var TestUtils = React.addons.TestUtils;
+
+var handlerReceived;
+
+before(function() {
+	postal.subscribe({
+		channel: bus.channels.filters,
+		topic : bus.topics.filters.yearBoundsChange,
+		callback: function(data) {
+			handlerReceived = data;
+		}
+	});
+});
+
+describe('the filter box', function() {
+	var filterBoxInputs;
+
+	beforeEach(function() {
+		handlerReceived = null;
+
+		//fake a DOM for React to use
+		global.document = jsdom.jsdom('<!doctype html><html><body></body></html>');
+		global.window = document.parentWindow;
+
+		var filterBox = TestUtils.renderIntoDocument(
+			<FilterBox initialEarliest={1990} initialLatest={2010}/>
+		);
+
+		filterBoxInputs = TestUtils.scryRenderedDOMComponentsWithTag(filterBox, 'input');
+	});
+
+	describe('has a single earliest year input that', function() {
+		var earliestInput;
+
+		beforeEach(function() {
+			var matchedInputs = filterBoxInputs.filter(function(element) {
+				return element.props != undefined
+						&& element.props.name === 'Earliest';
+			});
+			matchedInputs.length.should.be.exactly(1);
+			earliestInput = matchedInputs[0];
+		});
+
+		it('publishes an event when value changes', function() {
+			TestUtils.Simulate.change(earliestInput, {target: {value: '1991'}});
+			handlerReceived.should.match({earliest:1991});
+		});
+
+		it('sets initial earliest on render', function() {
+			earliestInput.props.value.should.be.exactly(1990);
+		});
+	});
+
+	describe('has a single latest year input that', function() {
+		// ..snip
+	});
+});
+
+ +

So, only once, we subscribe to the message we're expecting our React component to publish and store the message body.

+ +
var handlerReceived;
+
+before(function() {
+	postal.subscribe({
+		channel: bus.channels.filters,
+		topic : bus.topics.filters.yearBoundsChange,
+		callback: function(data) {
+			handlerReceived = data;
+		}
+	});
+});
+
+ +

and then need to have a setup for each test:

+ +
var filterBoxInputs;
+
+beforeEach(function() {
+	handlerReceived = null;
+
+	//fake a DOM for React to use
+	global.document = jsdom.jsdom('<!doctype html><html><body></body></html>');
+	global.window = document.parentWindow;
+
+	var filterBox = TestUtils.renderIntoDocument(
+		<FilterBox initialEarliest={1990} initialLatest={2010}/>
+	);
+
+	filterBoxInputs = TestUtils.scryRenderedDOMComponentsWithTag(filterBox, 'input');
+});
+
+ +

Here the handlerReceived is reset each time and then the global document and window variables that a browser would provide are setup.

+ +

TestUtils does the magic of rendering the component into that document. I guess it should be possible to compile the jsx to js and then use the actual React.render to put that into the document but that seems like a lot of work compared to TestUtils.renderIntoDocument.

+ +

Then a second use of TestUtils, with my first meeting of the word "scry" outside of fantasy novels, where TestUtils.scryRenderedDOMComponentsWithTag grabs input elements out of the rendered React component.

+ +

Well, no, it grabs any React components that shadow input controls. Not DOM elements as you might get from document.getElementById but the React equivalent.

+ +

For each input in the Filter box, as it stands, I want to run the same tests and since there are only two inputs right now I'm happy to stand that duplication until I need to remove it. So there are two describe blocks that are almost the same:

+ +
describe('has a single earliest year input that', function() {
+	var earliestInput;
+
+	beforeEach(function() {
+		var matchedInputs = filterBoxInputs.filter(function(element) {
+			return element.props != undefined
+					&& element.props.name === 'Earliest';
+		});
+		matchedInputs.length.should.be.exactly(1);
+		earliestInput = matchedInputs[0];
+	});
+
+	it('publishes an event when value changes', function() {
+		TestUtils.Simulate.change(earliestInput, {target: {value: '1991'}});
+		handlerReceived.should.match({earliest:1991});
+	});
+
+	it('sets initial earliest on render', function() {
+		earliestInput.props.value.should.be.exactly(1990);
+	});
+});
+
+ +

In this block's beforeEach it grabs any input with the desired name, asserts there is only one, and stores that component so that it can be asserted against.

+ +

One test is straightforward and asserts that the default value matches expectation.

+ +

In the other test TestUtils.Simulate.change saves our bacon and handles the work of changing the value of the input box. A little bit magic-incantation-y but readable enough that I can live with it.

+ +

That change should have caused a message to be published and the test is subscribed to those messages so it can assert that the message body was received and matches expectation.

+ +

But… But… Mocha can JSX?

+ +

No, I found this blog post which borrowed code from the Khan Academy which can be passed to mocha as a compiler so that it can JSX when it needs to…

+ +
var fs = require('fs'),
+    ReactTools = require('react-tools'),
+    origJs = require.extensions['.js'];
+
+require.extensions['.js'] = function(module, filename) {
+  // optimization: external code never needs compilation.
+  if (filename.indexOf('node_modules/') >= 0) {
+    return (origJs || require.extensions['.js'])(module, filename);
+  }
+  var content = fs.readFileSync(filename, 'utf8');
+  var compiled = ReactTools.transform(content, {harmony: true});
+  return module._compile(compiled, filename);
+};
+
+ +

This required the final NPM of the day adding in react-tools so that the JSX transformer was available.

+ +

And..

+ +

Ta-da

+ +

passing tests

+ +
+ + +
+
+ + + + + + + + + + diff --git a/reactotype/part-two.html b/reactotype/part-two.html new file mode 100644 index 000000000..b49af2507 --- /dev/null +++ b/reactotype/part-two.html @@ -0,0 +1,659 @@ + + + + + + + + + + + + + + + + + + + + + + Reactotype Part 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Thu Feb 19 2015
+

Reactotype Part 2

+
+
+ +
+ +
+ + tag-icon + + learning + + + + + tag-icon + + react + + + + + tag-icon + + js + + + + + tag-icon + + series + + + +
+
+
+
+ +
+ + +

I posted about my impressions of working with React slowly building an HTML table and banging on about it. I ended that post with one of the more memorable cliff-hangers in recent time.

+ +
+

Sorting and Filtering the Table

+ +

That we will leave till part two… because I introduced a relatively artifical constraint that I didn't want the filtering control to be a part of the table.

+ +

Imagine that there will be many tables with the same filter. I don't want to bind the filter to any one table or insist that every table has it.

+ +

At first I expected that it would force me to understand React's components and how to compose them… instead I stumbled on something really cool #cliffhanger

+
+ +

Exciting! Right?

+ +

I want to add a filter control and I don't want it to be bound to a particular table so that it can be re-used.

+ + + +

So, having squeezed the table to make space for a column for filter controls I needed to do two things

+ +
    +
  1. Add the filter controls
  2. +
  3. Make them affect the table
  4. +
+ +

screenshot of the web page

+ +

Adding the filter controls

+ +

Once I'd made a static HTML version of the filter controls and knew that I was aiming for a number input for the earliest year to display and one for the latest.

+ +
<div className="col-xs-12">
+  <div className="form-group">
+    <label htmlFor="earliest">Earliest</label>
+    <input
+      type="number"
+      name="earliest"
+      className="form-control"
+    />
+  </div>
+  <div className="form-group">
+    <label htmlFor="latest">Latest</label>
+    <input
+      type="number"
+      name="latest"
+      className="form-control"
+    />
+  </div>
+</div>
+
+ +

Copying what I already had to turn this into a React component was a very short job. And then I moved onto a little research to see what approaches there were to solve my problem and I stumbled on a JS pub/sub library called postal.js.

+ +
+

What is it?

+
+ +
+

Postal.js is an in-memory message bus - very loosely inspired by AMQP - written in JavaScript. Postal.js runs in the browser, or on the server using node.js. It takes the familiar "eventing-style" paradigm (of which most JavaScript developers are familiar) and extends it by providing "broker" and subscriber implementations which are more sophisticated than what you typically find in event emitting/aggregation.

+
+ +
/** @jsx React.DOM */
+"use strict";
+
+var React = (window.React = require("react"));
+var postal = (window.postal = require("postal"));
+
+var FilterBox = React.createClass({
+  getInitialState: function () {
+    return {
+      earliest: this.props.initialEarliest,
+      latest: this.props.initialLatest,
+    };
+  },
+  handleEarliestChange: function (event) {
+    this.setState({ earliest: parseInt(event.target.value, 10) }, function () {
+      postal.publish({
+        channel: "filters",
+        topic: "year.bounds.change",
+        data: this.state,
+      });
+    });
+  },
+  handleLatestChange: function (event) {
+    this.setState({ latest: parseInt(event.target.value, 10) }, function () {
+      postal.publish({
+        channel: "filters",
+        topic: "year.bounds.change",
+        data: this.state,
+      });
+    });
+  },
+  render: function () {
+    return (
+      <div className="col-xs-12">
+        <div className="form-group">
+          <label htmlFor="earliest">Earliest</label>
+          <input
+            type="number"
+            name="earliest"
+            className="form-control"
+            defaultValue={this.state.earliest}
+            min={this.props.initialEarliest}
+            max={this.props.initialLatest}
+            onChange={this.handleEarliestChange}
+          />
+        </div>
+        <div className="form-group">
+          <label htmlFor="latest">Latest</label>
+          <input
+            type="number"
+            name="latest"
+            className="form-control"
+            defaultValue={this.state.latest}
+            min={this.props.initialEarliest}
+            max={this.props.initialLatest}
+            onChange={this.handleLatestChange}
+          />
+        </div>
+      </div>
+    );
+  },
+});
+
+module.exports = FilterBox;
+
+ +

What do we have?

+ +

Render

+ +
render: function() {
+    return (
+    	<div className="col-xs-12">
+    		<div className="form-group">
+    			<label htmlFor="earliest">Earliest</label>
+    			<input type="number"
+    				   name="earliest"
+    				   className="form-control"
+    				   defaultValue={this.state.earliest}
+    				   min={this.props.initialEarliest}
+    				   max={this.props.initialLatest}
+    				   onChange={this.handleEarliestChange}/>
+    		</div>
+    		<div className="form-group">
+    			<label htmlFor="latest">Latest</label>
+    			<input type="number"
+    				   name="latest"
+    				   className="form-control"
+    				   defaultValue={this.state.latest}
+    				   min={this.props.initialEarliest}
+    				   max={this.props.initialLatest}
+    				   onChange={this.handleLatestChange}/>
+    		</div>
+    	</div>
+    );
+}
+
+ +

Here we've added a react specific attribute defaultValue to set the starting state of the inputs, added min and max validation using properties passed in to the component and an onChange handler specific to each number input.

+ +

Initial state

+ +
getInitialState: function() {
+    return {
+        earliest: this.props.initialEarliest,
+        latest: this.props.initialLatest
+    };
+}
+
+ +

Here the default values for the earliest and latest state are set.

+ +

Event Handlers

+ +

These two handlers are the same except for operating on a different property of the state object.

+ +

Yes, yes, remove all duplication. But… the duplicate methods are next to each other and I've half a mind to make each control a React component which would remove this duplication so why do that work twice.

+ +

(I got all excited about postal.js and wrote this post before finishing the component)

+ +
handleLatestChange: function(event) {
+	this.setState(
+		{latest: parseInt(event.target.value, 10)},
+		function() {
+			postal.publish({
+				channel: 'filters',
+				topic: 'year.bounds.change',
+				data: this.state
+			});
+		}
+	);
+}
+
+ +

Here when an event is received the function calls setState on the React component. This merges the object provided as the first argument with the component's current state.

+ +

Since that update doesn't necessarily occur immediately the method takes a callback which runs after the update completes.

+ +

In this case the callback uses postal to publish a message. Postal allows you to hold a reference to a channel but here we're using a convenience method that allows you to specify the channel.

+ +
postal.publish({
+  channel: "filters",
+  topic: "year.bounds.change",
+  data: this.state,
+});
+
+ +

So, on channel 'filters' publish a message with the topic 'year.bounds.change' including the component's state as the message data.

+ +

(and yes the first thing I did when subscribing was type in one of those magic strings incorrectly so there's an improvement to be made in my usage there!)

+ +

This gives us a phenomenally useless pub/sub mechanism with no subscribers…

+ +

Subscribing is even harder

+ +
componentWillMount: function() {
+	postal.subscribe({
+	  channel: "filters",
+	  topic : "year.bounds.change",
+	  callback: function(data, envelope) {
+	    this.filterData(data);
+	  }
+	}).context(this);
+
+ +

Postal's subscribe helper takes an object with the same properties as publish. Here for messages posted to a given channel and topic it will call the provided callback.

+ +

The componentWillMount method of the React component is called once before initial rendering so it is perfect for this setup.

+ +

Messy Pay Table Reacting to Filtering

+ +
var PayTable = React.createClass({
+  getInitialState: function () {
+    return {
+      sortDirection: "descending",
+      data: this.props.payYears.sort(sortDescending),
+    };
+  },
+  preparePayData: function (data, options) {
+    if (options.yearBounds) {
+      data = data.filter(function (element) {
+        return (
+          element.year >= options.yearBounds.earliest &&
+          element.year <= options.yearBounds.latest
+        );
+      });
+    }
+    if (options.sortDirection) {
+      data = data.sort(
+        options.sortDirection === "descending" ? sortDescending : sortAscending
+      );
+    }
+    this.setState({ data: data });
+  },
+  sortData: function () {
+    this.setState(
+      {
+        sortDirection:
+          this.state.sortDirection === "descending"
+            ? "ascending"
+            : "descending",
+      },
+      function () {
+        this.preparePayData(this.props.payYears, this.state);
+      }
+    );
+  },
+  filterData: function (filterBounds) {
+    this.setState({ yearBounds: filterBounds }, function () {
+      this.preparePayData(this.props.payYears, this.state);
+    });
+  },
+  componentWillMount: function () {
+    postal
+      .subscribe({
+        channel: "filters",
+        topic: "year.bounds.change",
+        callback: function (d, e) {
+          this.filterData(d);
+        },
+      })
+      .context(this);
+  },
+  render: function () {
+    return (
+      <table className="table table-striped">
+        <thead>
+          <tr>
+            <th
+              onClick={this.sortData}
+              className={this.state.sortDirection}
+            >
+              Year
+            </th>
+            <th>All</th>
+            <th>Full-time</th>
+            <th>Part-time</th>
+          </tr>
+        </thead>
+        <tbody>
+          {this.state.data.map(function (payYear) {
+            return (
+              <PayRow
+                key={payYear.year}
+                payYear={payYear}
+              />
+            );
+          })}
+        </tbody>
+      </table>
+    );
+  },
+});
+
+ +

PayTable now has a preparePayData method which has the responsibility of taking some data and the component's current state and setting the state's data property correctly.

+ +

Now all the filterData and sortData methods need to do is update state and then call preparePayData.

+ + + +

The point here is how useful it was to use postal.js to hook these two components together. I lurve this!

+ +

demo of the web page

+ +

Next Up

+ +

A little bit of tidying up and add a chart view. #holidaycode

+ +
+ + +
+
+ + + + + + + + + + diff --git a/real-vs-soft.html b/real-vs-soft.html new file mode 100644 index 000000000..75c6a3760 --- /dev/null +++ b/real-vs-soft.html @@ -0,0 +1,397 @@ + + + + + + + + + + + + + + + + + + + + + + Real vs. Software Engineering + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sun Nov 29 2015
+

Real vs. Software Engineering

+
+ +
+ +
+

I recently had some "fun".

+ +

Earlier this year we got a great deal on two ducks, three chickens, a coop, a run, and Gary the Rescue Cockerel

+ + + +
+ + +
+ +

the chickens

+ +

The ducks we gave to a local small-holding because they turned out to be too much work and we didn't like duck eggs. + +But we rebuilt the chicken run and kept the chickens (and Gary the Rescue Cockerel). The kids love them, they're good company in the garden (bizarrely), and we get two or three eggs a day.

+ +

This afternoon I looked outside and saw two things…

+ +

1) it was really windy +2) the wind had started to rip off the roof from the coop + +So I just spent half an hour or so doing some emergency maintenance to get us through this weather, learnt that using wet, wooden chairs as impromptu step ladders is dangerous, and got annoyed that I hadn't built a better roof last time. This got me to wondering if iterating on the chicken run had been a good idea.

+ +

It's quite common to make comparisons between software engineering and real world engineering since they're both building things. I thought I'd explore whether that bore out here…

+ +

1.0

+ +

I approached rebuilding the run in an iterative fashion. So 1.0 was taking the parts of the roof that survived transportation to our house back on to the run. These only covered half of the area. So while the chickens had some protection from the weather it wasn't enough to stop the ground getting wet.

+ +

2.0

+ +

As the summer ended and the weather started to turn we moved to roof 2.0. The requirements from the PM (a.k.a. my wife) were that it be:

+ +
    +
  • cheap
  • +
  • fast
  • +
  • effective
  • +
+ +

So far, so exactly like a software project. + +For 2.0 I wanted the roof to be sloped so that rain would run off, and I wanted the entire area covered. We had quite a lot of timber left over from various DIY projects so I built up one side of the roof to provide the slope and bought some more corrugated plastic sheets to cover the rest of the roof area.

+ +

It didn't cost too much and while it didn't keep out all of the rain it was a good improvement.

+ +

chicken run 2.0

+ +

3.0-alpha1

+ +

The roof (or rather parts of it) are too damaged to stay in place so I'll definitely need to bump to 3.0. + +I've definitely learned that the slope of the roof wasn't high enough and rain tended to pool on it (I've got a feeling that roof slopes need to be 15 degrees to get rain and snow to run off). And that there needs to be less freedom of movement of and between the individual pieces of roof.

+ +

How was iterating DIY like iterating software engineering?

+ +
    +
  • Releasing early provided value - because the chickens have been protected from rain and predators.
  • +
  • We've minimised time spent because I haven't had to dedicate a weekend to building a roof. Although I haven't measured that so I don't know for sure
  • +
  • And, since I'm not a natural DIYer, I've learnt a lot by having the roof in production.
  • +
+ +

How was iterating DIY not like iterating in software engineering?

+ +
    +
  • When the wind damaged the roof it wasn't possible to roll back the deployed chicken run to a known good state
  • +
  • When I come to work on it again I can't check out the resources for the project. I'll have to go to some hardware store and buy all the bits again
  • +
+ +

In Conclusion

+ +

The cost of experimentation is much lower in software engineering than in DIY. I should embrace that more.

+ +

I fall off chairs more often when iterating DIY than iterating software.

+ +
+ + +
+
+ + + + + + + + + + diff --git a/recipes/2022/09/pepper-salad.html b/recipes/2022/09/pepper-salad.html new file mode 100644 index 000000000..bd4537eba --- /dev/null +++ b/recipes/2022/09/pepper-salad.html @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + Roast Pepper Salad + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+

Ingredients

+
    + +
  • 6 peppers (any colour)
  • + +
  • to taste extra virgin olive oil
  • + +
  • to taste capers
  • + +
  • 25 black olives
  • + +
  • to taste thyme
  • + +
  • to taste basil
  • + +
  • to taste garlic
  • + +
+
+ + + +
+

Instructions

+
    +
  1. Set your grill to hot.
  2. chop the peppers into quarters or halves
  3. remove the seeds
  4. rub the skins with olive oil
  5. place them on a baking tray skin-side up
  6. grill until the skins are burning and bubbling away from the flesh
  7. place them straight into a bowl and cover with clingfilm
  8. after at least ten minutes
  9. remove them from the bowl
  10. the skin will peel away without much effort
  11. slice lengthways
  12. re-using the same bowl (which will have the pepper's juices at the bottom)
  13. add the sliced peppers
  14. add capers
  15. add basil
  16. add halved and pitted olives
  17. add a pinch of thyme
  18. slice and add garlic
  19. season with black pepper
  20. drizzle with olive oil
  21. stir
  22. leave overnight for the flavours to combine
  23. +
+
+ +
+ + + + + + +

Be careful to have lots of good bread available to soak up the tasty oil when you're finished! 👩‍🍳👌

+ +

the pepper salad in a bowl +the pepper salad on friselle

+ + +
+ + + + + + + + + + diff --git a/recipes/2022/09/pizza-dough.html b/recipes/2022/09/pizza-dough.html new file mode 100644 index 000000000..2aa1f04db --- /dev/null +++ b/recipes/2022/09/pizza-dough.html @@ -0,0 +1,212 @@ + + + + + + + + + + + + + + + + + + + + + + Pizza dough + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+

Ingredients

+
    + +
  • 1kg 00 flour
  • + +
  • 556g water
  • + +
  • 28 to 30g salt
  • + +
  • 1 sachet yeast (dried)
  • + +
+
+ + + +
+

Instructions

+
    +
  1. put the water in a large bowl
  2. add the salt
  3. add a third of the flour
  4. mix it all together
  5. add the yeast
  6. mix it
  7. add the rest of the flour
  8. mix it until it comes together
  9. knead for at least five minutes. Ideally up to 20 minutes
  10. leave in the bowl covered with a damp tea-towel somewhere about 21°c for 4 hours
  11. lightly flour a work surface
  12. take the flour out of the bowl and cut into roughly 250g lumps
  13. you'll get 6 to 8 lumps
  14. make into balls
  15. leave on the surface for another 2 hours covered with the damp towel
  16. +
+
+ +
+ + + + + + +

It turns out that Neapolitan pizza dough is a strictly described thing. The UK version of the EU rules are here. Those instructions use 1.8 kilograms of flour and 1 litre of water. But (if you're not going to a wholesaler) flour comes in 1 kilogram bags. Ingredients have been adjusted to 1kg for this recipe.

+ +

I sometimes use a biga starter. Instructions for that are here. But it takes more work, time, and nuance.

+ +

This recipe can be completed in a single day and makes a very consistently tasty dough

+ +

(Yes! That much salt)

+ +

the dough balls resting on a wooden surface

+ + +
+ + + + + + + + + + diff --git a/recipes/2023/01/pasta-e-fasule.html b/recipes/2023/01/pasta-e-fasule.html new file mode 100644 index 000000000..17ad592c3 --- /dev/null +++ b/recipes/2023/01/pasta-e-fasule.html @@ -0,0 +1,218 @@ + + + + + + + + + + + + + + + + + + + + + + Pasta e fasule + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+

Ingredients

+
    + +
  • 400 gram broken spaghetti and pasta
  • + +
  • 2 tins cannalini beans (washed and drained)
  • + +
  • 3 or 4 garlic cloves (peeled)
  • + +
  • 5 tablespoons (approx) olive oil
  • + +
  • 10 cherry tomatoes (optional)
  • + +
  • to taste salt & pepper
  • + +
+
+ + + +
+

Instructions

+
    +
  1. Put oil in a pan fry garlic till blond not brown
  2. If you have tomatoes add to the pan
  3. If you don't have tomatoes you can add a quarter of a tin of chopped tomatoes or leave them out entirely
  4. cook with garlic & oil for about 5mins
  5. Then add beans stir and cook all together for 5mins
  6. Cover with water 2 fingers above the beans
  7. Add salt & pepper to taste
  8. Cook for 20mins
  9. Add pasta, stir, cook till pasta is soft
  10. Stir often to stop pasta sticking to bottom of the pan
  11. If it goes dry add more water
  12. I like it thicker some people like it thinner
  13. When cooked cover with lid leave for 10/15 mins to rest so pasta absorbs the sauce
  14. +
+
+ +
+ + + + + + +

Pasta e Fasule, as made by my Neapolitan Nonna Luisa

+ +

Pasta e Fasule is Neapolitan for Pasta e Fagiole which is Italian for Pasta and Beans

+ +

Pasta and bean soup reminds me of being looked after when recovering from an illness as a kid. I like it so thick the spoon will stand up.

+ + + +

You can use odds and ends of pasta or break spaghetti in. When my dad was little he'd be sent to the pasta shop to get their broken odds and ends. They were cheaper.

+ +

steps in making pasta fasule as a gif

+ + +
+ + + + + + + + + + diff --git a/recipes/2023/07/zucchini-focaccia.html b/recipes/2023/07/zucchini-focaccia.html new file mode 100644 index 000000000..61a7f737a --- /dev/null +++ b/recipes/2023/07/zucchini-focaccia.html @@ -0,0 +1,232 @@ + + + + + + + + + + + + + + + + + + + + + + Zucchini focaccia + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+

Ingredients

+
    + +
  • 800 gram bread flour or 00 pizza flour
  • + +
  • 1 sachet dried yeast
  • + +
  • 20g salt
  • + +
  • 250ml water
  • + +
  • 1 big one zucchini
  • + +
  • 2 cloves garlic
  • + +
  • 10 cherry tomatoes (optional)
  • + +
  • 10 olives (optional)
  • + +
  • to taste rosemary (optional)
  • + +
  • to taste salt & pepper
  • + +
+
+ + + +
+

Instructions

+
    +
  1. Grate or slice the zucchini
  2. put garlic and oil in a pan fry garlic till blond not brown
  3. add the zucchini and cook till soft
  4. blitz the cooked zucchini in a blender
  5. put flour, salt, yeast, water, and blitzed zucchini in a stand mixer
  6. or, just, use a bowl and your hands
  7. mix until it makes a nice dough
  8. add water as needed
  9. leave on the windowsill with a wet tea towel on top for 2 hours
  10. Knock it back and split it into at least 2 (depends on the size of tin you have)
  11. Oil the tin, spread the dough into it
  12. Optionally leave for another hour to rise
  13. Add tomatoes, olives, rosemary, and sea salt to taste
  14. Use your fingers to make dimples in the dough
  15. Cool at 180C for 20mins or until golden brown
  16. +
+
+ +
+ + + + + + +

This year we've grown way too many zucchinis.

+ +

the plants

+ +

One way I've been trying to use them up is putting them in dough.

+ +

The kids love it!

+ + + +

chopped garlic and zucchini +blitzed cooked zucchini +dough +ready for the oven +ready for the oven two +cooked-one +cooked-two

+ + +
+ + + + + + + + + + diff --git a/register-service-worker.js b/register-service-worker.js new file mode 100644 index 000000000..4266fb1eb --- /dev/null +++ b/register-service-worker.js @@ -0,0 +1,84 @@ + +/* eslint-env browser */ +'use strict' + +const forTenSeconds = toast => parent => { + parent.appendChild(toast) + setTimeout(() => { + toast.parentNode.removeChild(toast) + }, 10000) +} + +const toast = text => { + const theDiv = document.createElement('div') + theDiv.style.cssText = `width:200px; + height:20px; + height:auto; + position:absolute; + left:50%; + margin-left:-100px; + bottom:10px; + background-color: #383838; + color: #F0F0F0; + font-size: 20px; + padding:10px; + text-align:center; + border-radius: 2px; + -webkit-box-shadow: 0px 0px 24px -1px rgba(56, 56, 56, 1); + -moz-box-shadow: 0px 0px 24px -1px rgba(56, 56, 56, 1); + box-shadow: 0px 0px 24px -1px rgba(56, 56, 56, 1); + }` + + theDiv.textContent = text + return theDiv +} + +const appendTo = (parent, toastAppendTo) => toastAppendTo(parent) + +// taken from https://raw.githubusercontent.com/GoogleChrome/sw-precache/master/demo/app/js/service-worker-registration.js + +if ('serviceWorker' in navigator) { + // Delay registration until after the page has loaded, to ensure that our + // precaching requests don't degrade the first visit experience. + // See https://developers.google.com/web/fundamentals/instant-and-offline/service-worker/registration + window.addEventListener('load', function () { + // Your service-worker.js *must* be located at the top-level directory relative to your site. + // It won't be able to control pages unless it's located at the same level or higher than them. + // *Don't* register service worker file in, e.g., a scripts/ sub-directory! + // See https://github.com/slightlyoff/ServiceWorker/issues/468 + navigator.serviceWorker.register('/service-worker.js').then(function (reg) { + // updatefound is fired if service-worker.js changes. + reg.onupdatefound = function () { + // The updatefound event implies that reg.installing is set; see + // https://w3c.github.io/ServiceWorker/#service-worker-registration-updatefound-event + var installingWorker = reg.installing + + installingWorker.onstatechange = function () { + switch (installingWorker.state) { + case 'installed': + if (navigator.serviceWorker.controller) { + // At this point, the old content will have been purged and the fresh content will + // have been added to the cache. + // It's the perfect time to display a "New content is available; please refresh." + // message in the page's interface. + appendTo(document.body, forTenSeconds(toast('New or updated content is available.'))) + console.log('New or updated content is available.') + } else { + // At this point, everything has been precached. + // It's the perfect time to display a "Content is cached for offline use." message. + appendTo(document.body, forTenSeconds(toast('Content is now available offline!'))) + console.log('Content is now available offline!') + } + break + + case 'redundant': + console.error('The installing service worker became redundant.') + break + } + } + } + }).catch(function (e) { + console.error('Error during service worker registration:', e) + }) + }) +} diff --git a/service-worker-config.js b/service-worker-config.js new file mode 100644 index 000000000..ae5770dcb --- /dev/null +++ b/service-worker-config.js @@ -0,0 +1,9 @@ +module.exports = { + staticFileGlobs: [ + '_site/**/**.html', + '_site/**/**.jpg', + '_site/**/**.png', + '_site/**/**.svg' + ], + stripPrefix: '_site' +} diff --git a/service-worker.js b/service-worker.js new file mode 100644 index 000000000..140dabda2 --- /dev/null +++ b/service-worker.js @@ -0,0 +1,268 @@ +/** + * Copyright 2016 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +// DO NOT EDIT THIS GENERATED OUTPUT DIRECTLY! +// This file should be overwritten as part of your build process. +// If you need to extend the behavior of the generated service worker, the best approach is to write +// additional code and include it using the importScripts option: +// https://github.com/GoogleChrome/sw-precache#importscripts-arraystring +// +// Alternatively, it's possible to make changes to the underlying template file and then use that as the +// new base for generating output, via the templateFilePath option: +// https://github.com/GoogleChrome/sw-precache#templatefilepath-string +// +// If you go that route, make sure that whenever you update your sw-precache dependency, you reconcile any +// changes made to this original template file with your modified copy. + +// This generated service worker JavaScript will precache your site's resources. +// The code needs to be saved in a .js file at the top-level of your site, and registered +// from your pages in order to be used. See +// https://github.com/googlechrome/sw-precache/blob/master/demo/app/js/service-worker-registration.js +// for an example of how you can register this script and handle various service worker events. + +/* eslint-env worker, serviceworker */ +/* eslint-disable indent, no-unused-vars, no-multiple-empty-lines, max-nested-callbacks, space-before-function-paren, quotes, comma-spacing */ +'use strict'; + +var precacheConfig = [["/2009/05/anonymous-methods-when-invoking-in-vb.html","eed5d4620f4c570493dce78d6e9cd025"],["/2009/10/bing-is-not-search-engine.html","c1c98aec4a7616f2820335501b62f69f"],["/2009/10/quack-quack-says-duck.html","636529746331f0cc4e4b41f7a5489c30"],["/2009/12/c-background-worker.html","ebf410458d091e23f1004498fe598e61"],["/2010/03/quack-quack-says-duck-now-with-added.html","3be1e68c7f18ecba3481641977f5d998"],["/2010/05/theres-more-in-them-that-hills.html","c80114065cb15b37c72aaaa17099c9f6"],["/2010/08/odd-odd-odd-login-behaviour.html","cd7431d8b126f7088358484ed7c13c42"],["/2010/10/refactor-fun.html","9a092d72935dc392a099ef401bf9ec4e"],["/2011/04/ssh-without-password.html","ceda0b5c88b7df62f3048d7dac1a6812"],["/2011/06/get-with-programming.html","58b28a8d335572a17a6d3168674af872"],["/2011/08/how-to-design-unsubscribe-link.html","5b6cf5aabb0c2d87f058817932d59bc5"],["/2011/09/unusbscribey-follow-up.html","9db61ff620acb5069a5fe5f9cf06ddb2"],["/2012/01/setting-up-mvc3-website-using-built-in.html","14278ec9b12c93595f0a1648f2dc13fc"],["/2012/02/is-there-really-just-ipad-market.html","1747d67d9951ac8f53004d67c5c912d8"],["/2012/07/y-u-no-sell-downloads-hollywood.html","799813adec7c76ed439e01ede2ec3647"],["/2012/09/obligatory-ios6-maps-post.html","6b32f2d6025637b0b7edee750b2dd4f0"],["/2013/03/automagical-search-ux.html","513547e853334265d1188be05236f1e9"],["/2013/11/astronomical-database-identfier.html","232e04640001820b92260aa5c999fe21"],["/2014/01/comparing-mongodb-and-tokumx.html","e280b83f766c0eb556e4ffbef7d8f4c7"],["/2014/02/websites-cms.html","8caf3facb77da5939a89f080acf475e3"],["/2014/03/testing-with-browserstack-and-selenium.html","057658e7334eecc634accaa978bdb211"],["/2014/03/website-cms-display-pages-part-2.html","cca0779609d205145fbf4bdfda3448a6"],["/2014/03/websites-cms-displaying-pages.html","124056a07282c723ca14e1a39a8be49c"],["/2014/04/a-dto-by-any-other-name-would-implement.html","1bf1ba9b36014f1ed020784bb8bc0265"],["/2016/yarn.html","48239c74eb7c0b536344ab9b75fbbb78"],["/2017/05/big-pile-of-soil.html","5f33662cda168a15873adcbfd8f4405c"],["/2017/05/ubictionary.html","a54ed4a18ac9416558ce9b1d9b59933d"],["/2017/06/radarban.html","4c694ac191fb19b39db8e131109eb231"],["/2017/07/retrosperiment.html","cc04002ae2364babf24f8c6defb6e4ec"],["/2017/10/constructiphor.html","1e110ab3da05be8dcbc4da44c34cd837"],["/2017/generating-static-amp.html","7adc80a8cfd9904622a71a8dcc2d96ea"],["/2017/testing-meaning.html","635d13d01333814a3a021af7f6351514"],["/2017/testing-static-sites.html","f13041c972d5fbee36206f08dea7fba5"],["/2018/01/direction.html","3879991e5841f6476253b525c4c042e0"],["/2018/02/serverless-1.html","5963bc9f2185dcd574a307835d8c409a"],["/2018/02/serverless-2.html","7e9df23443995c55048de8f1ada99883"],["/2018/02/serverless-3.html","f08e5585294ee5fbc47831a7a224e0ae"],["/2018/02/serverless-4.html","ab360204abb144cc6af53a1ad186d799"],["/2018/03/harmful-dry.html","a1939510867e943c06f39f8f87d5795c"],["/2018/06/serverless-5.html","1f1294a25bd90bcd236c3fcda201eebc"],["/2018/07/serverless-6.html","4b344cd863446f2b433e4fc47d761aa5"],["/2019/11/serverless-lessons-learned.html","4318948e9bbd6566903af95ba6389838"],["/2020/01/year-notes.html","d069b42765f33449da5fa7f4cc804a3c"],["/2021/01/year-notes.html","98fcf08eb7d9a438674607b98a8c8d76"],["/2021/07/tech-debts.html","98da34987f2c601198f082f4b1b5d19f"],["/2021/09/emotional-advice.html","7698d811c80f5e863ada9f89b0e88800"],["/2022/07/weather-vane-or-sign-post.html","3e972515cc82fcf0ecc16f4a8c86c4e1"],["/2022/08/solar-panels.html","1175818e33522982b1135334054bec45"],["/2023/01/year-notes.html","6cd39f8439c3d1c72ae148da8aeab017"],["/2023/02/jan-month-notes.html","175bc9be0817694f9faf72069d86b737"],["/2023/03/feb-month-notes.html","881e327df12bea6874361e903efd67e5"],["/2023/03/mar-month-notes.html","068071dff7a721d6b9a46f579ff53a69"],["/2023/03/office-365-mega-thread.html","2806a6a57840e56f6dca91bf98d3fa70"],["/2023/03/the-cloud.html","d3252d63f24498c7c914511c95e43f2c"],["/2023/06/pauls-law.html","ae3cfccb6b7291705f98b7ede4f8a41d"],["/On-Page-Editing.html","0832cef9a54abe4dcca600a4ec97bbd4"],["/Websites-CMS-Platform-Storing-Data.html","eaa644b96de80a517c8dc26aa30bfe09"],["/Websites-CMS-Platform-Storing-Data2.html","fb73f7ab28db1509df545c2de834ea41"],["/Websites-CMS-Platform-promises-part-2.html","1146a401f000c148796c5671bfbf90d0"],["/Websites-CMS-Platform-promises.html","a14bc962303d0a4d6686454f5d99e9f4"],["/better-affordance-js.html","ca445222280e9e30bec73a8389d64733"],["/better-affordance.html","6f4d931c210f233e8becdba3e29bfe19"],["/categories.html","29ceafced636f37fa6a659ed7a8f6986"],["/dear-diary-year-one.html","63eb5ad8ec8b6053f2e6d117c550abb0"],["/fun-with-structs.html","94bac02e6e923da6a4d1cb10025763a8"],["/happy-numbers-kata.html","5dfb86aca628cb8f92f6159cc4eb8a45"],["/images/1-no-event.jpg","1fed4f131651de4ab49e4aece56cfd88"],["/images/1.jpg","5f00566ed6cc0a69ea1d00217ed5755c"],["/images/2-events-written.png","67a033a4ae5b596ebf499d80b929645c"],["/images/2-one-event.jpg","ddb3c295e3f8f60c386b9c4eca9f4b9e"],["/images/2.jpg","1bb09839eca2a8320bf598a285c80324"],["/images/2022-contributions.png","37c287f4ad4ed995c681e947f55f66a2"],["/images/2022-family.jpg","043b4f2e0a20ec765cb668b4589fee6d"],["/images/2022-travel.png","dcc6355497064c5dfe5b854f9aaded9a"],["/images/2023/03/01/drawing.png","4866ae63d15bc7d92ce1685dd58d2a5f"],["/images/2023/04/02/planning.jpg","077ffd2aa8f98ceed9c62a2cf2c40f31"],["/images/2023/04/02/pool.jpg","4ec778a59ea6ee4448c8a6e2adce53b4"],["/images/3-many-events.jpg","ced75a3200e46952147eb108ff869ad6"],["/images/3.jpg","08882002e7dba0267e0f4f2acf79ed6b"],["/images/300/1-no-event.jpg","8b415b02f32601887f6eed287164517c"],["/images/300/1.jpg","94519ab441c0cd1bd9ffcc5af58550f5"],["/images/300/2-events-written.png","4cf6a8d45bd32a765a9db56ab24450d4"],["/images/300/2-one-event.jpg","8e8ee314d0d770c405fcf2081f1a5d69"],["/images/300/2.jpg","8a9ebb45fd241701b86734adfc0fc159"],["/images/300/2022-contributions.png","44f585abf44cd0be061952e601775dcc"],["/images/300/2022-family.jpg","910917766f6438b9cff0b0f0d0a490e7"],["/images/300/2022-travel.png","ec4b1cd07fb08379fdedda572c13c28c"],["/images/300/3-many-events.jpg","4a2384f470f9b968eb8eee760f3e5834"],["/images/300/3.jpg","0ab9a7d853c4dc4f69f365cf6cd159d3"],["/images/300/4-new-readmodel.jpg","f2b9d7c5a47762e6d95c80b38eacbe25"],["/images/300/5-caught-up.jpg","fd263b1a93c809e448ef57674214bdad"],["/images/300/6-graph.jpg","81145e84a5f648b049bad07084a1ba9c"],["/images/300/ABC.png","46a512d4dbbf5a638f74315b15caec32"],["/images/300/AMP-webmaster.png","c52aea35ba958f44f8da5fbc9cc28090"],["/images/300/GitHub-Mark-Light-32px.png","27f7497bd771f103f6e40cd65981b529"],["/images/300/actual_different_styles_meme_by_zeurel-d38c306.png","6c721ec9f658499bcc5436f26313f07b"],["/images/300/affordance-loggedin.png","07dd508c246919e33ac7a21aa0e093eb"],["/images/300/affordance-loggedout.png","c95f79bd89045b2f1ac1c57acdaee688"],["/images/300/agilecam.jpg","c50a086407a3f688626983a7caa21aa1"],["/images/300/api-console-output.png","0ec0ccf29504872fa879c64c2bf44377"],["/images/300/api-gateway.png","09915579beedd77eb202d37337b41315"],["/images/300/beach.jpg","aef5e20469908dc39f86abce46b22176"],["/images/300/bill.png","995afbfebccd69f42287f94e24684798"],["/images/300/both.png","834bc93867dcb752f5eef6da3cf2a3c1"],["/images/300/cardboard.jpg","11904351a6ee94c229bacaaadd2eb30d"],["/images/300/chickens.jpg","8ca47f1b2a43b0cb0e965dfb1f341604"],["/images/300/code.png","8dcb9460b3a367c8a1cabbf6798725b6"],["/images/300/common-language.jpg","c35f51cf0e31e4f6a23ecf5ec7c1f8ab"],["/images/300/cqrs.jpg","92f3b2d7f4c437d0b4cae246a5571af0"],["/images/300/crafts.jpg","86460ef882f76ffc03ba8fc9fca6ad9e"],["/images/300/current-sequence.jpg","f2fadf070382c839f38cc0a90ecaef6b"],["/images/300/dog-2019-01-01.jpg","fc410a9f2b93eab5702b03ba3ec6f355"],["/images/300/dog-2020-01-01.jpg","d6594d4b77ab53d11c3c7d363ee2343b"],["/images/300/dog-2021-01-01.jpg","00f13b473279d7a73c32db3cc9db9879"],["/images/300/dough-balls.jpg","c314ad70d625b8cc0d73bd6cabd60a75"],["/images/300/dough-wide.jpg","12b3c9fd3e69482e5547587d9f2d5232"],["/images/300/ducks.jpg","3ecb3a183943b1151784f35d5c3047bd"],["/images/300/dynamo-cheap-perf.png","3ea433609df3e72c3bf4d8072e89a908"],["/images/300/dynamo-console.png","4d9ddb433093616b4ffdd27c574c3e37"],["/images/300/east.jpg","dd9d8cab2c708009aa34e01a8b53ce13"],["/images/300/event-composed.jpg","47afc74a716e7cde8bbf3f4247df661b"],["/images/300/event-notification.jpg","736d803982ef0beffdfbc313e3633966"],["/images/300/event-sourced.jpg","ce41ce31f8255d849c3b4767805b6fa1"],["/images/300/facebook-black-32.png","5c7605ac74aee44cf908538c46691c13"],["/images/300/fifteen-dependencies.png","c3ac74c8ab0db24ededf5d1354043adf"],["/images/300/first-slice-2.jpg","610e1ec20e490874fc81bd8460b273cc"],["/images/300/foggy-day.jpg","949fd4c4a0e515e5a58dc456433efed5"],["/images/300/forecast.png","fc8042ae1d18ce4e0a8913e85b8e09de"],["/images/300/god-of-death.png","74d94d013f99e23be2ff666fb50a439a"],["/images/300/helpful-advice.png","235df6eb267705312b31fcfb8c7207af"],["/images/300/home404.png","bebafde146b901baf9df57fc779d1730"],["/images/300/homeBare.png","96cb2ce62554dc03b0e16a8fb91c72ee"],["/images/300/homeCarousel.png","054dfd9c483881037bf8ac487dda5262"],["/images/300/homeFull.png","3a3a4deeba2857a6b73f5109f2609a2d"],["/images/300/ideal-board.jpg","3228ba64d204bcefb87a6704de71991b"],["/images/300/initial-commit-log.png","9fd7202b4bd298d180dc773a35d85341"],["/images/300/integrates-with-github.png","1f4739ecf9e065357391ad23763127f9"],["/images/300/interactive-spelling.png","6136e6199d4c162fae8a6acba328260d"],["/images/300/lambda-console.png","1886fdd1240b4a16c79060b6c4456807"],["/images/300/logo.png","694aa93fc50169105d632b82b5ad7f33"],["/images/300/lost.jpg","bef1030f009e3cfba7dd2d762d6a5907"],["/images/300/mockup_5.png","0109764e57efdcd990859479ee6a4513"],["/images/300/new-sequence.jpg","80e3bebf4ec0cf08877e00efa0af5fc7"],["/images/300/nonewline.png","1374e73abb337aa17cc401f4db5346af"],["/images/300/npm-run.png","ddd25c92d5f3baa7d69bf9fa50669e09"],["/images/300/objects.png","688f9cd06728befc16e77ba03155e9cf"],["/images/300/one-board.jpg","1b9d648e509979281697c095462777a0"],["/images/300/part-four-flow.jpg","95584ce15873d78830fe8f9e8690a0d8"],["/images/300/pasta-fasule.jpg","9530163bcd2892d2c2bf686d40b0c568"],["/images/300/pepper-1.jpg","a48c0be1060a7615d9a796db2b097f25"],["/images/300/pepper-2.jpg","a5b04185a14e93d213db22450695fc66"],["/images/300/pepper-wide.jpg","222b63a74a30805e25c1f6d60847b3f3"],["/images/300/personal-access-tokens.png","19fd7b1512b4eb6249437180cdb56114"],["/images/300/radar.jpg","1c212f677d49a7c99ec26e501683693c"],["/images/300/reactotype_screenshot.png","769891963c483bcba1535fce0128b2d5"],["/images/300/run-nightwatch.png","17dbc2c513e0e4157344d27da04ea9fc"],["/images/300/run2.0.jpg","6632bf63cdb3d78f23e8cd34112ee44b"],["/images/300/second-slice-4.jpg","8c0e6c1d0549fa30bdfa56ba76b13e5d"],["/images/300/serverless-maintenance.png","658912fbabaa75162505fe8107ae03be"],["/images/300/serverless.jpg","c6c44646e69a53f2d9ecba87a2df498b"],["/images/300/stack.png","4aeef068982c01126c35e4068a09f16b"],["/images/300/start-api.png","e0ec0a4c6fb6c26e16f54eca5ae49fad"],["/images/300/structs.png","a92895807d8473341f4064bc2fe10a1a"],["/images/300/structured-data-crawled.png","21f480a644218bd03adc7a02774536f9"],["/images/300/sunny-day.jpg","188e7ea7a40dcaa7d8ac614fb4bedeaf"],["/images/300/tada.png","6a19bd23d1c99b7b32470ae4dd984833"],["/images/300/testing-api-1.png","4647459b1fb6cc73a243b7d27fd51740"],["/images/300/testing-api-result-2.png","5bf70cadbc7feb0b4a08543a27562b9e"],["/images/300/the-chart-1.png","082f6e2b26b2864a3cec426d9d337e48"],["/images/300/the-discussion.png","40dc38ffb136a5536bb64b5691edf42a"],["/images/300/the-graph.png","000399ea2efb4e355e4570fcc61fac23"],["/images/300/the-quadrants.png","89b7deb6fb5382d9e2dfa32b9fccdbed"],["/images/300/toot.png","1becd9cd4ebe4e7adfa8203457c5485a"],["/images/300/travis.png","9b8822912d80d2c7ae95b8065745f0ec"],["/images/300/tree.jpg","290f734fcdf49648d09ecc86250296e2"],["/images/300/twitter-32.png","0e21335ff0b1b322f75eaaeb32b81ef9"],["/images/300/twitter-black-32.png","c279b87a4805d22ea078147ff065f8d3"],["/images/300/unhappiness.png","1f2c8170b420c3ad4ebc4a8bb1af9d0a"],["/images/300/votes.png","4f5419207fca7e392a995164ccfbd004"],["/images/300/weeknotes-autoload-graph.png","6fe496702b8f1a81fed48d11592eb935"],["/images/300/weeknotes.png","2eadc54c31f4c79e2239b3aaa1aa32cd"],["/images/300/yarn-desc.png","e1392cec036db997d4a8e71ac2be8523"],["/images/300/yarn-run.png","5f5c27a364514228e9c6af5c54043462"],["/images/300/zero-velocity.png","8558fa64f3782d8c9ff38a31fbc289a5"],["/images/4-new-readmodel.jpg","abca23326d158047986f6ccb8221d156"],["/images/480/1-no-event.jpg","be0eeb53819930b79455e43730a03250"],["/images/480/1.jpg","2f88949815eb7cdc916840ce8a702fa6"],["/images/480/2-events-written.png","e32e74a826cd28240884dbef17f27998"],["/images/480/2-one-event.jpg","cc33b07109053e3146ebee10944c2318"],["/images/480/2.jpg","ac441d98aef0858b1c65a847921beed4"],["/images/480/2022-contributions.png","45131f29b8175ebe968fc45384e6b890"],["/images/480/2022-family.jpg","691503023d2c0fd6760f52bcea5a84c4"],["/images/480/2022-travel.png","e7cc887cb8ad4ddf244fae54f7f6378b"],["/images/480/3-many-events.jpg","dabe25c09d5ba16c47e2902b2f935162"],["/images/480/3.jpg","1b300ba9ddd0302acf512e8b60eac378"],["/images/480/4-new-readmodel.jpg","998aa6387d5a25e4457733647bed69bd"],["/images/480/5-caught-up.jpg","0b1a5b44929cdabe72540887ea47f52f"],["/images/480/6-graph.jpg","04544b19fd3a70729a6cd8d676cd4fde"],["/images/480/ABC.png","f57b16f8eb1ec6251fa9517acbcfd983"],["/images/480/AMP-webmaster.png","faa3f0e481fa440de7c3f3706e81991b"],["/images/480/GitHub-Mark-Light-32px.png","27f7497bd771f103f6e40cd65981b529"],["/images/480/actual_different_styles_meme_by_zeurel-d38c306.png","fd84f9d47e95a13c706d9cd62ad30d69"],["/images/480/affordance-loggedin.png","4acbfa0398b0e1876b0fd9fd0b2429de"],["/images/480/affordance-loggedout.png","6e05b899ce85cfee7ad05b432fdc699f"],["/images/480/agilecam.jpg","47387066f9964a972a84fdb86ad36350"],["/images/480/api-console-output.png","598580ce630e5e8213aa54207f33937c"],["/images/480/api-gateway.png","06caf16cb187fc71387d3561552cfd7a"],["/images/480/beach.jpg","fb25de4338ae12c5534eb181fa562ece"],["/images/480/bill.png","fa975a75bbdc34bd1d66aea1cd0479c4"],["/images/480/both.png","7c60fa5e45c27bed1328f213d51f8a2f"],["/images/480/cardboard.jpg","40f8a2ff39b597d0e4b9da032f475b13"],["/images/480/chickens.jpg","7e03fc3b2a53497455476c069e7478a0"],["/images/480/code.png","bd2e76c66cc65f111f5c45257aa362f8"],["/images/480/common-language.jpg","6176a1185e8cd216db2a077d954bd517"],["/images/480/cqrs.jpg","7cb0fd7e99f87d4ed05de3cd0f8892a3"],["/images/480/crafts.jpg","28691b36c88862bf82c507228f53e78d"],["/images/480/current-sequence.jpg","07d42f33bbfbfbea9dc1921e21eb7254"],["/images/480/dog-2019-01-01.jpg","d927f4f4046867e799990d08a3fff3a5"],["/images/480/dog-2020-01-01.jpg","74b8f0acfebe3bf89623be67f0f3dba5"],["/images/480/dog-2021-01-01.jpg","d837d9291d40cb85405ed988d9fac855"],["/images/480/dough-balls.jpg","5e3712ac8ae94cdc06ec335e86bffafd"],["/images/480/dough-wide.jpg","fba18d6ecf192f6fe9cdb9291254ac2f"],["/images/480/ducks.jpg","5b89514470923fb20949612eac101aac"],["/images/480/dynamo-cheap-perf.png","f1fbe9f95fb258116704c0f63c75ff78"],["/images/480/dynamo-console.png","0c888dc2ef4cfb201587a2bf471be8d1"],["/images/480/east.jpg","c1235413e53d543f9a44d25d71082e71"],["/images/480/event-composed.jpg","a6b02faa73f7eb4316aaa68e5604ecef"],["/images/480/event-notification.jpg","f169f241929c0345ef25ac7a18647a0c"],["/images/480/event-sourced.jpg","bf11e76a3dd23ae7e21c4f7ef9683cef"],["/images/480/facebook-black-32.png","5c7605ac74aee44cf908538c46691c13"],["/images/480/fifteen-dependencies.png","43150fa804d067249897883c8f85f411"],["/images/480/first-slice-2.jpg","38d186d5d145424e07ed0dba1c668766"],["/images/480/foggy-day.jpg","6279c5a7b47073bc297152701a365535"],["/images/480/forecast.png","1654aa59b4a4ffa91b23e3e1444f5cbe"],["/images/480/god-of-death.png","32df77d03bbd35bd2592951d65cb4d25"],["/images/480/helpful-advice.png","d43b3bac23e638d3147f4cee52b897bc"],["/images/480/home404.png","9cb173be84a875b40e63c293026c83b7"],["/images/480/homeBare.png","8e6b26baa8a3b39535f68238a32edd28"],["/images/480/homeCarousel.png","2692660218e7ee9e6b1952b96430e9be"],["/images/480/homeFull.png","f2b9c6185f555ead58e56a2fb6fa63e9"],["/images/480/ideal-board.jpg","36f51069bf84b8e0d3a94d53258f49cf"],["/images/480/initial-commit-log.png","5b969618e5234aae1adf5d061cf635e3"],["/images/480/integrates-with-github.png","3dc77f1801b9271944ae7e72a99a3b36"],["/images/480/interactive-spelling.png","8910d256cb8259e41a416806899d82c0"],["/images/480/lambda-console.png","424b17cc189d7d174d1bb883cf1067e7"],["/images/480/logo.png","694aa93fc50169105d632b82b5ad7f33"],["/images/480/lost.jpg","ec5d7e5898e515ef4d0f06135e1e1847"],["/images/480/mockup_5.png","19dd04db2551a113bffcd8304ed5e4c6"],["/images/480/new-sequence.jpg","42d5476a1d17454364e8adeaae66f929"],["/images/480/nonewline.png","606b55fae5dad8a5e3efcc55e58a3b67"],["/images/480/npm-run.png","3b057e3aff5c81a7866b01f3f3b9244a"],["/images/480/objects.png","949e97f4240055930779e965a70fccb5"],["/images/480/one-board.jpg","8b83f596ea93fd9b2f1752514e5720ad"],["/images/480/part-four-flow.jpg","0b87ef49e953622a7541010a6126fbf1"],["/images/480/pasta-fasule.jpg","27f11ed09609d3c4cf98f3cc8df8fbf1"],["/images/480/pepper-1.jpg","6ee15909b8d62747d8b97361c230ab51"],["/images/480/pepper-2.jpg","3796c90ed5d943d4d362c878ad3ecbf8"],["/images/480/pepper-wide.jpg","c497cccb4a74618497b8ecb77234e04a"],["/images/480/personal-access-tokens.png","fede6fa9cd520bf5ea8aa9721d284280"],["/images/480/radar.jpg","aae0514f9d4b2da363ee9f7129e38aa7"],["/images/480/reactotype_screenshot.png","808309af1379a01a3986ae4b978981e8"],["/images/480/run-nightwatch.png","029377b6ee7e81181eff584b9d5fc008"],["/images/480/run2.0.jpg","8d9476dd0de98978e7368bc39f5de8f8"],["/images/480/second-slice-4.jpg","07a86ec5d732acf06d64b93e9526945b"],["/images/480/serverless-maintenance.png","d8548d5a8a54bd49a1692deaa365b6a2"],["/images/480/serverless.jpg","5867fe9fa51fc4068b2b9f0eee973bfd"],["/images/480/stack.png","b3e3e510343c09e66fc0798ff84f035e"],["/images/480/start-api.png","159a4c8f4f60e61a6de0c2b1e55546e8"],["/images/480/structs.png","503c1fcc9d8fddb30510f8aac73fc649"],["/images/480/structured-data-crawled.png","fa8b0bb26128576c5d1bbe18ec8fde7d"],["/images/480/sunny-day.jpg","441835daeb175e742051c137eadc554b"],["/images/480/tada.png","647fa8196bf727a6d68887bdb3770135"],["/images/480/testing-api-1.png","607daa2b4bd7b4d357be91a2f6640fe1"],["/images/480/testing-api-result-2.png","54f539abbdeba364e2d3695c158c0f72"],["/images/480/the-chart-1.png","017ee742d12aa29eabe356450913dafb"],["/images/480/the-discussion.png","56acfe21e4104447b32b4d0931631e26"],["/images/480/the-graph.png","e2c75ce76a60d1e4d2c7e4f1c7a28903"],["/images/480/the-quadrants.png","877e3650a6621826a3e4e4be0ce94631"],["/images/480/toot.png","1b8fc1a6be2c8cf8033126b8bf608d67"],["/images/480/travis.png","886cda7b912c1535924304753b929cef"],["/images/480/tree.jpg","37855622ff1ad2a01ad60aad92e2fe73"],["/images/480/twitter-32.png","0e21335ff0b1b322f75eaaeb32b81ef9"],["/images/480/twitter-black-32.png","c279b87a4805d22ea078147ff065f8d3"],["/images/480/unhappiness.png","f32366249405aa4dd661b9f4f376534a"],["/images/480/votes.png","e944f47f3aa3a751e3b3199431c197f7"],["/images/480/weeknotes-autoload-graph.png","76eaed87458653e7e95e2710d839f650"],["/images/480/weeknotes.png","01618ca7cbe20103c25a503129e0f3ac"],["/images/480/yarn-desc.png","5d00326afdc2845c13f57fbbf0793bb2"],["/images/480/yarn-run.png","b555174f4f2ad7708481bd426f8e133c"],["/images/480/zero-velocity.png","95a4c4a148a4fc26644669d65097d7a0"],["/images/5-caught-up.jpg","a1c39e67a4d5fd9d20b569441c3564d9"],["/images/6-graph.jpg","0479708729b16c42155cd460f9f45bf4"],["/images/ABC.png","f57b16f8eb1ec6251fa9517acbcfd983"],["/images/AMP-webmaster.png","d27fa84d401044d9f7b4102e22173bb0"],["/images/GitHub-Mark-Light-32px.png","27f7497bd771f103f6e40cd65981b529"],["/images/actual_different_styles_meme_by_zeurel-d38c306.png","3a1266003cd719d97baa814a33423939"],["/images/affordance-loggedin.png","4acbfa0398b0e1876b0fd9fd0b2429de"],["/images/affordance-loggedout.png","6e05b899ce85cfee7ad05b432fdc699f"],["/images/agilecam.jpg","d76677e06ada3edeed1b0137be93c6e9"],["/images/api-console-output.png","11d54779fa7e4fd3ca82847e6262890e"],["/images/api-gateway.png","29cdfe963460f2f62b1476080fe16209"],["/images/beach.jpg","1112f690c57d20aeafc0e3caa0a70687"],["/images/bill.png","0a3050d854e186b802c6f791e210d3ce"],["/images/both.png","117be54b1fd4be093dbe09d86e8b9f39"],["/images/cardboard.jpg","c2fcdc95a4bbd8c9e37fdbd179fea6f5"],["/images/chickens.jpg","6045a8c71df67e2136dff627f8f9050b"],["/images/code.png","7dd836bb1dd9c95db01be8da8327de19"],["/images/common-language.jpg","f2efd3d553241239665916c8ecb132d1"],["/images/cqrs.jpg","9a43a1b926021619101b35e97ec67d71"],["/images/crafts.jpg","c6528adcb8618432dd22b0e05a389751"],["/images/current-sequence.jpg","340aa9724f411578a9adb93c6d563531"],["/images/dear_diary_year_one/1.2017week1.png","14c7067175b4fe7ddb3e51ff9b4cf3c5"],["/images/dear_diary_year_one/10.2017week11.png","f092f0175888d5a5b34f3ce1a9053521"],["/images/dear_diary_year_one/11.2017week12.png","026470f234c6365b70dafeef4e0954f4"],["/images/dear_diary_year_one/12.2017week13.png","6343b7c5143cbdc1ad41ec0e674a8eac"],["/images/dear_diary_year_one/13.2018week1.png","6c728b10a0c57a65a9f7ec889b180e49"],["/images/dear_diary_year_one/14.2018week2.png","92aecb09ca5292dad51a71bf85dc24a1"],["/images/dear_diary_year_one/15.2018week3.png","a757cfe7249032d4e32645073c46ed05"],["/images/dear_diary_year_one/16.2018week4.png","c05e98417ba6f36d71d2dcd6749548e8"],["/images/dear_diary_year_one/17.2018week5.png","fd32de3fc67c0fb53acec759da3b8d1b"],["/images/dear_diary_year_one/18.2018week6.png","53a96952dd96873f598c078bf7e36916"],["/images/dear_diary_year_one/19.2018week7.png","944d98b762359dee4bdfc776dfc77cd5"],["/images/dear_diary_year_one/2.2017week2.png","3034124b2de5e032b52b6b55acad9f5f"],["/images/dear_diary_year_one/20.2018week8.png","4b8b885ebf0a756c419696e0cb0883b2"],["/images/dear_diary_year_one/21.2018week9.png","57fdfe4275a18b46f8d78ce708cc3d94"],["/images/dear_diary_year_one/22.2018week10.png","baf13ea472fb1878c194a6c5af69bd97"],["/images/dear_diary_year_one/23.2018week11.png","634e992bb990394b74032238a28bd937"],["/images/dear_diary_year_one/24.2018week12.png","12a16c102d4818d1b1cff162d237d9f9"],["/images/dear_diary_year_one/25.2018week13.png","8ad2fc7e3ee16ad056f3bbfc295541c7"],["/images/dear_diary_year_one/26.2018week14.png","bbee64e125d6bebfab9456700bbbf487"],["/images/dear_diary_year_one/27.2018week15.png","644a3ab4e91145eb395d386bb9dd2062"],["/images/dear_diary_year_one/28.2018week16.png","e901f3875c1202a415e16c51e72a3df8"],["/images/dear_diary_year_one/29.2018week17.png","e72986889e6fa158e15a7be0d37d6639"],["/images/dear_diary_year_one/3.2017week3.png","3fcb0902312504a1a1d898df49c7a075"],["/images/dear_diary_year_one/30.2018week18.png","091f747835dec5d168c025e51f0aab75"],["/images/dear_diary_year_one/31.2018week19.png","3ccaea946648cd6ab95c5afa5f921a6e"],["/images/dear_diary_year_one/32.2018week20and21.png","4fb9bacda295b0c80bfa3b2a587847f7"],["/images/dear_diary_year_one/33.2018week22.png","6cbd8e184952c6b27b134557a15f1af3"],["/images/dear_diary_year_one/34.2018week23.png","a4d2a3503eadbe3e8379b91a301b733e"],["/images/dear_diary_year_one/35.2018week24.png","6017308f8a701cd6896e6ac7c8186f87"],["/images/dear_diary_year_one/36.2018week25.png","62d703a3ab54c90342afc3ca07dd4550"],["/images/dear_diary_year_one/37.2018week26.png","06163eecc9967eab61629d0196d41694"],["/images/dear_diary_year_one/38.2018week27.png","2c4accf3ec27d58731598146820b51ac"],["/images/dear_diary_year_one/39.2018week28.png","e71205825b5b42acea8cb95639553b9a"],["/images/dear_diary_year_one/4.2017week4.png","4d82a3ab152a7062fbb6388cddbc8e72"],["/images/dear_diary_year_one/40.2018week29.png","2bf249f14334d2f5c02a701ac05774f2"],["/images/dear_diary_year_one/41.2018week30.png","1426d11ced5b604a34ad412764216cd9"],["/images/dear_diary_year_one/42.2018week31.png","6dc08d2275ca6ade92c9ab0f601b4b91"],["/images/dear_diary_year_one/43.2018week32.png","cc103879b4532a49df1c6f1a8ce028b1"],["/images/dear_diary_year_one/44.2018week33.png","57d87d660c43bfa923c2e22c842bb0b2"],["/images/dear_diary_year_one/45.2018week34.png","c69414fc5f7cc95528e9a0547d345446"],["/images/dear_diary_year_one/46.2018week35.png","f6a78bb1963cd3663e7e43044ffd800c"],["/images/dear_diary_year_one/47.2018week36.png","0abae18c076ca2161c37eb739983696e"],["/images/dear_diary_year_one/48.2018week37.png","4d212647549a7553ebf7a0cfb125a0a8"],["/images/dear_diary_year_one/49.2018week38.png","8643ed63c86ff774bf53dd2ba96af6f9"],["/images/dear_diary_year_one/5.2017week5and6.png","cba25b318cd1a48c350e16410e141592"],["/images/dear_diary_year_one/6.2017week7.png","68c4d7aba312db46ff8657c8c36f278d"],["/images/dear_diary_year_one/7.2017week8.png","412eac1a7d20f00779905a5ae337b1a1"],["/images/dear_diary_year_one/8.2017week9.png","22de0e9a48b3cdb20331bb3f93d1bb05"],["/images/dear_diary_year_one/9.2017week10.png","d3894eb2051688a0609a857899197e85"],["/images/dog-2019-01-01.jpg","6fede355c9f5a1dbc915ec328431c783"],["/images/dog-2020-01-01.jpg","cabd5799879728620e528a33af49bbe3"],["/images/dog-2021-01-01.jpg","90f430f8a8ebd25f3c9eb55ce56c233d"],["/images/dough-wide.jpg","316385a0e544ad2a1541a030ad133bf1"],["/images/ducks.jpg","73c8eb91b7d788c57732b45db8fe99ea"],["/images/dynamo-cheap-perf.png","df79f29c0268929e045363f9b1715112"],["/images/dynamo-console.png","194642d0077cfbd67fb7e3e8125f30c6"],["/images/east.jpg","5231497a5f9229026c6db03a2b74612a"],["/images/event-composed.jpg","ac10d81fcd1c40f3b8ddfd1993ff1456"],["/images/event-notification.jpg","b4c7618a16038eeee7049419a7aaf831"],["/images/event-sourced.jpg","2d559ce2f60052cc296576f3f0613d63"],["/images/events/2-events-written.png","67a033a4ae5b596ebf499d80b929645c"],["/images/events/5/1-no-event.jpg","70168e4456388c01350576729a84d0f0"],["/images/events/5/2-one-event.jpg","cd75feda4a10d531fc2ce5e3008bdb99"],["/images/events/5/3-many-events.jpg","d84809dae82e55f31d0877c2b9f349bb"],["/images/events/5/4-new-readmodel.jpg","9dda45e60275570f33b0746e36f68c1d"],["/images/events/5/5-caught-up.jpg","fdad5af8791fc0a18a07d6b169da19a1"],["/images/events/5/6-graph.jpg","2cb774d4dc3966b9c056225594517731"],["/images/events/6/bill.png","0a3050d854e186b802c6f791e210d3ce"],["/images/events/6/current-sequence.jpg","3486dccd4a937cef7f941dba471e720d"],["/images/events/6/helpful-advice.png","cb07fad6486ba939fd91458dfe60ca1a"],["/images/events/6/new-sequence.jpg","46f44e9c2c1846562e2aa4bb18fd3ff6"],["/images/events/api-console-output.png","11d54779fa7e4fd3ca82847e6262890e"],["/images/events/api-gateway.png","29cdfe963460f2f62b1476080fe16209"],["/images/events/c4/1.jpg","c3ee3ea8942ec18854e0ab289e3e003c"],["/images/events/c4/2.jpg","5b4d7f542b5497f7e9fe40da740e28f7"],["/images/events/c4/3.jpg","4a6af9eb7929946b02554a11edc03978"],["/images/events/c4/first-slice-2.jpg","e07db3395b698125ffd1489a772a0425"],["/images/events/c4/second-slice-4.jpg","39dbff321bc46b2b3657147c2684bc58"],["/images/events/cqrs.jpg","7d3f1c9b1b1f1c117e9742f13fa2355e"],["/images/events/dynamo-console.png","194642d0077cfbd67fb7e3e8125f30c6"],["/images/events/east.jpg","788953a7200d8cef717d0f2df8c58483"],["/images/events/event-composed.jpg","ae616b97aec2d67d3f64688ec153c333"],["/images/events/event-notification.jpg","fcad6ec5a76ebdb15f962be3bd324762"],["/images/events/event-sourced.jpg","c7a48bca9e47506437049d4476c434f5"],["/images/events/lambda-console.png","5b7910271c8ae2ac56c67ea8a6c009a4"],["/images/events/part-four-flow.jpg","1cf13f9d84b5d64526315467b5be1824"],["/images/events/serverless.jpg","eb3d17136cf3526aa781ab5e613aeadf"],["/images/events/stack.png","3612bee6a7ba5356a8052d880c034b3c"],["/images/events/start-api.png","c408b21dd3dd2f0ca9ce0656b4fe0f3f"],["/images/events/testing-api-1.png","629bc6a72681c9746bb289c239e764a4"],["/images/events/testing-api-result-2.png","dd3a40d355e99902bceaad05a2b29ea9"],["/images/facebook-black-32.png","5c7605ac74aee44cf908538c46691c13"],["/images/fifteen-dependencies.png","3ffc601966da8eac80f48a2af0add3c5"],["/images/first-slice-2.jpg","da9312909b621990c26ae3609b77afa7"],["/images/foggy-day.jpg","caa63eabf76307b22998098b074830f2"],["/images/forecast.png","0dec546d740a84998e5ede60041f83fa"],["/images/god-of-death.png","9523f351ea74d10c382ca97dbdac7094"],["/images/helpful-advice.png","cb07fad6486ba939fd91458dfe60ca1a"],["/images/home404.png","b4ad9d10c8d8f771c7b5029bd50da432"],["/images/homeBare.png","aab499ba0c1c687bff7d60da3a83da5e"],["/images/homeCarousel.png","8054a67e81dcf1d2fde313a783a6f9cd"],["/images/homeFull.png","a3310503911535e2913214287f6d818d"],["/images/icons/icon-128x128.png","54cd283b0c1ec90108ce2051b15bf57a"],["/images/icons/icon-144x144.png","42b7873de7e33efe5bc966b4ff964dde"],["/images/icons/icon-152x152.png","30efdf4a576e11018b43d1df85665130"],["/images/icons/icon-192x192.png","2c5512011d149f48e7f3c210aa3c4584"],["/images/icons/icon-384x384.png","c202a45dfc404f2f315e8cc6cbf94d3a"],["/images/icons/icon-512x512.png","b9871b7b10d0582cbfcc8be37c31f9a4"],["/images/icons/icon-72x72.png","273f86f63d3eba378592de686ef6f33a"],["/images/icons/icon-96x96.png","157cf8c09db0ce0ee91cfd1fbf56135b"],["/images/ideal-board.jpg","92fa6deeb804a59f465ce2ea9887d563"],["/images/initial-commit-log.png","b0a6bdf1272a9e26350358a136c0aef2"],["/images/integrates-with-github.png","2172905f34eb6a44c908bfce1d60a501"],["/images/interactive-spelling.png","5c72133361d6a014f051a9e5ce52ce35"],["/images/lambda-console.png","5b7910271c8ae2ac56c67ea8a6c009a4"],["/images/logo.png","694aa93fc50169105d632b82b5ad7f33"],["/images/lost.jpg","4b3be674866c6247591da6077a89d092"],["/images/mockup_5.png","7e8a346b20c0c82e10c018d5cd48ef7b"],["/images/new-sequence.jpg","e4999e78f571992a9321ab99434efb62"],["/images/nonewline.png","5ae8bf57fdf185b6163b3bbf9e1b25e7"],["/images/npm-run.png","12746c403e0d9beb38006bc051518488"],["/images/objects.png","62ae5574aef54e97ee041318a87fef45"],["/images/office365/10.png","031cd3ea3569a163d80bdd65e15fc734"],["/images/office365/11.png","4d2d70e91bd99ea5a59efcf4dc533c50"],["/images/office365/12.png","b2b8e62d39b026301a49b6c6ef0afb78"],["/images/office365/14.png","91f0d277d39aae5dd08e6662d401bae3"],["/images/office365/15.png","d490e4568e3cd9fa16871718680c5715"],["/images/office365/21.png","46e16ef8707fa4f3c7b625b1aa85097e"],["/images/office365/22.png","3623920b00eeb1e0c414fb38bfb2ff8b"],["/images/office365/23.png","06a37d393363e7cf687cae4c62a3e769"],["/images/office365/25.png","7255578b47b644f215154cd0ad0df993"],["/images/office365/28.png","6e4a14da26d7cc530db920073045bb69"],["/images/office365/35.png","ad7550bf0f56dc1cc5bfd6a5c983c40c"],["/images/office365/40.png","3631c1372f8a30e431598c8ef3c6f177"],["/images/office365/42.png","e86f6dec5ff14c7829e2184678d36f61"],["/images/office365/46.png","49f8065354e9e17cf55ce72e7ad00569"],["/images/office365/47.png","a16d6184987dcb34f7df72ac9b51e135"],["/images/office365/48.png","e9ea49808c8ab61fbdf4ad0f3c9e22a8"],["/images/office365/5.png","785c18a10706d5c775f9c27536ef1a4d"],["/images/office365/50.png","db36f58f4cff55332cc7113c353032a4"],["/images/office365/51.png","b23c53ad38a9ec40ab1737c2d38ed826"],["/images/office365/52.png","f69450e57f7cdcdd1ec9029a6e41f4b0"],["/images/office365/54.png","cf6435593f00c899ae9c11d36538eab2"],["/images/office365/55.png","e4eb718efaf37dc043a1bb64bbc78852"],["/images/office365/57.png","f287a7df1647a28a10f1946770d146fc"],["/images/office365/58.png","cfecc32ffa5d988648b1b2482bd66199"],["/images/office365/60.png","8b13c4baba5499fe34587bd1f29f94e3"],["/images/office365/61.png","d4fb2bc8806721856610da9b9374a3cd"],["/images/office365/62.png","986d5fd7328e9e3dc915210213632a6c"],["/images/office365/65.png","187202435b7fc3e99420353a394eb600"],["/images/office365/68.png","bf257e29144e99df4ee00bbad7b55f97"],["/images/office365/7.png","9c8e29e7d5a9a3d92ebc2944bb681833"],["/images/office365/8.png","18f1b1af59e21ce4b9534b91b55d539b"],["/images/one-board.jpg","943cc8002737af095ae31844e9565172"],["/images/part-four-flow.jpg","b2f35eb42bdbc40cc95518fa1f6faf7b"],["/images/pepper-1.jpg","951c2328816bbbab26000acbc3b3b279"],["/images/pepper-wide.jpg","1d13326a7e4f7ae9393b23d3355bb710"],["/images/personal-access-tokens.png","08cc595af59d3694bd190cf762b5db00"],["/images/radar.jpg","ebfb218bc80abb2d4aefab52bde73214"],["/images/reactotype_screenshot.png","f6aead9c67782b19138b15a01115c1bd"],["/images/rss.svg","a0ac3b30d914a734a53a18587a9f4761"],["/images/run-nightwatch.png","027d2f57764c4eb0db860af39977cbdd"],["/images/run2.0.jpg","e7436a10ea8f30c266add9e9a65ddd6f"],["/images/second-slice-4.jpg","04ad1d901864e5fcab58f5d4262830e4"],["/images/serverless-maintenance.png","e749f4b5bd5e3e0853d7c3af14a8027c"],["/images/serverless.jpg","2b5e25f5a8faa9ce655f835cee1cbb15"],["/images/solar/feed-in-tariff-graph.png","358bbabff1aff602150e515060e9be0f"],["/images/solar/panels-east.png","dce455dfa894231703f15db73446e2c1"],["/images/solar/panels-south.png","33e75834dc6a885468e85b838f64f2b3"],["/images/solar/production.png","82f84fd541e711eda5121450909235de"],["/images/stack.png","3612bee6a7ba5356a8052d880c034b3c"],["/images/start-api.png","c408b21dd3dd2f0ca9ce0656b4fe0f3f"],["/images/structs.png","20e52ced221b8657ce0092cfa75855e4"],["/images/structured-data-crawled.png","2055fb931d19b0627655eaf46291ec52"],["/images/sunny-day.jpg","47943c980a7e0bf94b034b439309944d"],["/images/tada.png","4d76d62a0249ba843899a30b8ec9d67b"],["/images/tag.svg","34a504803dc8f5e431aae1dd5e0e9d6a"],["/images/tech-debts/actual_different_styles_meme_by_zeurel-d38c306.png","3a1266003cd719d97baa814a33423939"],["/images/tech-debts/tree.jpg","977a11d3e6a2bf039c20ba4359949517"],["/images/testing-api-1.png","629bc6a72681c9746bb289c239e764a4"],["/images/testing-api-result-2.png","dd3a40d355e99902bceaad05a2b29ea9"],["/images/the-chart-1.png","0daed6266a23a8d5dbe1ab099e1164d2"],["/images/the-discussion.png","368647833e9f77ed21c3e86a58539ead"],["/images/the-graph.png","ca24c8130d51319114a1214f7e907264"],["/images/the-quadrants.png","e8bd5a9a6067c01ccfbbf1aa6c43b64b"],["/images/toot.png","98265e943395b18fd1bf9d0e8d39a1d5"],["/images/travis.png","b9ebe32d382c66382d91f2a187b0ea6d"],["/images/tree.jpg","ae695d1a52f6098847d55f3773e236d3"],["/images/twitter-32.png","0e21335ff0b1b322f75eaaeb32b81ef9"],["/images/twitter-black-32.png","c279b87a4805d22ea078147ff065f8d3"],["/images/unhappiness.png","f64c4bb1418782b59a87762be9a6e3e4"],["/images/votes.png","d4a02c6f0dc225adb9765de94ad50ec5"],["/images/weeknotes-autoload-graph.png","0c438bbc92251768375086b2084ab0b4"],["/images/weeknotes.png","e819bc1ab10021155905e1d7077c025f"],["/images/yarn-desc.png","84a3a4792dd41ba78ce955aeb374ebaa"],["/images/yarn-run.png","b195b8bce5baa57df6e6ea51a3dfafda"],["/images/zero-velocity.png","b0c33c81b898bb29e102f3a1c8351199"],["/index.html","d37c0d4c45826fde1a35d88daa529f38"],["/kids-games.html","5a1a08891ec4b5275e4829129d67f80b"],["/kill-if-with-objects.html","fc25e2dfef7df50be3a19099d7a328d1"],["/page2/index.html","ce608256f579e96d63cd5dd6d1326778"],["/page3/index.html","7e132d910e4226d484c5deca8c003f2c"],["/page4/index.html","a133b425135e7058a3a3580d62c5c900"],["/page5/index.html","e0073f901d8d03a82f3499b0742deb7a"],["/page6/index.html","4eca848a76444ce53554c7579edee818"],["/page7/index.html","cf12390083d6ba7c817de0ec0b003954"],["/page8/index.html","a0bc32e5aa54e34fc73d0741f0448968"],["/powershell-on-linux.html","71d163dcada6c1d86c5ae5846b58e00f"],["/rake-transforms.html","be27ec3121d75ac4a2a54fb1f31b0424"],["/reactotype/part-one.html","802b1178f6ca502232dffa3dff848ec2"],["/reactotype/part-three.html","7e10deab15b3e7a78ddc7457a7bba555"],["/reactotype/part-two.html","957eb2a3561e5c69ddc9f2a5174d7c81"],["/real-vs-soft.html","64cbd9e4e2bd7274b9eaabc963450747"],["/recipes/2022/09/pepper-salad.html","2cfda721c6596c3afbde1d17dd2aeced"],["/recipes/2022/09/pizza-dough.html","ae49181845b9bfc1ff1c3aab2c24fd9e"],["/recipes/2023/01/pasta-e-fasule.html","0f99631590cc6d0935559f009e225a42"],["/recipes/2023/07/zucchini-focaccia.html","af61c8c5e505a2df5bd1e703bccb5cf6"],["/static-factory-methods.html","5575dc322d4f7805e0d869776f51c437"],["/structured-data-with-jekyll.html","46347ad8fbc5a9141918aefa3a13fca4"],["/tags.html","0856340562b419aeff6318ca32b3374e"],["/using-travis-to-build-jekyll.html","b6c5a457bc4649cf21fc39a7725b8d4a"],["/websites-CMS-platform-logging-in.html","013db36f667fadad3be4fded4499c1ad"],["/weeknotes.html","9b12030f2aa905a013878bfb229460f8"],["/weeknotes/2020/10.html","9d7daae2b2f49b02dc14fcf85f6d1807"],["/weeknotes/2020/10.png","203067b883782882d27503efe76cb4dc"],["/weeknotes/2020/11.html","2887dd75d1ee4b26b8267654e2c02219"],["/weeknotes/2020/11.png","78d291942a9d9a1c36a819fe8a475e46"],["/weeknotes/2020/12.html","8f32ff740e749b95e8e5cecc2a6c8473"],["/weeknotes/2020/12.png","86a177a13e2e85c16cf05314d186d5ea"],["/weeknotes/2020/13.html","8c1602f38699e5a3f4d48f21e0ad95ea"],["/weeknotes/2020/13.png","d53ea17e9568adb6b1bcd5771fe6717d"],["/weeknotes/2020/2.html","40070b72402f218d03a115099309fde2"],["/weeknotes/2020/2.png","6901562d4f3f2b2f5036cd7b94c3d34e"],["/weeknotes/2020/3.html","fdff25e2b685264a80bbd03ae1b71c8b"],["/weeknotes/2020/3.png","bdc13ffa9466279eff7baf74decc216d"],["/weeknotes/2020/4.html","5a2e4409e95931a27d8d3c735072fa33"],["/weeknotes/2020/4.png","e284ab83a13a23fcc7e5f0b21f2fe512"],["/weeknotes/2020/5.html","0c9f6f2cb6e83ed01551770d1eb323ca"],["/weeknotes/2020/5.png","c072c61d346fe9dc8577701cb7f52db2"],["/weeknotes/2020/6.html","b677682b2fe99f74056da9baeae88792"],["/weeknotes/2020/6.png","159c3ebdc0a4d3aadd1722fe4454b04b"],["/weeknotes/2020/7.html","aeca60593acc892eb856fb40f83fe660"],["/weeknotes/2020/7.png","35fdcc8f79fa73062897a19feb79caaa"],["/weeknotes/2020/9.html","8c9efeb6c72d8a7e59d274a791bab77f"],["/weeknotes/2020/9.png","c18e2cbefc00543c1de41d53a78ad683"],["/weeknotes/2021/12.html","337f5ee152da5d346939083f71ebdb39"],["/weeknotes/2021/12.png","d8caf2cb8188a75899aaf343ebfeab48"],["/weeknotes/2021/13.html","4766ec024ada558336ad88987dc296c3"],["/weeknotes/2021/13.png","21291b46dcae16053b115227c16fa877"],["/weeknotes/2021/14.html","eafe2d7a3baeeeb13ccbd4b334fd1da2"],["/weeknotes/2021/14.png","c2e7e6336c006d6bdeaf4da8ca0df786"],["/weeknotes/2021/15.html","8e31add1ad00bf1073fe0381a6a2683f"],["/weeknotes/2021/15.png","cc850beedfe06a33fefbdc32b7520e85"],["/weeknotes/2021/16.html","5f920b73b878dd49400c0ee458b99b25"],["/weeknotes/2021/16.png","eb460593c1c22836825590610043a95c"],["/weeknotes/2021/17.html","674c25bbc673ee1da5eadacaccb0099b"],["/weeknotes/2021/17.png","8a22aba4606c268626da6c81f279c2db"],["/weeknotes/2021/18.html","e459f03b88d60e82236ba6aa5fd92b3b"],["/weeknotes/2021/18.png","9324b934891841bb86f7b04d7409b5ca"],["/weeknotes/2021/19.html","d21da9abef08091ef0d40f6c6d3d943c"],["/weeknotes/2021/19.png","c948e2951ba0dc81e25ea6cfd137b0f2"],["/weeknotes/2021/2.html","b9293c05a8b1e01bc2ef1129b87762db"],["/weeknotes/2021/2.png","dae72ac89230411ddaf4d301657b4363"],["/weeknotes/2021/20.html","6e16ef145796a8737579b919018ad06d"],["/weeknotes/2021/20.png","54ceb9667385bfa41960c33b844c9803"],["/weeknotes/2021/21.html","13330c2657b857171602a993989b7a6b"],["/weeknotes/2021/21.png","70d93be3b1f428759556fdd4c141ff5b"],["/weeknotes/2021/23.html","e71d1ab21386944028804dbe171db8ce"],["/weeknotes/2021/23.png","2df4bcb189f0974b780fc2d02560b6db"],["/weeknotes/2021/24.html","cf741e1a3ac4aae2d04e254a669e48e9"],["/weeknotes/2021/24.png","255944f24fb9ede364d5f5f4e785dd95"],["/weeknotes/2021/25.html","897e8cbcdec425bb8119dcf675f98c22"],["/weeknotes/2021/25.png","979526b79b78730f726395466f27a025"],["/weeknotes/2021/26.html","8e99253a18b3cb93fbf641b1226b3287"],["/weeknotes/2021/26.png","ab76e621c6bfb100096661a9c3ddfb6e"],["/weeknotes/2021/27.html","60a7341ea2080b2b43315208eeced3f5"],["/weeknotes/2021/27.png","2a389cc9589cf88cf80ef33a58c62ecb"],["/weeknotes/2021/28.html","fa6e8ff451f3a2043185720e4f4a9411"],["/weeknotes/2021/28.png","1d37ae2ee5c068969168188f55512650"],["/weeknotes/2021/29.html","4b2c0c1cc9a23cce532a19baf3d4893f"],["/weeknotes/2021/29.png","ba15634d787cd2d67c423313e62b278f"],["/weeknotes/2021/3.html","6e257b5a1dfbdd7289ed42de8b0f5486"],["/weeknotes/2021/3.png","4231641190d854ab064b8629136a202d"],["/weeknotes/2021/38.html","52af0d087d5de219174ef0f63fa47040"],["/weeknotes/2021/38.png","7d76ef160a21e2c23ca6efdd0b7243a5"],["/weeknotes/2021/39.html","4cb0323bda02efc0e3faf976cc94ca5f"],["/weeknotes/2021/39.png","101e930adcf2020e5bbaab542321b526"],["/weeknotes/2021/4.html","9af5e822f7bf38923f859e6d944b4b64"],["/weeknotes/2021/4.png","0da1ad0f65d046cc1d474c1098040a8e"],["/weeknotes/2021/40.html","7d70b24064c8853e35be7950ba678e68"],["/weeknotes/2021/40.png","73ebf3808ed7659279cc5d8cb0fc7682"],["/weeknotes/2021/42.html","d4699e16b245d6ba36efe4ff533ba59d"],["/weeknotes/2021/42.png","6936fe54eedc4be0203fbe624dca4937"],["/weeknotes/2021/44.html","c0b7d1c26d1beb1651cd1139ee37a01e"],["/weeknotes/2021/44.png","4c3d90c21d209e42bddb6523029da44c"],["/weeknotes/2021/45.html","27efa61bf5bf161afa5b6e8b2fc07845"],["/weeknotes/2021/45.png","84de431bbb973c8b7b77e1b82fbc4d30"],["/weeknotes/2021/46.html","b76f13737384be3a10b2ee6db2a9f896"],["/weeknotes/2021/46.png","956790e8657e44095d0450c5653156f7"],["/weeknotes/2021/5.html","aa831912e0a7c080bb1654e6e577f2f0"],["/weeknotes/2021/5.png","fcc0d07c057b113587c817ec92893d2f"],["/weeknotes/2021/6.html","082b9ff1b637eef2945ee1db6939ccdf"],["/weeknotes/2021/6.png","db8bf012c86de5b912b516571091eaf3"],["/weeknotes/2021/7.html","76f2cb3429a8393f94fde29b85f5caaf"],["/weeknotes/2021/7.png","d3b30855bcd027e984ff192380605485"],["/weeknotes/2021/8.html","63b076ff328ba63ca14359d558714c03"],["/weeknotes/2021/8.png","34b564a8adc2d80df416852b43dc4e82"],["/weeknotes/2021/9.html","bd0b0a414fd5635068356e3eac88289e"],["/weeknotes/2021/9.png","2790f777703ea1b75c8801d5b56b9b5a"],["/weeknotes/page2/index.html","dc25adf8273472cf6cc8f5e8fc35f11e"],["/weeknotes/page3/index.html","ed6bb43f351635e42e6144f8fd47d1f0"],["/weeknotes/page4/index.html","c10ee69959bd7e2a419229ec524ecc26"],["/weeknotes/page5/index.html","165415abeb88736f4c2ce50173531983"],["/wrapping_up.html","fcb9af48c463d651daddc61d5829dac9"]]; +var cacheName = 'sw-precache-v3-sw-precache-' + (self.registration ? self.registration.scope : ''); + + +var ignoreUrlParametersMatching = [/^utm_/]; + + + +var addDirectoryIndex = function(originalUrl, index) { + var url = new URL(originalUrl); + if (url.pathname.slice(-1) === '/') { + url.pathname += index; + } + return url.toString(); + }; + +var cleanResponse = function(originalResponse) { + // If this is not a redirected response, then we don't have to do anything. + if (!originalResponse.redirected) { + return Promise.resolve(originalResponse); + } + + // Firefox 50 and below doesn't support the Response.body stream, so we may + // need to read the entire body to memory as a Blob. + var bodyPromise = 'body' in originalResponse ? + Promise.resolve(originalResponse.body) : + originalResponse.blob(); + + return bodyPromise.then(function(body) { + // new Response() is happy when passed either a stream or a Blob. + return new Response(body, { + headers: originalResponse.headers, + status: originalResponse.status, + statusText: originalResponse.statusText + }); + }); + }; + +var createCacheKey = function(originalUrl, paramName, paramValue, + dontCacheBustUrlsMatching) { + // Create a new URL object to avoid modifying originalUrl. + var url = new URL(originalUrl); + + // If dontCacheBustUrlsMatching is not set, or if we don't have a match, + // then add in the extra cache-busting URL parameter. + if (!dontCacheBustUrlsMatching || + !(url.pathname.match(dontCacheBustUrlsMatching))) { + url.search += (url.search ? '&' : '') + + encodeURIComponent(paramName) + '=' + encodeURIComponent(paramValue); + } + + return url.toString(); + }; + +var isPathWhitelisted = function(whitelist, absoluteUrlString) { + // If the whitelist is empty, then consider all URLs to be whitelisted. + if (whitelist.length === 0) { + return true; + } + + // Otherwise compare each path regex to the path of the URL passed in. + var path = (new URL(absoluteUrlString)).pathname; + return whitelist.some(function(whitelistedPathRegex) { + return path.match(whitelistedPathRegex); + }); + }; + +var stripIgnoredUrlParameters = function(originalUrl, + ignoreUrlParametersMatching) { + var url = new URL(originalUrl); + // Remove the hash; see https://github.com/GoogleChrome/sw-precache/issues/290 + url.hash = ''; + + url.search = url.search.slice(1) // Exclude initial '?' + .split('&') // Split into an array of 'key=value' strings + .map(function(kv) { + return kv.split('='); // Split each 'key=value' string into a [key, value] array + }) + .filter(function(kv) { + return ignoreUrlParametersMatching.every(function(ignoredRegex) { + return !ignoredRegex.test(kv[0]); // Return true iff the key doesn't match any of the regexes. + }); + }) + .map(function(kv) { + return kv.join('='); // Join each [key, value] array into a 'key=value' string + }) + .join('&'); // Join the array of 'key=value' strings into a string with '&' in between each + + return url.toString(); + }; + + +var hashParamName = '_sw-precache'; +var urlsToCacheKeys = new Map( + precacheConfig.map(function(item) { + var relativeUrl = item[0]; + var hash = item[1]; + var absoluteUrl = new URL(relativeUrl, self.location); + var cacheKey = createCacheKey(absoluteUrl, hashParamName, hash, false); + return [absoluteUrl.toString(), cacheKey]; + }) +); + +function setOfCachedUrls(cache) { + return cache.keys().then(function(requests) { + return requests.map(function(request) { + return request.url; + }); + }).then(function(urls) { + return new Set(urls); + }); +} + +self.addEventListener('install', function(event) { + event.waitUntil( + caches.open(cacheName).then(function(cache) { + return setOfCachedUrls(cache).then(function(cachedUrls) { + return Promise.all( + Array.from(urlsToCacheKeys.values()).map(function(cacheKey) { + // If we don't have a key matching url in the cache already, add it. + if (!cachedUrls.has(cacheKey)) { + var request = new Request(cacheKey, {credentials: 'same-origin'}); + return fetch(request).then(function(response) { + // Bail out of installation unless we get back a 200 OK for + // every request. + if (!response.ok) { + throw new Error('Request for ' + cacheKey + ' returned a ' + + 'response with status ' + response.status); + } + + return cleanResponse(response).then(function(responseToCache) { + return cache.put(cacheKey, responseToCache); + }); + }); + } + }) + ); + }); + }).then(function() { + + // Force the SW to transition from installing -> active state + return self.skipWaiting(); + + }) + ); +}); + +self.addEventListener('activate', function(event) { + var setOfExpectedUrls = new Set(urlsToCacheKeys.values()); + + event.waitUntil( + caches.open(cacheName).then(function(cache) { + return cache.keys().then(function(existingRequests) { + return Promise.all( + existingRequests.map(function(existingRequest) { + if (!setOfExpectedUrls.has(existingRequest.url)) { + return cache.delete(existingRequest); + } + }) + ); + }); + }).then(function() { + + return self.clients.claim(); + + }) + ); +}); + + +self.addEventListener('fetch', function(event) { + if (event.request.method === 'GET') { + // Should we call event.respondWith() inside this fetch event handler? + // This needs to be determined synchronously, which will give other fetch + // handlers a chance to handle the request if need be. + var shouldRespond; + + // First, remove all the ignored parameters and hash fragment, and see if we + // have that URL in our cache. If so, great! shouldRespond will be true. + var url = stripIgnoredUrlParameters(event.request.url, ignoreUrlParametersMatching); + shouldRespond = urlsToCacheKeys.has(url); + + // If shouldRespond is false, check again, this time with 'index.html' + // (or whatever the directoryIndex option is set to) at the end. + var directoryIndex = 'index.html'; + if (!shouldRespond && directoryIndex) { + url = addDirectoryIndex(url, directoryIndex); + shouldRespond = urlsToCacheKeys.has(url); + } + + // If shouldRespond is still false, check to see if this is a navigation + // request, and if so, whether the URL matches navigateFallbackWhitelist. + var navigateFallback = ''; + if (!shouldRespond && + navigateFallback && + (event.request.mode === 'navigate') && + isPathWhitelisted([], event.request.url)) { + url = new URL(navigateFallback, self.location).toString(); + shouldRespond = urlsToCacheKeys.has(url); + } + + // If shouldRespond was set to true at any point, then call + // event.respondWith(), using the appropriate cache key. + if (shouldRespond) { + event.respondWith( + caches.open(cacheName).then(function(cache) { + return cache.match(urlsToCacheKeys.get(url)).then(function(response) { + if (response) { + return response; + } + throw Error('The cached response that was expected is missing.'); + }); + }).catch(function(e) { + // Fall back to just fetch()ing the request if some unexpected error + // prevented the cached response from being valid. + console.warn('Couldn\'t serve response for "%s" from cache: %O', event.request.url, e); + return fetch(event.request); + }) + ); + } + } +}); + + + + + + + diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 000000000..e8976f587 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,1004 @@ + + + + https://pauldambra.dev + 2023-07-24T18:48:55+00:00 + weekly + 1.0 + + + + https://pauldambra.dev/recipes/2023/07/zucchini-focaccia.html + + 2023-07-24T07:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2023/06/pauls-law.html + + 2023-06-08T08:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2023/03/mar-month-notes.html + + 2023-04-02T18:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2023/03/the-cloud.html + + 2023-03-19T08:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2023/03/office-365-mega-thread.html + + 2023-03-05T08:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2023/03/feb-month-notes.html + + 2023-03-01T08:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2023/02/jan-month-notes.html + + 2023-02-03T08:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2023/01/year-notes.html + + 2023-01-15T08:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/recipes/2023/01/pasta-e-fasule.html + + 2023-01-08T07:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/recipes/2022/09/pizza-dough.html + + 2022-09-04T07:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/recipes/2022/09/pepper-salad.html + + 2022-09-04T07:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2022/08/solar-panels.html + + 2022-08-22T08:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2022/07/weather-vane-or-sign-post.html + + 2021-09-17T08:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2021/09/emotional-advice.html + + 2021-09-17T08:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2021/07/tech-debts.html + + 2021-07-21T08:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2021/01/year-notes.html + + 2021-01-03T08:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2020/01/year-notes.html + + 2020-01-07T08:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2019/11/serverless-lessons-learned.html + + 2019-11-30T10:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2018/07/serverless-6.html + + 2018-07-01T10:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2018/06/serverless-5.html + + 2018-06-10T10:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2018/03/harmful-dry.html + + 2018-03-30T10:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2018/02/serverless-4.html + + 2018-03-15T10:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2018/02/serverless-3.html + + 2018-02-05T10:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2018/02/serverless-2.html + + 2018-02-04T10:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2018/02/serverless-1.html + + 2018-02-03T10:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2018/01/direction.html + + 2018-01-29T10:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2017/10/constructiphor.html + + 2017-10-15T10:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2017/testing-meaning.html + + 2017-08-17T22:40:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2017/07/retrosperiment.html + + 2017-07-06T10:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2017/06/radarban.html + + 2017-06-24T20:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2017/05/big-pile-of-soil.html + + 2017-05-14T19:58:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2017/05/ubictionary.html + + 2017-05-10T19:58:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2017/generating-static-amp.html + + 2017-03-22T22:40:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2017/testing-static-sites.html + + 2017-03-19T18:40:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2016/yarn.html + + 2016-10-18T22:40:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/structured-data-with-jekyll.html + + 2016-09-20T17:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/using-travis-to-build-jekyll.html + + 2016-09-18T16:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/kill-if-with-objects.html + + 2016-09-07T18:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/powershell-on-linux.html + + 2016-08-28T09:55:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/real-vs-soft.html + + 2015-11-29T14:06:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/reactotype/part-three.html + + 2015-02-20T22:41:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/reactotype/part-two.html + + 2015-02-19T22:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/reactotype/part-one.html + + 2015-02-17T14:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/fun-with-structs.html + + 2015-02-01T14:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/happy-numbers-kata.html + + 2014-11-22T23:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/rake-transforms.html + + 2014-11-06T23:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/static-factory-methods.html + + 2014-09-13T00:00:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/wrapping_up.html + + 2014-08-03T22:11:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/better-affordance-js.html + + 2014-07-30T22:11:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/better-affordance.html + + 2014-07-20T22:11:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/On-Page-Editing.html + + 2014-06-11T08:11:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/Websites-CMS-Platform-promises-part-2.html + + 2014-06-01T08:11:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/Websites-CMS-Platform-promises.html + + 2014-05-18T08:11:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/websites-CMS-platform-logging-in.html + + 2014-04-27T08:11:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/Websites-CMS-Platform-Storing-Data2.html + + 2014-04-23T08:11:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/Websites-CMS-Platform-Storing-Data.html + + 2014-04-12T08:11:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2014/04/a-dto-by-any-other-name-would-implement.html + + 2014-04-01T20:44:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2014/03/testing-with-browserstack-and-selenium.html + + 2014-03-25T22:57:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2014/03/website-cms-display-pages-part-2.html + + 2014-03-23T12:33:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2014/03/websites-cms-displaying-pages.html + + 2014-03-17T11:08:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2014/02/websites-cms.html + + 2014-02-22T18:59:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2014/01/comparing-mongodb-and-tokumx.html + + 2014-01-23T13:48:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2013/11/astronomical-database-identfier.html + + 2013-11-22T20:51:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2013/03/automagical-search-ux.html + + 2013-03-20T08:15:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2012/09/obligatory-ios6-maps-post.html + + 2012-09-23T23:14:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2012/07/y-u-no-sell-downloads-hollywood.html + + 2012-07-25T19:06:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2012/02/is-there-really-just-ipad-market.html + + 2012-02-07T06:50:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2012/01/setting-up-mvc3-website-using-built-in.html + + 2012-01-12T13:38:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2011/09/unusbscribey-follow-up.html + + 2011-09-20T14:21:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2011/08/how-to-design-unsubscribe-link.html + + 2011-08-05T08:49:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2011/06/get-with-programming.html + + 2011-06-10T12:13:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2011/04/ssh-without-password.html + + 2011-04-11T09:05:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2010/10/refactor-fun.html + + 2010-10-20T13:43:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2010/08/odd-odd-odd-login-behaviour.html + + 2010-08-07T15:57:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2010/05/theres-more-in-them-that-hills.html + + 2010-05-08T19:12:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2010/03/quack-quack-says-duck-now-with-added.html + + 2010-03-29T12:01:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2009/12/c-background-worker.html + + 2009-12-01T13:15:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2009/10/bing-is-not-search-engine.html + + 2009-10-30T10:11:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2009/10/quack-quack-says-duck.html + + 2009-10-04T11:56:00+00:00 + + monthly + 0.5 + + + + https://pauldambra.dev/2009/05/anonymous-methods-when-invoking-in-vb.html + + 2009-05-28T19:32:00+00:00 + + monthly + 0.5 + + + + + https://pauldambra.dev/categories.html + + 2023-07-24 + + + monthly + + + 0.3 + + + + + https://pauldambra.dev/dear-diary-year-one.html + + 2023-07-24 + + + monthly + + + 0.3 + + + + + https://pauldambra.dev/facebook-instant-feed.xml + + 2023-07-24 + + + monthly + + + 0.3 + + + + + https://pauldambra.dev/feed.xml + + 2023-07-24 + + + monthly + + + 0.3 + + + + + https://pauldambra.dev/ + + 2023-07-24 + + + monthly + + + 0.3 + + + + + https://pauldambra.dev/kids-games.html + + 2023-07-24 + + + monthly + + + 0.3 + + + + + https://pauldambra.dev/assets/css/main.css + + 2023-07-24 + + + monthly + + + 0.3 + + + + + https://pauldambra.dev/sitemap.xml + + 2023-07-24 + + + monthly + + + 0.3 + + + + + https://pauldambra.dev/tags.html + + 2023-07-24 + + + monthly + + + 0.3 + + + + + https://pauldambra.dev/weeknotes.html + + 2023-07-24 + + + monthly + + + 0.3 + + + + + https://pauldambra.dev/page2/ + + 2023-07-24 + + + monthly + + + 0.3 + + + + + https://pauldambra.dev/page3/ + + 2023-07-24 + + + monthly + + + 0.3 + + + + + https://pauldambra.dev/page4/ + + 2023-07-24 + + + monthly + + + 0.3 + + + + + https://pauldambra.dev/page5/ + + 2023-07-24 + + + monthly + + + 0.3 + + + + + https://pauldambra.dev/page6/ + + 2023-07-24 + + + monthly + + + 0.3 + + + + + https://pauldambra.dev/page7/ + + 2023-07-24 + + + monthly + + + 0.3 + + + + + https://pauldambra.dev/page8/ + + 2023-07-24 + + + monthly + + + 0.3 + + + + + https://pauldambra.dev/weeknotes/page2/ + + 2023-07-24 + + + monthly + + + 0.3 + + + + + https://pauldambra.dev/weeknotes/page3/ + + 2023-07-24 + + + monthly + + + 0.3 + + + + + https://pauldambra.dev/weeknotes/page4/ + + 2023-07-24 + + + monthly + + + 0.3 + + + + + https://pauldambra.dev/weeknotes/page5/ + + 2023-07-24 + + + monthly + + + 0.3 + + + + \ No newline at end of file diff --git a/static-factory-methods.html b/static-factory-methods.html new file mode 100644 index 000000000..b8463d7c6 --- /dev/null +++ b/static-factory-methods.html @@ -0,0 +1,360 @@ + + + + + + + + + + + + + + + + + + + + + + Static Factory Methods FTW + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sat Sep 13 2014
+

Static Factory Methods FTW

+
+ +
+ +
+

It is relatively common to find (or write) a line of code like this

+ +
	var thingy = new Thingy(_someDependency, false);
+
+ +

Reading this line a person can know this is initialising a Thingy which takes a dependency on something… and something else is false. + +I'm really lazy and easily distracted so I don't like to have to think about anything except the one task I'm trying to not get distracted from. Having to think about what it means that something is false provides an opportunity for me to get distracted.

+ + + +

Also, this means there is some information or some decision that has been taken by a previous developer that they have kept in their brain - all the person reading this line sees is the result of that. Each time somebody comes to this class they may have to invest time reminding themselves what the boolean parameter means.

+ +

Can it be better?

+ +

Well, C# (as well as other languages) allows named parameters so this Thingy could be instantiated by using

+ +
	var thingy = new Thingy(_someDependency, disableUnexpectedBehaviour: false);
+
+ +

Now when a person reads this line of code they know more about what is happening and that should support them in introducing correct code. Here they learn that there is a mode where this class can do something in an unexpected way. Since the named parameter makes the intent clearer a developer can make a decision about how to instantiate the class with a reasonable idea of what will happen.

+ +

However, the next time someone writes code that uses a Thingy they don't have to use a named parameter so it is still possible to use the form which obscures intent.

+ +

Static Factory Method

+ +

It is possible to hide the constructor and provide static factory methods to create and return instances. Where there is more than one way to construct the object more than one factory method can be provided to clarify this difference.

+ +

These are my latest obsession - so I recommend taking the time to jump to the declaration of Thingy and do something like this:

+ +

+class Thingy 
+{
+    private readonly SomeDependency _someDependency;
+	private readonly bool _disableUnexpectedBehaviour
+
+    // make the constructor private - this isn't necessary 
+    // but enforces the use of the static factory methods
+	private Thingy(SomeDependency someDependency, bool disableUnexpectedBehaviour) 
+	{
+		_someDependency = someDependency;
+		_disableUnexpectedBehaviour = disableUnexpectedBehaviour;
+	}
+	
+	//add static factory methods
+	public static Thingy WithExpectedBehaviour(SomeDependency someDependency)
+	{
+		return new Thingy(someDependency, true);
+	}
+
+	public static Thingy WithUnexpectedBehaviour(SomeDependency someDependency)
+	{
+		return new Thingy(someDependency, false);
+	}
+
+	//<snip/>
+}
+
+	var thingy =  Thingy.WithExpectedBehaviour(_someDependency);
+	var crazyThingy =  Thingy.WithUnexpectedBehaviour(_someDependency);
+
+
+ +

Now you can't construct a Thingy without using one of these methods. This means that your decision to use a Thingy includes an explicit decision about what it means for it to be included in your code. Yay for clarity!

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/structured-data-with-jekyll.html b/structured-data-with-jekyll.html new file mode 100644 index 000000000..5b5123663 --- /dev/null +++ b/structured-data-with-jekyll.html @@ -0,0 +1,566 @@ + + + + + + + + + + + + + + + + + + + + + + Adding Structured Data to a Jekyll site + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Tue Sep 20 2016
+

Adding Structured Data to a Jekyll site

+
+ +
+ +
+ + +

Structured Data is a way of adding context to files served on the web so that computers (primarily but not only search engines) can respond to what your content means.

+ +

Google, for example, will alter and improve how your site appears in search results based on the context you give your data. And if your site is considered authoritative can use the data to build the knowledge cards it sits alongside other search results.

+ +

This blog is only authoritative for being unread but I've not worked with structured data and thought I'd investigate.

+ + + +

There are several formats with which you can add structured data which typically use the schema.org vocabulary. Google prefers JSON-LD which is added to the page in a script tag as opposed to, for example, Microdata which decorates the HTML.

+ +

Here is an (example taken from schema.org):

+ +
<div itemscope itemtype="http://schema.org/ScreeningEvent">
+  <h1 itemprop="name">Jaws 3-D"</h1>
+  <div itemprop="description">Jaws 3-D shown in 3D.</div>
+  <p>Location: <div itemprop="location" itemscope itemtype="http://schema.org/MovieTheater">
+    <span itemprop="name">ACME Cinemas 10</span>
+    <span itemprop="screenCount">10</span>
+    </div>
+  </p>
+  <div itemprop="workPresented" itemscope itemtype="http://schema.org/Movie">
+    <span itemprop="name">Jaws 3-D</span>
+    <link itemprop="sameAs" href="www.imdb.com/title/tt0085750/"/>
+  </div>
+  <p>Language: <span itemprop="inLanguage" content="en">English</span></p>
+  <p>Film format: <span itemprop="videoFormat">3D</span></p>
+</div>
+
+ +

The same markup in JSON-LD:

+ +
<script type="application/ld+json">
+{
+  "@context:": "http://schema.org",
+  "@type": "ScreeningEvent",
+  "name": "Jaws 3-D",
+  "description": "Jaws 3-D shown in 3D."
+  "location": {
+    "@type": "MovieTheater",
+    "name": "ACME Cinemas 10",
+    "screenCount": 10
+  },
+  "workPresented": {
+    "@type": "Movie",
+    "name": "Jaws 3-D",
+    "sameAs": "www.imdb.com/title/tt0085750/"
+  },
+  "inLanguage": "en",
+  "videoFormat": "3D"
+}
+</script>
+
+
+ +

Since the aim is to add this data to a Jekyll site where the HTML is generated from markdown adding Microdata would most likely be a massive faff. But adding a script tag should be trivial.

+ +

First, the home page.

+ +
<script type="application/ld+json">
+{ "@context": "http://schema.org",
+ "@type": "Blog",
+ "keywords": "software engineering agile refactoring c# ruby javascript",
+ "url": "https://pauldambra.github.io",
+ "mainEntityOfPage": {
+    "@type": "WebPage",
+    "@id": "https://pauldambra.github.io"
+  },
+ "author": {
+    "@type": "Person",
+    "name": "Paul D'Ambra",
+    "sameAs": [
+        "https://twitter.com/pauldambra",
+        "https://github.com/pauldambra",
+        "https://plus.google.com/u/0/+PaulDAmbraPlus"
+    ]
+  }
+ }
+</script>
+
+ +

The above can be dropped straight into the index.html that the home page is generated from.

+ +

It says that it use schema.org, is describing a blog, which has a bunch of keywords and is at a particular URL. It advertises that the main thing about this webpage is the page itself. And lists that I'm the author and that I can be identified at a set of other locations (twitter, github, and google+)

+ +

Pretty straightforward since it is totally static information.

+ +

Next, each blog post

+ +

There's more to grok here…

+ +

+{% assign wordcount = include.content | number_of_words %}
+
+<script type="application/ld+json">
+{
+   "@context":"http://schema.org",
+   "@type":"BlogPosting",
+   "headline":"{{ include.headline }}",
+   "genre":"{{ include.category }}",
+   "keywords":"{{ include.keywords }}",
+   "wordCount":"{{ wordcount }}",
+   "url":"https://pauldambra.github.io{{ include.link }}",
+   "datePublished":"{{ include.date | | date: '%Y-%m-%d' }}",
+   "author":{
+      "@type":"Person",
+      "name":"Paul D'Ambra",
+      "sameAs":[
+        "https://twitter.com/pauldambra",
+        "https://github.com/pauldambra",
+        "https://plus.google.com/u/0/+PaulDAmbraPlus"
+      ]
+   },
+   "publisher":{
+      "@type":"Person",
+      "name":"Paul D'Ambra",
+      "sameAs":[
+        "https://twitter.com/pauldambra",
+        "https://github.com/pauldambra",
+        "https://plus.google.com/u/0/+PaulDAmbraPlus"
+      ],
+      "logo": {
+  		"@type": "ImageObject",
+  		"contentUrl": "https://pauldambra.github.io/images/logo.png",
+  		  "url": "https://pauldambra.github.io"
+    }
+   },
+   "image":{
+      "@type":"ImageObject",
+      "contentUrl":"https://pauldambra.github.io/images/cardboard.jpg",
+      "url":"https://pauldambra.github.io",
+      "height":"450",
+      "width":"1000"
+   },
+   "mainEntityOfPage":{
+      "@type":"WebPage",
+      "@id":"https://pauldambra.github.io{{ include.link }}"
+   },
+   "articleBody":"{{ include.content | strip_html | xml_escape | normalize_whitepace | strip_newlines | strip }}"
+}
+</script>
+
+
+ +

As with the index page the context and type is set. That determines which properties are relevant. Also best to run the markup through the Google testing tool to make sure that you have included the properties Google requires.

+ +

The above is pasted into an html file in the _includes folder. And added to the layout used to render BlogPosts

+ +

+{%
+  include structuredData.html
+  headline=page.title
+  genre=page.category
+  keywords=page.keywords
+  content=page.content
+  link=page.permalink
+  date=page.date
+%}
+
+
+
+ +

BlogPosts (i.e. those items in the _posts folder) have a bunch of variables either automatically available or added in the post's YAML frontmatter. These are passed on into the structuredData include and referenced inside it as include.provided_name

+ +

That's not quite everything

+ +

The home page now has some data about the site itself but there's nothing in the markup to indicate that it is made up of a list of the site's published blog posts.

+ +

My solution to this highlights nicely how powerful the liquid templating language can be with a very limited set of operators and filters.

+ +

+<script type="application/ld+json">
+{
+  "@context": "http://schema.org",
+  "@type": "ItemList",
+  "itemListElement": [
+  {{ items }} <!-- see below -->
+  ]
+}
+</script>
+
+
+ +

This adds a second script tag to hold the item list of blog postings. It is then necessary to loop over each post in site.posts and add an entry for that item.

+ +

But an array with a trailing comma is not valid JSON+LD and including a template here ends up with a trailing comma.

+ +

The solution is led by two things:

+ +
    +
  • Liquid provides the join filter which correctly joins an array without adding a trailing separator
  • +
  • You can only initialise an array by splitting a string
  • +
+ +

so…

+ +

+  {% assign items = "" %}
+  {% for post in site.posts %}
+    {% capture list_item %}
+        {%
+            include blogListItem.html
+            index=forloop.index
+            url=post.url
+            title=post.title
+            date=post.date
+        %}
+    {% endcapture %}
+    {% assign items = items | append: list_item | append: "|||" %}
+  {% endfor %}
+
+  {% assign items = items | split: "|||" | join: ',' %}
+
+
+ +
    +
  • create an empty string
  • +
  • loop over site.posts
  • +
  • for each post capture the result of populating a blogListItem include into a string
  • +
  • append that to the original string with a known separator
  • +
  • then split that string
  • +
  • then join that array with commas
  • +
  • that can then be output into the script tag above
  • +
+ +

There may well be a different way of doing that. It seems a bit bonkers but it works…

+ +

Do the search engines use it?

+ +

I've submitted the site for crawling since adding structured data and Google has so far picked up the home page and four of the articles.

+ +

structured data crawled by Google

+ + + +

Several days on and there's no obvious change in how my site places in search rankings or how Google displays it but…

+ +
    +
  • their documentation says it can take some time and I've seen other people talking about it taking more than 10 days
  • +
  • as above this site isn't authoritative for anything so I may not be crossing a threshold of importance for processing or be at the back of a very long queue.
  • +
+ +

Anyway, I think Structured Data, as well as being interesting, is a prerequisite for getting AMP set up and that's next up…

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/tags.html b/tags.html new file mode 100644 index 000000000..5d771ba2e --- /dev/null +++ b/tags.html @@ -0,0 +1,13470 @@ + + + + + + + + + + + + + + + + + + + + + + Tag + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +

Tags

+
+ + tag-icon + + AMP + + + + + tag-icon + + CI + + + + + tag-icon + + GET + + + + + tag-icon + + agile + + + + + tag-icon + + agilemanc + + + + + tag-icon + + analysis + + + + + tag-icon + + android + + + + + tag-icon + + background-workers + + + + + tag-icon + + bad-metaphors + + + + + tag-icon + + bash + + + + + tag-icon + + benchmark + + + + + tag-icon + + bing + + + + + tag-icon + + blog + + + + + tag-icon + + browserstack + + + + + tag-icon + + c# + + + + + tag-icon + + cargo-cult + + + + + tag-icon + + changing jobs + + + + + tag-icon + + clean-code + + + + + tag-icon + + cloud + + + + + tag-icon + + cms + + + + + tag-icon + + co-op + + + + + tag-icon + + code + + + + + tag-icon + + comparison + + + + + tag-icon + + context + + + + + tag-icon + + ddd + + + + + tag-icon + + dear-diary + + + + + tag-icon + + design + + + + + tag-icon + + design-patterns + + + + + tag-icon + + detail + + + + + tag-icon + + dictionary + + + + + tag-icon + + domain + + + + + tag-icon + + dotnet + + + + + tag-icon + + dry + + + + + tag-icon + + dto + + + + + tag-icon + + energy + + + + + tag-icon + + entity-framework + + + + + tag-icon + + eventdriven + + + + + tag-icon + + events + + + + + tag-icon + + experience + + + + + tag-icon + + experience-report + + + + + tag-icon + + express + + + + + tag-icon + + focaccia + + + + + tag-icon + + follow-up + + + + + tag-icon + + git + + + + + tag-icon + + glut + + + + + tag-icon + + hollywood + + + + + tag-icon + + iOS + + + + + tag-icon + + iOS6 + + + + + tag-icon + + idempotence + + + + + tag-icon + + java + + + + + tag-icon + + jekyll + + + + + tag-icon + + js + + + + + tag-icon + + json+ld + + + + + tag-icon + + kata + + + + + tag-icon + + kotlin + + + + + tag-icon + + language + + + + + tag-icon + + laws + + + + + tag-icon + + learning + + + + + tag-icon + + linux + + + + + tag-icon + + meaning + + + + + tag-icon + + membership-provider + + + + + tag-icon + + metaphor + + + + + tag-icon + + metaphors + + + + + tag-icon + + microsoft + + + + + tag-icon + + misunderstandings + + + + + tag-icon + + mongodb + + + + + tag-icon + + mvc + + + + + tag-icon + + names + + + + + tag-icon + + netflix + + + + + tag-icon + + nosql + + + + + tag-icon + + npm + + + + + tag-icon + + office + + + + + tag-icon + + oracle + + + + + tag-icon + + parenting + + + + + tag-icon + + pasta + + + + + tag-icon + + pedantry + + + + + tag-icon + + physics + + + + + tag-icon + + powershell + + + + + tag-icon + + problem-solving + + + + + tag-icon + + promises + + + + + tag-icon + + python + + + + + tag-icon + + radar + + + + + tag-icon + + radarban + + + + + tag-icon + + rake + + + + + tag-icon + + rant + + + + + tag-icon + + react + + + + + tag-icon + + read model + + + + + tag-icon + + recipe + + + + + tag-icon + + recursion + + + + + tag-icon + + refactoring + + + + + tag-icon + + retrospective + + + + + tag-icon + + review + + + + + tag-icon + + ruby + + + + + tag-icon + + salad + + + + + tag-icon + + search + + + + + tag-icon + + selenium + + + + + tag-icon + + seo + + + + + tag-icon + + series + + + + + tag-icon + + serverless + + + + + tag-icon + + silliness + + + + + tag-icon + + software + + + + + tag-icon + + software engineering + + + + + tag-icon + + solar + + + + + tag-icon + + solid + + + + + tag-icon + + ssh + + + + + tag-icon + + streaming + + + + + tag-icon + + tdd + + + + + tag-icon + + terrible-software + + + + + tag-icon + + testing + + + + + tag-icon + + tokumx + + + + + tag-icon + + toots + + + + + tag-icon + + travisci + + + + + tag-icon + + ux + + + + + tag-icon + + vb.net + + + + + tag-icon + + views + + + + + tag-icon + + visual-studio + + + + + tag-icon + + visualisation + + + + + tag-icon + + wat + + + + + tag-icon + + web + + + + + tag-icon + + windows + + + + + tag-icon + + working-out-loud + + + + + tag-icon + + xp + + + + + tag-icon + + yarn + + + + + tag-icon + + zucchini + + + +
+
+ + +

AMP

+ + +

CI

+ + +

GET

+ + +

agile

+ + +

agilemanc

+ + +

analysis

+ + +

android

+ + +

background-workers

+ + +

bad-metaphors

+ + +

bash

+ + +

benchmark

+ + +

bing

+ + +

blog

+ + +

browserstack

+ + +

c#

+ + +

cargo-cult

+ + +

changing jobs

+ + +

clean-code

+ + +

cloud

+ + +

cms

+ + +

co-op

+ + +

code

+ + +

comparison

+ + +

context

+ + +

ddd

+ + +

dear-diary

+ + +

design

+ + +

design-patterns

+ + +

detail

+ + +

dictionary

+ + +

domain

+ + +

dotnet

+ + +

dry

+ + +

dto

+ + +

energy

+ + +

entity-framework

+ + +

eventdriven

+ + +

events

+ + +

experience

+ + +

experience-report

+ + +

express

+ + +

focaccia

+ + +

follow-up

+ + +

git

+ + +

glut

+ + +

hollywood

+ + +

iOS

+ + +

iOS6

+ + +

idempotence

+ + +

java

+ + +

jekyll

+ + +

js

+ + +

json+ld

+ + +

kata

+ + +

kotlin

+ + +

language

+ + +

laws

+ + +

learning

+ + +

linux

+ + +

meaning

+ + +

membership-provider

+ + +

metaphor

+ + +

metaphors

+ + +

microsoft

+ + +

misunderstandings

+ + +

mongodb

+ + +

mvc

+ + +

names

+ + +

netflix

+ + +

nosql

+ + +

npm

+ + +

office

+ + +

oracle

+ + +

parenting

+ + +

pasta

+ + +

pedantry

+ + +

physics

+ + +

powershell

+ + +

problem-solving

+ + +

promises

+ + +

python

+ + +

radar

+ + +

radarban

+ + +

rake

+ + +

rant

+ + +

react

+ + +

read model

+ + +

recipe

+ + +

recursion

+ + +

refactoring

+ + +

retrospective

+ + +

review

+ + +

ruby

+ + +

salad

+ + + + + +

selenium

+ + +

seo

+ + +

series

+ + +

serverless

+ + +

silliness

+ + +

software

+ + +

software engineering

+ + +

solar

+ + +

solid

+ + +

ssh

+ + +

streaming

+ + +

tdd

+ + +

terrible-software

+ + +

testing

+ + +

tokumx

+ + +

toots

+ + +

travisci

+ + +

ux

+ + +

vb.net

+ + +

views

+ + +

visual-studio

+ + +

visualisation

+ + +

wat

+ + +

web

+ + +

windows

+ + +

working-out-loud

+ + +

xp

+ + +

yarn

+ + +

zucchini

+ + + +
+ + + + + + + + diff --git a/using-travis-to-build-jekyll.html b/using-travis-to-build-jekyll.html new file mode 100644 index 000000000..b819d94b5 --- /dev/null +++ b/using-travis-to-build-jekyll.html @@ -0,0 +1,495 @@ + + + + + + + + + + + + + + + + + + + + + + Using Travis CI to build a Jekyll site + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sun Sep 18 2016
+

Using Travis CI to build a Jekyll site

+
+ +
+ +
+

<aside class=""mb-2 ml-4 border-l-2 border-l-sky-700 pl-1""> + <h1 class="text-base">This post is part of a series on improving this blog #recursion</h1> + <div class="flex flex-row"> + <div class="flex-grow"></div> + <div class="flex-grow content-end"> + Next Post + </div> + </div> +</aside>

+ +

I recently had a conversation where I said that I couldn't build an AMP version of my blog because I use Github Pages to build and serve it. Github don't allow any Jekyll plugins to run.

+ +

Later that day my subconscious prompted me to realise that, since Github pages will serve static HTML quite happily, I could use Travis CI to build a Github repository that held the source for the blog and push the static output to a second repository that Github would publish as is.

+ + + +

Travis CI

+ + + +

Travis is an online continuous integration system that hooks very neatly into Github. It's free for open source projects and adds build status to commits. It can be set to automatically build pull requests and adds output to those PRs so that people can see if it is safe to merge a request without building it locally themselves

+ +

Travis integrates with Github

+ +

Travis is configured by placing a YAML file in the root of the project. You can place commands directly into the config file but I prefer to put those commands in a script file - that way you can run them locally to confirm the steps should work before pushing the YAML file to Github for Travis to detect.

+ +

Continuous integration is the process of automating build and verification of your software. It's they way you avoid having to say "Works on my machine. ¯_(ツ)_/¯". I <3 CI so being able to fold it into my hobby workflow like this is super satisfying.

+ +

Desired outcome

+ + + +

I decided to have one repository called blog_source which would build the static site into a folder. And a second repository (the original one pauldambra.github.io) that would host the actual site. Previously I pushed the jekyll source to that repository for Github to build now I'll be pushing HTML for Github to host.

+ +

The process will be:

+ +
    +
  • git init the output repository in a known folder,
  • +
  • pull the current state,
  • +
  • write the new output over the top,
  • +
  • commit those changes and push to the remote.
  • +
+ +

That push makes any changes visible in short order online.

+ +

I was introduced to this "release repository" mechanism for deploying code in a previous job. It is particularly effective for projects that build some static output that can be run as-is like this static HTML site or a NodeJS project after transpilation.

+ +

You can extend it nicely by pushing a version that can be hosted in a CI environment and have acceptance tests run against it. Which, if they pass, cause that version to be tagged in the git repository allowing it to be used further down your deployment pipeline.

+ +

Initial steps

+ +
#! /bin/bash
+
+set -e
+
+DEPLOY_REPO="https://${DEPLOY_BLOG_TOKEN}@github.com/pauldambra/pauldambra.github.io.git"
+
+function main {
+	clean
+	get_current_site
+	build_site
+}
+
+function clean {
+	echo "cleaning _site folder"
+	if [ -d "_site" ]; then rm -Rf _site; fi
+}
+
+function get_current_site {
+	echo "getting latest site"
+	git clone --depth 1 $DEPLOY_REPO _site
+}
+
+function build_site {
+	echo "building site"
+	bundle exec jekyll build
+}
+
+main
+
+ +

These first steps are relatively straightforward.

+ +
    +
  • if the output folder exists delete it
  • +
  • then clone the latest revision of the output repository into it
  • +
  • finally run the jekyll build +
      +
    • I'm accepting the default output location of _site to simplify things
    • +
    +
  • +
+ +

The only 'complicated' bit here is the DEPLOY_BLOG_TOKEN environment variable that is being used to authenticate against Github.

+ +

Github Personal Access Tokens

+ +

Personal access tokens act in-place of passwords for github resources. You can limit what permissions those tokens have. Generating different tokens for different uses so you can delete them if you suspect they have been compromised.

+ +

personal access token setup screen

+ +

Since they act like passwords you should be very careful with them… +Travis allows you to encrypt variables before adding them to the .travis.yml file so that secure information doesn't need to be committed into the repository or stored in plain text by the CI system.

+ +

The Travis CLI encrypts the key in the context of the repository in which Travis is going to run so that it can only be decrypted in that context.

+ +

This secure variable will be used in the blog_source build so that's the encryption context.

+ +
travis encrypt DEPLOY_BLOG_TOKEN=SOME_SECRET_VALUE -r pauldambra/blog_source  --add
+
+ +

Here we provide the name that should be available in Travis and its value. With -r specify the repository context to operate in and with --add instruct the CLI to add the token to the .travis.yml file.

+ +

The .travis.yml file

+ +

The definition for the Travis YAML is online. It lets you define the build environment and what commands will be run at each stage of the lifecycle of your travis jobs.

+ +
language: ruby
+cache: bundler
+install:
+  - bundle install
+script:
+  - "./build.sh"
+env:
+  global:
+    secure: aGrEaTbIgLoNgEnCrYpTeDvAlUe
+
+ +

Here we tell Travis that

+ +
    +
  • this is a Ruby project
  • +
  • to cache the bundler output (most of the run turns out to be building nokogiri)
  • +
  • that the install setup is to run bundle install
  • +
  • that the build step is to run ./build.sh
  • +
  • and finally to add the secure variable to the environment.
  • +
+ +

Deploying the built output

+ + + +

The final step in the script is to push the changed code to the output repository, when we're on master and not in a pull request.

+ +
function deploy {
+	echo "deploying changes"
+
+	if [ -z "$TRAVIS_PULL_REQUEST" ]; then
+	    echo "except don't publish site for pull requests"
+	    exit 0
+	fi
+
+	if [ "$TRAVIS_BRANCH" != "master" ]; then
+	    echo "except we should only publish the master branch. stopping here"
+	    exit 0
+	fi
+
+	cd _site
+	git config --global user.name "Travis CI"
+    git config --global user.email paul.dambra+travis@gmail.com
+	git add -A
+	git status
+	git commit -m "Lastest site built on successful travis build $TRAVIS_BUILD_NUMBER auto-pushed to github"
+	git push $DEPLOY_REPO master:master
+}
+
+ + + +

Travis adds several convenience environment variables two of which which can be checked here to confirm that we don't want to deploy pull requests or branches other than master.

+ +

Then the script ensures that the commit is identified and has a message that can be tracked back to this Travis build. Before pushing to Github.

+ +

Finally

+ +

travis build history

+ +

This process turned out to be straightforward and Travis is a joy to work with. Next up it's time to add some plugins to the site so that an AMP version can be published

+ +
+
+ +

More like this...

+ +
+ +
+ +
+
+ + + + + + + + + + diff --git a/websites-CMS-platform-logging-in.html b/websites-CMS-platform-logging-in.html new file mode 100644 index 000000000..935054e04 --- /dev/null +++ b/websites-CMS-platform-logging-in.html @@ -0,0 +1,484 @@ + + + + + + + + + + + + + + + + + + + + + + Websites != CMS Platform - Logging in to the site + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sun Apr 27 2014
+

Websites != CMS Platform - Logging in to the site

+
+
+ +
+ +
+ + tag-icon + + learning + + + + + tag-icon + + cms + + + + + tag-icon + + design + + + + + tag-icon + + web + + + + + tag-icon + + series + + + + + tag-icon + + mongodb + + + + + tag-icon + + nosql + + + + + tag-icon + + express + + + +
+
+
+
+ +
+

This post is part of a series where I'm hoping to prove to myself that building a dynamic website with NodeJS is much more fun than using a CMS platform. See the first post for an explanation of why

+ +

The code can be found on GitHub

+ +

Previous Post

+ +

This was the first part of the process which felt 'hard' so where I've felt the absence of a CMS platform but it's also only the second time I've ever implemented authentication using NodeJS. And still only boiled down to a few hours work.

+ + + +

Passport

+

With an eye to future expansion of what authentication the site may do the choice of technology for managing login is a Node module called PassPort. PassportJS is a flexible and modular authentication middleware foor NodeJs.

+ +

Initially the site will only support logging in using users stored in the database but Passport once setup is extendable to allow login via oauth, openid, twitter, facebook, and more. Passport uses Strategies to manage the login process.

+ +

Tests

+

In order to test login the site will need to allow creation of users, GETing /login, POSTing to /login and GETing /logout

+ +

There's no need to support registration now but it's so similar to login and creation that implementation would be trivial.

+ +
describe('creating users', function() {
+  it('should be possible to create a user');
+  it('should not be possible to create a duplicate user');
+})
+
+describe('GET request to /login',function() {
+  it('should send back the login page');
+  //how to test this?!
+  it('should follow 302 when login is invalid and show flash message');
+});
+
+describe('logging in by POSTing to /login', function() {
+    it('without valid username cannot login');
+    it('without valid password cannot login');
+    it('with valid credentials can login');
+});
+
+describe('logging out by GETing /logout', function() {
+  it('should log out the logged in user');
+  it('should throw no errors if there is no user logged in');
+});
+
+ +

Firstly in order to create users it's necessary to npm install --save bcrypt and then (borrowing liberally from StackOverflow) create a module that hashes and salts a given password and saves a user with that hash into the database.

+ +
var bcrypt = require('bcrypt');
+var SALT_WORK_FACTOR = 10;
+
+module.exports = function(db) {
+  return {
+    create: function(username, password, callback) {
+        bcrypt.genSalt(SALT_WORK_FACTOR, function(err, salt) {
+            if (err) {
+                callback(err);
+                return;
+            }
+            bcrypt.hash(password, salt, function(err, hash) {
+                if (err) {
+                    callback(err);
+                    return;
+                }
+                db.users.save({
+                    username:username,
+                    password:hash
+                }, function(err, result) {
+                    if(err) {
+                        callback(err.err);
+                    } else {
+                        callback('user created');
+                    }
+                });
+            });
+        });
+    }
+  };
+};
+
+ +

This function takes a database parameter so that the tests and the command line runner that exercise it can pass in different databases. It also demonstrates the smelliness of nested callbacks that I've put off dealing with three times now… hitting the same problem three times is a definite flag it's time to deal with it!

+ +

(but not right now)

+ +

as an aside - a colleague spotted how smelly this code is in a screenshot on twitter from across a room!

+ +

Logging in Tests

+

The test setup for the logging in tests is slightly different as it's necessary to grab the underlying SuperAgent instance that SuperTest wraps. SuperAgent will manage its cookies so you can extend the example below to allow tests of behaviour once logged in.

+ +
var request = require('supertest');
+var expect = require('chai').expect;
+
+var server;
+var agent;
+var db;
+var login;
+
+beforeEach(function() {
+    //set environment to test and init things
+    process.env.NODE_ENV = 'test'; 
+    db = require('../server/db').db;
+    server = require('../server').app;
+    agent = request.agent(server);
+});
+
+ +

Having access to the agent and the server application then allows test that look like

+ +
    it('without valid username cannot login', function(done) {
+        agent
+          .post('/login')
+          .send({ username: 'not a real user', password: 'password' })
+          .end(function(err, res) {
+            expect(res.status).to.equal(302);
+            expect(res.header.location).to.equal('/login');
+            done();
+          });
+    });
+
+ +

Not hugely different in syntax to the SuperTest tests but necessary in order to interact with the session.

+ +
+ + +
+
+ + + + + + + + + + diff --git a/weeknotes.html b/weeknotes.html new file mode 100644 index 000000000..136194a68 --- /dev/null +++ b/weeknotes.html @@ -0,0 +1,471 @@ + + + + + + + + + + + + + + + + + + + + + + Mindless Rambling Nonsense + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ +
+
+ posted on: 05 Nov 2021 +
+
+
+ +
+ +
+
+ posted on: 05 Nov 2021 +
+
+
+ +
+ +
+
+ posted on: 05 Nov 2021 +
+
+
+ +
+ +
+
+ posted on: 25 Oct 2021 +
+
+
+ +
+ +
+
+ posted on: 09 Oct 2021 +
+
+
+ +
+ +
+
+ posted on: 01 Oct 2021 +
+
+
+ +
+ +
+
+ posted on: 24 Sep 2021 +
+
+
+ +
+ +
+
+ posted on: 23 Jul 2021 +
+
+
+ +
+ +
+
+ posted on: 16 Jul 2021 +
+
+
+ +
+ +
+
+ posted on: 09 Jul 2021 +
+
+
+ +
+ + « Prev + + + + + 1 + + + + + + 2 + + + + + + 3 + + + + + + 4 + + + + + + 5 + + + + + + + Next » + + +
+ + +
+ + + + + + + + diff --git a/weeknotes/2020/10.html b/weeknotes/2020/10.html new file mode 100644 index 000000000..0e8c6a5b2 --- /dev/null +++ b/weeknotes/2020/10.html @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + 2020 Week 10 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2020 Week 10

+ +

💖 multiple instances of being supported or seeing people be supported this week
+🌪 basically 50% of my time this week was meetings
+🏚 and most of the rest of the time trying to catch up ad-hoc on actual work
+⌨️ paired once, demoed TDD once and…
+🙈 spent an hour confused before I caught up and realised people had given me the answer and needed help implementing it
+🎛 remote pairing/mobbing is hard. I wasn't good at it
+😕 we had an API design session. I felt really confused and unprepared
+🎯 realised afterwards I've had a really clear direction for 18 months and the teams have overtaken me
+🤔 I'll have to do more thinking and get everyone else to own the design more
+🔨 went back this morning and refactored something we found yesterday because I couldn't live with myself if I'd left it
+✅ had fun

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2020/10.png b/weeknotes/2020/10.png new file mode 100644 index 000000000..60d42b42c Binary files /dev/null and b/weeknotes/2020/10.png differ diff --git a/weeknotes/2020/11.html b/weeknotes/2020/11.html new file mode 100644 index 000000000..fff56ad4f --- /dev/null +++ b/weeknotes/2020/11.html @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + 2020 Week 11 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2020 Week 11

+ +

🌪 felt like I was running around spinning plates this week
+💖 love that someone noticed and reminded me to talk and let people help
+🔭 worked remote two days. It was ok but strange to start something new on a background of anxiety
+🧭 feeling disconnected from the why of some things
+😍 new colleague joined us and has got stuck in right away
+👩‍👩‍👧‍👦 had our Friday show and tell remotely. Which worked well but was a bit weird
+🦄 discovered that two things I've been worrying about have been started and practically finished
+✅ had fun

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2020/11.png b/weeknotes/2020/11.png new file mode 100644 index 000000000..dab08e093 Binary files /dev/null and b/weeknotes/2020/11.png differ diff --git a/weeknotes/2020/12.html b/weeknotes/2020/12.html new file mode 100644 index 000000000..3bcf038b5 --- /dev/null +++ b/weeknotes/2020/12.html @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + 2020 Week 12 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2020 Week 12

+ +

🎢 this week has been wild
+ 💝 what a simply wonderful time to work with people that care at a company that wants to do good
+ 🤳 seems ridulously self-interested to waffle about how hard or not I've found this week when folk are losing their income

+ +

Schools are closing. We used to homeschool. I wondered if it would be useful to share what we learned.

+ +

I wrote hundreds of words on homeschooling. My wife laughed at me. Told me to stop waffling and said “write this down”

+ +

Obvs this was our experience, with our kids, in our context but…

+ +
    +
  • Let them play. Play is learning.
  • +
  • You’re not a teacher. That’s ok. Teaching is hard (kudos teachers 👑).
  • +
  • You’re not teaching 30 kids so don’t act like you are. A couple of hours of "teaching" a day will likely be plenty (and plenty exhausting).
  • +
  • Put stuff out they’ve not played with recently e.g. things to count with, puzzles, a book they hardly ever look at. Just leave it out for them to notice.
  • +
  • Use their interests. Talk to them about the topic you want to teach while they’re playing. Baking, shopping, accounting, planning an imaginary business.
  • +
  • Don’t get sucked in to the overwhelming amount of resources. twinkl is free right now. TES has resources for older kids. There’s so much online you can get sucked in to just comparing them. We had the most success when we just spent some time with them doing something at whatever level they’re at.
  • +
  • There are amazing homeschoolers that look like they’ve got it sorted. Social media will make it look like you’re not doing a good job cos others share at their best. But their house is messy and full of fighting kids too. They just don’t take photos while that’s happening
  • +
  • What worked for us was making sure we went outside every day. Even if it was wet
  • +
+ +

I made these:

+ + + +

Or rather they are the ones I kept. I’d see how quickly I could make something that helped them or held their attention. #3 loved being allowed to hold my phone and shouting at readerer The other two loved getting cat gifs as rewards

+ +

🎨I’d love to see what the digital community all make as learning resources.
+👩‍❤️‍👩It's such a strange time in the world. Remember to be kind to others and to yourself.

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2020/12.png b/weeknotes/2020/12.png new file mode 100644 index 000000000..b692e3a3d Binary files /dev/null and b/weeknotes/2020/12.png differ diff --git a/weeknotes/2020/13.html b/weeknotes/2020/13.html new file mode 100644 index 000000000..32544587d --- /dev/null +++ b/weeknotes/2020/13.html @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + 2020 Week 13 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2020 Week 13

+ +

🎥 a week of being frustrated and learning how (not?) to do video calls
+💖 still incredible to see everyone supporting each other
+🔭 it's lovely to be surrounded by family all the time but…
+❓ what am I supposed to do - turns out my job was looking for serendipitous conversations…
+🧭 and helping nudge things in the right direction or help give people courage to try something
+🏚 that's not going to work now so I need to find a better way to contribute
+🚀 fascinating to see this normally slow company try to act at furious pace
+👩‍⚕️ important to figure out how to keep some of this new bold pace once we're past the pandemic
+✅ had fun

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2020/13.png b/weeknotes/2020/13.png new file mode 100644 index 000000000..807aecade Binary files /dev/null and b/weeknotes/2020/13.png differ diff --git a/weeknotes/2020/2.html b/weeknotes/2020/2.html new file mode 100644 index 000000000..eb7c88635 --- /dev/null +++ b/weeknotes/2020/2.html @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + 2020 Week 2 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2020 Week 2

+ +

😫 this week was full on!
+🎩 had to change hats so often - people management, presenting, refactoring, coaching, architecting, and more.
+🛏️ I'm frazzled
+🦸‍♀️ lots of folk dealing with frustrating circumstances this week
+💖 wonderful to see people pull together
+💻 spent most of a day with the mob working on offers
+🗣️ banged on about pipes and filters
+💎 automated weeknotes out of my blog with some ruby
+✅ had fun

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2020/2.png b/weeknotes/2020/2.png new file mode 100644 index 000000000..051ede8cd Binary files /dev/null and b/weeknotes/2020/2.png differ diff --git a/weeknotes/2020/3.html b/weeknotes/2020/3.html new file mode 100644 index 000000000..c39fa9b58 --- /dev/null +++ b/weeknotes/2020/3.html @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + 2020 Week 3 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2020 Week 3

+ +

🏥 2 poorly kids and full of a cold. So 2 days working from home
+💼 2 days at internal events
+🛏️ I'm frazzled
+🦸‍♀️ Wonderful that we kicked off day two with a reminder that presenting is hard so send love to the stage
+💖 And brill to end with a very senior bod willing to show emotion to a very large audience
+💻 Wrote some code while I was stuck at home
+💪 Launched a secret thing
+🕵️‍♂️ Wish it wasn't secret
+💎 saw multiple same day fixes from more than one team
+📊 big decision to make
+✏️ wrote this in the github web UI. It wasn't too bad
+✅ had fun

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2020/3.png b/weeknotes/2020/3.png new file mode 100644 index 000000000..c9611c52a Binary files /dev/null and b/weeknotes/2020/3.png differ diff --git a/weeknotes/2020/4.html b/weeknotes/2020/4.html new file mode 100644 index 000000000..69bb653ee --- /dev/null +++ b/weeknotes/2020/4.html @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + 2020 Week 4 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2020 Week 4

+ +

💖lots of seeing folk caring this week
+🍼lots of home-life getting in the way
+💏lots of loving how flexible our workplace is
+🧬totally alpha code alongside production is making some things a bit harder
+🦸‍♀️ some really surprisingly productive meetings mean we get to do some incredible things
+✅ had fun

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2020/4.png b/weeknotes/2020/4.png new file mode 100644 index 000000000..c922c66a6 Binary files /dev/null and b/weeknotes/2020/4.png differ diff --git a/weeknotes/2020/5.html b/weeknotes/2020/5.html new file mode 100644 index 000000000..31c44b482 --- /dev/null +++ b/weeknotes/2020/5.html @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + 2020 Week 5 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2020 Week 5

+ +

💖 2 big incredible things this week
+💪 we're trialling locking your account when you log in if you have a known compromised (pwned) password
+🔐 we turned on the AWS WAF in front of a subset of our systems
+🚀 both things have been a labour of love
+🗣 lots of meetings but still feel like I've spent time with the team
+❓ wish I knew how so I could do that more
+✅ had fun

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2020/5.png b/weeknotes/2020/5.png new file mode 100644 index 000000000..33a682793 Binary files /dev/null and b/weeknotes/2020/5.png differ diff --git a/weeknotes/2020/6.html b/weeknotes/2020/6.html new file mode 100644 index 000000000..38515371e --- /dev/null +++ b/weeknotes/2020/6.html @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + 2020 Week 6 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2020 Week 6

+ +

☯️ saw the best and worst sides of being at a big company this week
+🗺 saw people hear the word "plan" and assume it means a loss of autonomy
+Ⓡ agile still has requirements
+💡 it just doesn't pretend it can get them all right on day one
+| agile still has deadlines
+🔭 but it plays with scope to meet them
+🚀 lots of same day fixes again this week
+✅ had fun

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2020/6.png b/weeknotes/2020/6.png new file mode 100644 index 000000000..2a3c11484 Binary files /dev/null and b/weeknotes/2020/6.png differ diff --git a/weeknotes/2020/7.html b/weeknotes/2020/7.html new file mode 100644 index 000000000..e3c0bf519 --- /dev/null +++ b/weeknotes/2020/7.html @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + 2020 Week 7 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2020 Week 7

+ +

🌪this week was wild
+🏃‍♂️ it was non-stop
+💰every meeting feels different now we've been told how much money we have to make for each pound we spend
+🗣lots of honesty this week and remembering that talking has been our super power
+⛈including talking about EventStorming at xpmanchester
+💤so draining but was fun too
+💖we challenged each other to give feedback more. I gave feedback every day. It's hard
+✅ had fun

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2020/7.png b/weeknotes/2020/7.png new file mode 100644 index 000000000..691707be0 Binary files /dev/null and b/weeknotes/2020/7.png differ diff --git a/weeknotes/2020/9.html b/weeknotes/2020/9.html new file mode 100644 index 000000000..974d6adb0 --- /dev/null +++ b/weeknotes/2020/9.html @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + 2020 Week 9 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2020 Week 9

+ +

🍾 some incredible secret news this week
+💪 people are overloaded but knuckling down and trying to figure a way through
+🚀 this week flew by
+🔍 really interesting retro helping a colleague prepare for a talk
+⌨️ I wrote some code this week
+🏚 I'd forgotten how much harder writing code is than drawing on a whiteboard and then waiting while others do
+✅ had fun

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2020/9.png b/weeknotes/2020/9.png new file mode 100644 index 000000000..533972319 Binary files /dev/null and b/weeknotes/2020/9.png differ diff --git a/weeknotes/2021/12.html b/weeknotes/2021/12.html new file mode 100644 index 000000000..ca3d39317 --- /dev/null +++ b/weeknotes/2021/12.html @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 12 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 12

+ +

🌪 have been off for 3 weeks, OMG a lot can happen in 3 weeks
+3️⃣ Only in work for 3 days this week
+🤩 attended the brill CTO craft conf for half of that time
+👎 spent zero time with the engineering team
+🤗 recently joined team member continues to be empathetic and wonderful (you know who you are Mr Burgers-for-tea-today)
+🏚 IR35 continues to be a royal pain in the behind
+🗣 attended a brilliantly facilitated session trying to bridge an expectation divide between two groups
+✅ had fun

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/12.png b/weeknotes/2021/12.png new file mode 100644 index 000000000..0f226ba27 Binary files /dev/null and b/weeknotes/2021/12.png differ diff --git a/weeknotes/2021/13.html b/weeknotes/2021/13.html new file mode 100644 index 000000000..b628bd99b --- /dev/null +++ b/weeknotes/2021/13.html @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 13 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 13

+ +

🌪 WHAT A WEEK, BLIMEY
+🏃‍♂️ has been incredible to see people working together
+💔 has been sad to see people leaving
+👎 out of eleven hours put to one side I managed one hour with engineers
+😱 attended the worst run meeting there has ever been
+🗣 talking, being open, being honest, that's all there really is
+✅ had fun

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/13.png b/weeknotes/2021/13.png new file mode 100644 index 000000000..93cdb3b30 Binary files /dev/null and b/weeknotes/2021/13.png differ diff --git a/weeknotes/2021/14.html b/weeknotes/2021/14.html new file mode 100644 index 000000000..2eaee6939 --- /dev/null +++ b/weeknotes/2021/14.html @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 14 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 14

+ +

🎢 this 4 day week has been at least 6 days long
+🗣 got to talk about eventsourcing
+👎 only two hours engineering this week
+😍 but that was two fun hours that ended on a high
+🏃‍♂️ there are some hard miles being worked through at the moment
+👩‍❤️‍👨 talking, being open, being honest, still the thing
+✅ had fun

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/14.png b/weeknotes/2021/14.png new file mode 100644 index 000000000..09438d6d9 Binary files /dev/null and b/weeknotes/2021/14.png differ diff --git a/weeknotes/2021/15.html b/weeknotes/2021/15.html new file mode 100644 index 000000000..2614d76c4 --- /dev/null +++ b/weeknotes/2021/15.html @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 15 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 15

+ +

🎢 this 5 day week has felt 2 days long - wat
+🗣 aimed to spend 10 hours with engineers doing the typey-typey
+👍 managed 6 hours
+😍 it is a balm for the soul
+🏃‍♂️ some of what is happening is really hard and draining
+👩‍❤️‍👨 but somehow this week still felt good for more time than not
+💖 seeing folk working to improve and be kind recharges me
+✅ had fun

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/15.png b/weeknotes/2021/15.png new file mode 100644 index 000000000..893044348 Binary files /dev/null and b/weeknotes/2021/15.png differ diff --git a/weeknotes/2021/16.html b/weeknotes/2021/16.html new file mode 100644 index 000000000..34b5dc07b --- /dev/null +++ b/weeknotes/2021/16.html @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 16 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 16

+ +

😮 thought it was Friday on Monday
+❌ didn't expect to get to spend time with the engineers
+👍 managed to drop in a few times
+👎 only short sessions though
+🏃‍♂️ still hard miles but some great, open, honest things this week
+💖 received feedback
+📈 I love not having to guess about where I can get better
+🚶‍♂️ met lovely people for a walk in the actual world
+✅ had fun

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/16.png b/weeknotes/2021/16.png new file mode 100644 index 000000000..09278edae Binary files /dev/null and b/weeknotes/2021/16.png differ diff --git a/weeknotes/2021/17.html b/weeknotes/2021/17.html new file mode 100644 index 000000000..43d68933d --- /dev/null +++ b/weeknotes/2021/17.html @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 17 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 17

+ +

🎢 another wild ride
+❌ didn't expect to get to spend time with the engineers
+🔬 awesome colleague suggested a smart experiment
+🪵 another awesome collegue spotted a problem and did the right thing
+🍒 and I got to pair on some low hanging fruit
+💖 started the week receiving lovely feedback
+💉 had covid vaccine number one
+✅ had fun

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/17.png b/weeknotes/2021/17.png new file mode 100644 index 000000000..3e8326e92 Binary files /dev/null and b/weeknotes/2021/17.png differ diff --git a/weeknotes/2021/18.html b/weeknotes/2021/18.html new file mode 100644 index 000000000..115cdfdb5 --- /dev/null +++ b/weeknotes/2021/18.html @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 18 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 18

+ +

😭 about as low as I've been at work ever at points this week
+💖 value that I felt safe to be open about it and that people were there
+❌ almost didn't get to spend any time with the engineers
+👀 the time I did get was eye-opening and useful
+✍️ finally getting to the point I have enough of an internalised model of funeralcare to start to communicate
+🧠 I still know nothing, but I think I might have found some of the edges
+🤗 I've too much to do, asked for help, and it was so much better
+🗣 spent some time with a friend from the local dev community talking about work stuff
+🤩 did similar recently with someone else about event sourcing questions they had
+💖 feel very lucky to be part of a community
+😍 and thankful for all the folk that run user groups and community events
+✅ had fun (barely)

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/18.png b/weeknotes/2021/18.png new file mode 100644 index 000000000..488ef08af Binary files /dev/null and b/weeknotes/2021/18.png differ diff --git a/weeknotes/2021/19.html b/weeknotes/2021/19.html new file mode 100644 index 000000000..af45e74be --- /dev/null +++ b/weeknotes/2021/19.html @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 19 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 19

+ +

😭 another rollercoaster week
+💖 received nice feedback, that got me through the week
+❌ almost didn't get to spend any time with the engineers
+👀 the time I did get was because of a heisenbug in production
+🤩 new starter from a new partner - if the others are even half as good we're in a great place
+🌗 thought that I'd achieved something I'd been heading at for weeks
+👎 but no (not yet!)
+✅ had fun (barely, thanks to awesome team)

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/19.png b/weeknotes/2021/19.png new file mode 100644 index 000000000..11cb20a9c Binary files /dev/null and b/weeknotes/2021/19.png differ diff --git a/weeknotes/2021/2.html b/weeknotes/2021/2.html new file mode 100644 index 000000000..1bb8f2c49 --- /dev/null +++ b/weeknotes/2021/2.html @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 2 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 2

+ +

💖 first week with funeralcare
+🗺 wardley mapping session - learned so much!
+⌨️ wrote some code and took part in a production deploy
+❓I don't know so much!
+🤔 which is exciting
+🤫 did my best to shut up more
+🤯 had a wonderful intro to how we talk about funerals and how important the service we provide is
+✅ had fun

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/2.png b/weeknotes/2021/2.png new file mode 100644 index 000000000..781e3a2ae Binary files /dev/null and b/weeknotes/2021/2.png differ diff --git a/weeknotes/2021/20.html b/weeknotes/2021/20.html new file mode 100644 index 000000000..c17d918b4 --- /dev/null +++ b/weeknotes/2021/20.html @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 20 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 20

+ +

😭 yet another rollercoaster week
+😥 too much work for everyone
+🍀 time with the engineers was only through luck
+📽 writing lots of decks this week
+🐦 a quick play with AWS canary - really very cool
+🙌 some awesome sessions run this week
+🗺 took part in some Wardley mapping
+✅ had fun (sometimes)

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/20.png b/weeknotes/2021/20.png new file mode 100644 index 000000000..681197380 Binary files /dev/null and b/weeknotes/2021/20.png differ diff --git a/weeknotes/2021/21.html b/weeknotes/2021/21.html new file mode 100644 index 000000000..2cbf3da4e --- /dev/null +++ b/weeknotes/2021/21.html @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 21 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 21

+ +

🛹 onboarded 10 new team members from Zuhlke this week
+🤷 it went surprisingly smoothly
+💖 so much effort and kindness from everyone to make it work
+🎹 no time writing code this week so, I ended the week test driving a state machine
+🗺 took part in more Wardley mapping
+🙌 attended a diversity and inclusion session
+🎓 the work that some of the senior leaders have put in to learn about D&I shines through in how they talk
+😍 promised Chris L that I'd include they said nice things, cos they did, and it always helps to get feedback +✅ had fun (despite the odds)

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/21.png b/weeknotes/2021/21.png new file mode 100644 index 000000000..92dffc591 Binary files /dev/null and b/weeknotes/2021/21.png differ diff --git a/weeknotes/2021/23.html b/weeknotes/2021/23.html new file mode 100644 index 000000000..f0d8e8f67 --- /dev/null +++ b/weeknotes/2021/23.html @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 23 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 23

+ +

⌨️ spent quite a bit of time (helping) writing code this week
+🔁 some code to automate browser performance testing
+😰 lots of time swearing at YAML and waiting for lambda@edge's slow feedback cycle
+💖 bits of time meeting different people to try and understand them better
+😱 some time staring into the middle distance in frustration
+🤗 some opportunities to give feedback
+✅ had fun (intermittently)

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/23.png b/weeknotes/2021/23.png new file mode 100644 index 000000000..e1c955b08 Binary files /dev/null and b/weeknotes/2021/23.png differ diff --git a/weeknotes/2021/24.html b/weeknotes/2021/24.html new file mode 100644 index 000000000..24f3bff40 --- /dev/null +++ b/weeknotes/2021/24.html @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 24 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 24

+ +

❌ it was a good week but…
+🤦 so disappointed with the lazy response from Co-op on advertising in GB News
+💸 if your principles are only firm when they don't cost you money, they're not principles
+🤔 a female colleague quote tweeted me about it. They had trolls respond and, I didn't. A little view of the misogynists that Co-op is choosing to pander to
+⌨️ only wrote code for fun this week
+😰 SO MANY BUSYWORK MEETINGS
+💖 ended the week with a great meeting of minds about how to improve enterprise testing
+😍 and a wonderful session run by Andy Tabberer that has changed how I see the world and behave
+💣 attended a CAB for the first time in three years, I'm certain this process adds nothing
+❓ what fun I had was all offset by organisational pathology

+ + +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/24.png b/weeknotes/2021/24.png new file mode 100644 index 000000000..fb64271d3 Binary files /dev/null and b/weeknotes/2021/24.png differ diff --git a/weeknotes/2021/25.html b/weeknotes/2021/25.html new file mode 100644 index 000000000..b59355901 --- /dev/null +++ b/weeknotes/2021/25.html @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 25 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 25

+ +

🤔 odd week, only three days in work
+🤷‍♀️ started with bittersweet news at home
+🦷 ended with one of the kids having dental work under general anaesthetic
+⌨️ only wrote code for fun this week
+🤯 can't get my head around some things at work
+💖 principles and empathy is the name of the game
+😭 some folk who have been doing great work are moving off the team
+🙌 zuhlke folk that have joined us are starting to build a head of steam
+~ fun at points

+ + +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/25.png b/weeknotes/2021/25.png new file mode 100644 index 000000000..a0d2e4e47 Binary files /dev/null and b/weeknotes/2021/25.png differ diff --git a/weeknotes/2021/26.html b/weeknotes/2021/26.html new file mode 100644 index 000000000..164a8ac8e --- /dev/null +++ b/weeknotes/2021/26.html @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 26 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 26

+ +

😥 long week
+🌓 how are we half-way through the year
+💉 started with second COVID jab
+💖 got some awesome, timely support this week
+😬 for some scary meetings
+😅 which weren't as scary as I worried
+😘 some awesome colleagues move off the team this week
+👋 and another left entirely
+👻 managed to completely break a project with an innocuous change - haunted NPM packages at fault
+✅ had fun

+ + +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/26.png b/weeknotes/2021/26.png new file mode 100644 index 000000000..98e3f5b58 Binary files /dev/null and b/weeknotes/2021/26.png differ diff --git a/weeknotes/2021/27.html b/weeknotes/2021/27.html new file mode 100644 index 000000000..de70cc94e --- /dev/null +++ b/weeknotes/2021/27.html @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 27 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 27

+ +

📝 have been keeping a daily journal and tagging what I'm doing
+💉 only a few days and my second COVID jab will have taken
+💖 got good time with awesome people this week
+📹 practice for my next youtube video
+2️⃣ two subscribers now
+🧠 time planning the future with Andy Tabberer
+🤔 made time to think
+⚡️ folk got some code in prod to get started on some important work
+🔎 joined a fascinating major incident
+🙌 couple of folk were awesome in joining and fixing things
+🪟 got to dust off my Windows Server skillz
+✅ had fun

+ + +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/27.png b/weeknotes/2021/27.png new file mode 100644 index 000000000..32064085d Binary files /dev/null and b/weeknotes/2021/27.png differ diff --git a/weeknotes/2021/28.html b/weeknotes/2021/28.html new file mode 100644 index 000000000..3288db9d1 --- /dev/null +++ b/weeknotes/2021/28.html @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 28 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 28

+ +

🦠 spent a few days utterly exhausted
+⌨️ a little time fixing a broken service
+🍠 the fix was two lines of YAML
+⏰ and a very lot of downloading and compiling ruby and ruby gems
+✍️ I miss when writing code involved writing code
+🎓 joined a class working through Jez Humble's Lean Agile university
+🤔 odd week
+✅ had fun

+ + +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/28.png b/weeknotes/2021/28.png new file mode 100644 index 000000000..ad85cff04 Binary files /dev/null and b/weeknotes/2021/28.png differ diff --git a/weeknotes/2021/29.html b/weeknotes/2021/29.html new file mode 100644 index 000000000..39d72c75e --- /dev/null +++ b/weeknotes/2021/29.html @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 29 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 29

+ +

🌞 I WAS BORN FOR THIS SUN
+🦶 although my middle-aged man feet got pretty sweaty
+🍽 very bitty week of spinning plates
+😈 got to join a meeting and say we shouldn't be using batch files to save 5 seconds of API calls
+🖖 started a couple of async meetings
+✅ had fun

+ + +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/29.png b/weeknotes/2021/29.png new file mode 100644 index 000000000..25a0ada63 Binary files /dev/null and b/weeknotes/2021/29.png differ diff --git a/weeknotes/2021/3.html b/weeknotes/2021/3.html new file mode 100644 index 000000000..0d261964e --- /dev/null +++ b/weeknotes/2021/3.html @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 3 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 3

+ +

❌ didn't manage to keep three days free for engineering
+🗣 but did get to meet even more people 1-2-1
+⌨️ did some CI fangling
+💣 joined an investigation of some networking confusion
+❓still fun not knowing so much!
+💖 had a great session to understand our systems
+✅ had fun

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/3.png b/weeknotes/2021/3.png new file mode 100644 index 000000000..9a25c36bd Binary files /dev/null and b/weeknotes/2021/3.png differ diff --git a/weeknotes/2021/38.html b/weeknotes/2021/38.html new file mode 100644 index 000000000..fb7c8db0c --- /dev/null +++ b/weeknotes/2021/38.html @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 38 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 38

+ +

🎬 first week in new job
+⌨️ code in production on Wednesday
+🤫 2 days with no meetings
+🤯 getting meta - I can use Posthog to graph adoption of my change to PostHog
+🔋 battery life on M1 macs is incredible
+🎥 saw old job folk for a retro/lean coffee
+✅ had fun

+ +

a graph comparing clicks of my new button with page views

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/38.png b/weeknotes/2021/38.png new file mode 100644 index 000000000..b8f83be37 Binary files /dev/null and b/weeknotes/2021/38.png differ diff --git a/weeknotes/2021/39.html b/weeknotes/2021/39.html new file mode 100644 index 000000000..81017f08b --- /dev/null +++ b/weeknotes/2021/39.html @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 39 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 39

+ +

🎬 week two in new job
+🌃 went to that London
+🍽 met loads of people
+🧠 such fun, if slow progress, tracking down a tricky bug
+💖 had nice feedback
+✅ had fun

+ + +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/39.png b/weeknotes/2021/39.png new file mode 100644 index 000000000..a6add4f8b Binary files /dev/null and b/weeknotes/2021/39.png differ diff --git a/weeknotes/2021/4.html b/weeknotes/2021/4.html new file mode 100644 index 000000000..1dcfeb6bd --- /dev/null +++ b/weeknotes/2021/4.html @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 4 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 4

+ +

👎 managed to hold precisely two hours for coding
+👍 meeting time was focussed on ongoing work
+🗣 now twenty people into meeting everyone 1-2-1
+🤔 really loving everyone's insight
+📝 did some mob diagramming
+🐷 talked nonsense about ham at Mega COP
+✅ had fun

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/4.png b/weeknotes/2021/4.png new file mode 100644 index 000000000..7bef302e0 Binary files /dev/null and b/weeknotes/2021/4.png differ diff --git a/weeknotes/2021/40.html b/weeknotes/2021/40.html new file mode 100644 index 000000000..2393fa296 --- /dev/null +++ b/weeknotes/2021/40.html @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 40 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 40

+ +

🎬 week three in new job
+🤔 practiced honesty in 1-2-1, meant I got supported and challenged, good result
+🧠 still slower than I'd like to be, but learning
+🔢 used property based testing to try to solve a problem
+🤯 asked for the rule around some spending -> "just do what you think is in company's best interest" #Trust
+💖 had nice feedback
+🇮🇹 set my phone to Italian so I could learn
+🪣 so many systems now serve content in Italian on other devices
+👨‍🦲 got a new webcam because old one was too low resolution. Can now see how bald I am :'(
+✅ had fun

+ + +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/40.png b/weeknotes/2021/40.png new file mode 100644 index 000000000..599d0510a Binary files /dev/null and b/weeknotes/2021/40.png differ diff --git a/weeknotes/2021/42.html b/weeknotes/2021/42.html new file mode 100644 index 000000000..d73cc78ac --- /dev/null +++ b/weeknotes/2021/42.html @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 42 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 42

+ +

📅 week five in the new job, so a month however you cut it
+🚀 I'd forgotten how much fun the dopamine fix of deep work on a feature was
+🗣 10.5 hours of meetings over two weeks, in the last job I would have that much in one day
+🤯 asked the team I'm on if it was OK to take a day off -> "don't need to ask, only inform" #Trust
+🏢 spent a day at the Manchester WeWork
+☕️ on-site barista in the morning
+🍕 had lunch with old colleagues
+🏃‍♀️ I'm forgetting to do exercise more and more as the weather gets worse
+🕰 lack of a commute means I tend to forget to write these
+✅ had fun

+ + +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/42.png b/weeknotes/2021/42.png new file mode 100644 index 000000000..4d1cf6035 Binary files /dev/null and b/weeknotes/2021/42.png differ diff --git a/weeknotes/2021/44.html b/weeknotes/2021/44.html new file mode 100644 index 000000000..58bf5ddd9 --- /dev/null +++ b/weeknotes/2021/44.html @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 44 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 44

+ +

🦸‍♂️ Support Hero this week - 5 days of helping people
+🚀 Helped a bunch of people
+🛠 Fixed 3 bugs, PR in for a 4th
+🤯 a little window into how much more there is to learn
+🗻 walked up a big hill with the dog
+✅ had fun

+ + +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/44.png b/weeknotes/2021/44.png new file mode 100644 index 000000000..dc141beba Binary files /dev/null and b/weeknotes/2021/44.png differ diff --git a/weeknotes/2021/45.html b/weeknotes/2021/45.html new file mode 100644 index 000000000..b847a3321 --- /dev/null +++ b/weeknotes/2021/45.html @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 45 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 45

+ +

🍐 Two superb pairing sessions this week
+🦥 Making slow progress on the feature I'm working on
+🚀 Made some cool improvements to some tests' speed and can apply to other tests
+✏️ I'm working on a "breakdown" feature. I have typed "breadkown" thousands of times
+✅ had fun

+ + +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/45.png b/weeknotes/2021/45.png new file mode 100644 index 000000000..5c11953fa Binary files /dev/null and b/weeknotes/2021/45.png differ diff --git a/weeknotes/2021/46.html b/weeknotes/2021/46.html new file mode 100644 index 000000000..a698d560f --- /dev/null +++ b/weeknotes/2021/46.html @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 46 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 46

+ +

🍐 More superb pairing sessions this week
+🦥 Still making slow progress on the feature I'm working on but, I've reached the point where I can see how I should have done it
+🗑 inadvertently wasted someone's time this week. I've moved on from knowing nothing to "a little knowledge is a dangerous thing"
+🎓 always be a beginner mindset is great but, I'd forgotten how hard new language, new framework, and a new system can be
+⏰ I need to be more purposeful about learning and carefulness
+❓ each time I asked for help, things got a tonne easier. I should be old enough to know that by now! I need to be better at spotting when I should put effort and when I should raise the flag
+📈 That all might seem negative, but making stuff is 💯
+✅ had fun

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/46.png b/weeknotes/2021/46.png new file mode 100644 index 000000000..6fadc70de Binary files /dev/null and b/weeknotes/2021/46.png differ diff --git a/weeknotes/2021/5.html b/weeknotes/2021/5.html new file mode 100644 index 000000000..fc89edea4 --- /dev/null +++ b/weeknotes/2021/5.html @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 5 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 5

+ +

🚀 got concentrated time coding with others this week
+💖 someone mentioned about being scared of a bit of code, so we fixed it
+🗣 privileged that I'm able to choose not to go to meetings to make that time to code
+🔐 facilitated some threat modelling
+🙌 lucky to have awesome colleagues to crib from
+🤯 it's been a hard a week, someone said something nice in an email, I nearly cried
+🤗 make sure you're saying nice things to people when you think them, they might really need it
+✅ had fun

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/5.png b/weeknotes/2021/5.png new file mode 100644 index 000000000..96cab82d6 Binary files /dev/null and b/weeknotes/2021/5.png differ diff --git a/weeknotes/2021/6.html b/weeknotes/2021/6.html new file mode 100644 index 000000000..4a7818217 --- /dev/null +++ b/weeknotes/2021/6.html @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 6 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 6

+ +

👎 managed to hold only a single hour for coding
+🤯 everything still feels new so I asked lots of questions
+🙌 I felt like I was slowing people down but the mob were welcoming
+👍 again meeting time was focussed on ongoing work
+🌪 but there's so much going on
+🤔 and I'm terrible at prioritising
+🏃‍♂️ I'm biased towards action
+☸️ so all the planning isn't my wheelhouse
+✅ had fun

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/6.png b/weeknotes/2021/6.png new file mode 100644 index 000000000..38be7256e Binary files /dev/null and b/weeknotes/2021/6.png differ diff --git a/weeknotes/2021/7.html b/weeknotes/2021/7.html new file mode 100644 index 000000000..d7fc1dbf1 --- /dev/null +++ b/weeknotes/2021/7.html @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 7 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 7

+ +

😱 thought I was only going to have spent 30 minutes coding this week
+💖 then spent a big chunk of Friday afternoon pairing
+🧠 learned some vue
+🔎 clarified some naming
+🔬 wrote some tests
+🗣 so many meetings though
+✅ had fun

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/7.png b/weeknotes/2021/7.png new file mode 100644 index 000000000..aa8e2ab70 Binary files /dev/null and b/weeknotes/2021/7.png differ diff --git a/weeknotes/2021/8.html b/weeknotes/2021/8.html new file mode 100644 index 000000000..21acdb473 --- /dev/null +++ b/weeknotes/2021/8.html @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 8 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 8

+ +

✅ I spent most of a day over the week in with the engineers
+💣 but it was all responding to an incident
+😚 two team members were awesome in spotting it
+📚 exciting new team member joined and is posting me a book
+💖 and learned someone else exciting is joining us soon
+☯️ needed to be a grown-up at points
+✅ had fun

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/8.png b/weeknotes/2021/8.png new file mode 100644 index 000000000..7294caa90 Binary files /dev/null and b/weeknotes/2021/8.png differ diff --git a/weeknotes/2021/9.html b/weeknotes/2021/9.html new file mode 100644 index 000000000..de31641cb --- /dev/null +++ b/weeknotes/2021/9.html @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + 2021 Week 9 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

2021 Week 9

+ +

½ Spent a good amount of time with the team but not during typey-typey
+🤯 very frustrating at points this week
+💖 lots of opportunities to be reminded of how much everyone cares
+📈 getting closer to understanding where systems are and could be in new team
+🤮 dusted off my Windows Server 2008 skills this week
+🏃‍♂️ shorts weather this week
+✅ had fun

+ +
+
+ +
+ + + + + + + diff --git a/weeknotes/2021/9.png b/weeknotes/2021/9.png new file mode 100644 index 000000000..9a52910af Binary files /dev/null and b/weeknotes/2021/9.png differ diff --git a/weeknotes/page2/index.html b/weeknotes/page2/index.html new file mode 100644 index 000000000..2a9e3de51 --- /dev/null +++ b/weeknotes/page2/index.html @@ -0,0 +1,482 @@ + + + + + + + + + + + + + + + + + + + + + + Mindless Rambling Nonsense - page 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ +
+
+ posted on: 02 Jul 2021 +
+
+
+ +
+ +
+
+ posted on: 25 Jun 2021 +
+
+
+ +
+ +
+
+ posted on: 19 Jun 2021 +
+
+
+ +
+ +
+
+ posted on: 11 Jun 2021 +
+
+
+ +
+ +
+
+ posted on: 28 May 2021 +
+
+
+ +
+ +
+
+ posted on: 21 May 2021 +
+
+
+ +
+ +
+
+ posted on: 14 May 2021 +
+
+
+ +
+ +
+
+ posted on: 07 May 2021 +
+
+
+ +
+ +
+
+ posted on: 30 Apr 2021 +
+
+
+ +
+ +
+
+ posted on: 23 Apr 2021 +
+
+
+ +
+ + + « Prev + + + + + + 1 + + + + + + 2 + + + + + + 3 + + + + + + 4 + + + + + + 5 + + + + + + + Next » + + +
+ + +
+ + + + + + + + diff --git a/weeknotes/page3/index.html b/weeknotes/page3/index.html new file mode 100644 index 000000000..68cd276d3 --- /dev/null +++ b/weeknotes/page3/index.html @@ -0,0 +1,464 @@ + + + + + + + + + + + + + + + + + + + + + + Mindless Rambling Nonsense - page 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ +
+
+ posted on: 16 Apr 2021 +
+
+
+ +
+ +
+
+ posted on: 09 Apr 2021 +
+
+
+ +
+ +
+
+ posted on: 01 Apr 2021 +
+
+
+ +
+ +
+
+ posted on: 26 Mar 2021 +
+
+
+ +
+ +
+
+ posted on: 26 Feb 2021 +
+
+
+ +
+ +
+
+ posted on: 20 Feb 2021 +
+
+
+ +
+ +
+
+ posted on: 12 Feb 2021 +
+
+
+ +
+ +
+
+ posted on: 05 Feb 2021 +
+
+
+ +
+ +
+
+ posted on: 29 Jan 2021 +
+
+
+ +
+ +
+
+ posted on: 22 Jan 2021 +
+
+
+ +
+ + + « Prev + + + + + + 1 + + + + + + 2 + + + + + + 3 + + + + + + 4 + + + + + + 5 + + + + + + + Next » + + +
+ + +
+ + + + + + + + diff --git a/weeknotes/page4/index.html b/weeknotes/page4/index.html new file mode 100644 index 000000000..5c8747d7c --- /dev/null +++ b/weeknotes/page4/index.html @@ -0,0 +1,495 @@ + + + + + + + + + + + + + + + + + + + + + + Mindless Rambling Nonsense - page 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ +
+
+ posted on: 15 Jan 2021 +
+
+
+ +
+ +
+
+ posted on: 08 Jan 2021 +
+
+
+ +
+ +
+
+ posted on: 28 Mar 2020 +
+
+
+ +
+
+

2020 Week 12

+ +

🎢 this week has been wild
+ 💝 what a simply wonderful time to work with people that care at a company that wants to do good
+ 🤳 seems ridulously self-interested to waffle about how hard or not I've found this week when folk are losing their income

+ +

Schools are closing. We used to homeschool. I wondered if it would be useful to share what we learned.

+ +

I wrote hundreds of words on homeschooling. My wife laughed at me. Told me to stop waffling and said “write this down”

+ +

Obvs this was our experience, with our kids, in our context but…

+ +
    +
  • Let them play. Play is learning.
  • +
  • You’re not a teacher. That’s ok. Teaching is hard (kudos teachers 👑).
  • +
  • You’re not teaching 30 kids so don’t act like you are. A couple of hours of "teaching" a day will likely be plenty (and plenty exhausting).
  • +
  • Put stuff out they’ve not played with recently e.g. things to count with, puzzles, a book they hardly ever look at. Just leave it out for them to notice.
  • +
  • Use their interests. Talk to them about the topic you want to teach while they’re playing. Baking, shopping, accounting, planning an imaginary business.
  • +
  • Don’t get sucked in to the overwhelming amount of resources. twinkl is free right now. TES has resources for older kids. There’s so much online you can get sucked in to just comparing them. We had the most success when we just spent some time with them doing something at whatever level they’re at.
  • +
  • There are amazing homeschoolers that look like they’ve got it sorted. Social media will make it look like you’re not doing a good job cos others share at their best. But their house is messy and full of fighting kids too. They just don’t take photos while that’s happening
  • +
  • What worked for us was making sure we went outside every day. Even if it was wet
  • +
+ +

I made these:

+ +
+ +

Or rather they are the ones I kept. I’d see how quickly I could make something that helped them or held their attention. #3 loved being allowed to hold my phone and shouting at readerer The other two loved getting cat gifs as rewards

+ +

🎨I’d love to see what the digital community all make as learning resources.
+👩‍❤️‍👩It's such a strange time in the world. Remember to be kind to others and to yourself.

+ +
+
+
+ posted on: 20 Mar 2020 +
+
+
+ +
+ +
+
+ posted on: 14 Mar 2020 +
+
+
+ +
+ +
+
+ posted on: 07 Mar 2020 +
+
+
+ +
+ +
+
+ posted on: 28 Feb 2020 +
+
+
+ +
+ +
+
+ posted on: 14 Feb 2020 +
+
+
+ +
+ +
+
+ posted on: 08 Feb 2020 +
+
+
+ +
+ +
+
+ posted on: 31 Jan 2020 +
+
+
+ +
+ + + « Prev + + + + + + 1 + + + + + + 2 + + + + + + 3 + + + + + + 4 + + + + + + 5 + + + + + + + Next » + + +
+ + +
+ + + + + + + + diff --git a/weeknotes/page5/index.html b/weeknotes/page5/index.html new file mode 100644 index 000000000..f50ba9080 --- /dev/null +++ b/weeknotes/page5/index.html @@ -0,0 +1,305 @@ + + + + + + + + + + + + + + + + + + + + + + Mindless Rambling Nonsense - page 5 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ +
+
+ posted on: 24 Jan 2020 +
+
+
+ +
+ +
+
+ posted on: 17 Jan 2020 +
+
+
+ +
+ +
+
+ posted on: 10 Jan 2020 +
+
+
+ +
+ + + « Prev + + + + + + 1 + + + + + + 2 + + + + + + 3 + + + + + + 4 + + + + + + 5 + + + + + + Next » + +
+ + +
+ + + + + + + + diff --git a/wrapping_up.html b/wrapping_up.html new file mode 100644 index 000000000..81f347417 --- /dev/null +++ b/wrapping_up.html @@ -0,0 +1,425 @@ + + + + + + + + + + + + + + + + + + + + + + Websites != CMS Platform - Wrapping Up + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
Sun Aug 03 2014
+

Websites != CMS Platform - Wrapping Up

+
+
+ +
+ +
+ + tag-icon + + learning + + + + + tag-icon + + cms + + + + + tag-icon + + design + + + + + tag-icon + + web + + + + + tag-icon + + series + + + +
+
+
+
+ +
+

This post is part of a series where I'm hoping to prove to myself that building a dynamic website with NodeJS is much more fun than using a CMS platform. See the first post for an explanation of why

+ +

The code can be found on GitHub

+ +

Previous Post + +I've been banging this drum into the internet's echo chamber for five months now and enough is enough!

+ + + +

My initial to-do list was to build a site that:

+ +
    +
  • Displays Web pages + +
  • +
  • Stores data + +
  • +
  • Edits web pages / Has an Admin section + +
  • +
  • Has templating + +
  • +
  • Allows more than one author + +
  • +
  • Has Server side caching +
      +
    • does anything not have server side caching these days?! ExpressJS does…
    • +
    +
  • +
  • Can be used by someone non-technical +
      +
    • totally subjective… I think this could be used by just-about-anyone but it would need polish and user-testing to really determine that.
    • +
    +
  • +
+ +

Aims

+ +

No heavy frameworks need apply

+ +

My initial aim was to prove to myself that there's a lot of friction using a big framework like Django when building a CMS site and that can be avoided. I still feel that's true and I haven't discovered anything in this process that changes my mind (yeah, confirmation bias).

+ +
    +
  • Implementing login was a pain - but it always is…
  • +
  • I switched backwards and forwards between view engines as I came to terms with express
  • +
+ +

This series of posts has 12 instalments. Each of which represents around 4 hours of work including typing up the posts so turning out a fairly straight-forward business website in 10 actual days would be gigantically achievable.

+ +

That doesn't mean you can't make websites with a big framework or even that you shouldn't… it does mean I won't and I hope it helps demonstrate why…

+ +

NodeJS is super fun, development, for the purposes of

+ +

I've learnt an amazing amount. I'd meant to explore Selenium, Browserstack, Promises, and contenteditable for ages - this series was the push to do that.

+ +

And, yes, Node is great to work with. My Node style isn't there yet but this has helped bring me closer to writing Node code I'm happy with.

+ + +

Killing the series

+ +

But I'm a bit bored with this now and I've got more children than when I started the series. It feels like a drag and an obligation to carry on which definitely isn't why I'm blogging (that's because I don't have any real hobbies any more).

+ +

So I'm stopping here…

+ +
+ + +
+
+ + + + + + + + + +