diff --git a/config/config.go b/config/config.go
new file mode 100644
index 0000000..3231f73
--- /dev/null
+++ b/config/config.go
@@ -0,0 +1,40 @@
+package config
+
+import (
+ "github.com/spf13/viper"
+)
+
+type Config struct {
+ Port uint16
+ DataService DataService
+}
+
+type DataService struct {
+ IP string
+ Port uint16
+ Username string
+ Password string
+ Database string
+}
+
+func New() (*Config, error) {
+ viper.SetConfigType("yaml")
+ viper.AddConfigPath(".")
+ viper.SetConfigName("config")
+ if err := viper.ReadInConfig(); err != nil {
+ return nil, err
+ }
+
+ setDefaults()
+
+ var config Config
+ err := viper.Unmarshal(&config)
+
+ return &config, err
+}
+
+func setDefaults() {
+ viper.SetDefault("Port", "8080")
+ viper.SetDefault("DataService.IP", "127.0.0.1")
+ viper.SetDefault("DataService.Port", "27017")
+}
diff --git a/features/api/channel/channel.go b/features/api/channel/channel.go
new file mode 100755
index 0000000..67e0f30
--- /dev/null
+++ b/features/api/channel/channel.go
@@ -0,0 +1,14 @@
+package channel
+
+type Service interface {
+ Channel(id int) (*Channel, error)
+ Channels() ([]*Channel, error)
+}
+
+type Channel struct {
+ ID int `json:"id"`
+ Name string `json:"name"`
+ Subchannels []Channel `json:"subchannels,omitempty"`
+ TotalClients int `json:"totalClients"`
+ NeededSubscribePower int `json:"neededSubscribePower"`
+}
diff --git a/features/api/channel/handler.go b/features/api/channel/handler.go
new file mode 100755
index 0000000..a0ec7cd
--- /dev/null
+++ b/features/api/channel/handler.go
@@ -0,0 +1,40 @@
+package channel
+
+import (
+ "net/http"
+ "strconv"
+
+ "git.cliffbreak.de/Cliffbreak/tsviewer/response"
+ "github.com/go-chi/chi"
+)
+
+func ChannelAPIHandler(s Service) http.HandlerFunc {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ response.Handler(w, r, response.HandlerFunc(func() (int, error) {
+ id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 64)
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+
+ c, err := s.Channel(int(id))
+ if err != nil {
+ return http.StatusNotFound, err
+ }
+
+ return response.New(c, r).Send(w, http.StatusOK)
+ }))
+ })
+}
+
+func ChannelsAPIHandler(s Service) http.HandlerFunc {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ response.Handler(w, r, response.HandlerFunc(func() (int, error) {
+ cc, err := s.Channels()
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+
+ return response.New(cc, r).Send(w, http.StatusOK)
+ }))
+ })
+}
diff --git a/features/api/channel/routes.go b/features/api/channel/routes.go
new file mode 100755
index 0000000..02532d8
--- /dev/null
+++ b/features/api/channel/routes.go
@@ -0,0 +1,14 @@
+package channel
+
+import (
+ "github.com/go-chi/chi"
+)
+
+func APIRoutes(s Service) *chi.Mux {
+ router := chi.NewRouter()
+
+ router.Get("/{id}", ChannelAPIHandler(s))
+ router.Get("/", ChannelsAPIHandler(s))
+
+ return router
+}
diff --git a/features/api/client/client.go b/features/api/client/client.go
new file mode 100755
index 0000000..e6ef949
--- /dev/null
+++ b/features/api/client/client.go
@@ -0,0 +1,15 @@
+package client
+
+type Service interface {
+ Client(id int) (*Client, error)
+ Clients() ([]*Client, error)
+}
+
+type Client struct {
+ DatabaseID int `json:"databaseId"`
+ ChannelID int `json:"channelId"`
+ Nickname string `json:"nickname"`
+ Type int `json:"type"`
+ Away bool `json:"away"`
+ AwayMessage string `json:"awayMessage"`
+}
diff --git a/features/api/client/handler.go b/features/api/client/handler.go
new file mode 100755
index 0000000..7dbfbe1
--- /dev/null
+++ b/features/api/client/handler.go
@@ -0,0 +1,40 @@
+package client
+
+import (
+ "net/http"
+ "strconv"
+
+ "git.cliffbreak.de/Cliffbreak/tsviewer/response"
+ "github.com/go-chi/chi"
+)
+
+func ClientAPIHandler(s Service) http.HandlerFunc {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ response.Handler(w, r, response.HandlerFunc(func() (int, error) {
+ id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 64)
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+
+ c, err := s.Client(int(id))
+ if err != nil {
+ return http.StatusNotFound, err
+ }
+
+ return response.New(c, r).Send(w, http.StatusOK)
+ }))
+ })
+}
+
+func ClientsAPIHandler(s Service) http.HandlerFunc {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ response.Handler(w, r, response.HandlerFunc(func() (int, error) {
+ cc, err := s.Clients()
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+
+ return response.New(cc, r).Send(w, http.StatusOK)
+ }))
+ })
+}
diff --git a/features/api/client/routes.go b/features/api/client/routes.go
new file mode 100755
index 0000000..9e20d55
--- /dev/null
+++ b/features/api/client/routes.go
@@ -0,0 +1,14 @@
+package client
+
+import (
+ "github.com/go-chi/chi"
+)
+
+func APIRoutes(s Service) *chi.Mux {
+ router := chi.NewRouter()
+
+ router.Get("/{id}", ClientAPIHandler(s))
+ router.Get("/", ClientsAPIHandler(s))
+
+ return router
+}
diff --git a/features/api/server/handler.go b/features/api/server/handler.go
new file mode 100755
index 0000000..916e3a6
--- /dev/null
+++ b/features/api/server/handler.go
@@ -0,0 +1,20 @@
+package server
+
+import (
+ "net/http"
+
+ "git.cliffbreak.de/Cliffbreak/tsviewer/response"
+)
+
+func InfoAPIHandler(s Service) http.HandlerFunc {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ response.Handler(w, r, response.HandlerFunc(func() (int, error) {
+ s, err := s.ServerInfo()
+ if err != nil {
+ return http.StatusNotFound, err
+ }
+
+ return response.New(s, r).Send(w, http.StatusOK)
+ }))
+ })
+}
diff --git a/features/api/server/routes.go b/features/api/server/routes.go
new file mode 100755
index 0000000..22342f3
--- /dev/null
+++ b/features/api/server/routes.go
@@ -0,0 +1,13 @@
+package server
+
+import (
+ "github.com/go-chi/chi"
+)
+
+func APIRoutes(s Service) *chi.Mux {
+ router := chi.NewRouter()
+
+ router.Get("/info", InfoAPIHandler(s))
+
+ return router
+}
diff --git a/features/api/server/server.go b/features/api/server/server.go
new file mode 100755
index 0000000..0cbdb34
--- /dev/null
+++ b/features/api/server/server.go
@@ -0,0 +1,22 @@
+package server
+
+import "time"
+
+type Service interface {
+ ServerInfo() (*Server, error)
+}
+
+type Server struct {
+ Name string `json:"name"`
+ Status string `json:"status"`
+ Version string `json:"version"`
+ WelcomeMessage string `json:"welcomeMessage"`
+ MaxClients int `json:"maxClients"`
+ ClientsOnline int `json:"clientsOnline"`
+ ReservedSlots int `json:"reservedSlots"`
+ Uptime time.Duration `json:"uptime"`
+ TotalPing float32 `json:"totalPing"`
+ MinAndroidVersion int `json:"minAndroidVersion"`
+ MinClientVersion int `json:"minClientVersion"`
+ MiniOSVersion int `json:"miniOSVersion"`
+}
diff --git a/features/web/index/handler.go b/features/web/index/handler.go
new file mode 100755
index 0000000..ca77456
--- /dev/null
+++ b/features/web/index/handler.go
@@ -0,0 +1,32 @@
+package index
+
+import (
+ "html/template"
+ "net/http"
+
+ "git.cliffbreak.de/Cliffbreak/tsviewer/features/web/weberror"
+)
+
+func IndexGUIHandler(s Service, t template.Template) http.HandlerFunc {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ server, err := s.ServerInfo()
+ if err != nil {
+ weberror.NewPage(err, http.StatusNotFound).Send(w, t)
+ return
+ }
+
+ channels, err := s.ChannelsRaw()
+ if err != nil {
+ weberror.NewPage(err, http.StatusNotFound).Send(w, t)
+ return
+ }
+
+ clients, err := s.Clients()
+ if err != nil {
+ weberror.NewPage(err, http.StatusNotFound).Send(w, t)
+ return
+ }
+
+ NewPage(*server, channels, clients).Send(w, t)
+ })
+}
diff --git a/features/web/index/page.go b/features/web/index/page.go
new file mode 100755
index 0000000..c706649
--- /dev/null
+++ b/features/web/index/page.go
@@ -0,0 +1,35 @@
+package index
+
+import (
+ "html/template"
+ "io"
+
+ "git.cliffbreak.de/Cliffbreak/tsviewer/features/api/channel"
+ "git.cliffbreak.de/Cliffbreak/tsviewer/features/api/client"
+ "git.cliffbreak.de/Cliffbreak/tsviewer/features/api/server"
+)
+
+type Service interface {
+ ServerInfo() (*server.Server, error)
+ Clients() ([]*client.Client, error)
+ Channels() ([]*channel.Channel, error)
+ ChannelsRaw() ([]*channel.Channel, error)
+}
+
+type IndexPage struct {
+ Server server.Server
+ Channels []*channel.Channel
+ Clients []*client.Client
+}
+
+func NewPage(server server.Server, channels []*channel.Channel, clients []*client.Client) *IndexPage {
+ return &IndexPage{
+ Server: server,
+ Channels: channels,
+ Clients: clients,
+ }
+}
+
+func (page IndexPage) Send(w io.Writer, t template.Template) {
+ t.Lookup("index.html").Execute(w, page)
+}
diff --git a/features/web/index/routes.go b/features/web/index/routes.go
new file mode 100755
index 0000000..d14aec2
--- /dev/null
+++ b/features/web/index/routes.go
@@ -0,0 +1,15 @@
+package index
+
+import (
+ "html/template"
+
+ "github.com/go-chi/chi"
+)
+
+func GUIRoutes(s Service, t template.Template) *chi.Mux {
+ router := chi.NewRouter()
+
+ router.Get("/", IndexGUIHandler(s, t))
+
+ return router
+}
diff --git a/features/web/weberror/page.go b/features/web/weberror/page.go
new file mode 100755
index 0000000..5bfa55a
--- /dev/null
+++ b/features/web/weberror/page.go
@@ -0,0 +1,22 @@
+package weberror
+
+import (
+ "html/template"
+ "io"
+)
+
+type ErrorPage struct {
+ Error error
+ StatusCode int
+}
+
+func NewPage(err error, statusCode int) *ErrorPage {
+ return &ErrorPage{
+ Error: err,
+ StatusCode: statusCode,
+ }
+}
+
+func (page ErrorPage) Send(w io.Writer, t template.Template) {
+ t.Lookup("error.html").Execute(w, page)
+}
diff --git a/gui/template.go b/gui/template.go
new file mode 100755
index 0000000..17f6cd5
--- /dev/null
+++ b/gui/template.go
@@ -0,0 +1,35 @@
+package gui
+
+import (
+ "fmt"
+ "html/template"
+ "io/ioutil"
+ "strings"
+)
+
+func LoadTemplates() (*template.Template, error) {
+ return loadTemplates("templates/")
+}
+
+func loadTemplates(path string) (*template.Template, error) {
+ dir, err := ioutil.ReadDir(path)
+ if err != nil {
+ return nil, err
+ }
+
+ var ff []string
+
+ for _, file := range dir {
+ filename := file.Name()
+ if strings.HasSuffix(filename, ".html") {
+ ff = append(ff, fmt.Sprintf("%s%s", path, filename))
+ }
+ }
+
+ templates, err := template.ParseFiles(ff...)
+ if err != nil {
+ return nil, err
+ }
+
+ return templates, nil
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..1480e3f
--- /dev/null
+++ b/main.go
@@ -0,0 +1,81 @@
+package main
+
+import (
+ "fmt"
+ "html/template"
+ "log"
+ "net/http"
+ "time"
+
+ "git.cliffbreak.de/Cliffbreak/tsviewer/config"
+ "git.cliffbreak.de/Cliffbreak/tsviewer/features/api/channel"
+ "git.cliffbreak.de/Cliffbreak/tsviewer/features/api/client"
+ "git.cliffbreak.de/Cliffbreak/tsviewer/features/api/server"
+ "git.cliffbreak.de/Cliffbreak/tsviewer/features/web/index"
+ "git.cliffbreak.de/Cliffbreak/tsviewer/gui"
+ "git.cliffbreak.de/Cliffbreak/tsviewer/service"
+ "github.com/go-chi/chi"
+ "github.com/go-chi/chi/middleware"
+ "github.com/go-chi/cors"
+)
+
+func Routes(s service.Service, t template.Template) *chi.Mux {
+ router := chi.NewRouter()
+ cors := cors.New(cors.Options{
+ AllowedOrigins: []string{"*"},
+ AllowedMethods: []string{"Get"},
+ })
+ router.Use(
+ middleware.Logger,
+ middleware.Timeout(5*time.Second),
+ middleware.DefaultCompress,
+ middleware.RedirectSlashes,
+ middleware.Recoverer,
+
+ cors.Handler,
+ )
+
+ router.Route("/v1", func(r chi.Router) {
+ r.Mount("/channels", channel.APIRoutes(s))
+ r.Mount("/clients", client.APIRoutes(s))
+ r.Mount("/server", server.APIRoutes(s))
+ })
+
+ router.Route("/", func(r chi.Router) {
+ r.Mount("/", index.GUIRoutes(s, t))
+ })
+
+ return router
+}
+
+func main() {
+ log.Println("Loading configurations...")
+ config, err := config.New()
+ if err != nil {
+ log.Fatal(err)
+ }
+ log.Println("Configurations loaded!")
+
+ log.Println("Starting query service...")
+ service, err := service.New(*config)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer service.TSClient.Close()
+ log.Println("Query service connected to", fmt.Sprintf("%s:%d!", config.ServerTS.IP, config.ServerTS.PortQuery))
+
+ log.Println("Loading templates...")
+ templates, err := gui.LoadTemplates()
+ if err != nil {
+ log.Fatal(err)
+ }
+ log.Println("All templates loaded!")
+
+ router := Routes(*service, *templates)
+ router.Get("/static/*", func(w http.ResponseWriter, r *http.Request) {
+ http.StripPrefix("/static", http.FileServer(http.Dir("./static"))).ServeHTTP(w, r)
+ })
+
+ log.Println("Starting the web server locally on port", config.ServerWeb.Port)
+ log.Fatal("Handler: ", http.ListenAndServe(fmt.Sprintf(":%d", config.ServerWeb.Port), router))
+}
diff --git a/request/meta.go b/request/meta.go
new file mode 100755
index 0000000..2cf8b75
--- /dev/null
+++ b/request/meta.go
@@ -0,0 +1,36 @@
+package meta
+
+import (
+ "net/http"
+ "strconv"
+)
+
+type Meta struct {
+ Pretty bool `json:"pretty"`
+ Envelope bool `json:"envelope"`
+}
+
+func ParseFromRequest(r *http.Request) *Meta {
+ meta := Meta{
+ Pretty: false,
+ Envelope: false,
+ }
+
+ pretty, ok := r.URL.Query()["pretty"]
+ if ok && len(pretty) > 0 {
+ prettyBool, err := strconv.ParseBool(pretty[0])
+ if err == nil {
+ meta.Pretty = prettyBool
+ }
+ }
+
+ envelope, ok := r.URL.Query()["envelope"]
+ if ok && len(envelope) > 0 {
+ envelopeBool, err := strconv.ParseBool(envelope[0])
+ if err == nil {
+ meta.Envelope = envelopeBool
+ }
+ }
+
+ return &meta
+}
diff --git a/response/error.go b/response/error.go
new file mode 100755
index 0000000..1d495bb
--- /dev/null
+++ b/response/error.go
@@ -0,0 +1,19 @@
+package response
+
+import "time"
+
+// Error data type
+type Error struct {
+ Status int `json:"status"`
+ Error string `json:"error"`
+ Timestamp string `json:"timestamp"`
+}
+
+// NewError creates a new Response fill with an error
+func NewError(status int, err error) *Error {
+ return &Error{
+ Status: status,
+ Error: err.Error(),
+ Timestamp: time.Now().Format("2006-01-02T15:04:05Z"), //.Format("02 Jan 2006, 15:04:05 MST"),
+ }
+}
diff --git a/response/response.go b/response/response.go
new file mode 100755
index 0000000..a0a37d8
--- /dev/null
+++ b/response/response.go
@@ -0,0 +1,87 @@
+package response
+
+import (
+ "encoding/json"
+ "log"
+ "net/http"
+
+ "git.cliffbreak.de/Cliffbreak/tsviewer/request/meta"
+)
+
+type HandlerFunc func() (int, error)
+
+// Responder is a service for responses
+type Responder interface {
+ Send(http.ResponseWriter, int)
+}
+
+// Response data type
+type Response struct {
+ Content interface{}
+ Pretty bool
+}
+
+type Envelope struct {
+ Data interface{} `json:"data,omitempty"`
+}
+
+// New creates a new Response
+func New(content interface{}, r *http.Request) *Response {
+ var resp Response
+
+ meta := meta.NewFromRequest(r)
+
+ if meta.Envelope {
+ resp = Response{
+ Content: Envelope{
+ Data: content,
+ },
+ }
+ } else {
+ resp = Response{
+ Content: content,
+ }
+ }
+
+ resp.Pretty = meta.Pretty
+
+ return &resp
+}
+
+// Send sends the response
+func (resp Response) Send(w http.ResponseWriter, status int) (int, error) {
+ var (
+ rawJSON []byte
+ err error
+ )
+
+ if resp.Pretty {
+ rawJSON, err = json.MarshalIndent(resp.Content, "", " ")
+ } else {
+ rawJSON, err = json.Marshal(resp.Content)
+ }
+
+ if err != nil {
+ return http.StatusInternalServerError, err
+ } else if string(rawJSON) == "null" {
+ rawJSON = make([]byte, 0)
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(status)
+ w.Write(rawJSON)
+
+ return status, nil
+}
+
+func Handler(w http.ResponseWriter, r *http.Request, hf HandlerFunc) {
+ status, err := hf()
+ if err == nil {
+ return
+ }
+
+ log.Printf("HTTP %d: %q", status, err)
+ if status, err = New(NewError(status, err), r).Send(w, status); err != nil {
+ http.Error(w, http.StatusText(status), status)
+ }
+}
diff --git a/service/crud.go b/service/crud.go
new file mode 100644
index 0000000..5723ab7
--- /dev/null
+++ b/service/crud.go
@@ -0,0 +1,69 @@
+package service
+
+import (
+ "context"
+ "reflect"
+ "time"
+
+ "github.com/haveachin/sawtexapi/service/mongodb/filter"
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+func (s DataService) Create(v interface{}) error {
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ result, err := s.collection(reflect.TypeOf(v)).InsertOne(ctx, v)
+ if err != nil {
+ return err
+ }
+
+ if reflect.ValueOf(v).Kind() == reflect.Ptr {
+ reflect.ValueOf(v).Elem().FieldByName("ID").Set(reflect.ValueOf(result.InsertedID))
+ }
+
+ return nil
+}
+
+func (s DataService) Query(v interface{}, filter *filter.Filter, multiQuery bool) error {
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ if !multiQuery {
+ if err := s.collection(reflect.TypeOf(v)).FindOne(ctx, filter).Decode(v); err != nil {
+ return err
+ }
+
+ return nil
+ }
+
+ cursor, err := s.collection(reflect.TypeOf(v)).Find(ctx, filter)
+ if err != nil {
+ return err
+ }
+
+ return cursor.All(ctx, v)
+}
+
+func (s DataService) Update(v interface{}, filter *filter.Filter) error {
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ _, err := s.collection(reflect.TypeOf(v)).UpdateOne(ctx, filter, bson.M{"$set": v})
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (s DataService) Delete(v interface{}, filter *filter.Filter) error {
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ if _, err := s.collection(reflect.TypeOf(v)).DeleteOne(ctx, filter); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/service/service.go b/service/service.go
new file mode 100644
index 0000000..ee198aa
--- /dev/null
+++ b/service/service.go
@@ -0,0 +1,61 @@
+package service
+
+import (
+ "context"
+ "fmt"
+ "net/url"
+ "reflect"
+ "time"
+
+ "git.cliffbreak.de/haveachin/scoreboard/config"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+ "go.mongodb.org/mongo-driver/mongo/readpref"
+)
+
+type DataService struct {
+ client *mongo.Client
+ db *mongo.Database
+ collections map[reflect.Type]*mongo.Collection
+}
+
+func New(ds config.DataService) (*DataService, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
+ defer cancel()
+
+ connectionString := fmt.Sprintf("mongodb://%s:%s@%s:%d/?authSource=admin", url.QueryEscape(ds.Username), url.QueryEscape(ds.Password), ds.IP, ds.Port)
+ client, err := mongo.Connect(ctx, options.Client().ApplyURI(connectionString))
+ if err != nil {
+ return nil, err
+ }
+
+ if err = client.Ping(ctx, readpref.Primary()); err != nil {
+ return nil, err
+ }
+
+ return &DataService{
+ client: client,
+ db: client.Database(ds.Database),
+ }, nil
+}
+
+func (s DataService) collection(t reflect.Type) *mongo.Collection {
+ for t.Kind() != reflect.Struct {
+ t = t.Elem()
+ }
+
+ collection, ok := s.collections[t]
+ if !ok {
+ panic(fmt.Sprintf("No collection registered for %T", t))
+ }
+
+ return collection
+}
+
+func (s DataService) RegisterCollection(t reflect.Type, collection string) {
+ for t.Kind() != reflect.Struct {
+ t = t.Elem()
+ }
+
+ s.collections[t] = s.db.Collection(collection)
+}
diff --git a/static/styles.css b/static/styles.css
new file mode 100755
index 0000000..70a16db
--- /dev/null
+++ b/static/styles.css
@@ -0,0 +1,22 @@
+*{
+ padding: 0;
+ margin: 0;
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+
+}
+.content {
+ max-width: 500px;
+ margin: auto;
+}
+
+h2{
+ padding-top: 20px;
+}
+
+small{
+ margin-left: 10px;
+}
+
+p.client{
+ margin-left: 10px;
+}
\ No newline at end of file
diff --git a/templates/error.html b/templates/error.html
new file mode 100755
index 0000000..5ff2593
--- /dev/null
+++ b/templates/error.html
@@ -0,0 +1,11 @@
+
+
+
+ Error
+
+
+ Oops, an error occurred!
+ Error {{.StatusCode}}
+ {{.Error}}
+
+
\ No newline at end of file
diff --git a/templates/index.html b/templates/index.html
new file mode 100755
index 0000000..73c31b4
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,33 @@
+
+
+
+
+ TS3 Viewer
+
+
+
+
+
+
+
Online Members
+
+ {{range .Clients}}
+ - {{.Nickname}}
+ {{end}}
+
+
{{.Server.Name}}
+
+ {{$clients := .Clients}}
+ {{range .Channels}}
+
{{.Name}}
+ {{$channelId := .ID}}
+ {{range $clients}}
+ {{if eq $channelId .ChannelID}}
+
{{.Nickname}}
+ {{end}}
+ {{end}}
+ {{end}}
+
+
+
+
\ No newline at end of file