go/svc/invoice: add statusz
diff --git a/go/svc/invoice/BUILD.bazel b/go/svc/invoice/BUILD.bazel
index d654bc9..0f8bd8b 100644
--- a/go/svc/invoice/BUILD.bazel
+++ b/go/svc/invoice/BUILD.bazel
@@ -6,11 +6,13 @@
         "main.go",
         "model.go",
         "render.go",
+        "statusz.go",
     ],
     importpath = "code.hackerspace.pl/hscloud/go/svc/invoice",
     visibility = ["//visibility:private"],
     deps = [
         "//go/mirko:go_default_library",
+        "//go/statusz:go_default_library",
         "//go/svc/invoice/templates:go_default_library",
         "//proto/invoice:go_default_library",
         "@com_github_golang_glog//:go_default_library",
diff --git a/go/svc/invoice/main.go b/go/svc/invoice/main.go
index 8a5ddbe..33bc125 100644
--- a/go/svc/invoice/main.go
+++ b/go/svc/invoice/main.go
@@ -111,45 +111,43 @@
 	}
 }
 
-func (s *service) RenderInvoice(req *pb.RenderInvoiceRequest, srv pb.Invoicer_RenderInvoiceServer) error {
-	sealed, err := s.m.getSealedUid(srv.Context(), req.Uid)
+func (s *service) invoicePDF(ctx context.Context, uid string) ([]byte, error) {
+	sealed, err := s.m.getSealedUid(ctx, uid)
 	if err != nil {
-		if _, ok := status.FromError(err); ok {
-			return err
-		}
-		glog.Errorf("getSealedUid(_, %q): %v", req.Uid, err)
-		return status.Error(codes.Unavailable, "internal server error")
+		return nil, err
 	}
 
 	var rendered []byte
 	if sealed != "" {
 		// Invoice is sealed, return stored PDF.
-		rendered, err = s.m.getRendered(srv.Context(), req.Uid)
+		rendered, err = s.m.getRendered(ctx, uid)
 		if err != nil {
-			if _, ok := status.FromError(err); ok {
-				return err
-			}
-			glog.Errorf("getRendered(_, %q): %v", req.Uid, err)
-			return status.Error(codes.Unavailable, "internal server error")
+			return nil, err
 		}
 	} else {
 		// Invoice is proforma, render.
-		invoice, err := s.m.getInvoice(srv.Context(), req.Uid)
+		invoice, err := s.m.getInvoice(ctx, uid)
 		if err != nil {
-			if _, ok := status.FromError(err); ok {
-				return err
-			}
-			glog.Errorf("getInvoice(_, %q): %v", req.Uid, err)
-			return status.Error(codes.Unavailable, "internal server error")
+			return nil, err
 		}
 
-		glog.Infof("%+v", invoice)
 		rendered, err = renderInvoicePDF(invoice, "xxxx", true)
 		if err != nil {
-			glog.Errorf("renderProformaPDF(_): %v", err)
-			return status.Error(codes.Unavailable, "internal server error")
+			return nil, err
 		}
 	}
+	return rendered, nil
+}
+
+func (s *service) RenderInvoice(req *pb.RenderInvoiceRequest, srv pb.Invoicer_RenderInvoiceServer) error {
+	rendered, err := s.invoicePDF(srv.Context(), req.Uid)
+	if err != nil {
+		if _, ok := status.FromError(err); ok {
+			return err
+		}
+		glog.Errorf("invoicePDF(_, %q): %v", req.Uid, err)
+		return status.Error(codes.Unavailable, "internal server error")
+	}
 
 	chunkSize := 16 * 1024
 	chunk := &pb.RenderInvoiceResponse{}
@@ -201,6 +199,7 @@
 	}
 
 	s := newService(m)
+	s.setupStatusz(mi)
 	pb.RegisterInvoicerServer(mi.GRPC(), s)
 
 	if err := mi.Serve(); err != nil {
diff --git a/go/svc/invoice/model.go b/go/svc/invoice/model.go
index 0ed8245..cb15a16 100644
--- a/go/svc/invoice/model.go
+++ b/go/svc/invoice/model.go
@@ -211,3 +211,62 @@
 	}
 	return p, nil
 }
+
+type invoice struct {
+	ID       int64
+	Number   string
+	Sealed   bool
+	Proto    *pb.Invoice
+	TotalNet int
+	Total    int
+}
+
+func (m *model) getInvoices(ctx context.Context) ([]invoice, error) {
+	q := `
+		select invoice_seal.final_uid, invoice.id, invoice.proto from invoice
+		left join invoice_seal
+		on invoice_seal.invoice_id = invoice.id
+	`
+	rows, err := m.db.QueryContext(ctx, q)
+	if err != nil {
+		return []invoice{}, err
+	}
+	defer rows.Close()
+
+	res := []invoice{}
+	for rows.Next() {
+		i := invoice{
+			Proto: &pb.Invoice{},
+		}
+		buf := []byte{}
+
+		number := sql.NullString{}
+		if err := rows.Scan(&number, &i.ID, &buf); err != nil {
+			return []invoice{}, err
+		}
+
+		if err := proto.Unmarshal(buf, i.Proto); err != nil {
+			return []invoice{}, err
+		}
+
+		if number.Valid {
+			i.Sealed = true
+			i.Number = number.String
+		} else {
+			i.Number = "proforma"
+		}
+
+		i.Total = 0
+		i.TotalNet = 0
+		for _, it := range i.Proto.Item {
+			rowTotalNet := int(it.UnitPrice * it.Count)
+			rowTotal := int(float64(rowTotalNet) * (float64(1) + float64(it.Vat)/100000))
+			i.TotalNet += rowTotalNet
+			i.Total += rowTotal
+		}
+
+		res = append(res, i)
+	}
+
+	return res, nil
+}
diff --git a/go/svc/invoice/proto/BUILD.bazel b/go/svc/invoice/proto/BUILD.bazel
index 63c82cc..6c78fa3 100644
--- a/go/svc/invoice/proto/BUILD.bazel
+++ b/go/svc/invoice/proto/BUILD.bazel
@@ -2,15 +2,7 @@
 
 go_library(
     name = "go_default_library",
-    srcs = [
-        "generate.go",
-        "inboice.pb.go",
-    ],
+    srcs = ["generate.go"],
     importpath = "code.hackerspace.pl/hscloud/go/svc/invoice/proto",
     visibility = ["//visibility:public"],
-    deps = [
-        "@com_github_golang_protobuf//proto:go_default_library",
-        "@org_golang_google_grpc//:go_default_library",
-        "@org_golang_x_net//context:go_default_library",
-    ],
 )
diff --git a/go/svc/invoice/statusz.go b/go/svc/invoice/statusz.go
new file mode 100644
index 0000000..dbc59f1
--- /dev/null
+++ b/go/svc/invoice/statusz.go
@@ -0,0 +1,93 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+
+	"code.hackerspace.pl/hscloud/go/mirko"
+	"code.hackerspace.pl/hscloud/go/statusz"
+	"github.com/golang/glog"
+)
+
+const invoicesFragment = `
+    <style type="text/css">
+		.table td,th {
+			background-color: #eee;
+			padding: 0.2em 0.4em 0.2em 0.4em;
+		}
+		.table th {
+			background-color: #c0c0c0;
+		}
+		.table {
+			background-color: #fff;
+			border-spacing: 0.2em;
+			margin-left: auto;
+			margin-right: auto;
+		}
+	</style>
+    <div>
+		{{ .Msg }}
+        <table class="table">
+            <tr>
+                <th>Internal ID</th>
+                <th>Number</th>
+				<th>Customer</th>
+				<th>Amount (net)</th>
+				<th>Actions</th>
+            </tr>
+            {{ range .Invoices }}
+				{{ if .Sealed }}
+				<tr>
+				{{ else }}
+				<tr style="opacity: 0.5">
+				{{ end }}
+				    <td>{{ .ID }}</td>
+				    <td>{{ .Number }}</td>
+					<td>{{ index .Proto.CustomerBilling 0 }}</td>
+					<td>{{ .TotalNetPretty }}</td>
+				    <td>
+						<a href="/debug/view?id={{ .ID }}">View</a>
+					</td>
+				</tr>
+			{{ end }}
+        </table>
+    </div>
+`
+
+type templateInvoice struct {
+	invoice
+	TotalNetPretty string
+}
+
+func (s *service) setupStatusz(m *mirko.Mirko) {
+	statusz.AddStatusPart("Invoices", invoicesFragment, func(ctx context.Context) interface{} {
+		var res struct {
+			Invoices []templateInvoice
+			Msg      string
+		}
+		invoices, err := s.m.getInvoices(ctx)
+		res.Invoices = make([]templateInvoice, len(invoices))
+		for i, inv := range invoices {
+			res.Invoices[i] = templateInvoice{
+				invoice:        inv,
+				TotalNetPretty: fmt.Sprintf("%.2f %s", float64(inv.TotalNet)/100, inv.Proto.Unit),
+			}
+		}
+
+		if err != nil {
+			glog.Errorf("Could not get invoices for statusz: %v", err)
+			res.Msg = fmt.Sprintf("Could not get invoices: %v", err)
+		}
+		return res
+	})
+
+	m.HTTPMux().HandleFunc("/debug/view", func(w http.ResponseWriter, r *http.Request) {
+		rendered, err := s.invoicePDF(r.Context(), r.URL.Query().Get("id"))
+		if err != nil {
+			fmt.Fprintf(w, "error: %v", err)
+		}
+		w.Header().Set("Content-type", "application/pdf")
+		w.Write(rendered)
+	})
+}