enhance build + add instances vue.js page

* remove previous useless assets for every build
* reproduce instances data to easily test it on Vite
* add instances page to build
* update instances builder to fit new components : list pairs and form data
* form submit working on instances actions
This commit is contained in:
Jordan Blasenhauer 2024-07-03 15:31:10 +02:00
parent 0c3b66899f
commit a3514d2bb2
34 changed files with 161 additions and 411 deletions

View file

@ -97,7 +97,7 @@ async function setBuildTempToUI() {
fs.readdir(resolve("./templates"), (err, files) => {
// Read content
files.forEach((file) => {
const data = fs.readFile(
fs.readFile(
resolve(`./templates/${file}`),
{
encoding: "utf8",
@ -109,7 +109,8 @@ async function setBuildTempToUI() {
}
let updateData = data;
// I want to remove the first "/" for href="/css", href="/js", href="/img", href="/favicon", src="/assets" or src="/js"
const regexPaths = /href="\/(css|js|img|favicon)|src="\/(assets|js)/g;
const regexPaths =
/href="\/(css|js|img|favicon|assets|js)|src="\/(assets|js)/g;
updateData = data.replace(regexPaths, (match) => {
return match.replace("/", "");
});
@ -163,8 +164,17 @@ async function moveBuildStaticToUI() {
});
}
async function delPrevDirs() {
// Delete prev existing dir
delElRecursive(`${appStaticDir}/css`);
delElRecursive(`${appStaticDir}/assets`);
delElRecursive(`${appStaticDir}/flags`);
delElRecursive(`${appStaticDir}/img`);
}
async function build() {
// Build client and setup
await delPrevDirs();
await buildVite();
await updateClientDir();
await setBuildTempToUI();

View file

@ -1,4 +1,6 @@
stats = [
import json
home = [
{
"type": "card",
"link": "https://panel.bunkerweb.io/?utm_campaign=self&utm_source=ui#pro",
@ -85,3 +87,7 @@ stats = [
],
},
]
# store on a file
with open("home.json", "w") as f:
json.dump(home, f, indent=4)

View file

@ -0,0 +1 @@
[{"type": "card", "containerColumns": {"pc": 6, "tablet": 6, "mobile": 12}, "widgets": [{"type": "Instance", "data": {"pairs": [{"key": "instances_hostname", "value": "bunkerweb"}, {"key": "instances_type", "value": "manual"}, {"key": "instances_status", "value": "instances_active"}], "status": "success", "title": "bunkerweb", "buttons": [{"attrs": {"data-submit-form": "{\"INSTANCE_ID\" : \"bunkerweb\", \"operation\" : \"reload\" }"}, "text": "action_reload", "color": "warning"}, {"attrs": {"data-submit-form": "{\"INSTANCE_ID\" : \"bunkerweb\", \"operation\" : \"stop\" }"}, "text": "action_stop", "color": "error"}]}}]}, {"type": "card", "containerColumns": {"pc": 6, "tablet": 6, "mobile": 12}, "widgets": [{"type": "Instance", "data": {"pairs": [{"key": "instances_hostname", "value": "bunkerweb"}, {"key": "instances_type", "value": "manual"}, {"key": "instances_status", "value": "instances_active"}], "status": "success", "title": "bunkerweb", "buttons": [{"attrs": {"data-submit-form": "{\"INSTANCE_ID\" : \"bunkerweb\", \"operation\" : \"reload\" }"}, "text": "action_reload", "color": "warning"}, {"attrs": {"data-submit-form": "{\"INSTANCE_ID\" : \"bunkerweb\", \"operation\" : \"stop\" }"}, "text": "action_stop", "color": "error"}]}}]}]

View file

@ -0,0 +1,77 @@
import json
# Create instance class using keys from the instances list
class Instance:
def __init__(self, _type, health, _id, hostname, name):
self._type = _type
self.health = health
self._id = _id
self.hostname = hostname
self.name = name
instances = [
Instance("manual", True, "bunkerweb", "bunkerweb", "bunkerweb"),
Instance("manual", True, "bunkerweb", "bunkerweb", "bunkerweb"),
]
def instances_builder(instances: list):
"""
It returns the home page in JSON format for the Vue.js builder
"""
builder = []
for instance in instances:
# setup actions buttons
actions = (
["restart", "stop"]
if instance._type == "local" and instance.health
else (
["reload", "stop"]
if not instance._type == "local" and instance.health
else ["start"] if instance._type == "local" and not instance.health else []
)
)
buttons = [
{
"attrs": {
"data-submit-form": f"""{{"INSTANCE_ID" : "{instance._id}", "operation" : "{action}" }}""",
},
"text": f"action_{action}",
"color": "success" if action == "start" else "error" if action == "stop" else "warning",
}
for action in actions
]
component = {
"type": "card",
"containerColumns": {"pc": 6, "tablet": 6, "mobile": 12},
"widgets": [
{
"type": "Instance",
"data": {
"pairs": [
{"key": "instances_hostname", "value": instance.hostname},
{"key": "instances_type", "value": instance._type},
{"key": "instances_status", "value": "instances_active" if instance.health else "instances_inactive"},
],
"status": "success" if instance.health else "error",
"title": instance.name,
"buttons": buttons,
},
}
],
}
builder.append(component)
return builder
builder = instances_builder(instances)
# store on a file
with open("instances.json", "w") as f:
json.dump(builder, f)

View file

@ -14,7 +14,7 @@
data-server-flash='[{"type" : "success", "title" : "title", "message" : "Success feedback"}, {"type" : "error", "title" : "title", "message" : "Error feedback"}, {"type" : "warning", "title" : "title", "message" : "Warning feedback"}, {"type" : "info", "title" : "title", "message" : "Info feedback"}]'>
</div>
<div class="hidden"
data-server-builder='[{"type":"card","containerColumns":{"pc":6,"tablet":6,"mobile":12},"widgets":[{"type":"Instance","data":{"pairs":[{"key":"instances_hostname","value":"bunkerweb"},{"key":"instances_type","value":"manual"},{"key":"instances_status","value":"instances_active"}],"status":"success","title":"bunkerweb","buttons":[{"attrs":{"data-submit-form": {"operation" : "reload", "INSTANCE_ID" : "bunkerweb"} },"text":"action_reload","color":"warning","size":"normal"},{"attrs":{"data-submit-form": { "operation" : "stop", "INSTANCE_ID" : "bunkerweb"} },"text":"action_stop","color":"error","size":"normal"}]}}]}]'>
data-server-builder='[{"type": "card", "containerColumns": {"pc": 6, "tablet": 6, "mobile": 12}, "widgets": [{"type": "Instance", "data": {"pairs": [{"key": "instances_hostname", "value": "bunkerweb"}, {"key": "instances_type", "value": "manual"}, {"key": "instances_status", "value": "instances_active"}], "status": "success", "title": "bunkerweb", "buttons": [{"attrs": {"data-submit-form": "{\"INSTANCE_ID\" : \"bunkerweb\", \"operation\" : \"reload\" }"}, "text": "action_reload", "color": "warning"}, {"attrs": {"data-submit-form": "{\"INSTANCE_ID\" : \"bunkerweb\", \"operation\" : \"stop\" }"}, "text": "action_stop", "color": "error"}]}}]}, {"type": "card", "containerColumns": {"pc": 6, "tablet": 6, "mobile": 12}, "widgets": [{"type": "Instance", "data": {"pairs": [{"key": "instances_hostname", "value": "bunkerweb"}, {"key": "instances_type", "value": "manual"}, {"key": "instances_status", "value": "instances_active"}], "status": "success", "title": "bunkerweb", "buttons": [{"attrs": {"data-submit-form": "{\"INSTANCE_ID\" : \"bunkerweb\", \"operation\" : \"reload\" }"}, "text": "action_reload", "color": "warning"}, {"attrs": {"data-submit-form": "{\"INSTANCE_ID\" : \"bunkerweb\", \"operation\" : \"stop\" }"}, "text": "action_stop", "color": "error"}]}}]}]'>
</div>
<div id="app"></div>
<script type="module" src="instances.js"></script>

View file

@ -52,7 +52,7 @@ function useSubmitForm(data) {
input.value = data[key];
form.appendChild(input);
}
// Append the form to the body and submit it
// Append to be able to submit
document.querySelector("body").appendChild(form);
form.submit();
}

View file

@ -36,6 +36,7 @@ export default defineConfig({
rollupOptions: {
input: {
home: resolve(__dirname, "./src/pages/home/index.html"),
instances: resolve(__dirname, "./src/pages/instances/index.html"),
},
},
},

View file

@ -1043,10 +1043,11 @@ def instances_builder(instances: list):
)
buttons = [
{
"attrs": {"data-form-INSTANCE_ID": instance._id, "data-form-operation": action, "data-submit-form": "true"},
"attrs": {
"data-submit-form": f"""{{"INSTANCE_ID" : "{instance._id}", "operation" : "{action}" }}""",
},
"text": f"action_{action}",
"color": "success" if action == "start" else "error" if action == "stop" else "warning",
"size": "normal",
}
for action in actions
]
@ -1058,7 +1059,7 @@ def instances_builder(instances: list):
{
"type": "Instance",
"data": {
"details": [
"pairs": [
{"key": "instances_hostname", "value": instance.hostname},
{"key": "instances_type", "value": instance._type},
{"key": "instances_status", "value": "instances_active" if instance.health else "instances_inactive"},
@ -1116,8 +1117,9 @@ def instances():
# Display instances
instances = app.config["INSTANCES"].get_instances()
data_server_builder = instances_builder(instances)
return render_template("instances.html", title="Instances", data_server_builder=data_server_builder, instances=instances, username=current_user.get_id())
return render_template("instances.html", title="Instances", data_server_builder=json.dumps(data_server_builder))
@app.route("/services", methods=["GET", "POST"])

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
import{c as v,r as q,a as _,o as x,b as a,d as u,w as n,e as b,t as m,n as S,f as y,g as o,h as d,m as p,i as $,_ as h,F as f,j as N,k as g,l as B,p as E,q as k,s as A,u as V,v as j,x as z,y as I,z as L,A as O,B as P}from"./Title-9ae7a316.js";const w={__name:"Subtitle",props:{subtitle:{type:String,required:!0},type:{type:String,required:!1,default:"card"},tag:{type:String,required:!1,default:""},color:{type:String,required:!1,default:""},bold:{type:Boolean,required:!1,default:!1},uppercase:{type:Boolean,required:!1,default:!1},subtitleClass:{type:String,required:!1,default:""}},setup(i){const e=i,s=v(()=>e.tag?e.tag:"p"),r=q({class:""}),t=_(null);return x(()=>{r.class=e.subtitleClass||t.value.closest("[data-is]")?`subtitle-${t.value.closest("[data-is]").getAttribute("data-is")}`:"subtitle-card"}),(l,c)=>e.subtitle?(a(),u(y(s.value),{key:0,ref_key:"subtitleEl",ref:t,"data-subtitle":"",class:S([r.class,e.color,"text-el",e.bold?"bold":"",e.uppercase?"uppercase":""])},{default:n(()=>[b(m(l.$t(e.subtitle,l.$t("dashboard_placeholder",e.subtitle))),1)]),_:1},8,["class"])):o("",!0)}},D={key:1,class:S(["flex justify-center items-center"])},F={__name:"Text",props:{text:{type:[String,Number],required:!0},textClass:{type:String,required:!1,default:""},color:{type:String,required:!1,default:""},bold:{type:Boolean,required:!1,default:!1},uppercase:{type:Boolean,required:!1,default:!1},tag:{type:String,required:!1,default:"p"},icon:{type:[Boolean,Object],required:!1,default:!1},attrs:{type:Object,required:!1,default:{}}},setup(i){const e=i,s=q({class:""}),r=_(null),t=_(null);return x(()=>{const l=r.value||t.value||null,c=l.closest('[data-is="void"]')?"void":"";s.class=e.textClass||l.closest("[data-is]")?`text-${c||l.closest("[data-is]").getAttribute("data-is")}`:"text-card"}),(l,c)=>(a(),d(f,null,[e.icon?o("",!0):(a(),u(y(e.tag),p({key:0},e.attrs,{ref_key:"textEl",ref:r,class:[s.class,e.color,"text-el",e.bold?"bold":"",e.uppercase?"uppercase":""]}),{default:n(()=>[b(m(l.$t(e.text,l.$t("dashboard_placeholder",e.text))),1)]),_:1},16,["class"])),e.icon?(a(),d("div",D,[e.icon?(a(),u(h,$(p({key:0},e.icon)),null,16)):o("",!0),(a(),u(y(e.tag),p({ref_key:"textIconEl",ref:t},e.attrs,{class:[s.class,e.color,"text-el","ml-2"]}),{default:n(()=>[b(m(l.$t(e.text,l.$t("dashboard_placeholder",e.text))),1)]),_:1},16,["class"]))])):o("",!0)],64))}},H={__name:"Stat",props:{title:{type:String,required:!0},stat:{type:[String,Number],required:!0},subtitle:{type:String,required:!1,default:""},iconName:{type:String,required:!1,default:""},color:{type:String,required:!1,default:""},subtitleColor:{type:String,required:!1,default:"info"},statClass:{type:String,required:!1,default:""}},setup(i){const e=i;return(s,r)=>(a(),u(E,{"data-is":"stat",columns:{pc:12,tablet:12,mobile:12}},{default:n(()=>[N("div",{class:S(["stat-content-container",e.iconName?"is-icon":"no-icon"])},[g(B,{tag:"h3",title:e.title},null,8,["title"]),g(F,{text:e.stat},null,8,["text"]),e.subtitle?(a(),u(w,{key:0,subtitle:e.subtitle,color:e.subtitleColor},null,8,["subtitle","color"])):o("",!0)],2),e.iconName?(a(),u(h,{key:0,iconName:e.iconName,isStick:!0},null,8,["iconName"])):o("",!0)]),_:1}))}},M={__name:"Home",props:{builder:{type:Array,required:!0}},setup(i){const e=i;return(s,r)=>(a(!0),d(f,null,k(e.builder,(t,l)=>(a(),u(V,{key:l,gridLayoutClass:t.containerClass,type:t.type,title:t.title,link:t.link,columns:t.containerColumns,id:t.id},{default:n(()=>[g(A,null,{default:n(()=>[(a(!0),d(f,null,k(t.widgets,(c,C)=>(a(),d(f,{key:C},[c.type==="Stat"?(a(),u(H,$(p({key:0},c.data)),null,16)):o("",!0)],64))),128))]),_:2},1024)]),_:2},1032,["gridLayoutClass","type","title","link","columns","id"]))),128))}},T={__name:"Home",setup(i){const e=q({builder:""});return j(()=>{const s="data-server-builder",r=document.querySelector(`[${s}]`),t=r&&!r.getAttribute(s).includes(s)?JSON.parse(r.getAttribute(s)):{};e.builder=t}),x(()=>{z()}),(s,r)=>(a(),u(I,null,{default:n(()=>[e.builder?(a(),u(M,{key:0,builder:e.builder},null,8,["builder"])):o("",!0)]),_:1}))}},G=L();O(T).use(G).use(P(["dashboard","action","inp","icons","home"])).mount("#app");

View file

@ -0,0 +1 @@
import{r as h,c as $,v as k,C as v,b as r,h as n,j as _,n as p,t as g,F as d,q as m,g as b,a as S,o as q,d as i,m as C,D as A,w as f,k as c,l as B,p as E,s as w,i as I,u as N,x as P,y as D,z as F,A as L,B as O}from"./Title-9ae7a316.js";const x=["aria-labelledby"],z=["id"],V={__name:"Status",props:{id:{type:String,required:!1,default:""},status:{type:String,required:!0,default:"info"},statusClass:{type:String,required:!1,default:""}},setup(a){const t=a,s=h({id:""}),e=$(()=>{if(t.status==="success")return"dashboard_status_success";if(t.status==="error")return"dashboard_status_error";if(t.status==="warning")return"dashboard_status_warning";if(t.status==="info")return"dashboard_status_info"});return k(()=>{s.id=v(t.id)}),(u,l)=>(r(),n("div",{class:p([t.statusClass,"status-svg-container"])},[_("div",{role:"img","aria-labelledby":`status-${s.id}`,class:p([t.status,"status-icon"])},null,10,x),_("p",{id:`status-${s.id}`,class:"sr-only"},g(u.$t(e.value)),9,z)],2))}},j={key:0,"data-is":"list-pairs",class:p(["list-pairs-container"])},G={class:"list-pairs-title"},J={class:"list-pairs-subtitle"},M={__name:"Pairs",props:{pairs:{type:Array,required:!0},columns:{type:Object,required:!1,default:{pc:12,tablet:12,mobile:12}}},setup(a){const t=a,s=$(()=>`col-span-${t.columns.mobile} md:col-span-${t.columns.tablet} lg:col-span-${t.columns.pc}`);return(e,u)=>t.pairs?(r(),n("ul",j,[(r(!0),n(d,null,m(t.pairs,l=>(r(),n("li",{class:p(["list-pairs-item",s.value])},[_("span",G,g(e.$t(l.key,e.$t("dashboard_placeholder",l.key))),1),_("span",J,g(e.$t(l.value,e.$t("dashboard_placeholder",l.value))),1)],2))),256))])):b("",!0)}},T={__name:"ButtonGroup",props:{buttons:{type:Array,required:!0,default:[]},groupClass:{type:String,required:!1,default:""}},setup(a){const t=a,s=h({class:""}),e=S(null);return q(()=>{s.class=t.groupClass||e.value.closest("[data-is]")?`button-group-${e.value.closest("[data-is]").getAttribute("data-is")}`:"button-group-default"}),(u,l)=>t.buttons.length>0?(r(),n("div",{key:0,ref_key:"groupEl",ref:e,class:p([s.class,t.groupClass])},[(r(!0),n(d,null,m(t.buttons,(o,y)=>(r(),i(A,C({key:o},o,{class:[y===t.buttons.length-1?"":"mr-2"]}),null,16,["class"]))),128))],2)):b("",!0)}},U={__name:"Instance",props:{title:{type:String,required:!0},status:{type:String,required:!0,default:""},pairs:{type:Array,required:!0,default:[]},buttons:{type:Array,required:!0,default:[]}},setup(a){const t=a;return(s,e)=>(r(),i(E,{"data-is":"instance",columns:{pc:12,tablet:12,mobile:12}},{default:f(()=>[c(V,{id:t.title,status:t.status},null,8,["id","status"]),c(B,{type:"card",title:t.title},null,8,["title"]),c(M,{pairs:t.pairs},null,8,["pairs"]),c(T,{buttons:t.buttons},null,8,["buttons"])]),_:1}))}},H={__name:"Instances",props:{builder:{type:Array,required:!0}},setup(a){const t=a;return(s,e)=>(r(!0),n(d,null,m(t.builder,(u,l)=>(r(),i(N,{key:l,gridLayoutClass:u.containerClass,type:u.type,title:u.title,link:u.link,columns:u.containerColumns,id:u.id},{default:f(()=>[c(w,null,{default:f(()=>[(r(!0),n(d,null,m(u.widgets,(o,y)=>(r(),n(d,{key:y},[o.type==="Instance"?(r(),i(U,I(C({key:0},o.data)),null,16)):b("",!0)],64))),128))]),_:2},1024)]),_:2},1032,["gridLayoutClass","type","title","link","columns","id"]))),128))}};function K(){window.addEventListener("click",a=>{if(a.target.hasAttribute("data-submit-form"))try{const t=JSON.parse(a.target.getAttribute("data-submit-form"));Q(t)}catch(t){console.log(t)}})}function Q(a){const t=document.createElement("form");t.style.display="none",t.method="POST";try{const s=document.querySelector("[data-csrf-token]");s&&(a.csrf_token=s.getAttribute("data-csrf-token"))}catch{}for(const s in a){const e=document.createElement("input");e.type="hidden",e.name=s,e.value=a[s],t.appendChild(e)}document.querySelector("body").appendChild(t),t.submit()}const R={__name:"Instances",setup(a){const t=h({builder:""});return k(()=>{const s="data-server-builder",e=document.querySelector(`[${s}]`),u=e&&!e.getAttribute(s).includes(s)?JSON.parse(e.getAttribute(s)):{};t.builder=u}),q(()=>{P(),K()}),(s,e)=>(r(),i(D,null,{default:f(()=>[t.builder?(r(),i(H,{key:0,builder:t.builder},null,8,["builder"])):b("",!0)]),_:1}))}},W=F();L(R).use(W).use(O(["dashboard","action","inp","icons","instances"])).mount("#app");

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -7,7 +7,8 @@
<link rel="stylesheet" href="css/flag-icons.min.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BunkerWeb | DASHBOARD</title>
<script nonce="{{ script_nonce }}" type="module" crossorigin src="assets/home-7b366ea2.js"></script>
<script nonce="{{ script_nonce }}" type="module" crossorigin src="assets/home-ef55bffd.js"></script>
<link rel="modulepreload" crossorigin href="assets/Title-9ae7a316.js">
</head>
<body>
<div class="hidden" data-csrf-token="{{ csrf_token() }}"></div>

View file

@ -1,74 +1,24 @@
{% extends "base.html" %}
{% block content %}
{% set attribute_name = "instances" %}
{% if instances|length == 0 %}
<div class="col-span-12 sm:col-span-4 sm:col-start-5">
<div class="text-center relative w-full p-4 text-white bg-blue-500 rounded-lg">No instance to show</div>
</div>
{% else %}
{% for instances_batched in instances|batch(2) %}
{% for instance in
instances_batched %}
<!-- instance card -->
<div class="overflow-hidden max-h-none sm:max-h- hover:scale-102 transition col-span-12 lg:col-span-6 3xl:col-span-4 flex p-4 justify-between w-full shadow-md break-words bg-white dark:bg-slate-850 dark:brightness-110 dark:shadow-dark-xl rounded-2xl bg-clip-border">
<form class="w-full" id="form-instance-{{ instance._id }}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="INSTANCE_ID" value="{{ instance._id }}" />
<!-- state and title-->
<div class="flex justify-start items-start overflow-hidden">
{% if instance.health %}<div class="min-w-4 mr-2 mt-2 h-4 w-4 rounded-full bg-green-500"></div>{% endif %}
{% if not instance.health %}<div class="min-w-4 mr-2 mt-2 h-4 w-4 rounded-full bg-red-500"></div>{% endif %}
<h5 class="break-words font-bold dark:text-white/90 transition duration-300 ease-in-out">{{ instance.name }}</h5>
</div>
<!-- end state and title-->
<!-- detail list -->
<div role="grid" class="card-detail-container">
{% set instance_details = [{"name" : "TYPE", "value" : instance['_type']},{"name" : "HOSTNAME", "value" : instance['hostname']}] %}
<!-- detail -->
{% for detail in instance_details %}
<div role="row" class="card-detail-item">
<p role="gridcell" class="card-detail-item-title">{{ detail['name'] }}</p>
<p role="gridcell" class="card-detail-item-subtitle">{{ detail['value'] }}</p>
</div>
{% endfor %}
<!-- end detail -->
</div>
<!-- end detail list-->
<!-- button list-->
<div class="relative w-full flex justify-center sm:justify-end">
{% if instance._type == "local" and instance.health %}
<button type="submit"
name="operation"
value="restart"
class="edit-btn mx-1 text-xs">Restart</button>
<button type="submit"
name="operation"
value="stop"
class="delete-btn mx-1 text-xs">Stop</button>
{% endif %}
{% if not instance._type == "local" and instance.health %}
<button type="submit"
name="operation"
value="reload"
class="edit-btn mx-1 text-xs">Reload</button>
<button type="submit"
name="operation"
value="stop"
class="delete-btn mx-1 text-xs">Stop</button>
{% endif %}
{% if instance._type == "local" and not instance.health or not
instance._type == "local" and not instance.health %}
<button type="submit"
name="operation"
value="start"
class="valid-btn mx-1 text-xs">Start</button>
{% endif %}
</div>
<!-- end button list-->
</form>
</div>
<!-- end instance card -->
{% endfor %}
{% endfor %}
{% endif %}
{% endblock content %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="img/favicon.ico" />
<link rel="stylesheet" href="css/style.css" />
<link rel="stylesheet" href="css/flag-icons.min.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BunkerWeb | DASHBOARD</title>
<script nonce="{{ script_nonce }}" type="module" crossorigin src="assets/instances-67da6d8b.js"></script>
<link rel="modulepreload" crossorigin href="assets/Title-9ae7a316.js">
</head>
<body>
<div class="hidden" data-csrf-token="{{ csrf_token() }}"></div>
<div class="hidden" data-server-global="{{data_server_global if data_server_global else {}}}"></div>
<div class="hidden" data-server-flash="{{data_server_flash if data_server_flash else []}}"></div>
<div class="hidden" data-server-builder="{{data_server_builder}}"></div>
<div id="app"></div>
</body>
</html>

File diff suppressed because one or more lines are too long