diff --git a/priv/www/hijack.css b/priv/www/hijack.css
new file mode 100644
index 0000000000000000000000000000000000000000..3eeb14e7d17995ce5cce7861495941c07c46ca35
--- /dev/null
+++ b/priv/www/hijack.css
@@ -0,0 +1,31 @@
+body {
+    padding: 1rem;
+}
+
+th, td {
+    padding: 0.6rem 0.75rem;
+}
+
+tr.active {
+    background: #eeeeee;
+}
+
+code {
+    padding: 1rem;
+}
+
+#error, #request {
+    display: none;
+}
+
+.button {
+    margin-right: 1rem;
+    height: 2.4rem;
+    line-height: 2.4rem;
+}
+
+.nowrap {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
diff --git a/priv/www/hijack.js b/priv/www/hijack.js
new file mode 100644
index 0000000000000000000000000000000000000000..05f128add862532541f88d65ddd87b2f9296418c
--- /dev/null
+++ b/priv/www/hijack.js
@@ -0,0 +1,92 @@
+var currentItem;
+
+function poll() {
+  $.getJSON('/esi/hijack_www:requests', function (items) {
+    for (var i in items) {
+      var item = items[i];
+      var row = document.createElement("tr");
+      $(row).data("item", JSON.stringify(item));
+      var fields = '<td>' + item.request.host + '</td><td>' + item.request.method + '</td><td>' + item.request.path + '</td>';
+      if (typeof item.response !== 'undefined') {
+        $(row).append(fields + '<td>' + item.response.status + '</td>');
+      } else if (typeof item.error !== 'undefined') {
+        $(row).append(fields + '<td>ERR</td>');
+      }
+      $('#requests').prepend(row);
+    }
+  });
+  window.setTimeout(poll, 1000);
+}
+
+function display(item) {
+  currentItem = item;
+  $("#intro").hide();
+  if (typeof item.response !== 'undefined') {
+    $("#error").hide();
+    $("#request_status").html(item.request.method + " " + item.request.path + " &rarr; " + item.response.status);
+    displayHeaders("#request_headers > tbody", item.request.headers);
+    displayHeaders("#response_headers > tbody", item.response.headers);
+    $("#request_headers").show();
+    $("#response_headers").show();
+    $("#request_body").hide().children("code").text(item.request.body);
+    $("#response_body").hide().children("code").text(item.response.body);
+    $("code").each(function (i, block) {
+      hljs.highlightBlock(block);
+    });
+    $("#request").show();
+  } else if (typeof item.error !== 'undefined') {
+    $("#request").hide();
+    $("#error_status").html(item.request.method + " " + item.request.path);
+    $("#error_reason").text(item.error);
+    $("#error").show();
+  }
+}
+
+function displayHeaders(id, headers) {
+  $(id).empty();
+  for (var name in headers) {
+    var value = headers[name];
+    $(id).append('<tr><td class="nowrap">' + name + '</td><td>' + value + '</td></tr>');
+  }
+}
+
+$(document).ready(function () {
+  window.setTimeout(poll, 500);
+  $('#requests').on("click", function (e) {
+    $('#requests').children("tr").removeClass("active");
+    display($(e.target).parent().addClass("active").data("item"));
+  });
+  $('#clear_button').on("click", function () {
+    $("#requests").empty();
+    $("#error").hide();
+    $("#request").hide();
+    $("#intro").show();
+  });
+  $('#request_header_button').on("click", function () {
+    $("#request_headers").show();
+    $("#request_body").hide();
+  });
+  $('#request_body_button').on("click", function () {
+    $("#request_headers").hide();
+    $("#request_body").show();
+  });
+  $('#request_replay_button').on("click", function () {
+    $.ajax({
+      type: 'POST',
+      url: '/esi/hijack_www:replay',
+      data: JSON.stringify(currentItem.request),
+      success: function (data) {
+      },
+      contentType: "application/json",
+      dataType: 'json'
+    });
+  });
+  $('#response_header_button').on("click", function () {
+    $("#response_headers").show();
+    $("#response_body").hide();
+  });
+  $('#response_body_button').on("click", function () {
+    $("#response_headers").hide();
+    $("#response_body").show();
+  });
+});
diff --git a/priv/www/index.html b/priv/www/index.html
index 414f86883e2cb0071f0bdec21a959e92e7841589..e2ab6a2a37e262cb167e21675ba2abd2f0a81bb9 100644
--- a/priv/www/index.html
+++ b/priv/www/index.html
@@ -1,14 +1,102 @@
 <!DOCTYPE html>
 <html lang="en">
 <head>
-    <meta charset="UTF-8">
-    <title></title>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <title>Hijack</title>
     <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/3.0.3/normalize.css">
     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/milligram/1.1.0/milligram.css">
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.3.0/styles/xcode.min.css">
+    <link rel="stylesheet" href="hijack.css">
     <script src="https://cdnjs.cloudflare.com/ajax/libs/zepto/1.1.6/zepto.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.3.0/highlight.min.js"></script>
+    <script src="hijack.js"></script>
 </head>
 <body>
-<h1>Hello</h1>
+<div class="container">
+    <div class="row">
+        <div class="column">
+
+            <h2 class="float-left">All Requests</h2>
+            <button id="clear_button" class="button float-right">Clear</button>
+
+            <table>
+                <thead>
+                <tr>
+                    <th>Host</th>
+                    <th>Method</th>
+                    <th>Path</th>
+                    <th>Status</th>
+                </tr>
+                </thead>
+                <tbody id="requests">
+                </tbody>
+            </table>
+
+        </div>
+        <div class="column">
+
+            <div id="intro">
+                Welcome to Hijack
+            </div>
+
+            <div id="error">
+
+                <h2 id="error_status"></h2>
+
+                <h3 id="error_reason"></h3>
+
+            </div>
+
+            <div id="request">
+
+                <h2 id="request_status"></h2>
+
+                <h3>Request</h3>
+
+                <button id="request_header_button" class="button float-left">Headers</button>
+                <button id="request_body_button" class="button float-left">Body</button>
+                <button id="request_replay_button" class="button float-right">Replay</button>
+
+                <table id="request_headers">
+                    <thead>
+                    <tr>
+                        <th>Name</th>
+                        <th>Value</th>
+                    </tr>
+                    </thead>
+                    <tbody>
+                    </tbody>
+                </table>
+
+                <br/>
+                <pre id="request_body"><code></code></pre>
+
+                <h3>Response</h3>
+
+                <button id="response_header_button" class="button float-left">Headers</button>
+                <button id="response_body_button" class="button float-left">Body</button>
+
+                <table id="response_headers">
+                    <thead>
+                    <tr>
+                        <th>Name</th>
+                        <th>Value</th>
+                    </tr>
+                    </thead>
+                    <tbody>
+                    </tbody>
+                </table>
+
+                <br/>
+                <pre id="response_body"><code></code></pre>
+
+            </div>
+
+        </div>
+    </div>
+</div>
 </body>
 </html>
diff --git a/rebar.config b/rebar.config
index a443400b4fe8ec28a3618d2e77a4eda927c83575..e815776c76da17507553ff90ee5ec94ace4da1cb 100644
--- a/rebar.config
+++ b/rebar.config
@@ -7,7 +7,8 @@
     {getopt, "0.8.2"},
     {msgpack, "0.4.0"},
     {ranch, "1.1.0"},
-    {gun, "1.0.0-pre.1"}
+    {gun, "1.0.0-pre.1"},
+    {jsx, "2.8.0"}
 ]}.
 
 {profiles, [
@@ -18,6 +19,9 @@
         {eunit_opts, [{report, {eunit_surefire, [{dir, "_build/test"}]}}]},
         {erl_opts, [debug_info, nowarn_unused_vars]}
     ]},
+    {development, [
+        {erl_opts, [debug_info, {d, 'debug', true}]}
+    ]},
     {production, [
         {erl_opts, [no_debug_info, warnings_as_errors]}
     ]}
diff --git a/rebar.lock b/rebar.lock
index e6b84a28718d37104dbc08c0d3f17c0592f06c73..fd278418846e6fe27db407289e88f0d8c3c46172 100644
--- a/rebar.lock
+++ b/rebar.lock
@@ -1,5 +1,6 @@
 [{<<"cowlib">>,{pkg,<<"cowlib">>,<<"1.3.0">>},1},
  {<<"getopt">>,{pkg,<<"getopt">>,<<"0.8.2">>},0},
  {<<"gun">>,{pkg,<<"gun">>,<<"1.0.0-pre.1">>},0},
+ {<<"jsx">>,{pkg,<<"jsx">>,<<"2.8.0">>},0},
  {<<"msgpack">>,{pkg,<<"msgpack">>,<<"0.4.0">>},0},
  {<<"ranch">>,{pkg,<<"ranch">>,<<"1.1.0">>},0}].
diff --git a/src/hijack.erl b/src/hijack.erl
index ec64e21bbad84700d031ba3d18a96799b85bbd68..f21d0fae07067c88be9dca6adaa72715a51c8bd2 100644
--- a/src/hijack.erl
+++ b/src/hijack.erl
@@ -22,6 +22,12 @@
     httpd_port
 }).
 
+-ifdef(debug).
+-define(LOG(Format, Data), io:format("{~p,~p}: " ++ Format ++ "~n", [?MODULE, ?LINE] ++ Data)).
+-else.
+-define(LOG(Format, Data), true).
+-endif.
+
 main(Args) ->
     application:ensure_all_started(gun),
     {ok, {Opts, _}} = getopt:parse(?OPT_SPEC, Args),
@@ -77,7 +83,13 @@ start_httpd(State = #state{opts = Opts}) ->
         {port, Port},
         {erl_script_alias, {"/esi", [hijack_www]}},
         {server_name, "hijack"},
-        {server_root, "."}
+        {server_root, "."},
+        {mime_types, [
+            {"html", "text/html"},
+            {"htm", "text/html"},
+            {"css", "text/css"},
+            {"js", "application/javascript"}
+        ]}
     ],
     ServiceConfig = try escript:script_name() of
                         File ->
@@ -119,7 +131,7 @@ execute(State = #state{opts = Opts}) ->
             {TargetHost, TargetPort} = parse_host_and_port(proplists:get_value(target, Opts), 80),
             NewState = State#state{transport = Transport, consul_socket = Socket, target_host = TargetHost, target_port = TargetPort},
             Parent = self(),
-            spawn(
+            spawn_link(
                 fun() ->
                     transport_loop(Parent, Transport, Socket)
                 end),
@@ -146,6 +158,7 @@ transport_loop(Parent, Transport, Socket) ->
         {ok, Data} ->
             case msgpack:unpack(Data) of
                 {ok, Terms} ->
+                    ?LOG("RCV ~p", [Terms]),
                     Parent ! {socket_recv, Terms},
                     transport_loop(Parent, Transport, Socket);
                 {error, _Reason} ->
@@ -176,6 +189,8 @@ loop(State) ->
             }));
         socket_closed ->
             ok;
+        {response, <<"replay">>, _Status, _Headers, _Body} ->
+            loop(State);
         {response, From, Status, Headers, Body} ->
             loop(send_to_consul(State, #{
                 <<"from">> => From,
@@ -193,14 +208,18 @@ loop(State) ->
                 <<"from">> => From,
                 <<"error">> => list_to_binary(io_lib:format("~p", [Reason]))
             }));
+        {replay, Request} ->
+            loop(handle(State, Request#{<<"from">> => <<"replay">>})),
+            loop(State);
         Msg ->
-            io:format("loop recv ~p~n", [Msg]),
+            ?LOG("Unandled message: ~p", [Msg]),
             loop(State)
     end.
 
 send_to_consul(State = #state{transport = Transport, consul_socket = Socket}, Map) ->
     case Transport:send(Socket, msgpack:pack(Map)) of
         ok ->
+            ?LOG("SND ~p", [Map]),
             State;
         {error, Reason} ->
             io:format("Error while sending: ~p~n", [Reason]),
@@ -224,6 +243,11 @@ handle(State = #state{opts = Opts}, #{<<"action">> := <<"authorized">>, <<"usern
 
 handle(State, #{<<"action">> := <<"bind">>, <<"host">> := Host, <<"path">> := Path, <<"mode">> := Mode}) ->
     io:format("Ready, ~sing ~s/~s~n", [Mode, Host, Path]),
+    Parent = self(),
+    register(www_state, spawn_link(
+        fun() ->
+            www_state_loop(Parent, {State, [], []})
+        end)),
     start_httpd(State);
 
 handle(State = #state{transport = Transport, consul_socket = Socket}, #{<<"action">> := <<"error">>, <<"message">> := Message}) ->
@@ -233,7 +257,7 @@ handle(State = #state{transport = Transport, consul_socket = Socket}, #{<<"actio
 
 handle(State = #state{target_host = Host, target_port = Port}, Request = #{<<"from">> := _From}) ->
     Parent = self(),
-    spawn(
+    spawn_link(
         fun() ->
             {ok, Pid} = gun:open(Host, Port),
             gun_loop(Parent, Request, Pid, undefined),
@@ -245,18 +269,22 @@ handle(State, #{<<"action">> := <<"pong">>}) ->
     State;
 
 handle(State, Terms) ->
-    io:format("~p~n", [Terms]),
+    ?LOG("Unandled terms ~p", [Terms]),
     State.
 
 gun_loop(Parent, Request = #{<<"from">> := From}, Pid, StreamRef) ->
     receive
         {gun_up, Pid, _Protocol} ->
-            gun_loop(Parent, Request, Pid, request(Pid, Request));
+            NewStreamRef = request(Pid, Request),
+            www_state ! {request, NewStreamRef, Request},
+            gun_loop(Parent, Request, Pid, NewStreamRef);
         {gun_down, Pid, _Protocol, _Reason, _, _} ->
             Parent ! {gun_down, Pid};
         {gun_error, Pid, StreamRef, Reason} ->
+            www_state ! {error, StreamRef, Reason},
             Parent ! {gun_error, Pid, From, Reason};
         {gun_response, Pid, StreamRef, fin, Status, Headers} ->
+            www_state ! {response, StreamRef, {Status, Headers, <<>>}},
             Parent ! {response, From, Status, Headers, <<>>};
         {gun_response, Pid, StreamRef, nofin, Status, Headers} ->
             gun_loop_data(Parent, Request, Pid, StreamRef, Status, Headers, <<>>)
@@ -269,8 +297,10 @@ gun_loop_data(Parent, Request = #{<<"from">> := From}, Pid, StreamRef, Status, H
         {gun_down, Pid, _Protocol, _Reason, _, _} ->
             Parent ! {gun_down, Pid};
         {gun_error, Pid, StreamRef, Reason} ->
+            www_state ! {error, StreamRef, Reason},
             Parent ! {gun_error, Pid, From, Reason};
         {gun_data, Pid, StreamRef, fin, Data} ->
+            www_state ! {response, StreamRef, {Status, Headers, <<Buffer/binary, Data/binary>>}},
             Parent ! {response, From, Status, Headers, <<Buffer/binary, Data/binary>>};
         {gun_data, Pid, StreamRef, nofin, Data} ->
             gun_loop_data(Parent, Request, Pid, StreamRef, Status, Headers, <<Buffer/binary, Data/binary>>)
@@ -287,3 +317,48 @@ request(Pid, Request = #{<<"method">> := Method})
     when Method == <<"POST">>; Method == <<"PUT">>;Method == <<"PATCH">> ->
     #{<<"path">> := Path, <<"headers">> := Headers, <<"body">> := Body} = Request,
     gun:request(Pid, Method, Path, maps:to_list(Headers), Body).
+
+www_state_loop(Parent, {State, Pending, Complete}) ->
+    receive
+        {request, Ref, Request} ->
+            NewPending = [{Ref, Request} | Pending],
+            www_state_loop(Parent, {State, NewPending, Complete});
+        {response, Ref, {Status, Headers, Body}} ->
+            Response = #{
+                <<"status">> => Status,
+                <<"headers">> => maps:from_list(Headers),
+                <<"body">> => Body
+            },
+            case lists:keytake(Ref, 1, Pending) of
+                {value, {Ref, Request}, NewPending} ->
+                    Entry = #{
+                        <<"request">> => Request,
+                        <<"response">> => Response
+                    },
+                    NewComplete = [Entry | Complete],
+                    www_state_loop(Parent, {State, NewPending, NewComplete});
+                false ->
+                    www_state_loop(Parent, {State, Pending, Complete})
+            end;
+        {error, Ref, Reason} ->
+            case lists:keytake(Ref, 1, Pending) of
+                {value, {Ref, Request}, NewPending} ->
+                    Entry = #{
+                        <<"request">> => Request,
+                        <<"error">> => Reason
+                    },
+                    NewComplete = [Entry | Complete],
+                    www_state_loop(Parent, {State, NewPending, NewComplete});
+                false ->
+                    www_state_loop(Parent, {State, Pending, Complete})
+            end;
+        {get, Pid, true} ->
+            Pid ! {requests, Complete},
+            www_state_loop(Parent, {State, Pending, []});
+        {get, Pid, _} ->
+            Pid ! {requests, Complete},
+            www_state_loop(Parent, {State, Pending, Complete});
+        Msg ->
+            Parent ! Msg,
+            www_state_loop(Parent, {State, Pending, Complete})
+    end.
diff --git a/src/hijack_www.erl b/src/hijack_www.erl
index c3d1ccde497bee971e989f7d41ae08c7a231d610..1d3f475315093720d4c0c6154f7f583457fb0751 100644
--- a/src/hijack_www.erl
+++ b/src/hijack_www.erl
@@ -1,12 +1,26 @@
 -module(hijack_www).
 
 -export([
-    echo/3,
-    hello/3
+    replay/3,
+    requests/3
 ]).
 
-echo(SessionID, _Env, Input) ->
-    mod_esi:deliver(SessionID, Input).
+replay(SessionID, _Env, Input) ->
+    try
+        www_state ! {replay, jsx:decode(list_to_binary(Input), [return_maps])},
+        mod_esi:deliver(SessionID, "ok")
+    catch
+        _:_ ->
+            mod_esi:deliver(SessionID, "error")
+    end.
 
-hello(SessionID, _Env, _Input) ->
-    mod_esi:deliver(SessionID, "world").
+requests(SessionID, _Env, _Input) ->
+    Requests = get_requests(true),
+    BinaryJSON = jsx:encode(Requests),
+    StringJSON = binary_to_list(BinaryJSON),
+    mod_esi:deliver(SessionID, "Content-Type:application/json\r\n\r\n"),
+    mod_esi:deliver(SessionID, StringJSON).
+
+get_requests(Purge) ->
+    www_state ! {get, self(), Purge},
+    receive {requests, Requests} -> Requests end.