init
This commit is contained in:
commit
5d0cd02c21
19
.editorconfig
Normal file
19
.editorconfig
Normal file
@ -0,0 +1,19 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[package.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
|
||||
[docker-compose.yml]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
182
.gitignore
vendored
Normal file
182
.gitignore
vendored
Normal file
@ -0,0 +1,182 @@
|
||||
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||
|
||||
# Logs
|
||||
|
||||
logs
|
||||
_.log
|
||||
npm-debug.log_
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Caches
|
||||
|
||||
.cache
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# Runtime data
|
||||
|
||||
pids
|
||||
_.pid
|
||||
_.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
|
||||
# Misc
|
||||
config.ini
|
||||
*.mdb
|
||||
*.exe
|
||||
/.*.tmp
|
||||
/grafana
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024-present Snazzah
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
BIN
client/bun.lockb
Normal file
BIN
client/bun.lockb
Normal file
Binary file not shown.
107
client/index.ts
Normal file
107
client/index.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import debounce from 'just-debounce-it';
|
||||
import { watch } from 'node:fs';
|
||||
import { stat, readFile, writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import readline from 'node:readline/promises';
|
||||
import { parse } from 'ini';
|
||||
import MDBReader from 'mdb-reader';
|
||||
import { diff } from 'just-diff';
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
async function promptToClose(message: string) {
|
||||
await rl.question(`== ${message}\n== Press enter to quit.`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Check for config.ini file
|
||||
const configPath = join(process.cwd(), 'config.ini');
|
||||
try {
|
||||
const configStat = await stat(configPath);
|
||||
if (configStat.isDirectory()) await promptToClose('The config file must be a file.');
|
||||
} catch (e) {
|
||||
await writeFile(configPath, [
|
||||
'; The database file to upload, defaults to C:\\Users\\<user>\\Documents\\Affirmatech\\N3FJP Software\\ARRL-Field-Day\\LogData.mdb',
|
||||
`; database_file = "${process.env.USERNAME ? `C:\\Users\\${process.env.USERNAME}\\Documents\\Affirmatech\\N3FJP Software\\ARRL-Field-Day\\LogData.mdb` : ''}"`,
|
||||
'',
|
||||
'; The endpoint to upload to',
|
||||
'log_endpoint = "https://example.local/up"'
|
||||
].join('\n'))
|
||||
await promptToClose('Failed to find config file. A new one was created, so put in your variables in there.');
|
||||
}
|
||||
|
||||
let databaseFile: string;
|
||||
let logEndpoint: string;
|
||||
try {
|
||||
const config = parse(await readFile(configPath, { encoding: 'utf-8' }));
|
||||
databaseFile = config.database_file;
|
||||
logEndpoint = config.log_endpoint;
|
||||
if (!logEndpoint) await promptToClose('A log endpoint is required in the config file.');
|
||||
} catch (e) {
|
||||
databaseFile = '';
|
||||
await promptToClose('Failed to parse config file. You can remove the file and re-run this program if more problems occur.');
|
||||
}
|
||||
|
||||
const dbPath = databaseFile ?? process.env.USERNAME ? join('C:/Users', process.env.USERNAME!, 'Documents/Affirmatech/N3FJP Software/ARRL-Field-Day/LogData.mdb') : '';
|
||||
|
||||
// Check for log file
|
||||
try {
|
||||
if (!dbPath) await promptToClose('A log path is needed to use this program.');
|
||||
const fileStat = await stat(dbPath);
|
||||
if (fileStat.isDirectory()) await promptToClose('The log path must point to a file.');
|
||||
console.log(`\n== Following changes for the file "${dbPath}"\n== Ctrl-C to exit.\n`);
|
||||
} catch (e) {
|
||||
await promptToClose(`Failed to find log file at "${dbPath}"`);
|
||||
}
|
||||
|
||||
// Upload logic
|
||||
let lastData: Record<string, any> = {};
|
||||
async function _upload() {
|
||||
console.log('-- Reading log...');
|
||||
const reader = new MDBReader(await readFile(dbPath));
|
||||
if (!reader.getTableNames().includes('tblContacts'))
|
||||
return console.log('!! No "tblContacts" table present.');
|
||||
|
||||
const table = reader.getTable("tblContacts");
|
||||
const tableData = table.getData<Record<string, string>>();
|
||||
|
||||
const data = tableData.reduce((p, r) => ({ ...p, [r.fldPrimaryKey]: r }), {} as Record<string, any>);
|
||||
const differences = diff(lastData, data);
|
||||
const changedIds = [...new Set(differences.filter((r) => r.op !== 'remove').map((r) => r.path[0]))];
|
||||
const changedRows = changedIds.map((id) => data[id]);
|
||||
|
||||
if (changedIds.length === 0) return void console.log('-- No changes found.');
|
||||
|
||||
console.log(`-> Uploading ${changedIds.length.toLocaleString()} row(s)...`);
|
||||
const response = await fetch(logEndpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(changedRows),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
console.log(`<- Recieved ${response.status} (${response.statusText})`);
|
||||
if (response.status === 200) lastData = data;
|
||||
}
|
||||
|
||||
const upload = debounce(_upload, 250);
|
||||
|
||||
const watcher = watch(dbPath, (event, filename) => {
|
||||
if (event === 'rename') return void console.log(`== ${filename} was renamed, something might break`);
|
||||
upload();
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
// close watcher when Ctrl-C is pressed
|
||||
console.log('\n== Closing watcher...');
|
||||
watcher.close();
|
||||
|
||||
process.exit(0);
|
||||
});
|
||||
rl.close();
|
||||
|
||||
// "Upload first" option
|
||||
if (process.argv[2] === '--upload') _upload();
|
22
client/package.json
Normal file
22
client/package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "fdlogup-client",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"exe": "bun build --compile --target=bun-windows-x64 ./index.ts --minify --outfile fdlogup-client",
|
||||
"start": "bun run ./index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/ini": "^4.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ini": "^4.1.3",
|
||||
"just-debounce-it": "^3.2.0",
|
||||
"just-diff": "^6.0.2",
|
||||
"mdb-reader": "^3.0.0"
|
||||
}
|
||||
}
|
27
client/tsconfig.json
Normal file
27
client/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Enable latest features
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
34
docker-compose.yml
Normal file
34
docker-compose.yml
Normal file
@ -0,0 +1,34 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '5432:5432'
|
||||
environment:
|
||||
POSTGRES_PASSWORD: fdlogup
|
||||
POSTGRES_USER: fdlogup
|
||||
POSTGRES_DB: fdlogup
|
||||
volumes:
|
||||
- "postgres:/var/lib/postgresql/data"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U fdlogup -d fdlogup"]
|
||||
interval: "10s"
|
||||
timeout: "5s"
|
||||
retries: 5
|
||||
start_period: "5s"
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '3000:3000'
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- GF_INSTALL_PLUGINS=orchestracities-map-panel
|
||||
volumes:
|
||||
- ./grafana:/var/lib/grafana
|
||||
|
||||
volumes:
|
||||
postgres:
|
||||
grafana:
|
BIN
server/bun.lockb
Normal file
BIN
server/bun.lockb
Normal file
Binary file not shown.
58
server/index.ts
Normal file
58
server/index.ts
Normal file
@ -0,0 +1,58 @@
|
||||
// import { InfluxDB, Point } from '@influxdata/influxdb-client';
|
||||
import pg from 'pg';
|
||||
const { Client } = pg;
|
||||
const client = new Client(process.env.DATABASE_URL);
|
||||
await client.connect();
|
||||
|
||||
// export const influx = new InfluxDB({ url: process.env.INFLUX_URL!, token: process.env.INFLUX_TOKEN! });
|
||||
// export const writeApi = influx.getWriteApi(process.env.INFLUX_ORG!, process.env.INFLUX_BUCKET!, 's');
|
||||
|
||||
const fields = ['id', 'call', 'prefix', 'mode', 'band', 'state', 'section', 'country', 'initials', 'operator', 'class', 'computer', 'time'];
|
||||
|
||||
const server = Bun.serve({
|
||||
hostname: process.env.HOST || '0.0.0.0',
|
||||
port: process.env.PORT ? parseInt(process.env.PORT) : 9992,
|
||||
async fetch (req) {
|
||||
const path = new URL(req.url).pathname;
|
||||
console.log('-> Recieved logs, parsing...');
|
||||
if (req.method !== 'POST') new Response('Invalid method', { status: 405 });
|
||||
if (req.headers.get('Content-Type') !== 'application/json') new Response('Invalid content type', { status: 404 });
|
||||
|
||||
try {
|
||||
const newRows: any[] = await req.json();
|
||||
|
||||
const query = `INSERT INTO contacts (${fields.join(', ')}) VALUES (${fields.map((_, i) => `$${i + 1}`).join(', ')}) ON CONFLICT (id) DO UPDATE SET ${fields.map((f, i) => `${f} = $${i + 1}`).slice(1).join(', ')}`
|
||||
console.log(query);
|
||||
|
||||
for (const row of newRows) {
|
||||
const data: Record<string, string> = {
|
||||
id: row.fldPrimaryKey,
|
||||
call: row.fldCall,
|
||||
prefix: row.fldPrefix,
|
||||
mode: row.fldModeContest,
|
||||
band: row.fldBand,
|
||||
state: row.fldState,
|
||||
section: row.fldSection,
|
||||
country: row.fldCountryWorked,
|
||||
initials: row.fldInitials,
|
||||
operator: row.fldOperator,
|
||||
class: row.fldClass,
|
||||
computer: row.fldComputerName,
|
||||
time: `${row.fldDateStr.replaceAll('/', '-')} ${row.fldTimeOnStr}+00`
|
||||
};
|
||||
await client.query(query, fields.map((f) => data[f]));
|
||||
}
|
||||
|
||||
// await writeApi.close();
|
||||
console.log(`<- Wrote ${newRows.length.toLocaleString()} row(s)!`);
|
||||
} catch (e) {
|
||||
console.log('!! Failed to push');
|
||||
console.error(e);
|
||||
new Response('Server error', { status: 500 });
|
||||
}
|
||||
|
||||
return new Response('Okay!');
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`Listening on ${server.url}`);
|
19
server/package.json
Normal file
19
server/package.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "fdlogup-server",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun --hot run ./index.ts",
|
||||
"start": "bun run ./index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/pg": "^8.11.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"pg": "^8.12.0"
|
||||
}
|
||||
}
|
82
server/schema.sql
Normal file
82
server/schema.sql
Normal file
File diff suppressed because one or more lines are too long
27
server/tsconfig.json
Normal file
27
server/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Enable latest features
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user