Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/backend/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,25 @@ async function deleteMaps(document: DfContentMap, ADMIN_LIST: string[]) {
return deleteResult;
}

export { addMaps, checkUser, deleteMaps, getMaps };
// Update `status` and `build_logs`
async function updateMapStatus(subdomain: string, status: string, logs: string = "") {
const query = {
collection: "content_maps",
database: DATABASE,
dataSource: DATA_SOURCE,
filter: { "subdomain": subdomain },
update: {
$set: {
"status": status,
"build_logs": logs
},
},
};

options.body = JSON.stringify(query);
const resp = await fetch(MONGO_URLs.update.toString(), options);
const data = await resp.json();
return data;
}

export { addMaps, checkUser, deleteMaps, getMaps, updateMapStatus };
34 changes: 32 additions & 2 deletions src/backend/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Context, Sentry } from "./dependencies.ts";
import { addScript, deleteScript } from "./scripts.ts";
import { checkJWT } from "./utils/jwt.ts";
import { addMaps, deleteMaps, getMaps } from "./db.ts";
import { addMaps, deleteMaps, getMaps, updateMapStatus } from "./db.ts";

const ADMIN_LIST = Deno.env.get("ADMIN_LIST")?.split("|");

Expand Down Expand Up @@ -97,4 +97,34 @@ async function deleteSubdomain(ctx: Context) {
ctx.response.body = data;
}

export { addSubdomain, deleteSubdomain, getSubdomains };
async function updateBuildStatus(ctx: Context) {
if (!ctx.request.hasBody) {
ctx.throw(415);
}
const body = await ctx.request.body().value;
let payload;
try {
payload = JSON.parse(body);
} catch (e) {
payload = body;
}

// Expect 'status' in the payload now
const { subdomain, status, logs } = payload;

if (status === "failed") {
// Log Error to Sentry
Sentry.captureMessage(`Build Failed for ${subdomain}: ${logs}`, "error");
} else if (status === "success") {
// Log Info to Sentry (Optional but good for tracking)
Sentry.captureMessage(`Build Success for ${subdomain}`, "info");
}

// Update DB with whatever status came in ("success" or "failed")
await updateMapStatus(subdomain, status, logs);

ctx.response.status = 200;
ctx.response.body = { received: true };
}

export { addSubdomain, deleteSubdomain, getSubdomains, updateBuildStatus };
7 changes: 4 additions & 3 deletions src/backend/scripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import dockerize from "./utils/container.ts";
import DfContentMap from "./types/maps_interface.ts";

const MEMORY_LIMIT = Deno.env.get("MEMORY_LIMIT");
const PORT = Deno.env.get("PORT_BACKEND");

async function addScript(
document: DfContentMap,
Expand All @@ -24,20 +25,20 @@ async function addScript(
} else if (document.resource_type === "GITHUB" && static_content == "Yes") {
Deno.writeTextFile(`/hostpipe/.env`, env_content);
await exec(
`bash -c "echo 'bash ../../src/backend/shell_scripts/container.sh -s ${document.subdomain} ${document.resource} 80 ${MEMORY_LIMIT}' > /hostpipe/pipe"`,
`bash -c "echo 'bash ../../src/backend/shell_scripts/container.sh -s ${document.subdomain} ${document.resource} 80 ${MEMORY_LIMIT} ${PORT}' > /hostpipe/pipe"`,
);
} else if (document.resource_type === "GITHUB" && static_content == "No") {
if(dockerfile_present === 'No'){
const dockerfile = dockerize(stack, port, build_cmds);
Deno.writeTextFile(`/hostpipe/Dockerfile`, dockerfile);
Deno.writeTextFile(`/hostpipe/.env`, env_content);
await exec(
`bash -c "echo 'bash ../../src/backend/shell_scripts/container.sh -g ${document.subdomain} ${document.resource} ${port} ${MEMORY_LIMIT}' > /hostpipe/pipe"`,
`bash -c "echo 'bash ../../src/backend/shell_scripts/container.sh -g ${document.subdomain} ${document.resource} ${port} ${MEMORY_LIMIT} ${PORT}' > /hostpipe/pipe"`,
);
}else if(dockerfile_present === 'Yes'){

await exec(
`bash -c "echo 'bash ../../src/backend/shell_scripts/container.sh -d ${document.subdomain} ${document.resource} ${port} ${MEMORY_LIMIT}' > /hostpipe/pipe"`,
`bash -c "echo 'bash ../../src/backend/shell_scripts/container.sh -d ${document.subdomain} ${document.resource} ${port} ${MEMORY_LIMIT} ${PORT}' > /hostpipe/pipe"`,
);
}

Expand Down
3 changes: 2 additions & 1 deletion src/backend/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
gitlabAuth,
handleJwtAuthentication,
} from "./auth/github.ts";
import { addSubdomain, deleteSubdomain, getSubdomains } from "./main.ts";
import { addSubdomain, deleteSubdomain, getSubdomains, updateBuildStatus } from "./main.ts";
import {
getContainerHealth,
getContainerMetrics,
Expand Down Expand Up @@ -71,6 +71,7 @@ router
.get("/map", (ctx) => getSubdomains(ctx))
.post("/map", (ctx) => addSubdomain(ctx))
.post("/mapdel", (ctx) => deleteSubdomain(ctx))
.post("/maplogs", (ctx) => updateBuildStatus(ctx))
// Health monitoring routes
.get("/health", (ctx) => getContainerHealth(ctx))
.get("/health/summary", (ctx) => getHealthDashboard(ctx))
Expand Down
33 changes: 30 additions & 3 deletions src/backend/shell_scripts/container.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
report_error() {
local msg="$1"
local sub="$2"
curl -X POST -H "Content-Type: application/json" \
-d "{\"subdomain\": \"$sub\", \"status\": \"failed\", \"logs\": \"$msg\"}" \
http://localhost:$BACKEND_PORT/maplogs
}

report_success() {
local sub="$1"
curl -X POST -H "Content-Type: application/json" \
-d "{\"subdomain\": \"$sub\", \"status\": \"success\", \"logs\": \"Build completed successfully\"}" \
http://localhost:$BACKEND_PORT/maplogs
}

PORT_MIN=8010
PORT_MAX=8099
flag=$1
name=$2
resource=$3
exp_port=$4
max_mem=$5
BACKEND_PORT=$6

available_ports=()

Expand All @@ -17,7 +33,10 @@ done
echo "Available ports: ${available_ports[56]}"
AVAILABLE=0
echo "Creating subdomain $name"
git clone $resource $name
if ! git clone $resource $name; then
report_error "Git clone failed for repository $resource" "$name"
exit 1
fi
sudo cp .env $name/
cd $name

Expand All @@ -30,8 +49,14 @@ elif [ $flag = "-s" ]; then
" > Dockerfile
fi

sudo docker build -t $name .
sudo docker run --memory=$max_mem --name=$name -d -p ${available_ports[$AVAILABLE]}:$exp_port $2
if ! sudo docker build -t $name .; then
report_error "Docker build failed. Please check your Dockerfile or dependency configurations." "$name"
exit 1
fi
if ! sudo docker run --memory=$max_mem --name=$name -d -p ${available_ports[$AVAILABLE]}:$exp_port $name; then
report_error "Docker run failed. Container could not start." "$name"
exit 1
fi
cd ..
sudo rm -rf $name
sudo rm Dockerfile
Expand All @@ -56,3 +81,5 @@ sudo echo "# Virtual Host configuration for $2
}" > /etc/nginx/sites-available/$2.conf
sudo ln -s /etc/nginx/sites-available/$2.conf /etc/nginx/sites-enabled/$2.conf
sudo systemctl reload nginx

report_success "$name"
2 changes: 2 additions & 0 deletions src/backend/types/maps_interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ interface DfContentMap {
resource: string;
author: string;
date: string;
status?: "building" | "success" | "failed";
build_logs?: string;
}

export default DfContentMap;
78 changes: 71 additions & 7 deletions src/frontend/src/components/Home.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,65 @@
<script setup type="module">
import { ref, onMounted, onUnmounted } from 'vue';
import { getMaps } from '../utils/maps.ts';
import { check_jwt } from '../utils/authorize.ts';
import modal from './modal.vue';
import deletemodal from './deletemodal.vue';
import ApiKeyModal from './ApiKeyModal.vue';
import LogModal from './LogModal.vue';

const token = localStorage.getItem("JWTUser");
const provider = localStorage.getItem("provider");
const user = await check_jwt(token, provider);
const apiKey = localStorage.getItem("apiKey");
const fields = ["date", "subdomain", "resource", "resource_type", ""];
const maps = await getMaps(user);
const fields = ["date", "subdomain", "resource", "resource_type", "status", ""];
const maps = ref(await getMaps(user));

let pollingInterval = null;

const updateMaps = async () => {
const newData = await getMaps(user);
maps.value = newData;
const hasActiveBuild = newData.some(item => item.status === 'building');

if (hasActiveBuild && !pollingInterval) {
startPolling();
} else if (!hasActiveBuild && pollingInterval) {
stopPolling();
}
};

const startPolling = () => {
if (pollingInterval) return;
pollingInterval = setInterval(updateMaps, 5000); // Poll every 5s
};

const stopPolling = () => {
if (pollingInterval) {
clearInterval(pollingInterval);
pollingInterval = null;
}
};

onMounted(() => {
// Check immediately if we need to start polling (e.g., if user refreshed page during a build)
const hasActiveBuild = maps.value.some(item => item.status === 'building');
if (hasActiveBuild) {
startPolling();
}
});

onUnmounted(() => {
stopPolling();
});

const showLogModal = ref(false);
const currentLogs = ref("");
const selectedItem = ref(null); // Ensure this exists for delete modal too

const viewLogs = (item) => {
currentLogs.value = item.build_logs || "No logs available.";
showLogModal.value = true;
};
</script>

<template>
Expand Down Expand Up @@ -51,14 +100,27 @@ const maps = await getMaps(user);
<tbody>
<tr v-for="item in maps" :key="item">
<td v-for="field in fields" :key="field" style="border-bottom: 1px solid #121212">
<span v-if="item[field] && field !== 'subdomain'">{{ item[field] }}</span>
<span v-else-if="field === 'subdomain'">
<a :href="'https://' + item[field]" target="_blank" rel="noopener noreferrer" style="text-decoration: none; color: inherit;">{{ item[field] }}</a>
<span v-if="field === 'subdomain'">
<a :href="'https://' + item[field]" target="_blank" rel="noopener noreferrer"
style="text-decoration: none; color: inherit;">
{{ item[field] }}
</a>
</span>
<span v-else>
<span v-else-if="field === 'status'">
<span v-if="item.status === 'building'" style="color: orange;">⏳ Building...</span>
<span v-else-if="item.status === 'success'" style="color: green;">✅ Live</span>
<span v-else-if="item.status === 'failed'" style="color: red; cursor: pointer; text-decoration: underline;"
@click="viewLogs(item)">
❌ Failed (View Logs)
</span>
<span v-else>Active</span> </span>
<span v-else-if="field === ''">
<deletemodal v-show="showDeleteModal" @close-modal="showDeleteModal = false" :selectedItem="selectedItem" />
<div style="text-align: center;"><button class="delete" @click="showDeleteModal=true;selectedItem=item">Delete!</button></div>
<div style="text-align: center;">
<button class="delete" @click="showDeleteModal=true;selectedItem=item">Delete!</button>
</div>
</span>
<span v-else>{{ item[field] }}</span>
</td>
</tr>
</tbody>
Expand All @@ -70,6 +132,8 @@ const maps = await getMaps(user);

<ApiKeyModal v-show="showApiKeyModal" :apiKey="apiKey" @close-modal="showApiKeyModal = false" />

<LogModal v-if="showLogModal" :logs="currentLogs" @close="showLogModal = false" />

<footer>
<p>Made with ❤️ by MDG Space</p>
</footer>
Expand Down
44 changes: 44 additions & 0 deletions src/frontend/src/components/LogModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<template>
<div class="modal-overlay" v-if="logs">
<div class="modal">
<div class="close">
<button class="close-button" @click="$emit('close')">X</button>
</div>
<h3>Build Failure Logs</h3>
<div class="log-container">
<pre>{{ logs }}</pre>
</div>
<button class="copy-btn" @click="copyLogs">Copy Logs</button>
</div>
</div>
</template>

<script>
export default {
props: ['logs'],
methods: {
copyLogs() {
navigator.clipboard.writeText(this.logs);
alert("Logs copied!");
}
}
}
</script>

<style scoped>
.modal-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5); display: flex; justify-content: center; align-items: center;
}
.modal {
background: white; padding: 20px; border-radius: 8px; width: 80%; max-width: 800px;
}
.log-container {
background: #1e1e1e; color: #ff5555; padding: 15px;
border-radius: 4px; overflow: auto; max-height: 400px; margin: 15px 0;
text-align: left;
}
pre { white-space: pre-wrap; font-family: monospace; }
.close-button { float: right; border: none; background: none; font-size: 20px; cursor: pointer; }
.copy-btn { background: #555; color: white; border: none; padding: 8px 16px; cursor: pointer; }
</style>
1 change: 1 addition & 0 deletions src/frontend/src/utils/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export async function create(
"date": new Date().toLocaleDateString(),
"token": localStorage.getItem("JWTUser"),
"provider": localStorage.getItem("provider"),
"status": "building",
};
const resp = await fetch(rootUrl.toString(), {
method: "POST",
Expand Down