Restructure Web App into server/ + web/ (GsmNode parity)

Reorganize the Web App to match the GsmNode project layout: a Go
backend-for-frontend in server/ that embeds the built SPA and reverse-proxies
/api/* to the API Server, with the Vue 3 + Vite frontend moved into web/.

- Move all frontend files into web/ (history preserved via renames)
- Point vite build output at ../server/dist for Go embedding
- Add server/ Go BFF (main.go, go.mod, .env.example, Run-WebApp.ps1)
- Drop Docker/nginx deploy (Dockerfile, docker-compose.yml, nginx.conf.template,
  .dockerignore) in favor of the BFF, matching GsmNode
- Update .claude/launch.json to run the dev server from web/
- Rewrite README.md for the new layout

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tajniak81
2026-07-13 11:34:02 +02:00
co-authored by Claude Opus 4.8
parent ba3f227361
commit 75a2ccc226
47 changed files with 230 additions and 161 deletions
+173
View File
@@ -0,0 +1,173 @@
<script setup>
import { ref } from "vue";
import { api } from "../api";
import Modal from "./Modal.vue";
const props = defineProps({ car: { type: Object, default: null } });
const emit = defineEmits(["saved", "close"]);
const isEdit = !!props.car;
const saving = ref(false);
const error = ref("");
const form = ref({
name: props.car?.name ?? "",
make: props.car?.make ?? "",
model: props.car?.model ?? "",
year: props.car?.year || "",
registration: props.car?.registration ?? "",
registrationCountry: props.car?.registrationCountry ?? "",
vin: props.car?.vin ?? "",
fuelType: props.car?.fuelType ?? "",
buildDate: props.car?.buildDate ?? "",
firstRegistrationDate: props.car?.firstRegistrationDate ?? "",
oilSpec: props.car?.oilSpec ?? "",
transmissionOilSpec: props.car?.transmissionOilSpec ?? "",
differentialOilSpec: props.car?.differentialOilSpec ?? "",
brakeFluidSpec: props.car?.brakeFluidSpec ?? "",
coolantSpec: props.car?.coolantSpec ?? "",
serviceIntervalDays: props.car?.serviceIntervalDays || 365,
serviceIntervalKm: props.car?.serviceIntervalKm || 15000,
currentKm: props.car?.currentKm || "",
});
async function submit() {
saving.value = true;
error.value = "";
try {
const payload = {
name: form.value.name.trim(),
make: form.value.make.trim(),
model: form.value.model.trim(),
year: form.value.year ? Number(form.value.year) : 0,
registration: form.value.registration.trim(),
registrationCountry: form.value.registrationCountry.trim(),
vin: form.value.vin.trim(),
fuelType: form.value.fuelType,
buildDate: form.value.buildDate,
firstRegistrationDate: form.value.firstRegistrationDate,
oilSpec: form.value.oilSpec.trim(),
transmissionOilSpec: form.value.transmissionOilSpec.trim(),
differentialOilSpec: form.value.differentialOilSpec.trim(),
brakeFluidSpec: form.value.brakeFluidSpec.trim(),
coolantSpec: form.value.coolantSpec.trim(),
serviceIntervalDays: Number(form.value.serviceIntervalDays) || 365,
serviceIntervalKm: Number(form.value.serviceIntervalKm) || 15000,
currentKm: form.value.currentKm ? Number(form.value.currentKm) : 0,
};
const saved = isEdit
? await api.updateCar(props.car.id, payload)
: await api.createCar(payload);
emit("saved", saved);
} catch (e) {
error.value = e.message;
} finally {
saving.value = false;
}
}
</script>
<template>
<Modal :title="isEdit ? 'Edit car' : 'Add a car'" @close="emit('close')">
<p v-if="error" class="mb-3 rounded-control bg-danger-soft px-3 py-2 text-sm font-medium text-danger">{{ error }}</p>
<form class="space-y-3" @submit.prevent="submit">
<div>
<label class="dh-label">Name *</label>
<input v-model="form.name" required placeholder="Toyota Yaris" class="dh-input" />
</div>
<div class="grid grid-cols-3 gap-2">
<div>
<label class="dh-label">Make</label>
<input v-model="form.make" placeholder="Toyota" class="dh-input" />
</div>
<div>
<label class="dh-label">Model</label>
<input v-model="form.model" placeholder="Yaris" class="dh-input" />
</div>
<div>
<label class="dh-label">Year</label>
<input v-model="form.year" type="number" placeholder="2015" class="dh-input data" />
</div>
</div>
<div class="grid grid-cols-3 gap-2">
<div>
<label class="dh-label">Registration</label>
<input v-model="form.registration" placeholder="ABC 1234" class="dh-input" />
</div>
<div>
<label class="dh-label">Registration country</label>
<input v-model="form.registrationCountry" placeholder="Poland" class="dh-input" />
</div>
<div>
<label class="dh-label">VIN</label>
<input v-model="form.vin" placeholder="Vehicle Identification Number" maxlength="17" class="dh-input data uppercase" />
</div>
</div>
<div class="grid grid-cols-3 gap-2">
<div>
<label class="dh-label">Fuel type</label>
<select v-model="form.fuelType" class="dh-input">
<option value=""></option>
<option value="petrol">Petrol (gasoline)</option>
<option value="diesel">Diesel</option>
<option value="hybrid">Hybrid</option>
<option value="electric">Electric</option>
</select>
</div>
<div>
<label class="dh-label">Build date</label>
<input v-model="form.buildDate" type="date" class="dh-input data" />
</div>
<div>
<label class="dh-label">First registration</label>
<input v-model="form.firstRegistrationDate" type="date" class="dh-input data" />
</div>
</div>
<div class="grid grid-cols-2 gap-2">
<div>
<label class="dh-label">Engine oil spec</label>
<input v-model="form.oilSpec" placeholder="0W20" class="dh-input" />
</div>
<div>
<label class="dh-label">Current odometer (km)</label>
<input v-model="form.currentKm" type="number" placeholder="270185" class="dh-input data" />
</div>
</div>
<div class="grid grid-cols-2 gap-2">
<div>
<label class="dh-label">Transmission oil spec</label>
<input v-model="form.transmissionOilSpec" placeholder="Toyota WS" class="dh-input" />
</div>
<div>
<label class="dh-label">Differential oil spec</label>
<input v-model="form.differentialOilSpec" placeholder="SAE 75W-90 GL-5" class="dh-input" />
</div>
</div>
<div class="grid grid-cols-2 gap-2">
<div>
<label class="dh-label">Brake fluid spec</label>
<input v-model="form.brakeFluidSpec" placeholder="DOT 4" class="dh-input" />
</div>
<div>
<label class="dh-label">Coolant spec</label>
<input v-model="form.coolantSpec" placeholder="Toyota Super Long Life Coolant" class="dh-input" />
</div>
</div>
<div class="grid grid-cols-2 gap-2">
<div>
<label class="dh-label">Service interval (days)</label>
<input v-model="form.serviceIntervalDays" type="number" class="dh-input data" />
</div>
<div>
<label class="dh-label">Service interval (km)</label>
<input v-model="form.serviceIntervalKm" type="number" class="dh-input data" />
</div>
</div>
<div class="mt-4 flex justify-end gap-2">
<button type="button" class="dh-btn dh-btn-ghost" @click="emit('close')">Cancel</button>
<button type="submit" :disabled="saving" class="dh-btn dh-btn-primary">
{{ saving ? "Saving…" : isEdit ? "Save changes" : "Add car" }}
</button>
</div>
</form>
</Modal>
</template>