Compare commits
2 Commits
6c43af50c0
...
3f0e821d0d
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f0e821d0d | |||
| 2bb5125254 |
@@ -1,3 +1,15 @@
|
|||||||
docker build -t kingArthurApp:latest .
|
# docker build -t kingArthurApp:latest .
|
||||||
docker save kingArthurApp:latest | gzip > kingArthurApp.tar.gz
|
# docker save kingArthurApp:latest | gzip > kingArthurApp.tar.gz
|
||||||
scp kingArthurApp.tar.gz youruser@your_server_ip:~/
|
# scp kingArthurApp.tar.gz youruser@your_server_ip:~/
|
||||||
|
|
||||||
|
# add login key first
|
||||||
|
# > docker login git.nerdyssey.de
|
||||||
|
# > Username: dockerLogin
|
||||||
|
# > Passwrord: appkey
|
||||||
|
# [DOMAIN]/[OWNER]/[IMAGE_NAME]:[TAG]
|
||||||
|
docker build -t git.nerdyssey.de/exodus/burg-stett-king-arth-web:latest .
|
||||||
|
docker push git.nerdyssey.de/exodus/burg-stett-king-arth-web:latest
|
||||||
|
|
||||||
|
#
|
||||||
|
# docker compose pull
|
||||||
|
# docker compose up -d
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
gitea-server:
|
||||||
|
image: gitea/gitea:latest
|
||||||
|
container_name: gitea
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- USER_UID=1000
|
||||||
|
- USER_GID=1000
|
||||||
|
# Database connection
|
||||||
|
- GITEA__database__DB_TYPE=postgres
|
||||||
|
- GITEA__database__HOST=gitea-db:5432
|
||||||
|
- GITEA__database__NAME=gitea
|
||||||
|
- GITEA__database__USER=gitea
|
||||||
|
- GITEA__database__PASSWD=###SomePassword###
|
||||||
|
# Web / Domain settings
|
||||||
|
- GITEA__server__DOMAIN=git.nerdyssey.de
|
||||||
|
- GITEA__server__ROOT_URL=https://git.nerdyssey.de/
|
||||||
|
- GITEA__server__SSH_DOMAIN=git.nerdyssey.de
|
||||||
|
- GITEA__server__SSH_PORT=2222
|
||||||
|
# Security
|
||||||
|
- GITEA__service__DISABLE_REGISTRATION=true
|
||||||
|
- GITEA__service__REQUIRE_SIGNIN_VIEW=false
|
||||||
|
volumes:
|
||||||
|
- ./data:/data
|
||||||
|
ports:
|
||||||
|
- "2222:22" # This allows Git over SSH
|
||||||
|
depends_on:
|
||||||
|
- gitea-db
|
||||||
|
|
||||||
|
gitea-db:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
container_name: gitea-db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=gitea
|
||||||
|
- POSTGRES_PASSWORD=###SomePassword###
|
||||||
|
- POSTGRES_DB=gitea
|
||||||
|
volumes:
|
||||||
|
- ./postgres:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: proxy_net
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
services:
|
||||||
|
mosquitto:
|
||||||
|
image: eclipse-mosquitto:latest
|
||||||
|
container_name: mosquitto
|
||||||
|
restart: unless-stopped
|
||||||
|
# Power are managed via private subnet "proxy_net"
|
||||||
|
# ports:
|
||||||
|
# - "1883:1883"
|
||||||
|
# - "9001:9001"
|
||||||
|
volumes:
|
||||||
|
- ./config:/mosquitto/config
|
||||||
|
- ./data:/mosquitto/data
|
||||||
|
- ./log:/mosquitto/log
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: proxy_net
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
nginx-proxy-manager:
|
||||||
|
image: 'jc21/nginx-proxy-manager:latest'
|
||||||
|
container_name: nginx-proxy-manager
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- '80:80' # Public HTTP Port (Required for Certbot challenges)
|
||||||
|
- '443:443' # Public HTTPS Port
|
||||||
|
- '81:81' # Admin Web Dashboard
|
||||||
|
- '8883:8883' # Added for Secure MQTT (MQTTS)
|
||||||
|
volumes:
|
||||||
|
- ./data:/data
|
||||||
|
- ./letsencrypt:/etc/letsencrypt
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: proxy_net
|
||||||
|
external: true
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
# MQTT Commands
|
||||||
|
|
||||||
|
## simple
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mosquitto_sub -h 91.99.29.8 -t outdoor/king-arthur/sword/run
|
||||||
|
```
|
||||||
|
|
||||||
|
## safe
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mosquitto_sub -L mqtts://nerdyssey.de:8883/outdoor/king-arthur/sword/run -d
|
||||||
|
mosquitto_sub -L mqtts://nerdyssey.de:8883/outdoor/king-arthur/sword/int -d
|
||||||
|
|
||||||
|
mosquitto_pub -L mqtts://nerdyssey.de:8883 -t outdoor/king-arthur/sword/run -m "SetMaintanance"
|
||||||
|
```
|
||||||
|
|
||||||
|
- `+` Single-level wildcard: `t1/+/sub2/sub3`
|
||||||
|
- `#` Multi-level wildcard: `t1/#/sub3`, `t1/#`
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Publishing topic with message
|
||||||
|
# -h: Host (optional)
|
||||||
|
# -t: Topic
|
||||||
|
# -m: Message
|
||||||
|
# -q: Quality of service
|
||||||
|
# -r: Retain (keep this after failed connection)
|
||||||
|
# -h: Print help info
|
||||||
|
mosquitto_pub -t someTopic -m 123 -q 1
|
||||||
|
mosquitto_pub -r -t someTopic -m "on"
|
||||||
|
mosquitto_pub -h 91.99.29.8 -t someTopic -m 123 -q 1
|
||||||
|
|
||||||
|
# Subscribing to
|
||||||
|
# -h: Host (optional)
|
||||||
|
# -t: Topic s
|
||||||
|
# + is wildcard
|
||||||
|
# # is wildcard for full subtree
|
||||||
|
# -T: Exclude topic
|
||||||
|
# -q: Quality of service
|
||||||
|
mosquitto_sub -t someTopic -q 1
|
||||||
|
mosquitto_sub -t someTopic/+/subTopic
|
||||||
|
mosquitto_sub -t bbc/# -T bbc/radio3
|
||||||
|
mosquitto_sub -h 91.99.29.8 -t someTopic -q 1
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
|
||||||
|
```sh
|
||||||
|
# install docker
|
||||||
|
sudo apt install ca-certificates curl -y
|
||||||
|
# Add Docker's official GPG key:
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install ca-certificates curl
|
||||||
|
sudo install -m 0755 -d /etc/apt/keyrings
|
||||||
|
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
|
||||||
|
sudo chmod a+r /etc/apt/keyrings/docker.asc
|
||||||
|
# Add the repository to Apt sources:
|
||||||
|
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
|
||||||
|
Types: deb
|
||||||
|
URIs: https://download.docker.com/linux/ubuntu
|
||||||
|
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
|
||||||
|
Components: stable
|
||||||
|
Architectures: $(dpkg --print-architecture)
|
||||||
|
Signed-By: /etc/apt/keyrings/docker.asc
|
||||||
|
EOF
|
||||||
|
# install and test
|
||||||
|
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
|
sudo systemctl status docker
|
||||||
|
sudo docker run hello-world
|
||||||
|
sudo usermod -aG docker $USER
|
||||||
|
newgrp docker
|
||||||
|
docker run hello-world
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo apt install ufw -y
|
||||||
|
sudo ufw default deny incoming
|
||||||
|
sudo ufw default allow outgoing
|
||||||
|
sudo ufw allow ssh
|
||||||
|
sudo ufw enable
|
||||||
|
sudo ufw allow ssh
|
||||||
|
sudo ufw status verbose
|
||||||
|
sudo nano /etc/ssh/sshd_config
|
||||||
|
sudo systemctl restart ssh
|
||||||
|
sudo ufw allow 80/tcp
|
||||||
|
sudo ufw allow 8080/tcp
|
||||||
|
sudo ufw allow 1883/tcp # mqtt
|
||||||
|
sudo ufw allow 9001/tcp # mqtt ws
|
||||||
|
sudo ufw allow 8883/tcp # secure mqtt
|
||||||
|
sudo ufw allow 2222/tcp # secure mqtt
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir -p ~/mosquitto/config
|
||||||
|
mkdir -p ~/mosquitto/data
|
||||||
|
mkdir -p ~/mosquitto/log
|
||||||
|
nano ~/mosquitto/config/mosquitto.conf
|
||||||
|
nano ~/mosquitto/docker-compose.yml
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
todo add also config files
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker load <~/my-vite-app.tar.gz
|
||||||
|
docker images
|
||||||
|
```
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Secure Shell
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ssh -o PubkeyAuthentication=no gustice@91.99.29.8
|
||||||
|
```
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
exodus-burg-stett-king-arth-web:
|
||||||
|
image: git.nerdyssey.de/exodus/burg-stett-king-arth-web:latest
|
||||||
|
container_name: exodus-burg-stett-king-arth-web
|
||||||
|
restart: unless-stopped
|
||||||
|
# ports:
|
||||||
|
# - "80:80"
|
||||||
|
# - "443:443"
|
||||||
|
# environment:
|
||||||
|
# - VITE_APP_ENV=production
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: proxy_net
|
||||||
|
external: true
|
||||||
Generated
+9
-6
@@ -14,7 +14,7 @@
|
|||||||
"vue-router": "^4.2.5"
|
"vue-router": "^4.2.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^6.0.0",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"vite": "^8.1.0",
|
"vite": "^8.1.0",
|
||||||
"vue-tsc": "^3.3.5"
|
"vue-tsc": "^3.3.5"
|
||||||
@@ -465,16 +465,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitejs/plugin-vue": {
|
"node_modules/@vitejs/plugin-vue": {
|
||||||
"version": "5.2.4",
|
"version": "6.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz",
|
||||||
"integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
|
"integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@rolldown/pluginutils": "^1.0.1"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.0.0 || >=20.0.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vite": "^5.0.0 || ^6.0.0",
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||||
"vue": "^3.2.25"
|
"vue": "^3.2.25"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^6.0.0",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"vite": "^8.1.0",
|
"vite": "^8.1.0",
|
||||||
"vue-tsc": "^3.3.5"
|
"vue-tsc": "^3.3.5"
|
||||||
|
|||||||
+1
-2
@@ -5,5 +5,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import Players from '../views/Players.vue'
|
|||||||
import Start from '../views/Start.vue'
|
import Start from '../views/Start.vue'
|
||||||
import Text from '../views/Text.vue'
|
import Text from '../views/Text.vue'
|
||||||
|
|
||||||
|
const routeBase: string = "nerdyssey.de/exodus/king-arthur"
|
||||||
|
|
||||||
const routes: any[] = [
|
const routes: any[] = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
|
|||||||
+144
-8
@@ -1,26 +1,34 @@
|
|||||||
import mqtt from 'mqtt'
|
import mqtt from 'mqtt'
|
||||||
|
|
||||||
const mqttBrokerHost = '91.99.29.8'
|
const mqttBrokerHost = 'mqtt.nerdyssey.de'
|
||||||
const mqttBrokerPort = 9001
|
const mqttBrokerPort = 443 // 9001 if not secure
|
||||||
const mqttBrokerPath = '/mqtt'
|
const mqttBrokerPath = '/mqtt'
|
||||||
const mqttUseTls = false
|
const mqttUseTls = true
|
||||||
const mqttTopic = 'king-arthur/page/startup'
|
const mqttTopicRoot = 'outdoor/king-arthur/sword/'
|
||||||
const mqttMessage = 'start-hit'
|
const mqttMessage = 'start-hit'
|
||||||
|
|
||||||
let client: any = null
|
let client: any = null
|
||||||
|
|
||||||
|
function getClient(): mqtt.MqttClient {
|
||||||
|
const protocol = mqttUseTls ? 'wss' : 'ws'
|
||||||
|
const brokerUrl = `${protocol}://${mqttBrokerHost}:${mqttBrokerPort}${mqttBrokerPath}`
|
||||||
|
const tempClient = mqtt.connect(brokerUrl)
|
||||||
|
console.log(`MQTT Connection string ${brokerUrl}`)
|
||||||
|
return tempClient
|
||||||
|
}
|
||||||
|
|
||||||
export function getMqttClient(): any {
|
export function getMqttClient(): any {
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
export function publishStartHit(): void {
|
export function publishStartHit(): void {
|
||||||
const protocol = mqttUseTls ? 'wss' : 'ws'
|
const tempClient = getClient()
|
||||||
const brokerUrl = `${protocol}://${mqttBrokerHost}:${mqttBrokerPort}${mqttBrokerPath}`
|
|
||||||
const tempClient = mqtt.connect(brokerUrl)
|
const mqttTopic = mqttTopicRoot + 'run'
|
||||||
|
|
||||||
tempClient.on('connect', () => {
|
tempClient.on('connect', () => {
|
||||||
tempClient.publish(mqttTopic, mqttMessage, { qos: 1 }, () => {
|
tempClient.publish(mqttTopic, mqttMessage, { qos: 1 }, () => {
|
||||||
tempClient.end()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -29,3 +37,131 @@ export function publishStartHit(): void {
|
|||||||
tempClient.end(true)
|
tempClient.end(true)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function subscribeToIntTopic(callback: (response: IntTopicResponse) => void): mqtt.MqttClient {
|
||||||
|
const tempClient = getClient()
|
||||||
|
const mqttTopic = mqttTopicRoot + 'int'
|
||||||
|
|
||||||
|
tempClient.on('connect', () => {
|
||||||
|
tempClient.subscribe(mqttTopic, { qos: 1 }, (error) => {
|
||||||
|
if (error) {
|
||||||
|
console.error('MQTT subscribe failed:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
tempClient.on('message', (topic: string, message: Buffer) => {
|
||||||
|
if (topic === mqttTopic) {
|
||||||
|
const parsedMessage = parseIntMessage(message.toString())
|
||||||
|
callback(parsedMessage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
tempClient.on('error', (error: any) => {
|
||||||
|
console.error('MQTT connection error:', error)
|
||||||
|
})
|
||||||
|
|
||||||
|
return tempClient
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IntTopicResponse {
|
||||||
|
version: string
|
||||||
|
mode: number
|
||||||
|
sE: boolean
|
||||||
|
sS: boolean
|
||||||
|
sP: boolean
|
||||||
|
lE: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseIntMessage(message: string): IntTopicResponse {
|
||||||
|
const result: IntTopicResponse = {
|
||||||
|
version: '',
|
||||||
|
mode: 0,
|
||||||
|
sE: false, // EndPoint
|
||||||
|
sS: false, // StartPoint
|
||||||
|
sP: false, // PullSensor
|
||||||
|
lE: false // LatchEnable
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionMatch = message.match(/T([\d.]+)/)
|
||||||
|
if (versionMatch) {
|
||||||
|
result.version = versionMatch[1]
|
||||||
|
}
|
||||||
|
const modeMatch = message.match(/Mode=(\d+)/)
|
||||||
|
if (modeMatch) {
|
||||||
|
result.mode = parseInt(modeMatch[1], 10)
|
||||||
|
}
|
||||||
|
const sEMatch = message.match(/sE:(true|false)/)
|
||||||
|
if (sEMatch) {
|
||||||
|
result.sE = sEMatch[1] === 'true'
|
||||||
|
}
|
||||||
|
const sSMatch = message.match(/sS:(true|false)/)
|
||||||
|
if (sSMatch) {
|
||||||
|
result.sS = sSMatch[1] === 'true'
|
||||||
|
}
|
||||||
|
const sPMatch = message.match(/sP:(true|false)/)
|
||||||
|
if (sPMatch) {
|
||||||
|
result.sP = sPMatch[1] === 'true'
|
||||||
|
}
|
||||||
|
const lEMatch = message.match(/lE:(true|false)/)
|
||||||
|
if (lEMatch) {
|
||||||
|
result.lE = lEMatch[1] === 'true'
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enterMaintananceMode(
|
||||||
|
callback: (sE: number, sS: number, sP: number, lE: number) => void)
|
||||||
|
: mqtt.MqttClient {
|
||||||
|
const client = getClient()
|
||||||
|
|
||||||
|
const mqttTopic = mqttTopicRoot + 'stat'
|
||||||
|
|
||||||
|
client.on('connect', () => {
|
||||||
|
publicMaintananceCmd(client, "SetMaintanance")
|
||||||
|
client.subscribe(mqttTopic, { qos: 1 }, (error) => {
|
||||||
|
if (error) {
|
||||||
|
console.error('MQTT subscribe failed:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
client.on('error', (error: any) => {
|
||||||
|
console.error('MQTT operation failed:', error)
|
||||||
|
client.end(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
client.on('message', (topic: string, message: Buffer) => {
|
||||||
|
if (topic === mqttTopic) {
|
||||||
|
const messageStr = message.toString()
|
||||||
|
const status = parseIntMessage(message.toString())
|
||||||
|
const sE = status.sE ? 1 : 0
|
||||||
|
const sS = status.sS ? 1 : 0
|
||||||
|
const sP = status.sP ? 1 : 0
|
||||||
|
const lE = status.lE ? 1 : 0
|
||||||
|
callback(sE, sS, sP, lE)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exitMaintananceMode(client: mqtt.MqttClient): void {
|
||||||
|
publicMaintananceCmd(client, "Exit")
|
||||||
|
const mqttTopic = mqttTopicRoot + 'stat'
|
||||||
|
client.unsubscribe(mqttTopic, (error) => {
|
||||||
|
if (error) {
|
||||||
|
console.error('MQTT unsubscribe failed:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
client.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function publicMaintananceCmd(client: mqtt.MqttClient, message : string): void {
|
||||||
|
const mqttTopic = mqttTopicRoot + 'run'
|
||||||
|
console.log(`MQTT: Publish ${mqttTopic}: ${message}`)
|
||||||
|
client.publish(mqttTopic, message, { qos: 1 }, (error) => {
|
||||||
|
if (error) {
|
||||||
|
console.error('MQTT publish failed:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -2,9 +2,12 @@
|
|||||||
<main class="app">
|
<main class="app">
|
||||||
<nav class="nav">
|
<nav class="nav">
|
||||||
<router-link to="/" class="nav-link">Home</router-link>
|
<router-link to="/" class="nav-link">Home</router-link>
|
||||||
<router-link to="/players" class="nav-link" :class="{ active: route.path === '/players' }">Page 1: Players</router-link>
|
<router-link to="/players" class="nav-link" :class="{ active: route.path === '/players' }">
|
||||||
<router-link to="/start" class="nav-link" :class="{ active: route.path === '/start' }">Page 2: Start</router-link>
|
Page 1: Players</router-link>
|
||||||
<router-link to="/text" class="nav-link" :class="{ active: route.path === '/text' }">Page 3: Text</router-link>
|
<router-link to="/start" class="nav-link" :class="{ active: route.path === '/start' }">
|
||||||
|
Page 2: Start</router-link>
|
||||||
|
<router-link to="/text" class="nav-link" :class="{ active: route.path === '/text' }">
|
||||||
|
Page 3: Text</router-link>
|
||||||
</nav>
|
</nav>
|
||||||
<section class="page-content">
|
<section class="page-content">
|
||||||
<Players v-if="route.path === '/players'" />
|
<Players v-if="route.path === '/players'" />
|
||||||
|
|||||||
+184
-4
@@ -1,10 +1,52 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="app">
|
<main class="app">
|
||||||
<h1>Simple Player Web App</h1>
|
<h1>Maintanance View</h1>
|
||||||
|
|
||||||
|
<h2>Sword Status</h2>
|
||||||
|
<div class="status-container">
|
||||||
|
<h3>Inputs</h3>
|
||||||
|
<div class="status-frame">
|
||||||
|
<div class="status-item" :class="{ active: endSensStatus === 1 }">
|
||||||
|
<span class="status-label">EndSens:</span>
|
||||||
|
<span class="status-value">{{ endSensStatus }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item" :class="{ active: startSensStatus === 1 }">
|
||||||
|
<span class="status-label">StartSens:</span>
|
||||||
|
<span class="status-value">{{ startSensStatus }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item" :class="{ active: pullSensStatus === 1 }">
|
||||||
|
<span class="status-label">PullSens:</span>
|
||||||
|
<span class="status-value">{{ pullSensStatus }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item" :class="{ active: lockStatus === 1 }">
|
||||||
|
<span class="status-label">Lock:</span>
|
||||||
|
<span class="status-value">{{ lockStatus }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Outputs</h3>
|
||||||
|
<div class="status-frame">
|
||||||
|
<div class="control-buttons">
|
||||||
|
<button @click="handleLock" class="btn-lock">Lock</button>
|
||||||
|
<button @click="handleUnlock" class="btn-unlock">Unlock</button>
|
||||||
|
<button @click="handlePlayX(1)" class="btn-unlock">Play sound 1</button>
|
||||||
|
<button @click="handlePlayX(2)" class="btn-unlock">Play sound 2</button>
|
||||||
|
<button @click="handlePlayX(3)" class="btn-unlock">Play sound 3</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Status</h3>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Game Views</h2>
|
||||||
<nav class="nav">
|
<nav class="nav">
|
||||||
<router-link to="/maintenance/players" class="nav-link" :class="{ active: isActive('/maintenance/players') }">Page 1: Players</router-link>
|
<router-link to="/maintenance/players" class="nav-link" :class="{ active: isActive('/maintenance/players') }">Page
|
||||||
<router-link to="/maintenance/start" class="nav-link" :class="{ active: isActive('/maintenance/start') }">Page 2: Start</router-link>
|
1: Players</router-link>
|
||||||
<router-link to="/maintenance/text" class="nav-link" :class="{ active: isActive('/maintenance/text') }">Page 3: Text</router-link>
|
<router-link to="/maintenance/start" class="nav-link" :class="{ active: isActive('/maintenance/start') }">Page 2:
|
||||||
|
Start</router-link>
|
||||||
|
<router-link to="/maintenance/text" class="nav-link" :class="{ active: isActive('/maintenance/text') }">Page 3:
|
||||||
|
Text</router-link>
|
||||||
</nav>
|
</nav>
|
||||||
<section class="page-content">
|
<section class="page-content">
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
@@ -13,11 +55,149 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { enterMaintananceMode, exitMaintananceMode, publicMaintananceCmd } from '../services/mqtt'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import type mqtt from 'mqtt'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
|
const endSensStatus = ref<number>(0)
|
||||||
|
const startSensStatus = ref<number>(0)
|
||||||
|
const pullSensStatus = ref<number>(0)
|
||||||
|
const lockStatus = ref<number>(0)
|
||||||
|
|
||||||
|
let statTopicClient: mqtt.MqttClient | null = null
|
||||||
|
|
||||||
function isActive(path: string): boolean {
|
function isActive(path: string): boolean {
|
||||||
return route.path === path
|
return route.path === path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cleanup(): void {
|
||||||
|
if (statTopicClient) {
|
||||||
|
exitMaintananceMode(statTopicClient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLock(): void {
|
||||||
|
if (statTopicClient) {
|
||||||
|
publicMaintananceCmd(statTopicClient, "ServiceClose")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUnlock(): void {
|
||||||
|
if (statTopicClient) {
|
||||||
|
publicMaintananceCmd(statTopicClient, "ServiceOpen")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePlayX(sound: number): void {
|
||||||
|
if (statTopicClient) {
|
||||||
|
if (sound === 1) {
|
||||||
|
publicMaintananceCmd(statTopicClient, "ServicePlay1")
|
||||||
|
} else if (sound === 2) {
|
||||||
|
publicMaintananceCmd(statTopicClient, "ServicePlay2")
|
||||||
|
} else if (sound === 3) {
|
||||||
|
publicMaintananceCmd(statTopicClient, "ServicePlay3")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
statTopicClient = enterMaintananceMode((endSens, startSens, pullSens, lock) => {
|
||||||
|
endSensStatus.value = endSens
|
||||||
|
startSensStatus.value = startSens
|
||||||
|
pullSensStatus.value = pullSens
|
||||||
|
lockStatus.value = lock
|
||||||
|
})
|
||||||
|
window.addEventListener('beforeunload', cleanup)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('beforeunload', cleanup)
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.status-container {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-frame {
|
||||||
|
border: 2px solid #d1d5db;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f9fafb;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #e5e7eb;
|
||||||
|
color: #6b7280;
|
||||||
|
transition: background 0.3s, color 0.3s;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item.active {
|
||||||
|
background: #10b981;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-lock,
|
||||||
|
.btn-unlock {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s, color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-lock {
|
||||||
|
background: #111827;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-lock:hover {
|
||||||
|
background: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-unlock {
|
||||||
|
background: #e5e7eb;
|
||||||
|
color: #111827;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-unlock:hover {
|
||||||
|
background: #d1d5db;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -2,12 +2,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2>Add players</h2>
|
<h2>Add players</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<input
|
<input v-model="playerInput" type="text" placeholder="Enter player name" @keydown.enter="addPlayer" />
|
||||||
v-model="playerInput"
|
|
||||||
type="text"
|
|
||||||
placeholder="Enter player name"
|
|
||||||
@keydown.enter="addPlayer"
|
|
||||||
/>
|
|
||||||
<button @click="addPlayer" type="button">Add</button>
|
<button @click="addPlayer" type="button">Add</button>
|
||||||
</div>
|
</div>
|
||||||
<ul class="player-list">
|
<ul class="player-list">
|
||||||
@@ -15,7 +10,8 @@
|
|||||||
<span class="player-name">{{ player }}</span>
|
<span class="player-name">{{ player }}</span>
|
||||||
<button class="delete-btn" @click="deletePlayer(index)" title="Delete player">
|
<button class="delete-btn" @click="deletePlayer(index)" title="Delete player">
|
||||||
<svg class="trash-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
<svg class="trash-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-9l-1 1H5v2h14V4z" fill="currentColor"/>
|
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-9l-1 1H5v2h14V4z"
|
||||||
|
fill="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
Reference in New Issue
Block a user