1)},removeAttr:function(a){return this.each(function(){n.removeAttr(this,a)})}}),n.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(a&&3!==f&&8!==f&&2!==f)return typeof a.getAttribute===U?n.prop(a,b,c):(1===f&&n.isXMLDoc(a)||(b=b.toLowerCase(),d=n.attrHooks[b]||(n.expr.match.bool.test(b)?Zb:Yb)),void 0===c?d&&"get"in d&&null!==(e=d.get(a,b))?e:(e=n.find.attr(a,b),null==e?void 0:e):null!==c?d&&"set"in d&&void 0!==(e=d.set(a,c,b))?e:(a.setAttribute(b,c+""),c):void n.removeAttr(a,b))
-},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(E);if(f&&1===a.nodeType)while(c=f[e++])d=n.propFix[c]||c,n.expr.match.bool.test(c)&&(a[d]=!1),a.removeAttribute(c)},attrHooks:{type:{set:function(a,b){if(!k.radioValue&&"radio"===b&&n.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}}}),Zb={set:function(a,b,c){return b===!1?n.removeAttr(a,c):a.setAttribute(c,c),c}},n.each(n.expr.match.bool.source.match(/\w+/g),function(a,b){var c=$b[b]||n.find.attr;$b[b]=function(a,b,d){var e,f;return d||(f=$b[b],$b[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,$b[b]=f),e}});var _b=/^(?:input|select|textarea|button)$/i;n.fn.extend({prop:function(a,b){return J(this,n.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[n.propFix[a]||a]})}}),n.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(a,b,c){var d,e,f,g=a.nodeType;if(a&&3!==g&&8!==g&&2!==g)return f=1!==g||!n.isXMLDoc(a),f&&(b=n.propFix[b]||b,e=n.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){return a.hasAttribute("tabindex")||_b.test(a.nodeName)||a.href?a.tabIndex:-1}}}}),k.optSelected||(n.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null}}),n.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){n.propFix[this.toLowerCase()]=this});var ac=/[\t\r\n\f]/g;n.fn.extend({addClass:function(a){var b,c,d,e,f,g,h="string"==typeof a&&a,i=0,j=this.length;if(n.isFunction(a))return this.each(function(b){n(this).addClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ac," "):" ")){f=0;while(e=b[f++])d.indexOf(" "+e+" ")<0&&(d+=e+" ");g=n.trim(d),c.className!==g&&(c.className=g)}return this},removeClass:function(a){var b,c,d,e,f,g,h=0===arguments.length||"string"==typeof a&&a,i=0,j=this.length;if(n.isFunction(a))return this.each(function(b){n(this).removeClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ac," "):"")){f=0;while(e=b[f++])while(d.indexOf(" "+e+" ")>=0)d=d.replace(" "+e+" "," ");g=a?n.trim(d):"",c.className!==g&&(c.className=g)}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):this.each(n.isFunction(a)?function(c){n(this).toggleClass(a.call(this,c,this.className,b),b)}:function(){if("string"===c){var b,d=0,e=n(this),f=a.match(E)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else(c===U||"boolean"===c)&&(this.className&&L.set(this,"__className__",this.className),this.className=this.className||a===!1?"":L.get(this,"__className__")||"")})},hasClass:function(a){for(var b=" "+a+" ",c=0,d=this.length;d>c;c++)if(1===this[c].nodeType&&(" "+this[c].className+" ").replace(ac," ").indexOf(b)>=0)return!0;return!1}});var bc=/\r/g;n.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=n.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,n(this).val()):a,null==e?e="":"number"==typeof e?e+="":n.isArray(e)&&(e=n.map(e,function(a){return null==a?"":a+""})),b=n.valHooks[this.type]||n.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=n.valHooks[e.type]||n.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(bc,""):null==c?"":c)}}}),n.extend({valHooks:{option:{get:function(a){var b=n.find.attr(a,"value");return null!=b?b:n.trim(n.text(a))}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],!(!c.selected&&i!==e||(k.optDisabled?c.disabled:null!==c.getAttribute("disabled"))||c.parentNode.disabled&&n.nodeName(c.parentNode,"optgroup"))){if(b=n(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=n.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=n.inArray(d.value,f)>=0)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),n.each(["radio","checkbox"],function(){n.valHooks[this]={set:function(a,b){return n.isArray(b)?a.checked=n.inArray(n(a).val(),b)>=0:void 0}},k.checkOn||(n.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})}),n.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){n.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),n.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}});var cc=n.now(),dc=/\?/;n.parseJSON=function(a){return JSON.parse(a+"")},n.parseXML=function(a){var b,c;if(!a||"string"!=typeof a)return null;try{c=new DOMParser,b=c.parseFromString(a,"text/xml")}catch(d){b=void 0}return(!b||b.getElementsByTagName("parsererror").length)&&n.error("Invalid XML: "+a),b};var ec=/#.*$/,fc=/([?&])_=[^&]*/,gc=/^(.*?):[ \t]*([^\r\n]*)$/gm,hc=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,ic=/^(?:GET|HEAD)$/,jc=/^\/\//,kc=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,lc={},mc={},nc="*/".concat("*"),oc=a.location.href,pc=kc.exec(oc.toLowerCase())||[];function qc(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(E)||[];if(n.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function rc(a,b,c,d){var e={},f=a===mc;function g(h){var i;return e[h]=!0,n.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function sc(a,b){var c,d,e=n.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&n.extend(!0,a,d),a}function tc(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function uc(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}n.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:oc,type:"GET",isLocal:hc.test(pc[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":nc,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":n.parseJSON,"text xml":n.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?sc(sc(a,n.ajaxSettings),b):sc(n.ajaxSettings,a)},ajaxPrefilter:qc(lc),ajaxTransport:qc(mc),ajax:function(a,b){"object"==typeof a&&(b=a,a=void 0),b=b||{};var c,d,e,f,g,h,i,j,k=n.ajaxSetup({},b),l=k.context||k,m=k.context&&(l.nodeType||l.jquery)?n(l):n.event,o=n.Deferred(),p=n.Callbacks("once memory"),q=k.statusCode||{},r={},s={},t=0,u="canceled",v={readyState:0,getResponseHeader:function(a){var b;if(2===t){if(!f){f={};while(b=gc.exec(e))f[b[1].toLowerCase()]=b[2]}b=f[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===t?e:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return t||(a=s[c]=s[c]||a,r[a]=b),this},overrideMimeType:function(a){return t||(k.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>t)for(b in a)q[b]=[q[b],a[b]];else v.always(a[v.status]);return this},abort:function(a){var b=a||u;return c&&c.abort(b),x(0,b),this}};if(o.promise(v).complete=p.add,v.success=v.done,v.error=v.fail,k.url=((a||k.url||oc)+"").replace(ec,"").replace(jc,pc[1]+"//"),k.type=b.method||b.type||k.method||k.type,k.dataTypes=n.trim(k.dataType||"*").toLowerCase().match(E)||[""],null==k.crossDomain&&(h=kc.exec(k.url.toLowerCase()),k.crossDomain=!(!h||h[1]===pc[1]&&h[2]===pc[2]&&(h[3]||("http:"===h[1]?"80":"443"))===(pc[3]||("http:"===pc[1]?"80":"443")))),k.data&&k.processData&&"string"!=typeof k.data&&(k.data=n.param(k.data,k.traditional)),rc(lc,k,b,v),2===t)return v;i=n.event&&k.global,i&&0===n.active++&&n.event.trigger("ajaxStart"),k.type=k.type.toUpperCase(),k.hasContent=!ic.test(k.type),d=k.url,k.hasContent||(k.data&&(d=k.url+=(dc.test(d)?"&":"?")+k.data,delete k.data),k.cache===!1&&(k.url=fc.test(d)?d.replace(fc,"$1_="+cc++):d+(dc.test(d)?"&":"?")+"_="+cc++)),k.ifModified&&(n.lastModified[d]&&v.setRequestHeader("If-Modified-Since",n.lastModified[d]),n.etag[d]&&v.setRequestHeader("If-None-Match",n.etag[d])),(k.data&&k.hasContent&&k.contentType!==!1||b.contentType)&&v.setRequestHeader("Content-Type",k.contentType),v.setRequestHeader("Accept",k.dataTypes[0]&&k.accepts[k.dataTypes[0]]?k.accepts[k.dataTypes[0]]+("*"!==k.dataTypes[0]?", "+nc+"; q=0.01":""):k.accepts["*"]);for(j in k.headers)v.setRequestHeader(j,k.headers[j]);if(k.beforeSend&&(k.beforeSend.call(l,v,k)===!1||2===t))return v.abort();u="abort";for(j in{success:1,error:1,complete:1})v[j](k[j]);if(c=rc(mc,k,b,v)){v.readyState=1,i&&m.trigger("ajaxSend",[v,k]),k.async&&k.timeout>0&&(g=setTimeout(function(){v.abort("timeout")},k.timeout));try{t=1,c.send(r,x)}catch(w){if(!(2>t))throw w;x(-1,w)}}else x(-1,"No Transport");function x(a,b,f,h){var j,r,s,u,w,x=b;2!==t&&(t=2,g&&clearTimeout(g),c=void 0,e=h||"",v.readyState=a>0?4:0,j=a>=200&&300>a||304===a,f&&(u=tc(k,v,f)),u=uc(k,u,v,j),j?(k.ifModified&&(w=v.getResponseHeader("Last-Modified"),w&&(n.lastModified[d]=w),w=v.getResponseHeader("etag"),w&&(n.etag[d]=w)),204===a||"HEAD"===k.type?x="nocontent":304===a?x="notmodified":(x=u.state,r=u.data,s=u.error,j=!s)):(s=x,(a||!x)&&(x="error",0>a&&(a=0))),v.status=a,v.statusText=(b||x)+"",j?o.resolveWith(l,[r,x,v]):o.rejectWith(l,[v,x,s]),v.statusCode(q),q=void 0,i&&m.trigger(j?"ajaxSuccess":"ajaxError",[v,k,j?r:s]),p.fireWith(l,[v,x]),i&&(m.trigger("ajaxComplete",[v,k]),--n.active||n.event.trigger("ajaxStop")))}return v},getJSON:function(a,b,c){return n.get(a,b,c,"json")},getScript:function(a,b){return n.get(a,void 0,b,"script")}}),n.each(["get","post"],function(a,b){n[b]=function(a,c,d,e){return n.isFunction(c)&&(e=e||d,d=c,c=void 0),n.ajax({url:a,type:b,dataType:e,data:c,success:d})}}),n._evalUrl=function(a){return n.ajax({url:a,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},n.fn.extend({wrapAll:function(a){var b;return n.isFunction(a)?this.each(function(b){n(this).wrapAll(a.call(this,b))}):(this[0]&&(b=n(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this)},wrapInner:function(a){return this.each(n.isFunction(a)?function(b){n(this).wrapInner(a.call(this,b))}:function(){var b=n(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=n.isFunction(a);return this.each(function(c){n(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){n.nodeName(this,"body")||n(this).replaceWith(this.childNodes)}).end()}}),n.expr.filters.hidden=function(a){return a.offsetWidth<=0&&a.offsetHeight<=0},n.expr.filters.visible=function(a){return!n.expr.filters.hidden(a)};var vc=/%20/g,wc=/\[\]$/,xc=/\r?\n/g,yc=/^(?:submit|button|image|reset|file)$/i,zc=/^(?:input|select|textarea|keygen)/i;function Ac(a,b,c,d){var e;if(n.isArray(b))n.each(b,function(b,e){c||wc.test(a)?d(a,e):Ac(a+"["+("object"==typeof e?b:"")+"]",e,c,d)});else if(c||"object"!==n.type(b))d(a,b);else for(e in b)Ac(a+"["+e+"]",b[e],c,d)}n.param=function(a,b){var c,d=[],e=function(a,b){b=n.isFunction(b)?b():null==b?"":b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};if(void 0===b&&(b=n.ajaxSettings&&n.ajaxSettings.traditional),n.isArray(a)||a.jquery&&!n.isPlainObject(a))n.each(a,function(){e(this.name,this.value)});else for(c in a)Ac(c,a[c],b,e);return d.join("&").replace(vc,"+")},n.fn.extend({serialize:function(){return n.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=n.prop(this,"elements");return a?n.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!n(this).is(":disabled")&&zc.test(this.nodeName)&&!yc.test(a)&&(this.checked||!T.test(a))}).map(function(a,b){var c=n(this).val();return null==c?null:n.isArray(c)?n.map(c,function(a){return{name:b.name,value:a.replace(xc,"\r\n")}}):{name:b.name,value:c.replace(xc,"\r\n")}}).get()}}),n.ajaxSettings.xhr=function(){try{return new XMLHttpRequest}catch(a){}};var Bc=0,Cc={},Dc={0:200,1223:204},Ec=n.ajaxSettings.xhr();a.attachEvent&&a.attachEvent("onunload",function(){for(var a in Cc)Cc[a]()}),k.cors=!!Ec&&"withCredentials"in Ec,k.ajax=Ec=!!Ec,n.ajaxTransport(function(a){var b;return k.cors||Ec&&!a.crossDomain?{send:function(c,d){var e,f=a.xhr(),g=++Bc;if(f.open(a.type,a.url,a.async,a.username,a.password),a.xhrFields)for(e in a.xhrFields)f[e]=a.xhrFields[e];a.mimeType&&f.overrideMimeType&&f.overrideMimeType(a.mimeType),a.crossDomain||c["X-Requested-With"]||(c["X-Requested-With"]="XMLHttpRequest");for(e in c)f.setRequestHeader(e,c[e]);b=function(a){return function(){b&&(delete Cc[g],b=f.onload=f.onerror=null,"abort"===a?f.abort():"error"===a?d(f.status,f.statusText):d(Dc[f.status]||f.status,f.statusText,"string"==typeof f.responseText?{text:f.responseText}:void 0,f.getAllResponseHeaders()))}},f.onload=b(),f.onerror=b("error"),b=Cc[g]=b("abort");try{f.send(a.hasContent&&a.data||null)}catch(h){if(b)throw h}},abort:function(){b&&b()}}:void 0}),n.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(a){return n.globalEval(a),a}}}),n.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),n.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(d,e){b=n("
-
-
-
-
-{% endblock %}
-
diff --git a/src/templates/gameservers/server_list.html b/src/templates/gameservers/server_list.html
deleted file mode 100644
index e3f95fc..0000000
--- a/src/templates/gameservers/server_list.html
+++ /dev/null
@@ -1,73 +0,0 @@
-{% extends "base_site.html" %}
-{% load staticfiles %}
-{% load server_utils %}
-
-{% block title %}
- Index
-{% endblock %}
-
-{% block content %}
-
-
-
- |
- Cur. |
- Avg. |
- Server |
-
-
-
- {% for server in server_list %}
- {% with delta_hours=server.last_updated|hours_since_now %}
- {% if delta_hours > 24 %}
-
- |
-
- |
- {% elif delta_hours >= 1 %}
-
- |
-
- |
- {% else %}
-
- |
-
- |
- {% endif %}
- {% endwith %}
- {{server.players_current}} |
- {{server.players_avg}} |
-
-
- {{server}}
-
- |
-
- {% empty %}
-
- | ? |
- Sorry, no servers! |
-
- {% endfor %}
-
-
-
-
-
-
-{% endblock %}
diff --git a/storage.go b/storage.go
new file mode 100644
index 0000000..8b4c676
--- /dev/null
+++ b/storage.go
@@ -0,0 +1,59 @@
+package ss13_se
+
+import (
+ "html/template"
+ "net/url"
+ "time"
+)
+
+type ServerEntry struct {
+ ID string `db:"id"`
+ Title string `db:"title"`
+ SiteURL string `db:"site_url"`
+ GameURL string `db:"game_url"`
+ Time time.Time `db:"time"`
+ Players int `db:"players"`
+}
+
+func (e ServerEntry) IsZero() bool {
+ return e.ID == ""
+}
+
+func (e ServerEntry) LastUpdated() string {
+ return e.Time.Format("2006-01-02 15:04 MST")
+}
+func (e ServerEntry) ByondURL() template.URL {
+ u, err := url.Parse(e.GameURL)
+ if err != nil {
+ return ""
+ }
+
+ if u.Scheme != "byond" {
+ return ""
+ }
+
+ return template.URL(u.String())
+}
+
+type ServerPoint struct {
+ Time time.Time `db:"time"`
+ ServerID string `db:"server_id"`
+ Players int `db:"players"`
+}
+
+func (p ServerPoint) IsZero() bool {
+ return p.ServerID == "" && p.Time.IsZero()
+}
+
+type Storage interface {
+ Open() error
+
+ SaveServers([]ServerEntry) error
+ GetServer(string) (ServerEntry, error)
+ GetServers() ([]ServerEntry, error)
+ RemoveServers([]ServerEntry) error
+
+ SaveServerHistory([]ServerPoint) error
+ GetServerHistory(int) ([]ServerPoint, error)
+ GetSingleServerHistory(string, int) ([]ServerPoint, error)
+}
diff --git a/storage_sqlite.go b/storage_sqlite.go
new file mode 100644
index 0000000..787d043
--- /dev/null
+++ b/storage_sqlite.go
@@ -0,0 +1,153 @@
+package ss13_se
+
+import (
+ "time"
+
+ "github.com/jmoiron/sqlx"
+ _ "github.com/mattn/go-sqlite3"
+)
+
+const sqliteScheme string = `
+CREATE TABLE IF NOT EXISTS server_entry(
+ id TEXT UNIQUE,
+ title STRING,
+ site_url STRING,
+ game_url STRING,
+ time DATETIME,
+ players INTEGER
+);
+
+CREATE INDEX IF NOT EXISTS idx_server_entry ON server_entry(time, players, title);
+
+CREATE TABLE IF NOT EXISTS server_history (
+ id INTEGER PRIMARY KEY,
+ time DATETIME,
+ server_id TEXT,
+ players INTEGER
+);
+
+CREATE INDEX IF NOT EXISTS idx_server_history ON server_history(time, server_id);
+`
+
+type StorageSqlite struct {
+ *sqlx.DB
+ Path string
+}
+
+func (store *StorageSqlite) Open() error {
+ db, err := sqlx.Connect("sqlite3", store.Path)
+ if err != nil {
+ return err
+ }
+
+ _, err = db.Exec(sqliteScheme)
+ if err != nil {
+ return err
+ }
+
+ store.DB = db
+ return nil
+}
+
+func (store *StorageSqlite) SaveServers(servers []ServerEntry) error {
+ tx, err := store.Begin()
+ if err != nil {
+ return err
+ }
+
+ q := `INSERT OR REPLACE INTO server_entry (id, title, site_url, game_url, time, players) VALUES(?, ?, ?, ?, ?, ?);`
+ for _, s := range servers {
+ _, err := tx.Exec(q, s.ID, s.Title, s.SiteURL, s.GameURL, s.Time, s.Players)
+ if err != nil {
+ tx.Rollback() // TODO: handle error?
+ return err
+ }
+ }
+
+ return tx.Commit()
+}
+
+func (store *StorageSqlite) GetServer(id string) (ServerEntry, error) {
+ var server ServerEntry
+ q := `SELECT * FROM server_entry WHERE id = ? LIMIT 1;`
+ err := store.Get(&server, q, id)
+ if err != nil {
+ return ServerEntry{}, err
+ }
+ return server, nil
+}
+
+func (store *StorageSqlite) GetServers() ([]ServerEntry, error) {
+ var servers []ServerEntry
+ q := `SELECT * FROM server_entry ORDER BY players DESC, id ASC;`
+ err := store.Select(&servers, q)
+ if err != nil {
+ return nil, err
+ }
+ return servers, nil
+}
+
+func (store *StorageSqlite) RemoveServers(servers []ServerEntry) error {
+ tx, err := store.Begin()
+ if err != nil {
+ return err
+ }
+
+ qHistory := `DELETE FROM server_history WHERE server_id = ?;`
+ qEntry := `DELETE FROM server_entry WHERE id = ?;`
+ for _, s := range servers {
+ _, err := tx.Exec(qHistory, s.ID)
+ if err != nil {
+ tx.Rollback() // TODO: handle error?
+ return err
+ }
+
+ _, err = tx.Exec(qEntry, s.ID)
+ if err != nil {
+ tx.Rollback() // TODO: handle error?
+ return err
+ }
+ }
+
+ return tx.Commit()
+}
+
+func (store *StorageSqlite) SaveServerHistory(points []ServerPoint) error {
+ tx, err := store.Begin()
+ if err != nil {
+ return err
+ }
+
+ q := `INSERT INTO server_history (time, server_id, players) VALUES(?, ?, ?);`
+ for _, p := range points {
+ _, err := tx.Exec(q, p.Time, p.ServerID, p.Players)
+ if err != nil {
+ tx.Rollback() // TODO: handle error?
+ return err
+ }
+ }
+
+ return tx.Commit()
+}
+
+func (store *StorageSqlite) GetServerHistory(days int) ([]ServerPoint, error) {
+ var points []ServerPoint
+ delta := time.Now().AddDate(0, 0, -days)
+ q := `SELECT time,server_id,players FROM server_history WHERE time > ? ORDER BY time DESC, server_id ASC;`
+ err := store.Select(&points, q, delta)
+ if err != nil {
+ return nil, err
+ }
+ return points, nil
+}
+
+func (store *StorageSqlite) GetSingleServerHistory(id string, days int) ([]ServerPoint, error) {
+ var points []ServerPoint
+ delta := time.Now().AddDate(0, 0, -days)
+ q := `SELECT time,server_id,players FROM server_history WHERE server_id = ? AND time > ? ORDER BY time DESC;`
+ err := store.Select(&points, q, id, delta)
+ if err != nil {
+ return nil, err
+ }
+ return points, nil
+}
diff --git a/supervisor.conf b/supervisor.conf
deleted file mode 100644
index 689081a..0000000
--- a/supervisor.conf
+++ /dev/null
@@ -1,7 +0,0 @@
-[program:ss13_hub]
-directory=/opt/ss13_hub/ss13_hub/src
-command=/opt/ss13_hub/venv/bin/gunicorn src.wsgi:application -c /opt/ss13_hub/ss13_hub/gunicorn.conf.py
-user=ss13_hub
-autostart=true
-autorestart=unexpected
-redirect_stderr=true
diff --git a/templates.go b/templates.go
new file mode 100644
index 0000000..4fbc7e1
--- /dev/null
+++ b/templates.go
@@ -0,0 +1,196 @@
+package ss13_se
+
+import (
+ "html/template"
+)
+
+func loadTemplates() (map[string]*template.Template, error) {
+ tmpls := make(map[string]*template.Template)
+ for name, src := range tmplList {
+ t, err := parseTemplate(tmplBase, src)
+ if err != nil {
+ return nil, err
+ }
+ tmpls[name] = t
+ }
+ return tmpls, nil
+}
+
+func parseTemplate(src ...string) (*template.Template, error) {
+ var err error
+ t := template.New("*")
+ for _, s := range src {
+ t, err = t.Parse(s)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return t, nil
+}
+
+// Using the awesome style from http://bettermotherfuckingwebsite.com/
+
+const tmplBase string = `
+
+
+
+
+ {{block "title" .}}NO TITLE{{end}} | ss13.se
+
+
+
+
+
+
+
+ {{block "body" .}}NO BODY{{end}}
+
+
+
+
+`
+
+var tmplList = map[string]string{
+ "index": `{{define "title"}}Index{{end}}
+{{define "body"}}
+Servers
+
+
+ | Players |
+ Server |
+
+
+
+ {{range .Servers}}
+
+ | {{.Players}} |
+ {{.Title}} |
+
+ {{else}}
+ | 0 | Sorry, no servers yet! |
+ {{end}}
+
+
+{{end}}
+`,
+
+ "news": `{{define "title"}}News{{end}}
+{{define "body"}}
+Latest mentions on reddit
+
+{{end}}
+`,
+
+ "server": `{{define "title"}}{{.Server.Title}}{{end}}
+{{define "body"}}
+{{.Server.Title}}
+
+{{if .Server.SiteURL}}
+ Website
+{{end}}
+
+{{if .Server.ByondURL}}
+ Join game
+{{end}}
+
+Current players: {{.Server.Players}}
+
+Daily History
+
+Weekly History
+
+Average per day
+
+Average per hour
+
+{{end}}
+`,
+}