Compare commits

...

3 commits

9 changed files with 106 additions and 100 deletions

View file

@ -39,7 +39,7 @@ async fn main() -> std::io::Result<()> {
let cors = if cfg!(debug_assertions) { let cors = if cfg!(debug_assertions) {
actix_cors::Cors::permissive() actix_cors::Cors::permissive()
} else { } else {
actix_cors::Cors::default() actix_cors::Cors::permissive() //change to default on push
}; };
App::new() App::new()
.wrap(cors) .wrap(cors)

View file

@ -43,7 +43,7 @@ docker image remove apfelnetzwerk-backend
docker ps docker ps
``` ```
3. attach a shell to the running postgres docker container 3. attach a shell to the running backend docker container
```bash ```bash
docker exec -it <it/name> /bin/bash docker exec -it <it/name> /bin/bash
``` ```

View file

@ -1,10 +1,8 @@
import axios from "axios"; import axios from "axios";
import { store } from "@/store";
let axiosInstance = axios.create({ let axiosInstance = axios.create({
baseURL: import.meta.env.VITE_BACKEND_URL, baseURL: import.meta.env.VITE_BACKEND_URL,
}); });
axiosInstance.interceptors.request.use((config) => { axiosInstance.interceptors.request.use((config) => {
config.headers.Authorization = `Bearer ${store.token}`;
return config; return config;
}); });

View file

@ -3,29 +3,9 @@
<v-toolbar color="main" dark prominent> <v-toolbar color="main" dark prominent>
<img src="../assets/turbologo.svg" alt="logo" class="logo" width="75" /> <img src="../assets/turbologo.svg" alt="logo" class="logo" width="75" />
<v-spacer></v-spacer> <v-spacer></v-spacer>
<!-- Users, Groups, Licenses -->
<div>
<!-- USER MANAGEMENT -->
<template v-if="!user" />
<UsersDialog v-else-if="user.admin" />
<UsersEditActions v-else :admin-menu="false" :user="user!" />
</div>
<!-- Search -->
<v-text-field
class="compact-form mr-2"
label="Search"
variant="solo"
density="compact"
prepend-inner-icon="mdi-magnify"
hide-details
single-line
clearable
v-model="search"
rounded="pill"
></v-text-field>
<!-- Logout --> <!-- Logout -->
<v-btn icon variant="outlined" @click="handlelogout"> <v-btn @click="handlelogout">
<LogOut /> <LogOut />
</v-btn> </v-btn>
</v-toolbar> </v-toolbar>
@ -46,13 +26,7 @@ import { axiosInstance } from "@/client";
import { User } from "@/types"; import { User } from "@/types";
import { search } from "@/store"; import { search } from "@/store";
const { data: user } = useQuery({
queryKey: ["user"],
queryFn: async () => {
const res = await axiosInstance.get<User>("/users/me");
return res.data;
},
});
function handlelogout() { function handlelogout() {
store.setToken(null); store.setToken(null);

View file

@ -27,11 +27,11 @@
</div> </div>
<div class="d-flex align-items-center mb-2"> <div class="d-flex align-items-center mb-2">
<span class="mr-2">Battery:</span> <span class="mr-2">Battery:</span>
<span>{{ node.battery }}%</span> <span>{{ node.batteryCurrent }}%</span>
</div> </div>
<div class="d-flex align-items-center mb-2"> <div class="d-flex align-items-center mb-2">
<span class="mr-2">Runtime:</span> <span class="mr-2">Runtime:</span>
<span>{{ node.runtime }} hours</span> <span>{{ node.uptime }} hours</span>
</div> </div>
</v-expansion-panel-text> </v-expansion-panel-text>
</v-expansion-panel> </v-expansion-panel>

View file

@ -9,54 +9,28 @@
<script setup lang="ts"> <script setup lang="ts">
import HeaderBar from "./HeaderBar.vue"; import HeaderBar from "./HeaderBar.vue";
import CategoryContainer from "./CategoryContainer.vue";
import { useQuery } from "@tanstack/vue-query"; import { useQuery } from "@tanstack/vue-query";
import { axiosInstance } from "@/client"; import { axiosInstance } from "@/client";
import { LicenseGroup, License } from "@/types"; import { NodeGroup } from "@/types";
import { search, key } from "@/store";
import MiniSearch from "minisearch";
import { computed, provide } from "vue";
import TableCategory from "./TableCategory.vue"; import TableCategory from "./TableCategory.vue";
const { isPending, isError, data, error } = useQuery({
queryKey: ["licenses"], const { data } = useQuery({
queryKey: ["nodes"],
queryFn: async () => { queryFn: async () => {
const res = await axiosInstance.get<LicenseGroup[]>("/licenses"); const res = await axiosInstance.get<NodeGroup[]>("/nodes");
console.log(res.data);
return res.data; return res.data;
}, },
refetchInterval: 60 * 1000, select: (data) => {
return data.map((group) => {
return {
title: group.name,
value: group.nodes,
};
}); });
const searchEngine = computed(() => {
let minisearch = new MiniSearch({
fields: ["name", "description", "id"],
searchOptions: {
boost: { name: 2 },
prefix: true,
}, },
}); });
let licenses: License[] = [];
data.value?.forEach((group) => {
group.licenses.forEach((license) => licenses.push(license));
});
console.log(licenses);
minisearch.addAll(licenses);
return minisearch;
});
const searching = computed(() => search.value !== "");
const visibleIds = computed(() => {
return searchEngine.value.search(search.value).map((searchResult) => {
return searchResult.id;
});
});
provide(key, {
visibleIds,
searching,
});
</script> </script>
<style scoped> <style scoped>

View file

@ -5,8 +5,8 @@
<tr> <tr>
<th>Node/Name</th> <th>Node/Name</th>
<th>Status</th> <th>Status</th>
<th>Latitude</th> <!-- Separate column for Latitude --> <th>Latitude</th>
<th>Longitude</th> <!-- Separate column for Longitude --> <th>Longitude</th>
<th>Battery</th> <th>Battery</th>
<th>Gemessene - Temperatur</th> <th>Gemessene - Temperatur</th>
<th>Laufzeit</th> <th>Laufzeit</th>
@ -20,9 +20,15 @@
{{ node.status }} {{ node.status }}
</span> </span>
</td> </td>
<td>{{ node.position.lat }}</td> <td
<td>{{ node.position.lng }}</td> contenteditable="true"
<td>{{ node.battery }}%</td> @blur="validateAndUpdateLatLng(node, 'lat', $event)"
>{{ node.position.lat }}</td>
<td
contenteditable="true"
@blur="validateAndUpdateLatLng(node, 'lng', $event)"
>{{ node.position.lng }}</td>
<td>{{ calculateBatteryPercentage(node.batteryVoltage, node.minVoltage, node.maxVoltage) }}%</td>
<td>{{ node.temperature }}°C</td> <td>{{ node.temperature }}°C</td>
<td>{{ node.runtime }}</td> <td>{{ node.runtime }}</td>
</tr> </tr>
@ -32,6 +38,9 @@
</template> </template>
<script> <script>
//folgende Mockdaten rausnehmen und mit axios requests abfragen
export default { export default {
data() { data() {
return { return {
@ -40,7 +49,9 @@ export default {
name: "XXXX-XXXX-XXXX-XXXX", name: "XXXX-XXXX-XXXX-XXXX",
status: "ONLINE", status: "ONLINE",
position: { lat: 40.7128, lng: 74.0012 }, position: { lat: 40.7128, lng: 74.0012 },
battery: 98, batteryVoltage: 3.9,
minVoltage: 3.0,
maxVoltage: 4.2,
temperature: 100, temperature: 100,
runtime: "12h 30m 12s", runtime: "12h 30m 12s",
}, },
@ -48,7 +59,9 @@ export default {
name: "XXXX-XXXX-XXXX-XXXX", name: "XXXX-XXXX-XXXX-XXXX",
status: "OFFLINE", status: "OFFLINE",
position: { lat: 40.7128, lng: 74.0012 }, position: { lat: 40.7128, lng: 74.0012 },
battery: 19, batteryVoltage: 3.2,
minVoltage: 3.0,
maxVoltage: 4.2,
temperature: 100, temperature: 100,
runtime: "30m", runtime: "30m",
}, },
@ -56,7 +69,9 @@ export default {
name: "Localnode-3", name: "Localnode-3",
status: "ONLINE", status: "ONLINE",
position: { lat: 40.7128, lng: 74.0012 }, position: { lat: 40.7128, lng: 74.0012 },
battery: 66, batteryVoltage: 3.7,
minVoltage: 3.0,
maxVoltage: 4.2,
temperature: 100, temperature: 100,
runtime: "12h 30m 12s", runtime: "12h 30m 12s",
}, },
@ -64,7 +79,9 @@ export default {
name: "XXXX-XXXX-XXXX-XXXX", name: "XXXX-XXXX-XXXX-XXXX",
status: "ONLINE", status: "ONLINE",
position: { lat: 40.7128, lng: 74.0012 }, position: { lat: 40.7128, lng: 74.0012 },
battery: 79, batteryVoltage: 3.8,
minVoltage: 3.0,
maxVoltage: 4.2,
temperature: 100, temperature: 100,
runtime: "12h 30m 12s", runtime: "12h 30m 12s",
}, },
@ -72,7 +89,9 @@ export default {
name: "XXXX-XXXX-XXXX-XXXX", name: "XXXX-XXXX-XXXX-XXXX",
status: "ONLINE", status: "ONLINE",
position: { lat: 40.7128, lng: 74.0012 }, position: { lat: 40.7128, lng: 74.0012 },
battery: 10, batteryVoltage: 3.1,
minVoltage: 3.0,
maxVoltage: 4.2,
temperature: 100, temperature: 100,
runtime: "12h 30m 12s", runtime: "12h 30m 12s",
}, },
@ -80,7 +99,9 @@ export default {
name: "XXXX-XXXX-XXXX-XXXX", name: "XXXX-XXXX-XXXX-XXXX",
status: "OFFLINE", status: "OFFLINE",
position: { lat: 40.7128, lng: 74.0012 }, position: { lat: 40.7128, lng: 74.0012 },
battery: 0, batteryVoltage: 3.0,
minVoltage: 3.0,
maxVoltage: 4.2,
temperature: 100, temperature: 100,
runtime: "12h 30m 12s", runtime: "12h 30m 12s",
}, },
@ -88,36 +109,71 @@ export default {
name: "XXXX-XXXX-XXXX-XXXX", name: "XXXX-XXXX-XXXX-XXXX",
status: "ONLINE", status: "ONLINE",
position: { lat: 40.7128, lng: 74.0012 }, position: { lat: 40.7128, lng: 74.0012 },
battery: 56, batteryVoltage: 3.6,
minVoltage: 3.0,
maxVoltage: 4.2,
temperature: 100, temperature: 100,
runtime: "12h 30m 12s", runtime: "12h 30m 12s",
}, },
], ],
}; };
}, },
methods: {
calculateBatteryPercentage(currentVoltage, minVoltage, maxVoltage) {
if (currentVoltage <= minVoltage) {
return 0;
} else if (currentVoltage >= maxVoltage) {
return 100;
} else {
return ((currentVoltage - minVoltage) / (maxVoltage - minVoltage) * 100).toFixed(2);
}
},
validateAndUpdateLatLng(node, field, event) {
const originalValue = node.position[field];
let newValue = event.target.innerText;
// normalize seperated values
newValue = newValue.replace(',', '.');
// check if float value
const validNumberRegex = /^-?\d+(\.\d+)?$/;
if (validNumberRegex.test(newValue)) {
const parsedValue = parseFloat(newValue);
// Update if valid
node.position[field] = parsedValue;
console.log(`Updated ${field} of ${node.name}: ${parsedValue}`);
} else {
// Reset to original value if invalid
event.target.innerText = originalValue;
console.log(`Failed to set ${field} of ${node.name}: Invalid input "${newValue}"`);
}
}
}
}; };
</script> </script>
<style scoped> <style scoped>
.table-container { .table-container {
background-color: white; background-color: white;
padding: 0.5rem 1rem; /* Reduce top padding to 0.5rem */ padding: 0.5rem 1rem;
margin: 0 auto; /* Remove margin at the top, keep it centered horizontally */ margin: 0 auto;
width: 100%; /* Full width of parent */ width: 100%;
overflow-x: auto; /* Allow horizontal scroll if necessary */ overflow-x: auto;
} }
.responsive-table { .responsive-table {
width: 100%; /* Full width */ width: 100%;
border-collapse: collapse; /* Ensure tight borders */ border-collapse: collapse;
margin: 0; /* Remove margin at the top of the table */ margin: 0;
table-layout: auto; /* Auto layout for slimmer columns */ table-layout: auto;
} }
.responsive-table th, .responsive-table th,
.responsive-table td { .responsive-table td {
padding: 0.5rem; /* Maintain slim padding */ padding: 0.5rem;
text-align: center; /* Center text */ text-align: center;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
} }
@ -126,7 +182,7 @@ export default {
} }
.responsive-table tr:nth-child(even) { .responsive-table tr:nth-child(even) {
background-color: #f9f9f9; /* Striped rows */ background-color: #f9f9f9;
} }
.status-online { .status-online {

View file

@ -19,7 +19,7 @@ export default createVuetify({
light: { light: {
dark: false, dark: false,
colors: { colors: {
main: "#024950", main: "#353A43",
darker: "#003135", darker: "#003135",
contrast: "#964734", contrast: "#964734",
accent: "#0FA4AF", accent: "#0FA4AF",

View file

@ -5,8 +5,12 @@ export interface Node {
coordla: number; coordla: number;
coordlong: number; coordlong: number;
temperature: number; temperature: number;
battery: number; batteryMinimum: number;
runtime: number; batteryCurrent: number;
batteryMaximum: number;
voltage: number;
uptime: number;
group: string;
} }
export interface NodeGroup { export interface NodeGroup {