diff --git a/webinstall/cibuild.css b/webinstall/cibuild.css
new file mode 100644
index 0000000..b5164fc
--- /dev/null
+++ b/webinstall/cibuild.css
@@ -0,0 +1,3 @@
+.hidden{
+ display: none;
+}
\ No newline at end of file
diff --git a/webinstall/cibuild.html b/webinstall/cibuild.html
new file mode 100644
index 0000000..d1de53a
--- /dev/null
+++ b/webinstall/cibuild.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+ Build your own ESP32-NMEA2000
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/webinstall/cibuild.js b/webinstall/cibuild.js
new file mode 100644
index 0000000..5627f32
--- /dev/null
+++ b/webinstall/cibuild.js
@@ -0,0 +1,98 @@
+import { setButtons,fillValues, setValue, buildUrl, fetchJson, setVisible, enableEl } from "./helper";
+(function(){
+ const STATUS_INTERVAL=2000;
+ const CURRENT_PIPELINE='pipeline';
+ let API="cibuild.php";
+ let currentPipeline=undefined;
+ let downloadUrl=undefined;
+ let timer=undefined;
+ const fetchStatus=()=>{
+ if (currentPipeline === undefined) return;
+ fetchJson(API,{api:'status',pipeline:currentPipeline})
+ .then((st)=>{
+ setValue('status',st.status);
+ let l=document.getElementById('link');
+ if (l){
+ if (st.status_url){
+ l.setAttribute('href',st.status_url);
+ setVisible(l.parentElement,true);
+ }
+ else{
+ setVisible(l.parentElement,false);
+ }
+ }
+ if (st.status === 'success'){
+ enableEl('start',true);
+ fetchJson(API,{api:'artifacts',pipeline:currentPipeline})
+ .then((ar)=>{
+ if (! ar.items || ar.items.length < 1){
+ throw new Error("no download link");
+ }
+ downloadUrl=ar.items[0].url;
+ setVisible(document.getElementById('download'),true,true);
+
+ })
+ .catch((err)=>alert("Unable to get build result: "+err));
+ return;
+ }
+ else{
+ setVisible(document.getElementById('download'),false,true);
+ }
+ timer=window.setTimeout(fetchStatus,STATUS_INTERVAL)
+ })
+ .catch((e)=>{
+ timer=window.setTimeout(fetchStatus,STATUS_INTERVAL);
+ })
+ }
+ const setCurrentPipeline=(pipeline)=>{
+ currentPipeline=pipeline;
+ window.localStorage.setItem(CURRENT_PIPELINE,pipeline);
+ };
+ const startBuild=()=>{
+ let param={};
+ currentPipeline=undefined;
+ if (timer) window.clearTimeout(timer);
+ timer=undefined;
+ fillValues(param,['environment','buildflags']);
+ setValue('status','requested');
+ fetchJson(API,Object.assign({
+ api:'start'},param))
+ .then((json)=>{
+ if (json.status === 'error'){
+ throw new Error("unable to create job "+(json.error||''));
+ }
+ if (!json.id) throw new Error("unable to create job, no id");
+ setCurrentPipeline(json.id);
+ setValue('pipeline',currentPipeline);
+ setValue('status',json.status);
+ enableEl('start',false);
+ timer=window.setTimeout(fetchStatus,STATUS_INTERVAL);
+ })
+ .catch((err)=>{
+ setValue('status','error');
+ enableEl('start',true);
+ alert(err);
+ });
+ }
+ const runDownload=()=>{
+ if (! downloadUrl) return;
+ let df=document.getElementById('dlframe');
+ if (df){
+ df.setAttribute('src',null);
+ df.setAttribute('src',downloadUrl);
+ }
+ }
+ const btConfig={
+ start:startBuild,
+ download:runDownload
+ };
+ window.onload=()=>{
+ setButtons(btConfig);
+ currentPipeline=window.localStorage.getItem(CURRENT_PIPELINE);
+ if (currentPipeline){
+ setValue('pipeline',currentPipeline);
+ enableEl('start',false);
+ fetchStatus();
+ }
+ }
+})();
\ No newline at end of file
diff --git a/webinstall/helper.js b/webinstall/helper.js
new file mode 100644
index 0000000..82c3085
--- /dev/null
+++ b/webinstall/helper.js
@@ -0,0 +1,96 @@
+const getParam = (key) => {
+ let value = RegExp("" + key + "[^&]+").exec(window.location.search);
+ // Return the unescaped value minus everything starting from the equals sign or an empty string
+ return decodeURIComponent(!!value ? value.toString().replace(/^[^=]+./, "") : "");
+};
+/**
+ * add an HTML element
+ * @param {*} type
+ * @param {*} clazz
+ * @param {*} parent
+ * @param {*} text
+ * @returns
+ */
+const addEl = (type, clazz, parent, text) => {
+ let el = document.createElement(type);
+ if (clazz) {
+ if (!(clazz instanceof Array)) {
+ clazz = clazz.split(/ */);
+ }
+ clazz.forEach(function (ce) {
+ el.classList.add(ce);
+ });
+ }
+ if (text) el.textContent = text;
+ if (parent) parent.appendChild(el);
+ return el;
+}
+/**
+ * call a function for each matching element
+ * @param {*} selector
+ * @param {*} cb
+ */
+const forEachEl = (selector, cb) => {
+ let arr = document.querySelectorAll(selector);
+ for (let i = 0; i < arr.length; i++) {
+ cb(arr[i]);
+ }
+}
+
+const setButtons=(config)=>{
+ for (let k in config){
+ let bt=document.getElementById(k);
+ if (bt){
+ bt.addEventListener('click',config[k]);
+ }
+ }
+}
+const fillValues=(values,items)=>{
+ items.forEach((it)=>{
+ let e=document.getElementById(it);
+ if (e){
+ values[it]=e.value; //TODO: type of el
+ }
+ })
+};
+const setValue=(id,value)=>{
+ let el=document.getElementById(id);
+ if (! el) return;
+ if (el.tagName == 'DIV'){
+ el.textContent=value;
+ return;
+ }
+ if (el.tagName == 'INPUT'){
+ el.value=value;
+ }
+}
+const buildUrl=(url,pars)=>{
+ let delim=(url.match("[?]"))?"&":"?";
+ for (let k in pars){
+ url+=delim;
+ delim="&";
+ url+=encodeURIComponent(k);
+ url+="=";
+ url+=encodeURIComponent(pars[k]);
+ }
+ return url;
+}
+const fetchJson=(url,pars)=>{
+ let furl=buildUrl(url,pars);
+ return fetch(furl).then((rs)=>rs.json());
+}
+const setVisible=(el,vis,useParent)=>{
+ if (! el) return;
+ if (useParent) el=el.parentElement;
+ if (! el) return;
+ if (vis) el.classList.remove('hidden');
+ else el.classList.add('hidden');
+}
+const enableEl=(id,en)=>{
+ let el=document.getElementById(id);
+ if (!el) return;
+ if (en) el.disabled=false;
+ else el.disabled=true;
+}
+
+export { getParam, addEl, forEachEl,setButtons,fillValues, setValue,buildUrl,fetchJson,setVisible, enableEl }
\ No newline at end of file
diff --git a/webinstall/install.html b/webinstall/install.html
index e578a82..414d431 100644
--- a/webinstall/install.html
+++ b/webinstall/install.html
@@ -9,9 +9,9 @@
-
+