diff --git a/web/manage/.gitignore b/web/manage/.gitignore
new file mode 100644
index 0000000..da93220
--- /dev/null
+++ b/web/manage/.gitignore
@@ -0,0 +1,4 @@
+/node_modules/
+/public/build/
+
+.DS_Store
diff --git a/web/manage/Dockerfile b/web/manage/Dockerfile
new file mode 100644
index 0000000..4fc1cc8
--- /dev/null
+++ b/web/manage/Dockerfile
@@ -0,0 +1,15 @@
+FROM node:latest
+
+WORKDIR /working
+
+COPY package.json ./
+COPY rollup.config.js ./
+RUN npm install
+
+VOLUME /working/src
+VOLUME /working/public
+
+EXPOSE 5000
+
+ENTRYPOINT []
+CMD ["npm", "run", "dev"]
diff --git a/web/manage/Makefile b/web/manage/Makefile
new file mode 100644
index 0000000..8d2999f
--- /dev/null
+++ b/web/manage/Makefile
@@ -0,0 +1,65 @@
+DOCKER ?= podman
+
+NAME ?= photos-web-manage
+OUTPUT ?= public/build
+SOURCE ?= src
+ARG_PUBLISH ?= --publish 5000:5000
+
+SHARED ?= ../shared
+shared_copy = public/shared
+
+current_dir = $(shell pwd)
+shared_source = $(wildcard $(SHARED)/*/*)
+shared_target = $(foreach path,$(shared_source),$(subst $(SHARED),$(shared_copy),$(path)))
+
+container_workdir = /working
+arg_base = \
+ --rm --interactive --tty \
+ --workdir $(container_workdir) \
+ --security-opt label=disable
+arg_volume = \
+ -v $(current_dir)/public:$(container_workdir)/public \
+ -v $(current_dir)/src:$(container_workdir)/src \
+
+.PHONY: default
+default: build install
+
+.PHONY: clean
+clean:
+ $(RM) -r $(shared_copy) $(OUTPUT) public/ .container
+
+
+.PHONY: container .containertest
+container: .containertest .container
+.containertest:
+ $(DOCKER) image inspect $(NAME)-build 2>&1 >/dev/null || rm .container || exit 0
+.container: Dockerfile package.json rollup.config.js
+ $(DOCKER) build --tag $(NAME)-build --no-cache .
+ touch $@
+
+
+.PHONY: dev
+# Start a development server
+dev: public/index.html $(shared_target) | container
+ $(DOCKER) run $(arg_base) $(ARG_PUBLISH) $(arg_volume) \
+ $(NAME)-build
+
+public/index.html: index.tmpl
+ mkdir -p $(@D)
+ go run indextmpl.go -t $? -o $@ -a $(subst public/,,$(shared_copy))/ -b $(subst public/,,$(OUTPUT))/ -mt "[dev] Manage"
+
+.SECONDEXPANSION:
+$(shared_target): $$(subst $$(shared_copy),$$(SHARED),$$@)
+ mkdir -p $(@D) && cp $? $@
+
+
+.PHONY: build install
+# Build with rollup
+build: $(OUTPUT)/manage.js
+$(OUTPUT)/manage.js: src/* | container
+ $(DOCKER) run $(arg_base) $(arg_volume) \
+ $(NAME)-build npm run build
+install: build
+ cp $(OUTPUT)/manage.js $(SHARED)/js/manage.js
+ cp $(OUTPUT)/manage.css $(SHARED)/css/manage.css
+
diff --git a/web/manage/README.md b/web/manage/README.md
new file mode 100644
index 0000000..97aadae
--- /dev/null
+++ b/web/manage/README.md
@@ -0,0 +1,43 @@
+
+# [svelte app](https://github.com/sveltejs/template)
+
+## Get started
+
+Install the dependencies...
+
+```bash
+npm install
+```
+
+...then start [Rollup](https://rollupjs.org):
+
+```bash
+npm run dev
+```
+
+Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes.
+
+By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`.
+
+
+## Building and running in production mode
+
+To create an optimised version of the app:
+
+```bash
+npm run build
+```
+
+You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com).
+
+
+## Single-page app mode
+
+By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere.
+
+If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json:
+
+```js
+"start": "sirv public --single"
+```
+
diff --git a/web/manage/index.tmpl b/web/manage/index.tmpl
new file mode 100644
index 0000000..86f91e4
--- /dev/null
+++ b/web/manage/index.tmpl
@@ -0,0 +1,15 @@
+
+
+
+
+
+ {{ .Metadata.Title }}
+
+
+
+
+
+
+
+
+
diff --git a/web/manage/indextmpl.go b/web/manage/indextmpl.go
new file mode 100644
index 0000000..00d155d
--- /dev/null
+++ b/web/manage/indextmpl.go
@@ -0,0 +1,63 @@
+package main
+
+import (
+ "bytes"
+ "flag"
+ "html/template"
+ "io/ioutil"
+ "os"
+)
+
+type IndexData struct {
+ Assets string
+ Development bool
+ Build string
+ Metadata Metadata
+}
+
+type Metadata struct {
+ Title string
+}
+
+var data = IndexData{
+ Development: true,
+}
+var tmpl string
+var out string
+
+func main() {
+ flag.StringVar(&tmpl, "t", "", "template file")
+ flag.StringVar(&out, "o", "", "output file")
+ flag.StringVar(&data.Assets, "a", "", ".Assets")
+ flag.StringVar(&data.Build, "b", "", ".Build")
+ flag.StringVar(&data.Metadata.Title, "mt", "", ".Metadata.Title")
+ flag.Parse()
+
+ f, err := os.Open(tmpl)
+ if err != nil {
+ panic(err)
+ }
+ buf, err := ioutil.ReadAll(f)
+ if err != nil {
+ panic(err)
+ }
+
+ t := template.Must(
+ template.New("index").Parse(string(buf)),
+ )
+
+ indexBuf := new(bytes.Buffer)
+ err = t.Execute(indexBuf, data)
+ if err != nil {
+ panic(err)
+ }
+
+ of, err := os.OpenFile(out, os.O_WRONLY|os.O_CREATE, 0644)
+ if err != nil {
+ panic(err)
+ }
+ _, err = of.Write(indexBuf.Bytes())
+ if err != nil {
+ panic(err)
+ }
+}
diff --git a/web/manage/package.json b/web/manage/package.json
new file mode 100644
index 0000000..d945b6e
--- /dev/null
+++ b/web/manage/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "svelte-app",
+ "version": "1.0.0",
+ "scripts": {
+ "build": "rollup -c",
+ "dev": "rollup -c -w",
+ "start": "sirv public --host 0.0.0.0"
+ },
+ "devDependencies": {
+ "@rollup/plugin-commonjs": "^12.0.0",
+ "@rollup/plugin-node-resolve": "^8.0.0",
+ "rollup": "^2.3.4",
+ "rollup-plugin-livereload": "^1.0.0",
+ "rollup-plugin-svelte": "^5.0.3",
+ "rollup-plugin-terser": "^5.1.2",
+ "svelte": "^3.0.0"
+ },
+ "dependencies": {
+ "sirv-cli": "^0.4.4"
+ }
+}
diff --git a/web/manage/rollup.config.js b/web/manage/rollup.config.js
new file mode 100644
index 0000000..3649681
--- /dev/null
+++ b/web/manage/rollup.config.js
@@ -0,0 +1,71 @@
+import svelte from 'rollup-plugin-svelte';
+import resolve from '@rollup/plugin-node-resolve';
+import commonjs from '@rollup/plugin-commonjs';
+import livereload from 'rollup-plugin-livereload';
+import { terser } from 'rollup-plugin-terser';
+
+const production = !process.env.ROLLUP_WATCH;
+
+export default {
+ input: 'src/main.js',
+ output: {
+ sourcemap: true,
+ format: 'iife',
+ name: 'app',
+ file: 'public/build/manage.js'
+ },
+ plugins: [
+ svelte({
+ // enable run-time checks when not in production
+ dev: !production,
+ // we'll extract any component CSS out into
+ // a separate file - better for performance
+ css: css => {
+ css.write('public/build/manage.css');
+ }
+ }),
+
+ // If you have external dependencies installed from
+ // npm, you'll most likely need these plugins. In
+ // some cases you'll need additional configuration -
+ // consult the documentation for details:
+ // https://github.com/rollup/plugins/tree/master/packages/commonjs
+ resolve({
+ browser: true,
+ dedupe: ['svelte']
+ }),
+ commonjs(),
+
+ // In dev mode, call `npm run start` once
+ // the bundle has been generated
+ !production && serve(),
+
+ // Watch the `public` directory and refresh the
+ // browser on changes when not in production
+ !production && livereload('public'),
+
+ // If we're building for production (npm run build
+ // instead of npm run dev), minify
+ production && terser()
+ ],
+ watch: {
+ clearScreen: false
+ }
+};
+
+function serve() {
+ let started = false;
+
+ return {
+ writeBundle() {
+ if (!started) {
+ started = true;
+
+ require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
+ stdio: ['ignore', 'inherit', 'inherit'],
+ shell: true
+ });
+ }
+ }
+ };
+}
diff --git a/web/manage/src/App.svelte b/web/manage/src/App.svelte
new file mode 100644
index 0000000..10faec7
--- /dev/null
+++ b/web/manage/src/App.svelte
@@ -0,0 +1,30 @@
+
+
+
+
Hello {name}!
+
Visit the Svelte tutorial to learn how to build Svelte apps.
+
+
+
\ No newline at end of file
diff --git a/web/manage/src/main.js b/web/manage/src/main.js
new file mode 100644
index 0000000..1fe1b22
--- /dev/null
+++ b/web/manage/src/main.js
@@ -0,0 +1,10 @@
+import App from './App.svelte';
+
+const app = new App({
+ target: document.body,
+ props: {
+ name: 'ambrose'
+ }
+});
+
+export default app;
diff --git a/web/shared/js/bleh.js b/web/shared/js/bleh.js
deleted file mode 100644
index c51709e..0000000
--- a/web/shared/js/bleh.js
+++ /dev/null
@@ -1,2 +0,0 @@
-import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.esm.browser.js';
-