Compare commits
6 commits
v2.0.0
...
developmen
Author | SHA1 | Date | |
---|---|---|---|
bbad13f570 | |||
628855b5d9 | |||
b61a166649 | |||
1d62a62f33 | |||
f5265a4e1d | |||
1ddfc8c769 |
13 changed files with 144 additions and 45 deletions
40
README.md
40
README.md
|
@ -11,16 +11,9 @@ This app will send an I-Ching horoscope to the pre-configured mailhog instance i
|
||||||
|
|
||||||
This app uses the I-Ching library app https://github.com/Velfi/i-ching.git.
|
This app uses the I-Ching library app https://github.com/Velfi/i-ching.git.
|
||||||
|
|
||||||
<!--## Configuration
|
## Configuration
|
||||||
Properties of the app can be configured in the file config.json. It is possible to configure the mail host and port.
|
Properties of the app can be configured in the file config.json. It is possible to configure the mail host and port.
|
||||||
It is also possible to configure the intervall of days between the sending of horoscopes and the time of sending. The default is to send one email every seven days at 8 am.-->
|
It is also possible to configure the intervall of days between the sending of horoscopes and the time of sending. The default is to send one email every seven days at 8 am.
|
||||||
|
|
||||||
## First Start
|
|
||||||
The app can be deployed by running:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ kubectl apply -f deployment.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
This will start the deployment of the app in the pod, the service and an ingress for the HTTP frontend server and an ingress for the backend server.
|
This will start the deployment of the app in the pod, the service and an ingress for the HTTP frontend server and an ingress for the backend server.
|
||||||
When a pod with the app is initally started, one email will be sent to the configured receiver.
|
When a pod with the app is initally started, one email will be sent to the configured receiver.
|
||||||
|
@ -38,10 +31,35 @@ The backend server has a separate ingress, so API requests can get forwarded to
|
||||||
The frontend server is running inside the pod on the address http://localhost:8080/iching. It provides an HTML homepage with a button. Upon pressing the button, the backend server will be called (to send an E-Mail).
|
The frontend server is running inside the pod on the address http://localhost:8080/iching. It provides an HTML homepage with a button. Upon pressing the button, the backend server will be called (to send an E-Mail).
|
||||||
The frontend server has a separate ingress from the backend server, since the frontend server needs the 'rewrite target' annotation: So the Frontend is not running in the root folder but using the sub-path 'iching'.
|
The frontend server has a separate ingress from the backend server, since the frontend server needs the 'rewrite target' annotation: So the Frontend is not running in the root folder but using the sub-path 'iching'.
|
||||||
|
|
||||||
<!--## Testing
|
## Testing
|
||||||
The Jest unit tests can be run with
|
The Jest unit tests can be run with
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ npm test
|
$ npm test
|
||||||
```
|
```
|
||||||
-->
|
|
||||||
|
## Running the App
|
||||||
|
|
||||||
|
### Locally
|
||||||
|
The app can be deployed locally by running:
|
||||||
|
The app can be deployed by running:
|
||||||
|
```bash
|
||||||
|
$ ./start-local.sh
|
||||||
|
```
|
||||||
|
Der HTTP-Server can be access in a browser with the address http://localhost:8080/.
|
||||||
|
|
||||||
|
### Locally with Docker
|
||||||
|
The app can be deployed by running:
|
||||||
|
```bash
|
||||||
|
$ docker run -e DEPLOY_MODE=docker -p 8080:8080 -p 8090:8090 --rm my-typescript-app
|
||||||
|
```
|
||||||
|
Der HTTP-Server can be access in a browser with the address http://localhost:8080/.
|
||||||
|
|
||||||
|
### In Kubernetes
|
||||||
|
|
||||||
|
The app can be deployed by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ kubectl apply -f deployment.yaml
|
||||||
|
```
|
||||||
|
Der HTTP-Server can be access in a browser with the address https://192-168-197-2.c-one-infra.de/iching/
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*import { html } from "./broker";
|
import { html } from "./broker";
|
||||||
import { test } from "@jest/globals";
|
//import { test } from "@jest/globals";
|
||||||
|
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
@ -17,4 +17,3 @@ test("Generate HTML for images", () => {
|
||||||
expect(html("Images:\nOver the earth, the lake:\nThe image of Gathering Together.\nThus the superior man renews his weapons\nIn order to meet the unforseen."))
|
expect(html("Images:\nOver the earth, the lake:\nThe image of Gathering Together.\nThus the superior man renews his weapons\nIn order to meet the unforseen."))
|
||||||
.toBe("<p><h3>Images:</h3>Over the earth, the lake:<br>The image of Gathering Together.<br>Thus the superior man renews his weapons<br>In order to meet the unforseen.<br></p></div>");
|
.toBe("<p><h3>Images:</h3>Over the earth, the lake:<br>The image of Gathering Together.<br>Thus the superior man renews his weapons<br>In order to meet the unforseen.<br></p></div>");
|
||||||
});
|
});
|
||||||
*/
|
|
|
@ -1,13 +1,15 @@
|
||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as nodemailer from 'nodemailer';
|
import * as nodemailer from 'nodemailer';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
// Config for scheduling the next time the main process (sending of horoscope) is run
|
// Config for scheduling the next time the main process (sending of horoscope) is run
|
||||||
/*interface Config {
|
interface Config {
|
||||||
emailReceiver: string;
|
emailReceiver: string;
|
||||||
mailHost: string;
|
mailHost: string;
|
||||||
mailPort: number;
|
mailPort: number;
|
||||||
}*/
|
}
|
||||||
|
|
||||||
// Node structure for the Parse Tree (it's a degenerated tree with one or zero children per node)
|
// Node structure for the Parse Tree (it's a degenerated tree with one or zero children per node)
|
||||||
interface Node {
|
interface Node {
|
||||||
|
@ -16,13 +18,13 @@ interface Node {
|
||||||
value?: string;
|
value?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
//var config : Config;
|
var config : Config;
|
||||||
|
|
||||||
export function executeCommand(): void {
|
export function executeCommand(): void {
|
||||||
|
|
||||||
exec('iching divine', (error, stdout, stderr) => {
|
exec('iching divine', (error, stdout, stderr) => {
|
||||||
|
|
||||||
console.log(`Begin`);
|
console.log(`I-Ching-Broker: \'iching divine\' called`);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(`Error: ${error.message}`);
|
console.error(`Error: ${error.message}`);
|
||||||
|
@ -35,11 +37,11 @@ export function executeCommand(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
//Load config once
|
//Load config once
|
||||||
/*if (config == undefined) {
|
if (config == undefined) {
|
||||||
config = loadConfig();
|
config = loadConfig();
|
||||||
} */
|
}
|
||||||
|
|
||||||
console.log(`Send E-Mail`);
|
console.log(`Horoscope received; sending E-Mail next`);
|
||||||
sendEmail(stdout);
|
sendEmail(stdout);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -48,36 +50,43 @@ export function executeCommand(): void {
|
||||||
executeCommand();
|
executeCommand();
|
||||||
|
|
||||||
// Load the Configuration
|
// Load the Configuration
|
||||||
/*function loadConfig(): Config {
|
function loadConfig(): Config {
|
||||||
|
|
||||||
console.log(`Load Config`);
|
console.log(`Load Config`);
|
||||||
|
|
||||||
const data = fs.readFileSync('config.json', 'utf-8');
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const configPath = path.join(__dirname, 'config.json');
|
||||||
|
|
||||||
|
const data = fs.readFileSync(configPath, 'utf-8');
|
||||||
|
|
||||||
return JSON.parse(data);
|
return JSON.parse(data);
|
||||||
}*/
|
}
|
||||||
|
|
||||||
// Send E-Mail
|
// Send E-Mail
|
||||||
async function sendEmail(content: string) {
|
async function sendEmail(content: string) {
|
||||||
|
|
||||||
// Create Transporter
|
// Create Transporter
|
||||||
const transporter = nodemailer.createTransport({
|
const transporter = nodemailer.createTransport({
|
||||||
host: "mailhog.mailhog.svc.cluster.local", //config.mailHost,
|
host: config.mailHost, //"mailhog.mailhog.svc.cluster.local", //config.mailHost,
|
||||||
port: 1025, //config.mailPort,
|
port: config.mailPort, //1025, //config.mailPort,
|
||||||
secure: false
|
secure: false
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const info = await transporter.sendMail({
|
const info = await transporter.sendMail({
|
||||||
from: '"The Oracle" <the.oracle@holy.mountain>',
|
from: '"The Oracle" <the.oracle@holy.mountain>',
|
||||||
to: "test@mailhog.local", //config.emailReceiver,
|
to: config.emailReceiver, //"test@mailhog.local", //config.emailReceiver,
|
||||||
subject: "Your Horoscope Is Ready",
|
subject: "Your Horoscope Is Ready",
|
||||||
text: content,
|
text: content,
|
||||||
html: html(content)
|
html: html(content)
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("E-Mail sent: ", info.messageId + "\n");
|
console.log("E-Mail sent: ", info.messageId + "\n\n");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error Sending E-Mail:", error);
|
console.error("Error Sending E-Mail:", error + "\n\n");
|
||||||
|
|
||||||
|
console.log("Failed to send this horoscope: \n", content + "\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +128,7 @@ function parse(input: string): Node {
|
||||||
const changingLines: Node = { type: "ChangingLines"};
|
const changingLines: Node = { type: "ChangingLines"};
|
||||||
currentNode.child = changingLines;
|
currentNode.child = changingLines;
|
||||||
currentNode = changingLines;
|
currentNode = changingLines;
|
||||||
currentNode.value = line; // + "<br>"; TODO: try without this <br>
|
currentNode.value = line;
|
||||||
} else {
|
} else {
|
||||||
currentNode.value = currentNode.value + line + "<br>";
|
currentNode.value = currentNode.value + line + "<br>";
|
||||||
}
|
}
|
||||||
|
@ -132,13 +141,11 @@ function parse(input: string): Node {
|
||||||
function render(node: Node): string {
|
function render(node: Node): string {
|
||||||
|
|
||||||
if (node == undefined) {
|
if (node == undefined) {
|
||||||
console.log("Rendering of nodes finished!")
|
console.log("I-Ching-Broker: Rendering of nodes finished!")
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Render node" + node.type);
|
|
||||||
|
|
||||||
var outputHTML: string = "";
|
var outputHTML: string = "";
|
||||||
|
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
|
@ -161,7 +168,6 @@ function render(node: Node): string {
|
||||||
case "ChangingLines" :
|
case "ChangingLines" :
|
||||||
const regex = new RegExp("~", "g");
|
const regex = new RegExp("~", "g");
|
||||||
node.value = node.value?.replace(regex, "");
|
node.value = node.value?.replace(regex, "");
|
||||||
//outputHTML = "<br><p><div style=\"border: 1px dashed gray; border-radius: 10px; padding-left: 10px; padding-right: 10px; padding-top: 10px; width: auto; display: inline-block; color: gray; text-align: center; box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.2);\">" + node.value + "</div></p><br>";
|
|
||||||
outputHTML = "<br><br><p><div style=\"padding-left: 20px; padding-right: 20px; padding-top: 20px; display: inline-block; color: gray; text-align: center;\">" + node.value + "</div></p><br>";
|
outputHTML = "<br><br><p><div style=\"padding-left: 20px; padding-right: 20px; padding-top: 20px; display: inline-block; color: gray; text-align: center;\">" + node.value + "</div></p><br>";
|
||||||
outputHTML = outputHTML + render(node.child!);
|
outputHTML = outputHTML + render(node.child!);
|
||||||
return outputHTML;
|
return outputHTML;
|
||||||
|
|
|
@ -8,10 +8,13 @@ const port = 8090;
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
|
|
||||||
app.post('/iching/api/command', (req, res) => {
|
app.post('/iching/api/command', (req, res) => {
|
||||||
|
//TODO no logging from inside this method???
|
||||||
|
console.log(`Backend-Server: receiving POST to /iching/api/command`);
|
||||||
|
|
||||||
executeCommand();
|
executeCommand();
|
||||||
res.status(200).send('Command executed\n');
|
res.status(200).send('Backend-Server: Broker was called\n');
|
||||||
});
|
});
|
||||||
|
|
||||||
app.listen(port, '0.0.0.0', () => {
|
app.listen(port, '0.0.0.0', () => {
|
||||||
console.log(`Server läuft auf http://0.0.0.0:${port}`);
|
console.log(`Backend-Server running on http://0.0.0.0:${port}`);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +1,11 @@
|
||||||
|
#env:
|
||||||
|
# - name: DEPLOY_MODE
|
||||||
|
# value: "kubernetes"
|
||||||
|
# - name: BACKEND_PORT
|
||||||
|
# value: "8090"
|
||||||
|
|
||||||
|
#---
|
||||||
|
|
||||||
# --- DEPLOYMENT --------------------------------------------
|
# --- DEPLOYMENT --------------------------------------------
|
||||||
|
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
|
|
1
frontend/deploy.js
Normal file
1
frontend/deploy.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
window.DEPLOY_MODE = "k8s"
|
|
@ -1,10 +1,20 @@
|
||||||
function handleClick(): void {
|
function handleClick(): void {
|
||||||
console.log("Der Button wurde geklickt!");
|
console.log("Der Button wurde geklickt!");
|
||||||
|
|
||||||
fetch("/iching/api/command", { method: "POST" })
|
//const deployMode = (window as { DEPLOY_MODE?: 'docker' | 'k8s' }).DEPLOY_MODE ?? 'k8s';
|
||||||
|
const deployMode = (window as { DEPLOY_MODE?: 'docker' | 'k8s' | 'local' }).DEPLOY_MODE ?? 'k8s';
|
||||||
|
|
||||||
|
const endpoint =
|
||||||
|
deployMode === 'docker' || deployMode === 'local'
|
||||||
|
? 'http://localhost:8090/iching/api/command'
|
||||||
|
: '/iching/api/command';
|
||||||
|
|
||||||
|
console.log(`DEPLOY_MODE=${deployMode}, sende POST an: ${endpoint}`);
|
||||||
|
|
||||||
|
fetch(endpoint, { method: 'POST' })
|
||||||
.then(res => res.text())
|
.then(res => res.text())
|
||||||
.then(text => console.log("Server sagt:", text))
|
.then(text => console.log("HTTP-Server says:", text))
|
||||||
.catch(error => console.error("Fehler:", error));
|
.catch(error => console.error("HTTP-Server - Error:", error));
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|
|
@ -53,12 +53,15 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>🧘 My I-Ging Horoscope</h1>
|
<h1>Do you want to know the future?</h1>
|
||||||
|
<h2> ☝️ Look into the future with your personal I-Ging Horoscope! ☝️</h2>
|
||||||
<p>Click on the Button to receive your personal Horoscope. 100% accuracy guaranteed!</p>
|
<p>Click on the Button to receive your personal Horoscope. 100% accuracy guaranteed!</p>
|
||||||
<button id="myButton">✨ Receive Horoscope ✨</button>
|
<button id="myButton">✨ Receive Horoscope ✨</button>
|
||||||
<div class="footer">With Love & Fortune from the Cosmos 💫</div>
|
<div class="footer">🧘 With Love & Fortune from the Cosmos 💫</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="./deploy.js"></script>
|
||||||
|
|
||||||
<script type="module" src="./event.mjs"></script>
|
<script type="module" src="./event.mjs"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -24,5 +24,9 @@
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
"nodemailer": "^6.10.0"
|
"nodemailer": "^6.10.0"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"preset": "ts-jest",
|
||||||
|
"testEnvironment": "node"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
38
start-local.sh
Executable file
38
start-local.sh
Executable file
|
@ -0,0 +1,38 @@
|
||||||
|
|
||||||
|
# build
|
||||||
|
#npm install
|
||||||
|
#npx tsc -p tsconfig.backend.json
|
||||||
|
#npx tsc -p tsconfig.frontend.json
|
||||||
|
|
||||||
|
#npm run start:local
|
||||||
|
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Exit on error
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# 1. Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 2. Compile backend and frontend
|
||||||
|
npx tsc -p tsconfig.backend.json
|
||||||
|
npx tsc -p tsconfig.frontend.json
|
||||||
|
|
||||||
|
# 3. Ensure dist/frontend exists and copy index.html
|
||||||
|
mkdir -p dist/frontend
|
||||||
|
cp frontend/index.html dist/frontend/
|
||||||
|
|
||||||
|
mkdir -p dist/backend
|
||||||
|
cp backend/config.json dist/backend/
|
||||||
|
|
||||||
|
# 4. Rename compiled frontend event.js → event.mjs
|
||||||
|
find dist/frontend -name "event.js" -exec bash -c 'mv "$0" "${0%.js}.mjs"' {} \;
|
||||||
|
|
||||||
|
# 5. Inject deploy mode
|
||||||
|
echo "window.DEPLOY_MODE = 'local';" > dist/frontend/deploy.js
|
||||||
|
|
||||||
|
# 6. Start backend
|
||||||
|
node dist/backend/server.js &
|
||||||
|
|
||||||
|
# 7. Start frontend
|
||||||
|
npx http-server dist/frontend -p 8080 --cors --mime application/javascript=js
|
10
start.sh
Normal file → Executable file
10
start.sh
Normal file → Executable file
|
@ -1,3 +1,13 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Setze Standard-Wert auf "k8s" wenn DEPLOY_MODE nicht gesetzt ist
|
||||||
|
DEPLOY_MODE=${DEPLOY_MODE:-k8s}
|
||||||
|
|
||||||
|
echo "DEPLOY_MODE ist: $DEPLOY_MODE"
|
||||||
|
|
||||||
|
# Injektiere ins Frontend
|
||||||
|
echo "window.DEPLOY_MODE = \"$DEPLOY_MODE\";" > dist/frontend/deploy.js
|
||||||
|
|
||||||
# start backend in the background
|
# start backend in the background
|
||||||
node dist/backend/server.js &
|
node dist/backend/server.js &
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["backend/**/*.ts"],
|
||||||
"backend/**/*.ts"
|
"exclude": ["backend/**/*.test.ts"]
|
||||||
]
|
|
||||||
}
|
}
|
Loading…
Reference in a new issue