Hack24 2017

Hack24 2017 Header Image

Today’s header image was created by Paul Seal, and it was taken as AbstractSausageFactory took an early morning break while hacking together their entry for Hack24.

This week’s post will be a slight departure from .NET Core stuff… kinda

Hack24 The Planet!

Not quite.

This past weekend, I took part in Hack24 in Nottingham with Zac Braddy, Paul Seal and James Studdart; we entered the competition as AbstractSausageFactory and, in less than 24 hours, created an entire line of business application from nothing.

An Entire Application?

Yup. We started work at 12 midday on the Saturday and by 12 midday on the Sunday we had an entire application which could measure employee engagement.

The event itself actually started at 9 am on the Saturday

It used an impressive list of technologies too:

  • GitHub integration
  • .NET Core
  • Node.js
  • ESLint
  • .NET Framework
  • ReactJS
  • Firebase

That’s an end-to-end description of the technologies used in the stack.

Although, I’m sure that I’m leaving something out.

Zac has already published a blog post about the things that he learnt with regards to ReactJS over at thereactionary.net. I’d recommend that you give it a read, as there’s a lot of distilled knowledge in that post.

As with his other posts too, of course

The challenge we had entered called for us to measure employee engagement across a fictional company. I’ll let MHR (the challenge sponsor) describe it best:

Hack24 - The Challenge
Taken from https://www.mhr.co.uk/resources/events/exhibitions/hack24-2017/

Based on that brief we set about creating an application which would measure engagement via work completed.

If you to skip directly to our submission video, you can do so by clicking here.

The Idea

We started where we live: programming.

Let’s make programming into a game.

We started with by taking a look at Code.RPGify to see if we could figure out how that worked and whether we could rip it off gain inspiration from it.

If we could gamify cutting and committing code, then we could produce stats on it. We could then provide an admin panel and let managers and tech leads see how their developers are doing with their tasks.

We retired to a safe distance (Starbucks across the street), white board in hand, and created the first of our design diagrams.

All The Small Things - Initial Diagram
I know what you’re thinking: “My goodness, Jamie’s handwriting is fabulous”… and you’d be right

We had a simple idea:

  • User commits code to GitHub
  • GitHub talks to an API that I would write (in .NET Core)

Because it’s important to be relevant to this blog, right?

  • My API would pass on relevant information to Zac’s Node instance
  • Zac’s Node instance would do some magic and send data to James’ game server
  • James’ game server would send some data to Paul’s Firebase DB server

There’s a lot of the magic missing here, because you’re only interested in the parts I put together, right?

My API

I needed to know how to communicate with GitHub, so I looked up their version of GitHooks.

GitHub is just Git in the cloud, so it should have GitHooks, right?

Turns out that GitHub has a thing called Webhooks. A GitHub repo can be configured to send messages to a given external API when certain events happen. These events are:

  • A repository is pushed to
  • A pull request is opened
  • A GitHub Pages site is built
  • A new member is added to a team

The only one we were interested in was a repo push. So I navigated to a GitHub repo and set one up. It was surprisingly easy to do.

Setting up a new GitHub Webhook

I headed over to the settings page of the GitHub repo that I was looking at, found the big “Webhooks” button, and gave it a click.

GitHub new Webhook
It’s pretty simple, really.

A Webhook will send you either JSON data or a urlencoded web-form. I chose to receive web-form data, that way the data would be sent to my API in a string called “payload” – making parsing it a little easier.

The WebApi Project

Then I threw together a .NET Core WebApi application which would act as the POST end point for GitHub to communicate with. I spun up an Azure instance

I’ve written about spinning one of those up before, but as a cloud VM

and published to it.

To test that it was working (after adding a single GET method), I fired up postman and sent it a GET request.

GET Success
I’ve blurred the API address because I’ve since taken it down

Now that it was responding to GET requests, I needed to get it to receive and parse JSON data as part of a POST and I would be set.

Parsing the JSON Data

Since the data would be in JSON, I threw together some classes which represented the JSON data and decorated the properties with JsonProperty attributes.

I didn’t have time to represent every property of the JSON, so I picked the most important ones for our application

We only had 24 hours, after all

I threw the following classes together:

namespace asti.GitHubHookApi.Models
{
public class GitHubPushJson
{
[JsonProperty("ref")]
public string Ref { get; set; }
[JsonProperty("compare")]
public string CompareUrl { get; set; }
[JsonProperty("after")]
public string AfterSha { get; set; }
[JsonProperty("pusher")]
public PullPusherJson Pusher { get; set; }
[JsonProperty("commits")]
public ICollection<PushCommitJson> Commits { get; set; }
[JsonProperty("head_commit")]
public PushHeadCommitJson HeadCommit { get; set; }
[JsonProperty("repository")]
public PullRepositoryJson Repository { get; set; }
public string CommitsApiUrl()
{
return Repository != null ? Repository.CommitsUrl.Replace("{/sha}", $"/{AfterSha}") : string.Empty;
}
/*
"pusher": {
"name": "baxterthehacker",
"email": "baxterthehacker@users.noreply.github.com"
},
*/
/// <summary>
/// Represents the user who pushed the commit to a push JSON
/// (all fields mapped)
/// https://developer.github.com/v3/activity/events/types/#pushevent
/// </summary>
public class PullPusherJson
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("email")]
public string Email { get; set; }
}
/*
{
"id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
"tree_id": "f9d2a07e9488b91af2641b26b9407fe22a451433",
"distinct": true,
"message": "Update README.md",
"timestamp": "2015-05-05T19:40:15-04:00",
"url": "https://github.com/baxterthehacker/public-repo/commit/0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
"author": {
"name": "baxterthehacker",
"email": "baxterthehacker@users.noreply.github.com",
"username": "baxterthehacker"
},
"committer": {
"name": "baxterthehacker",
"email": "baxterthehacker@users.noreply.github.com",
"username": "baxterthehacker"
},
"added": [
],
"removed": [
],
"modified": [
"README.md"
]
}
*/
/// <summary>
/// Represents a commit form a Push event
/// (not all fields represented here)
/// https://developer.github.com/v3/activity/events/types/#pushevent
/// </summary>
public class PushCommitJson
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("message")]
public string CommitMessage { get; set; }
[JsonProperty("timestamp")]
public string TimeStampAsString { get; set; }
[JsonProperty("url")]
public string CommitUrl { get; set; }
[JsonProperty("added")]
public ICollection<string> Added { get; set; }
[JsonProperty("removed")]
public ICollection<string> Removed { get; set; }
[JsonProperty("modified")]
public ICollection<string> Modified { get; set; }
}
/*
"head_commit": {
"id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
"tree_id": "f9d2a07e9488b91af2641b26b9407fe22a451433",
"distinct": true,
"message": "Update README.md",
"timestamp": "2015-05-05T19:40:15-04:00",
"url": "https://github.com/baxterthehacker/public-repo/commit/0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
"author": {
"name": "baxterthehacker",
"email": "baxterthehacker@users.noreply.github.com",
"username": "baxterthehacker"
},
}
*/
public class PushHeadCommitJson
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("distinct")]
public bool Distinct { get; set; }
[JsonProperty("message")]
public string Message { get; set; }
[JsonProperty("timestamp")]
public string TimestampAsString { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
}
public class PushHeadCommitAuthor : PullPusherJson
{
[JsonProperty("username")]
public string UserName { get; set; }
}
/*
"repository": {
"id": 35129377,
"name": "public-repo",
"full_name": "baxterthehacker/public-repo",
"owner": {
"name": "baxterthehacker",
"email": "baxterthehacker@users.noreply.github.com"
},
"private": false,
"html_url": "https://github.com/baxterthehacker/public-repo",
"description": "",
"fork": false,
"url": "https://github.com/baxterthehacker/public-repo",
"forks_url": "https://api.github.com/repos/baxterthehacker/public-repo/forks",
"keys_url": "https://api.github.com/repos/baxterthehacker/public-repo/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/baxterthehacker/public-repo/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/baxterthehacker/public-repo/teams",
"hooks_url": "https://api.github.com/repos/baxterthehacker/public-repo/hooks",
"issue_events_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/events{/number}",
"events_url": "https://api.github.com/repos/baxterthehacker/public-repo/events",
"assignees_url": "https://api.github.com/repos/baxterthehacker/public-repo/assignees{/user}",
"branches_url": "https://api.github.com/repos/baxterthehacker/public-repo/branches{/branch}",
"tags_url": "https://api.github.com/repos/baxterthehacker/public-repo/tags",
"blobs_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/baxterthehacker/public-repo/statuses/{sha}",
"languages_url": "https://api.github.com/repos/baxterthehacker/public-repo/languages",
"stargazers_url": "https://api.github.com/repos/baxterthehacker/public-repo/stargazers",
"contributors_url": "https://api.github.com/repos/baxterthehacker/public-repo/contributors",
"subscribers_url": "https://api.github.com/repos/baxterthehacker/public-repo/subscribers",
"subscription_url": "https://api.github.com/repos/baxterthehacker/public-repo/subscription",
"commits_url": "https://api.github.com/repos/baxterthehacker/public-repo/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/baxterthehacker/public-repo/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/baxterthehacker/public-repo/contents/{+path}",
"compare_url": "https://api.github.com/repos/baxterthehacker/public-repo/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/baxterthehacker/public-repo/merges",
"archive_url": "https://api.github.com/repos/baxterthehacker/public-repo/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/baxterthehacker/public-repo/downloads",
"issues_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues{/number}",
"pulls_url": "https://api.github.com/repos/baxterthehacker/public-repo/pulls{/number}",
"milestones_url": "https://api.github.com/repos/baxterthehacker/public-repo/milestones{/number}",
"notifications_url": "https://api.github.com/repos/baxterthehacker/public-repo/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/baxterthehacker/public-repo/labels{/name}",
"releases_url": "https://api.github.com/repos/baxterthehacker/public-repo/releases{/id}",
"created_at": 1430869212,
"updated_at": "2015-05-05T23:40:12Z",
"pushed_at": 1430869217,
"git_url": "git://github.com/baxterthehacker/public-repo.git",
"ssh_url": "git@github.com:baxterthehacker/public-repo.git",
"clone_url": "https://github.com/baxterthehacker/public-repo.git",
"svn_url": "https://github.com/baxterthehacker/public-repo",
"homepage": null,
"size": 0,
"stargazers_count": 0,
"watchers_count": 0,
"language": null,
"has_issues": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": true,
"forks_count": 0,
"mirror_url": null,
"open_issues_count": 0,
"forks": 0,
"open_issues": 0,
"watchers": 0,
"default_branch": "master",
"stargazers": 0,
"master_branch": "master"
}
*/
public class PullRepositoryJson
{
[JsonProperty("html_url")]
public string HtmlUrl { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("commits_url")]
public string CommitsUrl { get; set; }
}
}
}
view raw GitHubPushJson.cs hosted with ❤ by GitHub

I’ve left in comments showing the format of the original JSON data

All I had to do then was deserialise the JSON data back into an instance of the GitHubPushJson class.

[HttpPost]
[Route("/PushEndPoint")]
public async string PushEndPoint(string payload)
{
try
{
var pushDetails = JsonConvert.DeserializeObject<GitHubPushJson>(payload);
}
catch (Exception e)
{
return "fail";
}
return "parsed Ok";
}

Again, with Postman I checked that the API was working, and it was.

We Need More Data

Looking at the returned data, we didn’t really have a great deal. Namely, we had filenames, but not file paths or branch names.

Bother!

Luckily the GitHub data exposed an API that I could call to get the data for a commit.

And here’s why I left the comments in the above code

"commits_url": "https://api.github.com/repos/baxterthehacker/public-repo/commits{/sha}",
view raw GitHubPushJson.cs hosted with ❤ by GitHub

Which is the same format as the GitHub Commit API method for getting all of the data for a single commit.

which you can read here

All I had to do was get the sha of the commit, replace “{sha}” with it, and put in a GET request to GitHub.

The keen eyed of you will have already spotted the method I used to get the full URL to use, earlier.

The first thing I did was write a Helper to perform the GET for me:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace asti.GitHubHookApi.Helpers
{
public static class GitHubGetHelper
{
public static async Task<string> PerformGetAsync(string urlToResource)
{
var fullUrlWithAuth = GitHubApiUrlHelper.AddOathToApiCall(urlToResource);
using (var client = new HttpClient())
{
try
{
var request = new HttpRequestMessage()
{
RequestUri = fullUrlWithAuth,
Method = HttpMethod.Get
};
client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent",
"Mozilla/5.0 (Windows NT 6.2; WOW64; rv:19.0) Gecko/20100101 Firefox/19.0");
var response = await client.SendAsync(request);
var taskData = response.Content.ReadAsStringAsync();
return await taskData;
}
catch (HttpRequestException)
{
return String.Empty;
}
}
}
}
}

Then I added code to my POST method to perform the GET

[HttpPost]
[Route("/PushEndPoint")]
public async string PushEndPoint(string payload)
{
try
{
var pushDetails = JsonConvert.DeserializeObject<GitHubPushJson>(payload);
var commitsString = await GitHubGetHelper.PerformGetAsync(pushDetails.CommitsApiUrl());
}
catch (Exception e)
{
return "fail";
}
return "parsed Ok";
}

This didn’t work, as GitHub kept refusing my connection due to a Protocol Violation. But it worked when I performed the GET from Postman and in my browser.

It turned out

thanks to this Stack Overflow answer

that the answer was to include a user agent. It didn’t matter what the user agent was, as long as one was included. So I chose to pretend that the WebApi was Firefox

Because why not

It was a simple change:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace asti.GitHubHookApi.Helpers
{
public static class GitHubGetHelper
{
public static async Task<string> PerformGetAsync(string urlToResource)
{
var fullUrlWithAuth = GitHubApiUrlHelper.AddOathToApiCall(urlToResource);
using (var client = new HttpClient())
{
try
{
var request = new HttpRequestMessage()
{
RequestUri = fullUrlWithAuth,
Method = HttpMethod.Get
};
client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent",
"Mozilla/5.0 (Windows NT 6.2; WOW64; rv:19.0) Gecko/20100101 Firefox/19.0");
var response = await client.SendAsync(request);
var taskData = response.Content.ReadAsStringAsync();
return await taskData;
}
catch (HttpRequestException)
{
return String.Empty;
}
}
}
}
}
view raw GitHubGetHelper.cs hosted with ❤ by GitHub

The GET started working after that.

Committing to the Commit

Now that I had the commit JSON

and the documentation

I could write a class which would have the file paths for all of the files contained within the pushed commit.

namespace asti.GitHubHookApi.Models
{
public class GitHubCommitJson
{
[JsonProperty("files")]
public ICollection<GitHubCommitFileDetails> Files { get; set; }
[JsonProperty("commit")]
public GitHubCommitDetails CommitDetails { get; set; }
}
public class GitHubCommitDetails
{
[JsonProperty("author")]
public GitHubAuthorDetails Author { get; set; }
}
public class GitHubAuthorDetails
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("email")]
public string Email { get; set; }
}
public class GitHubCommitFileDetails
{
[JsonProperty("filename")]
public string Filename { get; set; }
[JsonProperty("modified")]
// TODO make this into an enum
public string Status { get; set; }
[JsonProperty("raw_url")]
public string RawUrl { get; set; }
[JsonProperty("additions")]
public int Additions { get; set; }
[JsonProperty("deletions")]
public int Deletions { get; set; }
public int NumberOfModifications()
{
return Additions + Deletions;
}
}
}
view raw GitHubCommitJson.cs hosted with ❤ by GitHub

As with the original JSON data from the push, I decided to only parse the most important data

otherwise I’d have been there all night, creating a class hierarchy

The initial version of our application would deal with javascript files only. We had plans to extend this, but for now it would do.

we only had 24 hours, you know

So I needed to figure out which files where javascript and send their file paths (along with the number of changes in the file and the author of the commit) to Zac’s Node endpoint.

I decided to use a Singleton to do the actual POST to Zac’s endpoint

Because why not?

and a helper which the Singleton would use to perform the POST.

using asti.GitHubHookApi.Models;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace asti.GitHubHookApi.Helpers
{
public sealed class LinterSingleton
{
private static readonly LinterSingleton instance = new LinterSingleton();
static LinterSingleton()
{
}
private LinterSingleton()
{
}
public static LinterSingleton Instance
{
get
{
return instance;
}
}
public string SendFilesToLinter(List<LintData> fileData)
{
var json = JsonConvert.SerializeObject(fileData);
var response = LinterHelper.PerformPostAsyc(json);
return response.Result;
}
}
}
view raw LinterSingleton.cs hosted with ❤ by GitHub

This Singleton isn’t thread safe. See: my comment about 24 hours

using asti.GitHubHookApi.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace asti.GitHubHookApi.Helpers
{
public class LinterHelper
{
private static string LinterPostUrl = "list-post-url";
public static async Task<string> PerformPostAsyc(string data)
{
using (var client = new HttpClient())
{
var responce = await client.PostAsync(LinterPostUrl,
new StringContent(data, Encoding.UTF8, "application/json"));
var responseData = await responce.Content.ReadAsStringAsync();
return responseData;
}
}
}
}
view raw LinterHelper.cs hosted with ❤ by GitHub

and I consumed these in the new version of the PushEndPoint like this:

[HttpPost]
[Route("/PushEndPoint")]
public async void PushEndPoint(string payload)
{
var pushDetails = JsonConvert.DeserializeObject<GitHubPushJson>(payload);
var commitsString = await GitHubGetHelper.PerformGetAsync(pushDetails.CommitsApiUrl());
var commits = JsonConvert.DeserializeObject<GitHubCommitJson>(commitsString);
var jsFileUrls = commits.Files
.Where(fi => fi.Filename.EndsWith(".js"))
.Select(fi => new LintData
{
FileUrl = fi.RawUrl,
ModCount = fi.NumberOfModifications(),
UserName = commits.CommitDetails.Author.Email
}).ToList();
LinterSingleton.Instance.SendFilesToLinter(jsFileUrls);
}

A quick check with Zac showed that the file paths were coming through OK.

What Now?

I started looking into both Trello and Google Docs APIs.

I’m not going to lie, I didn’t get very far with them. Then again, it was 3 or 4 am by this point and I’d been working solidly since midday.

Not to mention an early start, after a late night

We took the group decision that, since I wasn’t getting anywhere with them, I should take a look at building an MVC app which would represent a call centre timer.

Short Brief

A start button, a stop button, a timer, and post the data to the server. Done

Can’t really get more succinct than that, right?

I created a .NET MVC application

NOT a .NET Core one – which was amazing foresight

and threw this code into the Home view

<div class="jumbotron">
<h1>Customer Service Logger</h1>
<div class="row">
<div class="col-xs-6">
<input type="text" id="username" placeholder="username" />
</div>
<div class="col-xs-6">
<a id="btn-signin" class="btn btn-success btn-lg">Sign in</a>
</div>
</div>
<p class="lead">Log your customer service interaction by clicking "Start" when you begin your conversation with a customer</p>
<p class="lead">Once the customer is satisfied with the service you have suplied, click "Stop"</p>
<div class="pull-left"><a id="btn-start" class="btn btn-success btn-lg">Start</a></div>
<div class="pull-left"><a id="btn-stop" class="btn btn-success btn-lg" style="display:none;">Stop</a></div>
<div id="clockdiv" style="display:none;">
<div>
<span class="minutes"></span>
<div class="smalltext">Minutes</div>
</div>
<div>
<span class="seconds"></span>
<div class="smalltext">Seconds</div>
</div>
</div>
</div>
@section scripts{
<script type="text/javascript">
var running = false;
var timeInterval = null;
function getTimeRemaining(endtime) {
var t = Date.parse(endtime) - Date.parse(new Date());
var seconds = Math.floor((t / 1000) % 60);
var minutes = Math.floor((t / 1000 / 60) % 60);
return {
'minutes': minutes,
'seconds': seconds
};
}
function initializeClock(id, endtime) {
var clock = document.getElementById(id);
var minutesSpan = clock.querySelector('.minutes');
var secondsSpan = clock.querySelector('.seconds');
function updateClock() {
if (running) {
var t = getTimeRemaining(endtime);
minutesSpan.innerHTML = ('0' + t.minutes).slice(-2);
secondsSpan.innerHTML = ('0' + t.seconds).slice(-2);
if (t.total <= 0) {
clearInterval(timeinterval);
}
}
}
updateClock();
var timeinterval = setInterval(updateClock, 1000);
}
$(document).ready(function () {
$('#btn-signin').on('click', function () {
$('#username').attr('disabled', 'disabled');
$('#btn-signin').hide();
});
$('#btn-start').on('click', function () {
if ($('#username').val().length === 0) {
alert('Please sign in');
} else {
$('#btn-start').hide();
$('#btn-stop').show();
$('#clockdiv').show();
running = true;
var deadline = new Date(Date.parse(new Date()) + 2 * 60 * 1000);
initializeClock('clockdiv', deadline);
}
});
$('#btn-stop').on('click', function () {
$('#btn-start').hide();
$('#btn-stop').hide();
running = false;
var timeRemaining = ($('.minutes').text() * 60) + $('.seconds').text();
var route = 'game-server-url';
var model = {
username: $('#username').val(),
completed: timeRemaining > 0,
timeRemaining: timeRemaining
}
$.post(route, model, function () {
}).fail(function () {
}).always(function () {
$('#btn-start').show();
$('#btn-stop').hide();
var deadline = new Date(Date.parse(new Date()) + 15 * 60 * 1000);
initializeClock('clockdiv', deadline);
});
});
});
</script>
}
view raw callCentre.cshtml hosted with ❤ by GitHub

A call centre operative supplies their name, clicks “Start” when they pick up the phone, and clicks “Stop” when they hang up. The call data is JSON’d then posted directly to the game server.

Simple.

Tidying Up, UI and Video

By this point, it was time for breakfast. Only 5 hours to go and we still had a lot to do.

We needed:

  • A UI dashboard (showing all stats for all users)
  • A user profile page (showing the stats for a single user)
  • Various tidying up jobs
  • A video to actually submit our app

After a chat with James about what I needed to do to get the user data, I created the classes and threw together an MVC app for the dashboard (adding it to the MVC app that I’d created for the Call Centre application).

all the while an exhaustion based case of impostor syndrome started to set in

All that was left was to implement the styling created by James and Paul. James did the design on our whiteboard and Paul threw it together in Bootstrap studio in minutes.

Then it was down to me to apply the styling.

Once that as done, it was testing time.

During which Zac created our submission video, which you can watch here:

Submitting, Chilling, and Results

Once Zac had put the video together

and it really is ace!

 with parts from each of us, we submitted the video and chilled the eff out.

there was Rocket League on a PS4 and extremely comfy chairs. I chose the chairs and nearly fell asleep

The next thing we knew it was results time.

The short version is that we didn’t win.

We’d overshot the mark, by going from 0 to a full line of business app in 24 hours. The other submissions (for the challenge we had entered) were great and targeted the problem better than we had, because we’d wandered a little from the scope a little.

Scope and feature creep are real things.

Reflections

The amount of work that we’d put in was phenomenal.

 even with me complaining that I hadn’t done enough; stupid Impostor Syndrome

I remain convinced that if this application was built properly, without cutting any corners, and in a fully Agile environment, it would have taken us a week to get to where we were on Sunday at 12 midday.

And we did it in 24 hours.

That’s an amazing achievement.

A .NET developer specialising in ASP.NET MVC websites and services, with a background in WinForms and Games Development. When not programming using .NET, he is either learning about .NET Core (and usually building something cross platform with it), speaking Japanese to anyone who'll listen, learning about languages, writing for his non-dev blog, or writing for a blog about Retro Gaming (which he runs with his brother)