+ `);
+ render($page);
+ withEffect(animate($page).pipe(CSSTransition()));
+
+ renderComponentLog($page.querySelector(".component_logger"));
+ renderComponentAuditor($page.querySelector(".component_reporter"));
+}
+
+export default AdminOnly(WithAdminMenu(Page));
+
+function renderComponentLog($component) {
+ // console.log($component);
+}
+
+function renderComponentAuditor($component) {
+ // console.log($component);
+}
diff --git a/public/pages/adminpage/ctrl_settings.js b/public/pages/adminpage/ctrl_settings.js
new file mode 100644
index 00000000..ceccf62d
--- /dev/null
+++ b/public/pages/adminpage/ctrl_settings.js
@@ -0,0 +1,18 @@
+import { createElement } from "../../lib/skeleton/index.js";
+import { withEffect } from "../../lib/rxjs/index.js";
+import { animate, CSSTransition } from "../../lib/animate/index.js";
+
+import AdminOnly from "./decorator_admin_only.js";
+import WithAdminMenu from "./decorator_sidemenu.js";
+
+export default AdminOnly(WithAdminMenu(function(render) {
+ const $page = createElement(`
+
+
+
+ `);
+ render($page);
+ withEffect(animate($page).pipe(CSSTransition()));
+}));
diff --git a/public/pages/adminpage/decorator_admin_only.js b/public/pages/adminpage/decorator_admin_only.js
new file mode 100644
index 00000000..5640b852
--- /dev/null
+++ b/public/pages/adminpage/decorator_admin_only.js
@@ -0,0 +1,37 @@
+import { createElement, onDestroy } from "../../lib/skeleton/index.js";
+import { withEffect } from "../../lib/rxjs/index.js";
+import rxjs from "../../lib/rxjs/index.js";
+
+import AdminSessionManager from "./model_admin_session.js";
+
+export default function AdminOnly(ctrl) {
+ return async (render) => {
+ const loader$ = rxjs.timer(1000).subscribe(() => render(`
loading
`));
+ onDestroy(() => loader$.unsubscribe());
+
+ const handlerUserIsAdminPassthrough = () => ctrl(render);
+ const handlerUserIsNOTAdmin = () => {
+ const $form = createElement(`
+
+ login page
+
+
+ `);
+ render($form);
+ withEffect(rxjs.fromEvent($form.querySelector("form"), "submit").pipe(
+ rxjs.tap((e) => e.preventDefault()),
+ rxjs.map(() => ({ password: $form.querySelector(`[data-bind="password"]`).value })),
+ AdminSessionManager.startSession(),
+ rxjs.tap((success) => console.log("FAIL LOGIN make things move", success)),
+ ));
+ };
+
+ withEffect(AdminSessionManager.state().pipe(
+ rxjs.tap(() => loader$.unsubscribe()),
+ rxjs.map(({ isAdmin }) => isAdmin ? handlerUserIsAdminPassthrough : handlerUserIsNOTAdmin),
+ rxjs.tap((fn) => fn()),
+ ));
+ };
+}
diff --git a/public/pages/adminpage/decorator_sidemenu.js b/public/pages/adminpage/decorator_sidemenu.js
new file mode 100644
index 00000000..8a3b1f1b
--- /dev/null
+++ b/public/pages/adminpage/decorator_sidemenu.js
@@ -0,0 +1,30 @@
+import { createElement } from "../../../lib/skeleton/index.js";
+import rxjs, { withEffect, textContent } from "../../../lib/rxjs/index.js";
+
+import Release from "./model_release.js";
+
+export default function(ctrl) {
+ return (render) => {
+ const $page = createElement(`
+
+ `);
+ render($page);
+ ctrl(($node) => $page.querySelector(`[data-bind="admin"]`).appendChild($node));
+
+ withEffect(Release.get().pipe(
+ rxjs.map(({ version }) => version),
+ textContent($page, `[data-bind="version"]`),
+ ));
+ };
+}
diff --git a/public/pages/adminpage/home.js b/public/pages/adminpage/home.js
new file mode 100644
index 00000000..8547be2d
--- /dev/null
+++ b/public/pages/adminpage/home.js
@@ -0,0 +1,5 @@
+import { navigate } from "../../lib/skeleton/index.js";
+
+export default function() {
+ navigate("/admin/backend");
+}
diff --git a/public/pages/adminpage/index.js b/public/pages/adminpage/index.js
new file mode 100644
index 00000000..56e27298
--- /dev/null
+++ b/public/pages/adminpage/index.js
@@ -0,0 +1,31 @@
+import { createElement, navigate } from "../../lib/skeleton/index.js";
+import rxjs, { withEffect, textContent } from "../../lib/rxjs/index.js";
+import { animate, CSSTransition } from "../../lib/animate/index.js";
+
+export default function(render) {
+ const $page = createElement(`
+
+
Admin
+
+ Settings
+
+
go home
+
+
`);
+ render($page);
+ withEffect(animate($page).pipe(CSSTransition()));
+
+ const formValues = rxjs.combineLatest(
+ rxjs.
+ fromEvent($page.querySelector("#ftp_username"), "input").
+ pipe(rxjs.map((e) => ({name: e.target.id, value: e.target.value})), rxjs.startWith(null)),
+ rxjs.
+ fromEvent($page.querySelector("#ftp_password"), "input").
+ pipe(rxjs.map((e) => ({name: e.target.id, value: e.target.value}), rxjs.startWith(null)))
+ ).subscribe((e) => { // pipe onto reducer function that build our form object
+ console.log("OK", e)
+ });
+};
diff --git a/public/pages/adminpage/model_admin_session.js b/public/pages/adminpage/model_admin_session.js
new file mode 100644
index 00000000..6b4e05f2
--- /dev/null
+++ b/public/pages/adminpage/model_admin_session.js
@@ -0,0 +1,29 @@
+import rxjs, { ajax } from "../../lib/rxjs/index.js";
+import { API_SERVER } from "../../model/index.js";
+
+window.rxjs = rxjs;
+
+class AdminSessionManager {
+ constructor() {
+ this.session = new rxjs.ReplaySubject(1);
+ ajax(API_SERVER + "/admin/api/session").subscribe(
+ () => this.session.next({ isAdmin: false }),
+ () => this.session.next({ isAdmin: false }),
+ );
+ }
+
+ state() {
+ return this.session.asObservable().pipe(rxjs.delay(100));
+ }
+
+ startSession() {
+ return rxjs.pipe(
+ rxjs.delay(1000),
+ rxjs.tap(() => this.session.next({ isAdmin: true })),
+ rxjs.mapTo(false),
+ );
+ }
+
+}
+
+export default new AdminSessionManager();
diff --git a/public/pages/adminpage/model_release.js b/public/pages/adminpage/model_release.js
new file mode 100644
index 00000000..e9b50524
--- /dev/null
+++ b/public/pages/adminpage/model_release.js
@@ -0,0 +1,24 @@
+import rxjs, { ajax } from "../../lib/rxjs/index.js";
+import { API_SERVER } from "../../model/index.js";
+
+const release$ = ajax({
+ url: API_SERVER + "/about",
+ responseType: "text",
+}).pipe(rxjs.shareReplay(1));
+
+class ReleaseImpl {
+ get() {
+ return release$.pipe(
+ rxjs.map((xhr) => {
+ const a = document.createElement("html")
+ a.innerHTML = xhr.response;
+ return {
+ html: a.querySelector("table").outerHTML,
+ version: xhr.responseHeaders["x-powered-by"].trim().replace(/^Filestash\/([v\.0-9]*).*$/, "$1"),
+ };
+ }),
+ );
+ }
+}
+
+export default new ReleaseImpl();
diff --git a/public/pages/boot/nyancat.js b/public/pages/boot/nyancat.js
new file mode 100644
index 00000000..13b427d5
--- /dev/null
+++ b/public/pages/boot/nyancat.js
@@ -0,0 +1,164 @@
+class NyanCat extends HTMLElement {
+ constructor() {
+ super();
+ this.innerHTML = this.render();
+ }
+
+ render() {
+ return `
+
+
+
+
+
+
+
+`;
+ }
+}
+
+customElements.define("data-nyancat", NyanCat);
+
+function CSS() {
+ return `
+ #n-lder{ max-width: 100%; overflow: hidden; }
+ #n-lder #cat{ position: absolute; top: calc(50% + 45px); left: 0%; margin-left: -250px; margin-top: -125px; width: 100%; height: 150px; }
+ #n-lder.loading #cat{ left: 20%; left: calc(50% + 125px); transition: left 4s ease-out; }
+ #n-lder.loading.done #cat { left: 100%; left: calc(100% + 250px); transition: left 0.5s linear; }
+ #n-lder #cat svg{ height: 160px; width: 250px; position: absolute; }
+ #n-lder #cat #hide-behind{ position: absolute; top: 0; left: 55px; bottom: 0; right: -250px; }
+
+ #n-lder #rbw{ position: absolute; top: calc(50% + 45px); left: 0; overflow: hidden; height: 145px; margin-top: -110px; width: 100%; }
+ #n-lder #rbw .w{ width: 10000px; }
+ #n-lder #rbw .rbw { z-index: -1; font-size: 16em; float: left; position: relative; }
+ #n-lder #rbw .rbw .wv { height: 20px; width: 55px; }
+ #n-lder #rbw .rbw .wv.wv-1 { background: #ff0000; }
+ #n-lder #rbw .rbw .wv.wv-2 { background: #ff9900; }
+ #n-lder #rbw .rbw .wv.wv-3 { background: #ffff00; }
+ #n-lder #rbw .rbw .wv.wv-4 { background: #33ff00; }
+ #n-lder #rbw .rbw .wv.wv-5 { background: #0099ff; }
+ #n-lder #rbw .rbw .wv.wv-6 { background: #6633ff; }
+
+ #n-lder #rbw .rbw{ top: 0px; animation: rbw .6s linear infinite; }
+ #n-lder #rbw .rbw.f1{ animation-delay: 0s; }
+ #n-lder #rbw .rbw.f2{ animation-delay: 0.1s; }
+ #n-lder #rbw .rbw.f3{ animation-delay: 0.2s; }
+ #n-lder #rbw .rbw.f4{ animation-delay: 0.3s; }
+ #n-lder #rbw .rbw.f5{ animation-delay: 0.4s; }
+ #n-lder #rbw .rbw.f6{ animation-delay: 0.5s; }
+ @keyframes rbw {
+ 0%{ top: 0px; }
+ 50%{ top: 15px; }
+ 100%{ top: 0px; }
+ }
+ @keyframes nyan_all {
+ 0%{ transform: translateY(0px); }
+ 33%{ transform: translateY(0px); }
+ 34%{ transform: translateY(1px); }
+ 100%{ transform: translateY(1px); }
+ } #n-lder svg g#nyan_all{ animation: nyan_all 0.40s linear infinite; }
+ @keyframes nyan_head {
+ 0%{ transform: translateX(0px) translateY(0px); }
+ 16%{ transform: translateX(0px) translateY(0px); }
+ 17%{ transform: translateX(1px) translateY(0px); }
+ 66%{ transform: translateX(1px) translateY(0px); }
+ 67%{ transform: translateX(0px) translateY(0px); }
+ 83%{ transform: translateX(0px) translateY(0px); }
+ 84%{ transform: translateX(0px) translateY(-1px); }
+ 100%{ transform: translateX(0px) translateY(-1px); }
+ } #n-lder svg g#nyan_head{ animation: nyan_head 0.4s linear infinite; }
+ @keyframes nyan_walk {
+ 0%{ transform: translateX(0px); }
+ 16%{ transform: translateX(0px); }
+ 17%{ transform: translateX(1px); }
+ 33%{ transform: translateX(1px); }
+ 34%{ transform: translateX(2px); }
+ 50%{ transform: translateX(2px); }
+ 51%{ transform: translateX(1px); }
+ 100%{ transform: translateX(0px); }
+ } #n-lder svg g#nyan_feet{ animation: nyan_walk 0.5s linear infinite; }
+ @keyframes nyan_tail {
+ 0%{ transform: rotate(0); }
+ 16%{ transform: rotate(0); }
+ 17%{ transform: rotate(-5deg); }
+ 33%{ transform: rotate(-5deg); }
+ 34%{ transform: rotate(-10deg); }
+ 49%{ transform: rotate(-10deg); }
+ 50%{ transform: rotate(-20deg); }
+ 66%{ transform: rotate(-20deg); }
+ 67%{ transform: rotate(-10deg); }
+ 83%{ transform: rotate(-10deg); }
+ 84%{ transform: rotate(-5deg); }
+ 99%{ transform: rotate(-5deg); }
+ 100%{ transform: rotate(0deg); }
+ } #n-lder svg g#nyan_tail{ animation: nyan_tail 0.5s linear infinite; transform-origin: 4px 8px; }
+`;
+};
diff --git a/public/pages/home/index.js b/public/pages/home/index.js
new file mode 100644
index 00000000..9390f3d4
--- /dev/null
+++ b/public/pages/home/index.js
@@ -0,0 +1,3 @@
+export default function(render) {
+ render(`
HOMEPAGE
`);
+};
diff --git a/public/pages/home/index.test.js b/public/pages/home/index.test.js
new file mode 100644
index 00000000..e2727b47
--- /dev/null
+++ b/public/pages/home/index.test.js
@@ -0,0 +1,7 @@
+import home from "./index.js";
+
+test("home", () => {
+ const render = jest.fn();
+ home(render);
+ expect(render).toBeCalledTimes(1);
+});
diff --git a/public/server.go b/public/server.go
new file mode 100644
index 00000000..782443d2
--- /dev/null
+++ b/public/server.go
@@ -0,0 +1,90 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "mime"
+ "net/http"
+ "net/http/httputil"
+ "net/url"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+const (
+ CHROOT = "/"
+ ENDPOINT = "127.0.0.1:8000"
+)
+
+func main() {
+ fmt.Printf("starting server http://%s ...\n", ENDPOINT)
+ err := http.ListenAndServe(ENDPOINT, http.HandlerFunc(serveStatic))
+ if err != nil {
+ fmt.Printf("Oops %s\n", err.Error())
+ }
+}
+
+func serveStatic(w http.ResponseWriter, r *http.Request) {
+ if strings.HasPrefix(r.URL.Path, "/proxy/") {
+ u, _ := url.Parse("http://127.0.0.1:8334")
+ r.URL.Path = strings.TrimPrefix(r.URL.Path, "/proxy")
+ httputil.NewSingleHostReverseProxy(u).ServeHTTP(w, r)
+ return
+ }
+ var (
+ fs http.Dir
+ f http.File
+ err error
+ )
+ defer func() {
+ now := time.Now().Format("2006-01-02 15:04:05")
+ if err != nil {
+ fmt.Printf("%s HTTP %s %s err[%s]\n", now, r.Method, r.URL.Path, err.Error())
+ return
+ }
+ fmt.Printf("%s HTTP %s %s\n", now, r.Method, r.URL.Path)
+ }()
+ fs = http.Dir(".")
+ if strings.HasSuffix(r.URL.Path, "/") {
+ http.FileServer(fs).ServeHTTP(w, r)
+ return
+ }
+ f, err = openFile(fs, r, w)
+ if err != nil {
+ r.URL.Path = CHROOT + "index.html"
+ f, err = openFile(fs, r, w)
+ }
+ if err != nil {
+ w.WriteHeader(http.StatusNotFound)
+ w.Write([]byte("Oops"))
+ return
+ }
+ // TODO: etag
+ w.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(r.URL.Path)))
+ io.Copy(w, f)
+ f.Close()
+}
+
+func openFile(fs http.Dir, r *http.Request, w http.ResponseWriter) (http.File, error) {
+ encs := [][]string{}
+ if strings.Contains(r.Header.Get("Accept-Encoding"), "br") {
+ encs = append(encs, []string{".br", "br"})
+ }
+ if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
+ encs = append(encs, []string{".gz", "gzip"})
+ }
+ encs = append(encs, []string{"", ""})
+
+ for _, enc := range encs {
+ f, err := fs.Open(r.URL.Path + enc[0])
+ if err == nil {
+ if enc[1] != "" {
+ w.Header().Set("Content-Encoding", enc[1])
+ }
+ return f, nil
+ }
+ }
+ return nil, errors.New("not found")
+}