fixed config + cli-args + admin form

This commit is contained in:
Leon van Kammen 2026-07-22 18:36:02 +02:00
parent 2671f43b8b
commit af2e1bfdb2
24 changed files with 549 additions and 368 deletions

View file

@ -34,3 +34,11 @@ manual scan (via cli?)
curl -L -X 'GET' 'http://xrforge.isvery.ninja/models?page=1&order=name' -H 'accept: application/vnd.manyfold.v0+json'
curl -L -X 'GET' 'http://xrforge.isvery.ninja/models?page=1&order=recent' -H 'accept: application/vnd.manyfold.v0+json'
```
## Permacomputing Spirit
* developed on [Dillo](https://dillo-browser.github.io/) as lowest common denominator / compatibility
* does not use javascript unless really needed (WebXR being the exception)
* server is using lua-scripting via cosmopolitan libc's [redbean](https://redbean.dev)
> So yes in case you'd think the codebase is 90-ish: that's **exactly why it works** on potato-devices AND modern devices.

3
run.sh Executable file
View file

@ -0,0 +1,3 @@
#!/bin/sh
cd src
../result/bin/xrforge.com -D . -- server

View file

@ -3,6 +3,8 @@ package.path = package.path .. ";src/.lua/?.lua"
package.path = package.path .. ";middleware/?.lua"
package.path = package.path .. ";cmd/?.lua"
print = function(...) Log(kLogInfo, ... or "''") end -- nice redbean logging from now on
util = require "util"
json = require "json"
@ -15,24 +17,32 @@ dir = require "opendirectory"
-- create
app = require("soakbean") {
bin = "./xrforge.com",
bindir = arg[-1]:gsub("xrforge.com",""),
cache = {},
opts = {
title="My XRForge experiences",
subtitle="Portable XR experiences for PhoneVR, VRheadset, desktop & mobile."
},
opts = {},
cmd={
help = {file="help.lua", info="displays help cmds"},
server = {file="server.lua", info="starts the webserver"}
},
title = 'XRForge - local-first XR experiences',
subtitle = 'XRForge makes local-first XR easy',
notes = {'🤩 portable selfsovereign', '🖧 JanusXR / XR Fragments', '♻ decentralized'}
help = {file="cmd/help.lua", info="display all possible flags"},
server = {file="cmd/server.lua", info="starts the webserver"}
}
}
-- api skeleton (implemented via soakbean's app.on(...))
app.set = function(k,v) end
app.init = function() end
-- load config
app.opts = require('config')(app)
app.runcmd(app) -- run clicmds
-- init URL router
app.url['^/data'] = '/data.lua' -- setup custom file endpoint
app.url['^/$'] = '/index.lua' -- alias for app.tpl( LoadAsset('index.html'), app )
app.get('^/admin/$', app.template('admin/index.html') ) -- alias for app.tpl( LoadAsset('index.html'), app )
app.url['^/$'] = '/page/index.lua' -- alias for app.tpl( LoadAsset('index.html'), app )
app.get('^/design[/]?$', util.pagerender('page/design.html'))
app.url['^/admin[/]?$'] = '/page/admin/index.lua'
app.post('^/save', function(req,res,next) -- setup inline POST endpoint
-- also .get(), .put(), .delete(), .options()
@ -43,23 +53,22 @@ app.post('^/save', function(req,res,next) -- setup inline POST endpoint
end)
app --
.use( require("httpauth")() )
.use( require("middleware/httpauth")() )
.use( require("json").middleware() ) -- try plug'n'play json API middleware
.use( app.router( app.url ) ) -- try url router
.use( app.response() ) -- try serve app response (if any)
.use( function(req,next) Route() end) -- fallback default redbean fileserver
app.on('template', function(a,b,c,d,e)
print("TEMPLATE!")
end)
function OnHttpRequest() app.run() end
print("ja")
--function OnHttpRequest()
-- if( not dir.serve("./experiences") ) then
-- print("default")
-- Route()
-- end
--end
--
app.on('init', function()
if assert(unix.fork()) == 0 then
os.execute("ls ; sleep 5")
print("ok")
--local ok, headers, body = Fetch('http://127.0.0.1:8080/test')
end
end)
OnServerStart = function()
app.init() -- this triggers init event
end

1
src/.lua/cmd Symbolic link
View file

@ -0,0 +1 @@
../cmd

51
src/.lua/config.lua Normal file
View file

@ -0,0 +1,51 @@
local util = require('util')
return function(app)
local config = { -- default
name = "XRForge",
baseurl="/",
title="My XRForge experiences",
subtitle="Portable XR experiences for Phone, VRheadset & desktop.",
admin_title = "XRForge Admin",
admin_subtitle = "Manage your XR experiences here",
disk_dir = ".",
manyfold_instance_1 = "https://xrforge.isvery.ninja",
manyfold_instance_2 = "",
manyfold_instance_3 = "",
sync_interval = 30,
sync_strategy = "recent"
}
local configfile = "xrforge.lua"
local savedconfig = util.readfile( app.bindir .. configfile, 'r', true) -- check local config
or LoadAsset(configfile) -- then internal saved config
if savedconfig then
local eval = function(str) return load(str)() end
local status, result = pcall(eval, savedconfig ) -- Pass "string" as the first argument
if not status then
print("error: xrforge.lua contains errors:")
else
config = result -- overrule default config
end
end
app.on('set', function(k,v)
if k == 'app.opts' then
app.opts = v
app.opts.updated = os.time()
local newconfig = "app.on('init', function()\n\tprint('config loaded')\nend)\n\nreturn " .. util.dump(app.opts)
newconfig = newconfig:gsub("^}$"," init = function() print('xrforge.lua config loaded') end\n}")
util.writefile( app.bindir .. configfile, newconfig )
print("stored xrforge.lua")
end
end)
-- store local config if not exist
if not util.readfile( app.bindir .. configfile, 'r', true) then
app.set('app.opts', config )
end
return config
end

1
src/.lua/middleware Symbolic link
View file

@ -0,0 +1 @@
../middleware

View file

@ -23,7 +23,7 @@ sb = {
sb.data[k] = (function(k,v)
return function(a,b,c,d,e,f)
v(a,b,c,d,e,f)
sb.pub(k,v)
sb.pub(k,v,a,b,c,d,e,f)
end
end)(k,v)
else
@ -38,10 +38,14 @@ sb = {
return sb.app
end,
pub = function(k,v)
pub = function(k,v,a,b,c,d,e,f)
if sb.handler[k] ~= nil then
for i,handler in pairs(sb.handler[k]) do
coroutine.wrap(handler)(v,k)
if a then --
coroutine.wrap(handler)(a,b,c,d,e,f) -- forward function args
else
coroutine.wrap(handler)(v,k) -- forward assignment
end
end
end
end,
@ -187,13 +191,12 @@ sb = {
end,
init = function(app)
for k,v in pairs(argv) do
app.opts[ v:gsub("=.*","") ] = v:gsub(".*=","")
end
if( keys(app.cmd) > 0 ) then sb.runcmd(app) end
end,
runcmd = function(app)
for k,v in pairs(argv) do
app.opts[ v:gsub("=.*","") ] = v:gsub(".*=","")
end
for k,v in pairs(app.opts) do
if app.cmd[k] then
local file = app.cmd[k].file

View file

@ -1,5 +1,26 @@
local util = { }
util.readfile = function(filename, mode, silent)
local file, err = io.open(filename, mode or "r")
if not file then
if not silent then print("Error opening file:", err) end
return
end
local content = file:read("*a")
file:close()
return content
end
util.writefile = function(filename, content, mode)
local file, err = io.open(filename, mode or "w") -- Default write mode
if not file then
return print("Error opening file:", err)
end
file:write(content)
file:close()
end
util.clone = function(orig)
local orig_type = type(orig)
local copy
@ -28,11 +49,20 @@ util.flatten = function(tbl, prefix, result)
return result
end
util.pagerender = function(file,data)
return function(req,res,next)
res.status(200)
res.header('content-type','text/html; charset=utf-8')
res.body( util.template( LoadAsset(file),data or app) )
next()
end
end
util.template = function(content,data)
local finaldata = util.merge(app,{})
local finaldata = util.merge(data or app,{})
finaldata = util.merge(finaldata, util.flatten(app.opts,"opts") )
finaldata.header = app.tpl( LoadAsset('template/header.html'), finaldata )
finaldata.footer = app.tpl( LoadAsset('template/footer.html'), finaldata )
finaldata.header = app.tpl( LoadAsset('page/header.html'), finaldata )
finaldata.footer = app.tpl( LoadAsset('page/footer.html'), finaldata )
return app.tpl( content, finaldata )
end

File diff suppressed because one or more lines are too long

View file

@ -1,13 +1,13 @@
return function(app,argv)
print("help!")
foreach( app.cmd, function(k,v)
io.write( app.bin .. " " .. k .. "\t\t # " .. v.info .. "\n")
end)
io.write("\nexample: " .. app.bin .. " title='My XRForge experiences'\n\n")
io.write("Flags: (overrides xrforge.lua configfile)\n\n")
foreach( app.opts, function(k,v)
if not k:match("/") and not k:match("^help") then
print( app.bin .. " -- " .. k .. "=...\t # '" .. v .. "' e.g." )
if not k:match("/") and not k:match("^help") and not k:match("updated") then
io.write( " " .. k .. "='" .. v .. "'\n")
end
end)
foreach( app.cmd, function(k,v)
print( app.bin .. " -- " .. k .. "\t\t # " .. v.info )
end)
os.exit()
end

View file

@ -8,7 +8,7 @@
.col2{ color:#AAD; }
.col3{ color:#878; }
.col4{ color:#7070ff; }
.col5{ color:#557; }
.col5{ color:#447; }
.col5{ color:#224; }
.col5{ color:#112; }
@ -40,7 +40,28 @@ td,div,p{
line-height:24px;
}
.btn{
/* form elements */
input,
select{
box-sizing: border-box;
margin: 0px 0px 10px 0px;
padding:7px;
display:block;
max-width:300px;
width:100%;
background: #000;
border: 1px solid #557;
color: #AAD;
}
input[type=checkbox]{
transform:scale(1.7) translate(3px,4px)
}
/* buttons */
.btn,
input[type=submit]{
display: inline-block;
margin: 0px 10px 10px 0px;
}
@ -48,21 +69,28 @@ td,div,p{
.btn a,
.btn a:active,
.btn a:visited,
.btn a:link {
.btn a:link,
input[type=submit] {
font-size:15px;
display: inline-block;
padding:8px 16px;
color: #f3f;
text-decoration:none;
}
.btn a:hover{
.btn a:hover,
input[type=submit] {
color:#F7F;
background:#000;
}
.btn a, .border{ border:1px dotted #AAD; }
.btn a,
.border,
input[type=submit]{
border:1px dotted #AAD;
}
a, a:active, a:visited, a:link {
a, a:active, a:visited, a:link, input[type=submit] {
text-decoration: none;
color:#FFF;
border-bottom: 1px dotted #AAD;
@ -74,6 +102,27 @@ a:hover{
border-bottom:1px dotted #f3f;
}
table{
table-layout: fixed;
}
table tr td {
vertical-align: top;
}
form > table > tbody > tr > td{
height:32px;
}
form > table > tbody > tr > td:nth-child(1) {
font-weight:bold;
padding-top: 4px;
}
form > table > tbody > tr > td:nth-child(3) {
font-size: 13px;
padding-top: 4px;
text-align:right;
}
.border.primary{ border:1px dotted #87a; }
.border.secondary{ border:1px dotted #7070ff; }
@ -123,10 +172,17 @@ ul{
padding-left:0.8em;
}
.menu{
text-align: right;
}
.menu a {
padding-right:30px;
}
.menu br {
display:none
}
.grid .tile{ border-radius:5px; }
.grid .tile .text{ padding:3px 0px; }
.grid .tile .img{
@ -136,6 +192,35 @@ ul{
img.icon{ display:none; }
/* tab */
.tab{
border-bottom: 1px solid #224;
margin-bottom: 25px;
margin-left: 140px;
}
.tab .label{
display: block;
background: #000;
margin-bottom: -4px;
width: 158px;
margin-left: -140px;
border-top: 1px dotted #557;
border-left: 1px dotted #556;
border-right: 1px dotted #556;
padding: 9px 0px 0px 20px;
}
.notification{
background: #7070ff;
padding: 20px;
text-align: center;
font-weight: bold;
border: 4px dashed #222;
margin-bottom: 25px;
}
/* progressive enhancement quirks */
@media (min-width:1px) {
.grid .tile{

View file

@ -29,6 +29,13 @@ body{
width:100%;
margin: 0px 0px 15px 0px;
}
form table tr > th:nth-child(3),
form table tr > td:nth-child(3) {
display:none;
}
.menu br {
display:initial !important;
}
}
.grid .tile .text{
@ -44,8 +51,12 @@ body{
}
.jumbotron{
padding:20px 0px;
padding:20px 20px;
background:linear-gradient(45deg,#0000,#0000,#F7F2,#0000,#07F2,#0000,#0000);
background:url(./../img/backdrop.jpg);
background-size: cover;
background-position: bottom;
background-repeat: no-repeat;
}
img.icon{
@ -55,6 +66,6 @@ img.icon{
width: 17px;
}
.btn a.primary{ background: #ff33Ff; color:#000 !important;}
.btn a.secondary{ background: #7070ff; color:#000 !important;}
.btn .primary{ background: #ff33Ff; color:#000 !important;}
.btn .secondary{ background: #7070ff; color:#000 !important;}

BIN
src/img/backdrop.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

128
src/page/admin/index.html Normal file
View file

@ -0,0 +1,128 @@
${header}
<div style="height:120px;" class="jumbotron">
<h1>${opts.admin_title}</h1>
${opts.admin_subtitle}
</div>
<hr style="margin-top:0">
${notification}
<div class="tab">
<div class="label col2">
Local content
</div>
</div>
<form method="POST">
<div class="margin1">
<table width="100%">
<tr>
<th width="165"/>
<th />
<th />
</tr>
<tr>
<td>
Directory
</td>
<td >
<input type="text" value="${opts.disk_dir}"></input>
</td>
<td class="col3">
Directory to scan for experiences (use '.' for current)
</td>
</tr>
</table>
</div>
<br/>
<br/>
<div class="tab">
<div class="label col2">
Manyfold instances
</div>
</div>
<div class="margin1">
<table width="100%">
<tr>
<th width="165"/>
<th />
<th />
</tr>
<tr>
<td>
Instance URL 1
</td>
<td >
<input type="text" name="opts.manyfold_instance_1" value="${opts.manyfold_instance_1}"></input>
</td>
<td class="col3">
<a href="https://manyfold.app/" target="_blank">Manyfold</a> URL goes here (https://xrforge.isvery.ninja e.g.)
</td>
</tr>
<tr>
<td>
Instance URL 2
</td>
<td>
<input type="text" name="opts.manyfold_instance_2" value="${opts.manyfold_instance_2}"></input>
</td>
<td class="col3">
<a href="https://manyfold.app/" target="_blank">Manyfold</a> URL goes here (https://xrforge.isvery.ninja e.g.)
</td>
</tr>
<tr>
<td>
Instance URL 3
</td>
<td>
<input type="text" name="opts.manyfold_instance_3" value="${opts.manyfold_instance_3}"></input>
</td>
<td class="col3">
<a href="https://manyfold.app/" target="_blank">Manyfold</a> URL goes here (https://xrforge.isvery.ninja e.g.)
</td>
</tr>
<tr>
<td>
Sync strategy
</td>
<td>
<select value="${opts.manyfold_sync_strategy}">
<option value="recent" default>only recent</option>
<option value="all+recent">everything + recent</option>
<option value="disabled">disabled</option>
</select>
</td>
<td class="col3">
Specify how manyfold instances should be synced.
</td>
</tr>
<tr>
<td>
Sync interval
</td>
<td>
<select value="${opts.manyfold_sync_interval}">
<option value="30">every 30 mins</option>
<option value="15" selected>every 15 mins</option>
<option value="5">every 5 minutes</option>
</select>
</td>
<td class="col3">
Specify how often manyfold instances should be checked for recent items.
</td>
</tr>
</table>
<div style="margin-left:165px">
<br/>
<div class="btn">
<input type="submit" class="btn secondary" value="save" />
</div>
</div>
</div>
</form>
${footer}

27
src/page/admin/index.lua Normal file
View file

@ -0,0 +1,27 @@
local util = require("util")
local data = util.merge({notification=''},{ opts = app.opts })
if GetMethod() == 'POST' then
local params = GetParams()
local saveconfig = false
foreach(params, function(k,v)
if v[1]:match("^opts.") then
app.opts[ v[1]:gsub("^opts.","") ] = v[2] -- update
saveconfig = true
end
end)
if saveconfig then
app.set('app.opts', app.opts )
data.notification = '<div class="notification">updated</div>'
else
data.notification = '<div class="notification">no changes detected</div>'
end
end
local html = util.template( LoadAsset("page/admin/index.html"),data)
SetStatus(200)
SetHeader('Content-Type', 'text/html; charset=utf-8')
Write( html )

127
src/page/design.html Normal file
View file

@ -0,0 +1,127 @@
${header}
<h1>HEADER 1</h1>
Lorem ipsum dolor sit amter.<br>
Amet puti solor fati corpus <a href="#foo" >a link</a> flap.<br><br>
<hr>
<div class="btn">
<a href="#foo">button</a>
</div>
<div class="btn">
<a href="#foo" class="btn primary">button primary ⮮</a>
</div>
<div class="btn">
<a href="https://foo.com" target="_blank" class="btn secondary">button secondary ⮩</a>
</div>
<br>
<hr>
<div class="tab">
<div class="label col3">
Category B
</div>
</div>
<div class="margin1">
<form>
<table width="100%">
<tr>
<td width="165">
Instance URL 1
</td>
<td >
<input type="text"/>
</td>
<td class="col3">
Infotext about input
</td>
</tr>
<tr>
<td>
Sync interval
</td>
<td>
<select>
<option value="30">every 30 mins</option>
<option value="15" selected>every 15 mins</option>
<option value="5">every 5 minutes</option>
</select>
</td>
<td class="col3">
Infotext about select (tip: use selectboxes not checkboxes)
</td>
</tr>
</table>
</form>
<ul>
<li>foo bar</li>
<li>flop flap</li>
</ul>
<pre>
</pre>
<pre>
┌─────────────────┐┌────────────────┐
│ Foo CLI ││ Foo Web │
└─────────────────┘└────────────────┘
┌───────────────────────────────────┐
│ Foo Repository │
│ ┌────────┐ ┌────────┐ ┌─────────┐ │
│ │ code │ │ issues │ │ patches │ │
│ └────────┘ └────────┘ └─────────┘ │
├───────────────────────────────────┤
│ Foo Storage (Git) │
└───────────────────────────────────┘
┌────────────────┐┌─────────────────┐
│ Foo Node ││ Foo HTTPD │
├────────────────┤├─────────────────┤
│ NoiseXK ││ HTTP + JSON │
└────────────────┘└─────────────────┘
</pre>
<pre class="primary">
┌─────────────────┐┌────────────────┐
│ Foo CLI ││ Foo Web │
└─────────────────┘└────────────────┘
┌───────────────────────────────────┐
│ Foo Repository │
│ ┌────────┐ ┌────────┐ ┌─────────┐ │
│ │ code │ │ issues │ │ patches │ │
│ └────────┘ └────────┘ └─────────┘ │
├───────────────────────────────────┤
│ Foo Storage (Git) │
└───────────────────────────────────┘
┌────────────────┐┌─────────────────┐
│ Foo Node ││ Foo HTTPD │
├────────────────┤├─────────────────┤
│ NoiseXK ││ HTTP + JSON │
└────────────────┘└─────────────────┘
</pre>
<pre class="secondary">
┌─────────────────┐┌────────────────┐
│ Foo CLI ││ Foo Web │
└─────────────────┘└────────────────┘
┌───────────────────────────────────┐
│ Foo Repository │
│ ┌────────┐ ┌────────┐ ┌─────────┐ │
│ │ code │ │ issues │ │ patches │ │
│ └────────┘ └────────┘ └─────────┘ │
├───────────────────────────────────┤
│ Foo Storage (Git) │
└───────────────────────────────────┘
┌────────────────┐┌─────────────────┐
│ Foo Node ││ Foo HTTPD │
├────────────────┤├─────────────────┤
│ NoiseXK ││ HTTP + JSON │
└────────────────┘└─────────────────┘
</pre>
</div>
${footer}

View file

@ -3,25 +3,28 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<base href="${opts.baseurl}" />
<title>XRForge - selfsovereign interoperable XR experiences</title>
<link rel="stylesheet" href="xrforge.css"></link>
<link rel="stylesheet" href="css/xrforge.css"></link>
</head>
<body background="#000000">
<table width="100%" class="header">
<tr>
<td style="width:120px">
<td width="100">
<div class="margin1L">
<div class="logo col1">XR<div class="logo col2">f̅o̅r̅g̅e̅</div>
</div>
<a href="${opts.baseurl}">
<div class="logo col1">XR<div class="logo col2">f̅o̅r̅g̅e̅</div>
</a>
</div>
</td>
<td class="menu">
<div class="margin1L">
<a href="#">Experiences</a>
<a href="#">Collections</a>
<a href="#">About</a>
<a href="#">Howto</a>
<a href="#">Experiences</a><br/>
<a href="#">Collections</a><br/>
<a href="#">About</a><br/>
<a href="#">Howto</a><br/>
</div>
</td>
</tr>

View file

@ -4,7 +4,7 @@ if not app.cache['index.html'] then
app.cache['index.html'] = util.memoize(
function()
print("processing!")
local html = util.template( LoadAsset("template/index.html"),app)
local html = util.template( LoadAsset("page/index.html"),app)
return html
end,
60*5 -- re-run every 5 mins (otherwise return cache)

View file

@ -1,2 +0,0 @@
#!/bin/sh
../result/bin/xrforge.com -D . -p 8081 -- server

File diff suppressed because one or more lines are too long

1
src/xrforge.lua Symbolic link
View file

@ -0,0 +1 @@
../result/bin/xrforge.lua

View file

@ -25,7 +25,7 @@ download(){
build(){
download redbean
cd src
zip -r ../result/bin/xrforge.com * .lua .init.lua
zip -r ../result/bin/xrforge.com * .lua .init.lua .args
}
"$@"