diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 292fac460..aad54be69 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,8 +22,12 @@ jobs: yarn build:production - uses: actions/upload-artifact@v1 with: - name: hydro - path: .build + name: development.js + path: .build/development.js + - uses: actions/upload-artifact@v1 + with: + name: module + path: .build/module - uses: elgohr/Publish-Docker-Github-Action@master with: name: hydrooj/hydro:alpine diff --git a/hydro/handler/problem.js b/hydro/handler/problem.js index 491ce775e..323c9af8d 100644 --- a/hydro/handler/problem.js +++ b/hydro/handler/problem.js @@ -269,7 +269,7 @@ class ProblemSolutionHandler extends ProblemDetailHandler { ['problem_solution', null], ]; this.response.body = { - path, psdocs, page, pcount, pscount, udict, pssdict, + path, psdocs, page, pcount, pscount, udict, pssdict, pdoc: this.pdoc, }; } diff --git a/hydro/lib/mail.js b/hydro/lib/mail.js index fb1c0d86e..fe6934caf 100644 --- a/hydro/lib/mail.js +++ b/hydro/lib/mail.js @@ -1,22 +1,6 @@ const nodemailer = require('nodemailer'); -const options = require('../options'); const { SendMailError } = require('../error'); -let transporter = null; -try { - transporter = nodemailer.createTransport({ - host: options.VJ_SMTP_HOST, - port: options.VJ_DB_PORT, - secure: options.VJ_SMTP_SECURE, - auth: { - user: options.VJ_SMTP_USER, - pass: options.VJ_SMTP_PASSWORD, - }, - }); -} catch (e) { - console.error(e); -} - module.exports = { /** * @param {string} to @@ -27,9 +11,21 @@ module.exports = { async sendMail(to, subject, text, html) { let t; try { + const system = require('../model/system'); + const [host, port, secure, user, pass, from] = await Promise.all([ + system.get('smtp.host'), + system.get('smtp.port'), + system.get('smtp.secure'), + system.get('smtp.user'), + system.get('smtp.pass'), + system.get('smtp.from'), + ]); + const transporter = nodemailer.createTransport({ + host, port, secure, auth: { user, pass }, + }); t = await new Promise((resolve, reject) => { transporter.sendMail({ - from: options.VJ_SMTP_FROM, + from, to, subject, text, diff --git a/hydro/loader.js b/hydro/loader.js index 6e630c3a8..3b8fca412 100644 --- a/hydro/loader.js +++ b/hydro/loader.js @@ -1,3 +1,4 @@ +/* eslint-disable import/no-dynamic-require */ /* eslint-disable no-await-in-loop */ /* eslint-disable no-eval */ const fs = require('fs'); @@ -19,6 +20,7 @@ function ensureDir(dir) { } const active = []; +const fail = []; try { // Let webpack pack builtin module together. // eslint-disable-next-line import/no-unresolved @@ -59,7 +61,7 @@ async function preload() { for (const i of active) { try { if (i.os) { - if (!i.os.includes(os.platform())) throw new Error('Unsupported OS'); + if (!i.os.includes(os.platform().toLowerCase())) throw new Error('Unsupported OS'); } if (i.file) { i.files = {}; @@ -87,6 +89,7 @@ async function handler() { for (const i of active) { if (i.handler && !i.fail) { try { + console.log(`Handler init: ${i.id}`); console.time(`Handler init: ${i.id}`); const exports = {}; // eslint-disable-next-line no-unused-vars @@ -136,6 +139,7 @@ async function model() { for (const i of active) { if (i.model && !i.fail) { try { + console.log(`Model init: ${i.id}`); console.time(`Model init: ${i.id}`); const exports = {}; const module = { exports, file: i.files || {} }; @@ -155,6 +159,7 @@ async function lib() { for (const i of active) { if (i.lib && !i.fail) { try { + console.log(`Lib init: ${i.id}`); console.time(`Lib init: ${i.id}`); const exports = {}; // eslint-disable-next-line no-unused-vars @@ -184,44 +189,15 @@ async function service() { i.fail = true; fail.push(i.id); console.error(`Service Load Fail: ${i.id}`); + console.error(e); } } } } async function install() { - await Promise.all([lib(), locale(), template()]); - require('./service/setup'); -} - -async function installDb() { - const system = require('./model/system'); - const def = { - PROBLEM_PER_PAGE: 100, - RECORD_PER_PAGE: 100, - SOLUTION_PER_PAGE: 20, - CONTEST_PER_PAGE: 20, - TRAINING_PER_PAGE: 10, - DISCUSSION_PER_PAGE: 50, - REPLY_PER_PAGE: 50, - CONTESTS_ON_MAIN: 5, - TRAININGS_ON_MAIN: 5, - DISCUSSIONS_ON_MAIN: 20, - 'db.ver': 1, - 'listen.https': false, - 'listen.port': 8888, - 'session.keys': ['Hydro'], - 'session.secure': false, - 'session.saved_expire_seconds': 3600 * 24, - 'session.unsaved_expire_seconds': 600, - changemail_token_expire_seconds: 3600 * 24, - registration_token_expire_seconds: 600, - }; - const tasks = []; - for (const key in def) { - tasks.push(system.set(key, def[key])); - } - await Promise.all(tasks); + const setup = require('./service/setup'); + await setup.setup(); } async function load() { @@ -243,11 +219,12 @@ async function load() { require('./utils'); require('./error'); require('./permission'); + await Promise.all([locale(), template()]); try { require('./options'); } catch (e) { await install(); - return; + require('./options'); } const bus = require('./service/bus'); await new Promise((resolve) => { @@ -265,7 +242,7 @@ async function load() { 'template', 'validator', 'nav', ]; for (const i of builtinLib) require(`./lib/${i}`); - await Promise.all([lib(), locale(), template()]); + await lib(); require('./service/gridfs'); const server = require('./service/server'); await server.prepare(); @@ -281,7 +258,10 @@ async function load() { } const system = require('./model/system'); const dbVer = await system.get('db.ver'); - if (dbVer !== 1) await installDb(); + if (dbVer !== 1) { + const ins = require('./script/install'); + await ins.run(); + } const builtinHandler = [ 'home', 'problem', 'record', 'judge', 'user', 'contest', 'training', 'discussion', 'manage', 'import', @@ -300,4 +280,4 @@ async function load() { await server.start(); } -module.exports = { load, active }; +module.exports = { load, active, fail }; diff --git a/hydro/model/problem.js b/hydro/model/problem.js index 16c91d173..1743508bd 100644 --- a/hydro/model/problem.js +++ b/hydro/model/problem.js @@ -160,7 +160,13 @@ async function setTestdata(_id, readStream) { return await edit(_id, { data: f.id }); // eslint-disable-line no-return-await } -module.exports = { +async function getData(pid) { + const pdoc = await get(pid); + if (!pdoc.data) return null; + return gridfs.openDownloadStream(pdoc.data); +} + +global.Hydro.model.problem = module.exports = { add, inc, get, @@ -175,4 +181,5 @@ module.exports = { getMultiStatus, setTestdata, updateStatus, + getData, }; diff --git a/hydro/model/task.js b/hydro/model/task.js index efff10215..a1dbe70de 100644 --- a/hydro/model/task.js +++ b/hydro/model/task.js @@ -21,7 +21,7 @@ function del(_id) { async function getFirst(query) { const res = await coll.find(query).sort('_id', 1).limit(1).toArray(); - if (res) { + if (res.length) { await coll.deleteOne({ _id: res[0]._id }); return res[0]; } @@ -35,6 +35,6 @@ async function consume(query, cb) { }, 100); } -module.exports = { +global.Hydro.model.task = module.exports = { add, get, del, count, getFirst, consume, }; diff --git a/hydro/options.js b/hydro/options.js index d704d3887..d5970802b 100644 --- a/hydro/options.js +++ b/hydro/options.js @@ -1,59 +1,20 @@ const fs = require('fs'); const os = require('os'); const path = require('path'); -const { defaultsDeep } = require('lodash'); +const { defaults } = require('lodash'); let options = { - db: { - name: 'hydro', - host: '127.0.0.1', - port: 27017, - username: '', - password: '', - }, - template: { - path: path.join(process.cwd(), 'templates'), - }, - listen: { - host: '127.0.0.1', - port: 8888, - domain: '', - https: false, - }, - smtp: { - from: '', - host: '', - port: 465, - user: '', - pass: '', - secure: false, - }, - session: { - keys: ['Hydro'], - secure: false, - domain: '127.0.0.1', - saved_expire_seconds: 3600 * 24, - unsaved_expire_seconds: 600, - registration_token_expire_seconds: 600, - }, - constants: { - PROBLEM_PER_PAGE: 100, - RECORD_PER_PAGE: 100, - SOLUTION_PER_PAGE: 20, - CONTEST_PER_PAGE: 20, - TRAINING_PER_PAGE: 10, - DISCUSSION_PER_PAGE: 50, - REPLY_PER_PAGE: 50, - CONTESTS_ON_MAIN: 5, - TRAININGS_ON_MAIN: 5, - DISCUSSIONS_ON_MAIN: 20, - }, + name: 'hydro', + host: '127.0.0.1', + port: 27017, + username: '', + password: '', }; let f = path.resolve(process.cwd(), 'config.json'); if (!fs.existsSync(f)) f = path.resolve(os.homedir(), '.config', 'hydro', 'config.json'); if (!fs.existsSync(f)) f = path.resolve('/config/config.json'); const t = JSON.parse(fs.readFileSync(f)); -options = defaultsDeep(t, options); +options = defaults(t, options); module.exports = options; diff --git a/hydro/script/install.js b/hydro/script/install.js index 188213724..a72293318 100644 --- a/hydro/script/install.js +++ b/hydro/script/install.js @@ -1,46 +1,82 @@ const { defaults } = require('lodash'); const db = require('../service/db'); const builtin = require('../model/builtin'); +const system = require('../model/system'); const pwhash = require('../lib/pwhash'); const { udoc } = require('../interface'); const collUser = db.collection('user'); const collRole = db.collection('role'); -const collSystem = db.collection('system'); async function run() { - const salt = pwhash.salt(); - await collUser.insertMany([ - defaults({ - _id: 0, - uname: 'Hydro', - unameLower: 'hydro', - mail: 'hydro@hydro', - mailLower: 'hydro@hydro', - role: 'guest', - }, udoc), - defaults({ - _id: 1, - mail: 'guest@hydro', - mailLower: 'guest@hydro', - uname: 'Guest', - unameLower: 'guest', - role: 'guest', - }, udoc), - defaults({ - _id: -1, - mail: 'root@hydro', - mailLower: 'root@hydro', - uname: 'Root', - unameLower: 'root', - hash: pwhash.hash('rootroot', salt), - salt, - gravatar: 'root@hydro', - role: 'root', - }, udoc), - ]); + const def = { + PROBLEM_PER_PAGE: 100, + RECORD_PER_PAGE: 100, + SOLUTION_PER_PAGE: 20, + CONTEST_PER_PAGE: 20, + TRAINING_PER_PAGE: 10, + DISCUSSION_PER_PAGE: 50, + REPLY_PER_PAGE: 50, + CONTESTS_ON_MAIN: 5, + TRAININGS_ON_MAIN: 5, + DISCUSSIONS_ON_MAIN: 20, + 'smtp.host': '', + 'smtp.port': 465, + 'smtp.from': '', + 'smtp.user': '', + 'smtp.pass': '', + 'smtp.secure': false, + 'db.ver': 1, + 'listen.https': false, + 'listen.port': 8888, + 'session.keys': ['Hydro'], + 'session.secure': false, + 'session.saved_expire_seconds': 3600 * 24, + 'session.unsaved_expire_seconds': 600, + changemail_token_expire_seconds: 3600 * 24, + registration_token_expire_seconds: 600, + }; + const tasks = []; + for (const key in def) { + tasks.push(system.set(key, def[key])); + } + await Promise.all(tasks); + try { + const salt = pwhash.salt(); + await collUser.insertMany([ + defaults({ + _id: 0, + uname: 'Hydro', + unameLower: 'hydro', + mail: 'hydro@hydro', + mailLower: 'hydro@hydro', + role: 'guest', + }, udoc), + defaults({ + _id: 1, + mail: 'guest@hydro', + mailLower: 'guest@hydro', + uname: 'Guest', + unameLower: 'guest', + role: 'guest', + }, udoc), + defaults({ + _id: -1, + mail: 'root@hydro', + mailLower: 'root@hydro', + uname: 'Root', + unameLower: 'root', + hash: pwhash.hash('rootroot', salt), + salt, + gravatar: 'root@hydro', + role: 'root', + }, udoc), + ]); + } catch (e) { + // Ignore user already exist error + } + await collRole.deleteMany({ _id: { $in: builtin.BUILTIN_ROLES.map((role) => role._id) } }); await collRole.insertMany(builtin.BUILTIN_ROLES); - console.log('Installed'); } module.exports = { run }; diff --git a/hydro/service/db.js b/hydro/service/db.js index e1010faed..dd5019d9d 100644 --- a/hydro/service/db.js +++ b/hydro/service/db.js @@ -3,15 +3,15 @@ const bus = require('./bus'); const options = require('../options'); let mongourl = 'mongodb://'; -if (options.db.username) mongourl += `${options.db.username}:${options.db.password}@`; -mongourl += `${options.db.host}:${options.db.port}/${options.db.name}`; +if (options.username) mongourl += `${options.username}:${options.password}@`; +mongourl += `${options.host}:${options.port}/${options.name}`; /** * @type {import('mongodb').Db} */ let db = null; Mongo.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true }) .then((Database) => { - db = Database.db(options.db.name); + db = Database.db(options.name); db.Db = Database; bus.publish('system_database_connected', null); }); diff --git a/hydro/service/server.js b/hydro/service/server.js index 5a1c7ba59..bca15a26d 100644 --- a/hydro/service/server.js +++ b/hydro/service/server.js @@ -249,27 +249,30 @@ function Route(route, RouteHandler) { const args = { ...ctx.params, ...ctx.query, ...ctx.request.body }; if (h.___prepare) await h.___prepare(args); - - for (const l of check) { - if (args[l]) { - args[l] = new ObjectID(args[l]); - if (!args[l]) throw new ValidationError(l); + try { + for (const l of check) { + if (args[l]) { + args[l] = new ObjectID(args[l]); + if (!args[l]) throw new ValidationError(l); + } } + if (args.content) validator.checkContent(args.content); + if (args.title) validator.checkContent(args.title); + if (args.uid) args.uid = parseInt(validator.checkUid(args.uid)); + if (args.password) validator.checkPassword(args.password); + if (args.mail) validator.checkEmail(args.mail); + if (args.uname) validator.checkUname(args.uname); + if (args.page) args.page = parseInt(args.page); + if (args.duration) args.duration = parseFloat(args.duration); + if (args.pids) args.pids = args.pids.split(',').map((i) => i.trim()); + if (args.role) validator.checkRole(args.role); + if (args.roles) { + for (const i of args.roles) validator.checkRole(i); + } + } catch (e) { + if (e instanceof ValidationError) throw e; + throw new ValidationError('Argument check failed'); } - if (args.content) validator.checkContent(args.content); - if (args.title) validator.checkContent(args.title); - if (args.uid) args.uid = parseInt(validator.checkUid(args.uid)); - if (args.password) validator.checkPassword(args.password); - if (args.mail) validator.checkEmail(args.mail); - if (args.uname) validator.checkUname(args.uname); - if (args.page) args.page = parseInt(args.page); - if (args.duration) args.duration = parseFloat(args.duration); - if (args.pids) args.pids = args.pids.split(',').map((i) => i.trim()); - if (args.role) validator.checkRole(args.role); - if (args.roles) { - for (const i of args.roles) validator.checkRole(i); - } - if (h.__prepare) await h.__prepare(args); if (h._prepare) await h._prepare(args); if (h.prepare) await h.prepare(args); diff --git a/hydro/uninstall.js b/hydro/uninstall.js deleted file mode 100644 index 577ead16a..000000000 --- a/hydro/uninstall.js +++ /dev/null @@ -1,20 +0,0 @@ -const Mongo = require('mongodb'); -const options = require('./options'); - -async function run() { - let mongourl = 'mongodb://'; - if (options.db.username) mongourl += `${options.db.username}:${options.db.password}@`; - mongourl += `${options.db.host}:${options.db.port}/${options.db.name}`; - const Database = await Mongo.MongoClient.connect(mongourl, { - useNewUrlParser: true, useUnifiedTopology: true, - }); - const db = Database.db(options.db.name); - await db.dropDatabase(); - console.log('Dropped'); - process.exit(0); -} - -run().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/module/judger/build.js b/module/judger/build.js deleted file mode 100644 index ca601653b..000000000 --- a/module/judger/build.js +++ /dev/null @@ -1,14 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const child = require('child_process'); -const yaml = require('js-yaml'); - -exports.prebuild = async function prebuild() { - if (!fs.existsSync(path.resolve(__dirname, 'HydroJudger'))) { - const res = child.spawnSync('git clone https://github.com/hydro-dev/HydroJudger.git'); - if (res.error) throw res.error; - } - let lang = fs.readFileSync(path.resolve(__dirname, 'langs.yaml')).toString(); - lang = yaml.safeLoad(lang); - fs.writeFileSync(path.resolve(__dirname, '__lang.json'), JSON.stringify(lang)); -}; diff --git a/module/judger/hydro.json b/module/judger/hydro.json deleted file mode 100644 index ff1c9906e..000000000 --- a/module/judger/hydro.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "judger", - "version": "1.0.0", - "description": "HydroJudger" -} \ No newline at end of file diff --git a/module/judger/service.js b/module/judger/service.js deleted file mode 100644 index 3f4f202d6..000000000 --- a/module/judger/service.js +++ /dev/null @@ -1,167 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const child = require('child_process'); - -async function postInit() { - const config = require('./HydroJudger/judger/config'); - // eslint-disable-next-line import/no-unresolved - config.LANGS = require('./__langs.json'); - const { mkdirp, rmdir, compilerText } = require('./HydroJudger/judger/utils'); - const log = require('./HydroJudger/judger/log'); - const tmpfs = require('./HydroJudger/judger/tmpfs'); - const { FormatError, CompileError, SystemError } = require('./HydroJudger/judger/error'); - const { STATUS_COMPILE_ERROR, STATUS_SYSTEM_ERROR } = require('./HydroJudger/judger/status'); - const readCases = require('./HydroJudger/judger/cases'); - const judger = require('./HydroJudger/judger/judger'); - - const fsp = fs.promises; - const { problem, task } = global.Hydro.model; - const { judge } = global.Hydro.handler; - - async function problemData(pid, savePath) { - const tmpFilePath = path.resolve(config.CACHE_DIR, `download_${pid}`); - const res = await problem.getData(pid); - const w = await fs.createWriteStream(tmpFilePath); - res.data.pipe(w); - await new Promise((resolve, reject) => { - w.on('finish', resolve); - w.on('error', reject); - }); - mkdirp(path.dirname(savePath)); - await new Promise((resolve, reject) => { - child.exec(`unzip ${tmpFilePath} -d ${savePath}`, (e) => { - if (e) reject(e); - else resolve(); - }); - }); - await fsp.unlink(tmpFilePath); - await this.processData(savePath).catch(); - return savePath; - } - - async function cacheOpen(pid, version) { - const filePath = path.join(config.CACHE_DIR, pid); - if (fs.existsSync(filePath)) { - let ver; - try { - ver = fs.readFileSync(path.join(filePath, 'version')).toString(); - } catch (e) { /* ignore */ } - if (version === ver) return filePath; - rmdir(filePath); - } - mkdirp(filePath); - await problemData(pid, filePath); - fs.writeFileSync(path.join(filePath, 'version'), version); - return filePath; - } - - function getNext(that) { - that.nextId = 1; - that.nextWaiting = []; - return (data, id) => { - data.key = 'next'; - data.rid = that.rid; - if (id) { - if (id === that.nextId) { - judge.next(data); - that.nextId++; - let t = true; - while (t) { - t = false; - for (const i in that.nextWaiting) { - if (that.nextId === that.nextWaiting[i].id) { - judge.next(that.nextWaiting[i].data); - that.nextId++; - that.nextWaiting.splice(i, 1); - t = true; - } - } - } - } else that.nextWaiting.push({ data, id }); - } else judge.next(data); - }; - } - - function getEnd(rid) { - return (data) => { - data.key = 'end'; - data.rid = rid; - log.log({ - status: data.status, - score: data.score, - time_ms: data.time_ms, - memory_kb: data.memory_kb, - }); - judge.end(data); - }; - } - - class JudgeTask { - constructor(request) { - this.stat = {}; - this.stat.receive = new Date(); - this.request = request; - } - - async handle() { - try { - this.stat.handle = new Date(); - this.event = this.request.event; - this.pid = this.request.pid; - this.rid = this.request.rid; - this.lang = this.request.lang; - this.code = this.request.code; - this.data = this.request.data; - this.next = getNext(this); - this.end = getEnd(this.rid); - this.tmpdir = path.resolve(config.TEMP_DIR, 'tmp', this.rid); - this.clean = []; - mkdirp(this.tmpdir); - tmpfs.mount(this.tmpdir, '64m'); - log.submission(`${this.rid}`, { pid: this.pid }); - if (!this.event) await this.submission(); - else throw new SystemError(`Unsupported type: ${this.type}`); - } catch (e) { - if (e instanceof CompileError) { - this.next({ compiler_text: compilerText(e.stdout, e.stderr) }); - this.end({ - status: STATUS_COMPILE_ERROR, score: 0, time_ms: 0, memory_kb: 0, - }); - } else if (e instanceof FormatError) { - this.next({ judge_text: `${e.message}\n${JSON.stringify(e.params)}` }); - this.end({ - status: STATUS_SYSTEM_ERROR, score: 0, time_ms: 0, memory_kb: 0, - }); - } else { - log.error(e); - this.next({ judge_text: `${e.message}\n${e.stack}\n${JSON.stringify(e.params)}` }); - this.end({ - status: STATUS_SYSTEM_ERROR, score: 0, time_ms: 0, memory_kb: 0, - }); - } - } - // eslint-disable-next-line no-await-in-loop - for (const clean of this.clean) await clean().catch(); - tmpfs.umount(this.tmpdir); - await rmdir(this.tmpdir); - } - - async submission() { - this.stat.cache_start = new Date(); - this.folder = await cacheOpen(this.pid, this.data); - this.stat.read_cases = new Date(); - this.config = await readCases( - this.folder, - { detail: true }, - { next: this.next }, - ); - this.stat.judge = new Date(); - await judger[this.config.type || 'default'].judge(this); - } - } - task.consume({ type: 'judge' }, (t) => { - (new JudgeTask(t)).handle(); - }); -} - -module.exports = { postInit }; diff --git a/module/sandbox-linux-amd64/hydro.json b/module/sandbox-linux-amd64/hydro.json index f4061dfe3..1619dde75 100644 --- a/module/sandbox-linux-amd64/hydro.json +++ b/module/sandbox-linux-amd64/hydro.json @@ -1,6 +1,8 @@ { "name": "sandbox-linux", "version": "1.0.0", - "os": "Linux", + "os": [ + "linux" + ], "description": "Sandbox service for linux" } \ No newline at end of file diff --git a/module/sandbox-linux-amd64/service.js b/module/sandbox-linux-amd64/service.js index 83f057de5..c0ac7e31e 100644 --- a/module/sandbox-linux-amd64/service.js +++ b/module/sandbox-linux-amd64/service.js @@ -2,7 +2,7 @@ const os = require('os'); const path = require('path'); const child = require('child_process'); -const p = child.spawn(path.resolve(os.tmpdir(), 'hydro', 'sandbox-linux-amd64', 'executorserver', ['-silent'])); +const p = child.spawn(path.resolve(os.tmpdir(), 'hydro', 'sandbox-linux-amd64', 'executorserver'), ['-silent']); if (!p.stdout) throw new Error('Cannot start executorserver'); else { p.stdout.on('data', (data) => { diff --git a/package.json b/package.json index 752cc4fec..3ebca68ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydro", - "version": "0.0.0", + "version": "1.0.0-preview", "main": "hydro/development.js", "repository": "https://github.com/hydro-dev/Hydro.git", "author": "masnn", diff --git a/templates/setup_done.html b/templates/setup_done.html deleted file mode 100644 index 340a29dc8..000000000 --- a/templates/setup_done.html +++ /dev/null @@ -1,32 +0,0 @@ - - -
- - - - - - - -