commit 354c7a9d99cfb5feb4b7adce587c003627809689 Author: rouggy Date: Tue Mar 17 20:20:23 2026 +0100 first commit diff --git a/app.go b/app.go new file mode 100644 index 0000000..af53038 --- /dev/null +++ b/app.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "fmt" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called when the app starts. The context is saved +// so we can call the runtime methods +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +// Greet returns a greeting for the given name +func (a *App) Greet(name string) string { + return fmt.Sprintf("Hello %s, It's show time!", name) +} diff --git a/frontend/.vscode/extensions.json b/frontend/.vscode/extensions.json new file mode 100644 index 0000000..b869ef8 --- /dev/null +++ b/frontend/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "svelte.svelte-vscode" + ] +} diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..a346289 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,63 @@ +# Svelte + Vite + +This template should help get you started developing with Svelte in Vite. + +## Recommended IDE Setup + +[VS Code](https://code.visualstudio.com/) + ++ [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). + +## Need an official Svelte framework? + +Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its +serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, +and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. + +## Technical considerations + +**Why use this over SvelteKit?** + +- It brings its own routing solution which might not be preferable for some users. +- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. + `vite dev` and `vite build` wouldn't work in a SvelteKit environment, for example. + +This template contains as little as possible to get started with Vite + Svelte, while taking into account the developer +experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` +templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. + +Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been +structured similarly to SvelteKit so that it is easy to migrate. + +**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?** + +Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash +references keeps the default TypeScript setting of accepting type information from the entire workspace, while also +adding `svelte` and `vite/client` type information. + +**Why include `.vscode/extensions.json`?** + +Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to +install the recommended extension upon opening the project. + +**Why enable `checkJs` in the JS template?** + +It is likely that most cases of changing variable types in runtime are likely to be accidental, rather than deliberate. +This provides advanced typechecking out of the box. Should you like to take advantage of the dynamically-typed nature of +JavaScript, it is trivial to change the configuration. + +**Why is HMR not preserving my local component state?** + +HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` +and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the +details [here](https://github.com/rixo/svelte-hmr#svelte-hmr). + +If you have state that's important to retain within a component, consider creating an external store which would not be +replaced by HMR. + +```js +// store.js +// An extremely simple external store +import { writable } from 'svelte/store' +export default writable(0) +``` diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..3be39ed --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + FlexDXCluster2 + + +
+ + + diff --git a/frontend/jsconfig.json b/frontend/jsconfig.json new file mode 100644 index 0000000..3918b4f --- /dev/null +++ b/frontend/jsconfig.json @@ -0,0 +1,38 @@ +{ + "compilerOptions": { + "moduleResolution": "Node", + "target": "ESNext", + "module": "ESNext", + /** + * svelte-preprocess cannot figure out whether you have + * a value or a type, so tell TypeScript to enforce using + * `import type` instead of `import` for Types. + */ + "importsNotUsedAsValues": "error", + "isolatedModules": true, + "resolveJsonModule": true, + /** + * To have warnings / errors of the Svelte compiler at the + * correct position, enable source maps by default. + */ + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable this if you'd like to use dynamic types. + */ + "checkJs": true + }, + /** + * Use global.d.ts instead of compilerOptions.types + * to avoid limiting type declarations. + */ + "include": [ + "src/**/*.d.ts", + "src/**/*.js", + "src/**/*.svelte" + ] +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..699e13a --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,781 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^1.0.1", + "svelte": "^3.49.0", + "vite": "^3.0.7" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", + "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz", + "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.4.0.tgz", + "integrity": "sha512-6QupI/jemMfK+yI2pMtJcu5iO2gtgTfcBdGwMZZt+lgbFELhszbDl6Qjh000HgAV8+XUA+8EY8DusOFk8WhOIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "deepmerge": "^4.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.26.7", + "svelte-hmr": "^0.15.1", + "vitefu": "^0.2.2" + }, + "engines": { + "node": "^14.18.0 || >= 16" + }, + "peerDependencies": { + "svelte": "^3.44.0", + "vite": "^3.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/esbuild": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", + "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.15.18", + "@esbuild/linux-loong64": "0.15.18", + "esbuild-android-64": "0.15.18", + "esbuild-android-arm64": "0.15.18", + "esbuild-darwin-64": "0.15.18", + "esbuild-darwin-arm64": "0.15.18", + "esbuild-freebsd-64": "0.15.18", + "esbuild-freebsd-arm64": "0.15.18", + "esbuild-linux-32": "0.15.18", + "esbuild-linux-64": "0.15.18", + "esbuild-linux-arm": "0.15.18", + "esbuild-linux-arm64": "0.15.18", + "esbuild-linux-mips64le": "0.15.18", + "esbuild-linux-ppc64le": "0.15.18", + "esbuild-linux-riscv64": "0.15.18", + "esbuild-linux-s390x": "0.15.18", + "esbuild-netbsd-64": "0.15.18", + "esbuild-openbsd-64": "0.15.18", + "esbuild-sunos-64": "0.15.18", + "esbuild-windows-32": "0.15.18", + "esbuild-windows-64": "0.15.18", + "esbuild-windows-arm64": "0.15.18" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz", + "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz", + "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz", + "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz", + "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz", + "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz", + "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz", + "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz", + "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz", + "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz", + "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz", + "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz", + "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz", + "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz", + "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz", + "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz", + "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz", + "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz", + "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", + "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", + "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/magic-string": { + "version": "0.26.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.7.tgz", + "integrity": "sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", + "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "3.59.2", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.59.2.tgz", + "integrity": "sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/svelte-hmr": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz", + "integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^12.20 || ^14.13.1 || >= 16" + }, + "peerDependencies": { + "svelte": "^3.19.0 || ^4.0.0" + } + }, + "node_modules/vite": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.11.tgz", + "integrity": "sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.15.9", + "postcss": "^8.4.18", + "resolve": "^1.22.1", + "rollup": "^2.79.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", + "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..8c9ae62 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,16 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^1.0.1", + "svelte": "^3.49.0", + "vite": "^3.0.7" + } +} \ No newline at end of file diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 new file mode 100644 index 0000000..0a2df21 --- /dev/null +++ b/frontend/package.json.md5 @@ -0,0 +1 @@ +d9dc84f0d17ed164f36dd584057aae68 \ No newline at end of file diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte new file mode 100644 index 0000000..2a2ce22 --- /dev/null +++ b/frontend/src/App.svelte @@ -0,0 +1,79 @@ + + +
+ +
{resultText}
+
+ + +
+
+ + diff --git a/frontend/src/assets/fonts/OFL.txt b/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 0000000..9cac04c --- /dev/null +++ b/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 0000000..2f9cc59 Binary files /dev/null and b/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/frontend/src/assets/images/logo-universal.png b/frontend/src/assets/images/logo-universal.png new file mode 100644 index 0000000..d63303b Binary files /dev/null and b/frontend/src/assets/images/logo-universal.png differ diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..95c41a5 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,8 @@ +import './style.css' +import App from './App.svelte' + +const app = new App({ + target: document.getElementById('app') +}) + +export default app diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..3940d6c --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,26 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..4078e74 --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..d37616f --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,7 @@ +import {defineConfig} from 'vite' +import {svelte} from '@sveltejs/vite-plugin-svelte' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [svelte()] +}) diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts new file mode 100644 index 0000000..02a3bb9 --- /dev/null +++ b/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,4 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Ă‚ MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1:string):Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js new file mode 100644 index 0000000..c71ae77 --- /dev/null +++ b/frontend/wailsjs/go/main/App.js @@ -0,0 +1,7 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Ă‚ MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1) { + return window['go']['main']['App']['Greet'](arg1); +} diff --git a/frontend/wailsjs/runtime/package.json b/frontend/wailsjs/runtime/package.json new file mode 100644 index 0000000..1e7c8a5 --- /dev/null +++ b/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/frontend/wailsjs/runtime/runtime.d.ts b/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 0000000..4445dac --- /dev/null +++ b/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,249 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width : number + height : number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string, ...additionalEventNames: string[]): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen) +// Returns the state of the window, i.e. whether the window is in full screen mode or not. +export function WindowIsFullscreen(): Promise; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised) +// Returns the state of the window, i.e. whether the window is maximised or not. +export function WindowIsMaximised(): Promise; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised) +// Returns the state of the window, i.e. whether the window is minimised or not. +export function WindowIsMinimised(): Promise; + +// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal) +// Returns the state of the window, i.e. whether the window is normal or not. +export function WindowIsNormal(): Promise; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; + +// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext) +// Returns the current text stored on clipboard +export function ClipboardGetText(): Promise; + +// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext) +// Sets a text on the clipboard +export function ClipboardSetText(text: string): Promise; + +// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop) +// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. +export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void + +// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff) +// OnFileDropOff removes the drag and drop listeners and handlers. +export function OnFileDropOff() :void + +// Check if the file path resolver is available +export function CanResolveFilePaths(): boolean; + +// Resolves file paths for an array of files +export function ResolveFilePaths(files: File[]): void \ No newline at end of file diff --git a/frontend/wailsjs/runtime/runtime.js b/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 0000000..7cb89d7 --- /dev/null +++ b/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,242 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + return EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName, ...additionalEventNames) { + return window.runtime.EventsOff(eventName, ...additionalEventNames); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + return EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowIsFullscreen() { + return window.runtime.WindowIsFullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowIsMaximised() { + return window.runtime.WindowIsMaximised(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function WindowIsMinimised() { + return window.runtime.WindowIsMinimised(); +} + +export function WindowIsNormal() { + return window.runtime.WindowIsNormal(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} + +export function ClipboardGetText() { + return window.runtime.ClipboardGetText(); +} + +export function ClipboardSetText(text) { + return window.runtime.ClipboardSetText(text); +} + +/** + * Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * + * @export + * @callback OnFileDropCallback + * @param {number} x - x coordinate of the drop + * @param {number} y - y coordinate of the drop + * @param {string[]} paths - A list of file paths. + */ + +/** + * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. + * + * @export + * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target) + */ +export function OnFileDrop(callback, useDropTarget) { + return window.runtime.OnFileDrop(callback, useDropTarget); +} + +/** + * OnFileDropOff removes the drag and drop listeners and handlers. + */ +export function OnFileDropOff() { + return window.runtime.OnFileDropOff(); +} + +export function CanResolveFilePaths() { + return window.runtime.CanResolveFilePaths(); +} + +export function ResolveFilePaths(files) { + return window.runtime.ResolveFilePaths(files); +} \ No newline at end of file diff --git a/internal/bands/bands.go b/internal/bands/bands.go new file mode 100644 index 0000000..525325d --- /dev/null +++ b/internal/bands/bands.go @@ -0,0 +1,132 @@ +package bands + +import ( + "fmt" + "strconv" + "strings" +) + +type BandDefinition struct { + Name string + MinFreqMHz float64 + MaxFreqMHz float64 + UseUSB bool +} + +var AmateurBands = []BandDefinition{ + {Name: "160M", MinFreqMHz: 1.800, MaxFreqMHz: 2.000, UseUSB: false}, + {Name: "80M", MinFreqMHz: 3.500, MaxFreqMHz: 3.800, UseUSB: false}, + {Name: "60M", MinFreqMHz: 5.330, MaxFreqMHz: 5.405, UseUSB: false}, + {Name: "40M", MinFreqMHz: 7.000, MaxFreqMHz: 7.300, UseUSB: false}, + {Name: "30M", MinFreqMHz: 10.100, MaxFreqMHz: 10.150, UseUSB: true}, + {Name: "20M", MinFreqMHz: 14.000, MaxFreqMHz: 14.350, UseUSB: true}, + {Name: "17M", MinFreqMHz: 18.068, MaxFreqMHz: 18.168, UseUSB: true}, + {Name: "15M", MinFreqMHz: 21.000, MaxFreqMHz: 21.450, UseUSB: true}, + {Name: "12M", MinFreqMHz: 24.890, MaxFreqMHz: 24.990, UseUSB: true}, + {Name: "10M", MinFreqMHz: 28.000, MaxFreqMHz: 29.700, UseUSB: true}, + {Name: "6M", MinFreqMHz: 50.000, MaxFreqMHz: 54.000, UseUSB: true}, + {Name: "QO-100", MinFreqMHz: 10489.500, MaxFreqMHz: 10490.000, UseUSB: true}, +} + +func FrequencyToBand(freqMHz float64) string { + for _, band := range AmateurBands { + if freqMHz >= band.MinFreqMHz && freqMHz < band.MaxFreqMHz { + return band.Name + } + } + return "N/A" +} + +// FrequencyKHzToBand convertit depuis kHz (format interne du projet) +func FrequencyKHzToBand(freqKHz float64) string { + return FrequencyToBand(freqKHz / 1000.0) +} + +func FrequencyStringToBand(freqStr string) string { + freqMHz, err := strconv.ParseFloat(freqStr, 64) + if err != nil { + return "N/A" + } + return FrequencyToBand(freqMHz) +} + +func GetBandDefinition(bandName string) *BandDefinition { + for _, band := range AmateurBands { + if band.Name == bandName { + return &band + } + } + return nil +} + +func IsUSBBand(bandName string) bool { + band := GetBandDefinition(bandName) + if band == nil { + return false + } + return band.UseUSB +} + +func IsLSBBand(bandName string) bool { + band := GetBandDefinition(bandName) + if band == nil { + return false + } + return !band.UseUSB +} + +func NormalizeSSBMode(mode string, band string) string { + if mode != "SSB" { + return mode + } + if IsUSBBand(band) { + return "USB" + } + return "LSB" +} + +func NormalizeSSBModeByFrequency(mode string, freqMHz float64) string { + if mode != "SSB" { + return mode + } + band := FrequencyToBand(freqMHz) + return NormalizeSSBMode(mode, band) +} + +func GetAllBandNames() []string { + names := make([]string, len(AmateurBands)) + for i, band := range AmateurBands { + names[i] = band.Name + } + return names +} + +func IsBandValid(bandName string) bool { + return GetBandDefinition(bandName) != nil +} + +func GetBandFrequencyRange(bandName string) string { + band := GetBandDefinition(bandName) + if band == nil { + return "" + } + return fmt.Sprintf("%.3f - %.3f MHz", band.MinFreqMHz, band.MaxFreqMHz) +} + +// FreqMHzString formate une frĂ©quence kHz en string MHz pour FlexRadio +func FreqMHzString(freqKHz float64) string { + return fmt.Sprintf("%.6f", freqKHz/1000.0) +} + +// GetBandFromFrequencyString gère kHz et MHz automatiquement +func GetBandFromFrequencyString(freqStr string) string { + freqStr = strings.TrimSpace(freqStr) + freqMHz, err := strconv.ParseFloat(freqStr, 64) + if err != nil { + return "N/A" + } + if freqMHz > 1000 { + freqMHz = freqMHz / 1000.0 + } + return FrequencyToBand(freqMHz) +} diff --git a/internal/cluster/client.go b/internal/cluster/client.go new file mode 100644 index 0000000..81219e3 --- /dev/null +++ b/internal/cluster/client.go @@ -0,0 +1,332 @@ +package cluster + +import ( + "bufio" + "context" + "fmt" + "math" + "net" + "strings" + "sync" + "time" + + "github.com/user/flexdxcluster2/internal/spot" +) + +const ( + MaxReconnectAttempts = 10 + BaseReconnectDelay = 1 * time.Second + MaxReconnectDelay = 60 * time.Second + ConnectionTimeout = 10 * time.Second + ReadTimeout = 5 * time.Minute +) + +// Config reprend ClusterConfig de l'ancien code +type Config struct { + Name string + Server string + Port string + Login string + Password string + Skimmer bool + FT8 bool + FT4 bool + Beacon bool + Command string + LoginPrompt string + Enabled bool + Master bool + Type string // dxspider, cc_cluster, ar_cluster — vide = auto +} + +// Client gère la connexion TCP Ă  un cluster DX +type Client struct { + cfg Config + conn net.Conn + reader *bufio.Reader + writer *bufio.Writer + mu sync.Mutex + loggedIn bool + clusterType string + ctx context.Context + cancel context.CancelFunc + reconnects int + + // SpotChan reçoit les spots parsĂ©s — consommĂ© par le SpotProcessor + SpotChan chan *spot.Spot + // ConsoleChan reçoit toutes les lignes brutes — pour l'onglet Console + ConsoleChan chan string + // CmdChan reçoit les commandes Ă  envoyer au cluster depuis l'UI + CmdChan chan string +} + +func New(cfg Config) *Client { + ctx, cancel := context.WithCancel(context.Background()) + return &Client{ + cfg: cfg, + clusterType: cfg.Type, + ctx: ctx, + cancel: cancel, + SpotChan: make(chan *spot.Spot, 200), + ConsoleChan: make(chan string, 200), + CmdChan: make(chan string, 100), + } +} + +func (c *Client) Name() string { return c.cfg.Name } +func (c *Client) IsMaster() bool { return c.cfg.Master } +func (c *Client) ClusterType() string { return c.clusterType } + +// Start dĂ©marre le client avec reconnexion automatique +func (c *Client) Start() { + // Goroutine commandes UI → cluster + go c.commandLoop() + + for { + select { + case <-c.ctx.Done(): + return + default: + } + + if err := c.connect(); err != nil { + c.reconnects++ + if c.reconnects >= MaxReconnectAttempts { + return + } + delay := c.backoff() + select { + case <-c.ctx.Done(): + return + case <-time.After(delay): + continue + } + } + + c.readLoop() + c.loggedIn = false + time.Sleep(2 * time.Second) + } +} + +func (c *Client) Close() { + c.cancel() + if c.conn != nil { + c.write([]byte("bye\r\n")) + c.conn.Close() + } +} + +// ReloadFilters renvoie les commandes de filtres au cluster +func (c *Client) ReloadFilters() { + if c.loggedIn { + c.setFilters() + } +} + +// SendCommand envoie une commande arbitraire au cluster depuis l'UI +func (c *Client) SendCommand(cmd string) { + select { + case c.CmdChan <- cmd: + default: + } +} + +func (c *Client) connect() error { + addr := c.cfg.Server + ":" + c.cfg.Port + conn, err := net.DialTimeout("tcp", addr, ConnectionTimeout) + if err != nil { + return fmt.Errorf("connect %s: %w", addr, err) + } + c.conn = conn + c.reader = bufio.NewReader(conn) + c.writer = bufio.NewWriter(conn) + c.loggedIn = false + c.reconnects = 0 + return nil +} + +func (c *Client) commandLoop() { + for { + select { + case <-c.ctx.Done(): + return + case cmd := <-c.CmdChan: + c.write([]byte(cmd + "\r\n")) + } + } +} + +func (c *Client) readLoop() { + defer c.conn.Close() + + for { + select { + case <-c.ctx.Done(): + return + default: + } + + if !c.loggedIn { + c.conn.SetReadDeadline(time.Now().Add(30 * time.Second)) + msg, err := c.reader.ReadBytes(':') + if err != nil { + return + } + c.conn.SetReadDeadline(time.Time{}) + + line := string(msg) + c.detectType(line) + c.sendConsole(line) + + if strings.Contains(line, c.cfg.LoginPrompt) || strings.Contains(line, "login:") { + time.Sleep(time.Second) + c.write([]byte(c.cfg.Login + "\n\r")) + c.loggedIn = true + go func() { + time.Sleep(3 * time.Second) + c.setFilters() + }() + } + continue + } + + c.conn.SetReadDeadline(time.Now().Add(ReadTimeout)) + msg, err := c.reader.ReadBytes('\n') + if err != nil { + return + } + c.conn.SetReadDeadline(time.Time{}) + + line := string(msg) + if strings.TrimSpace(line) == "" { + continue + } + + if c.clusterType == "" { + c.detectType(line) + } + + if strings.Contains(line, "password") { + c.write([]byte(c.cfg.Password + "\r\n")) + } + + if strings.Contains(line, "Hello") || strings.Contains(line, "Welcome") { + if c.cfg.Command != "" { + c.write([]byte(c.cfg.Command + "\n\r")) + } + } + + // Tenter de parser comme spot DX + isDX := strings.Contains(line, "DX de ") || spot.ShortSpotDetectRe.MatchString(line) + if isDX && !c.shouldSkip(line) { + if parsed := spot.ParseLine(line, c.cfg.Name); parsed != nil { + select { + case c.SpotChan <- parsed: + default: + } + } + } + + // Console — marquer les messages "To ALL" + consoleMsg := line + if strings.HasPrefix(strings.TrimSpace(line), "To ALL de ") { + consoleMsg = "TO_ALL:" + strings.TrimSpace(line) + } + c.sendConsole(consoleMsg) + } +} + +// shouldSkip applique le filtre applicatif selon la config du cluster +func (c *Client) shouldSkip(line string) bool { + upper := strings.ToUpper(line) + if strings.Contains(upper, "FT8") && !c.cfg.FT8 { + return true + } + if strings.Contains(upper, "FT4") && !c.cfg.FT4 { + return true + } + if (strings.Contains(upper, "CW SKIMMER") || strings.Contains(upper, "SKIMMER")) && !c.cfg.Skimmer { + return true + } + if strings.Contains(upper, "BEACON") && !c.cfg.Beacon { + return true + } + return false +} + +func (c *Client) detectType(line string) { + if c.cfg.Type != "" { + c.clusterType = c.cfg.Type + return + } + lower := strings.ToLower(line) + switch { + case strings.Contains(lower, "dxspider"): + c.clusterType = "dxspider" + case strings.Contains(lower, "cc cluster") || strings.Contains(lower, "cc-cluster"): + c.clusterType = "cc_cluster" + case strings.Contains(lower, "ar-cluster") || strings.Contains(lower, "arcluster"): + c.clusterType = "ar_cluster" + } +} + +func (c *Client) setFilters() { + switch c.clusterType { + case "dxspider": + c.setFiltersDXSpider() + case "ar_cluster": + c.setFiltersAR() + default: + c.setFiltersCC() + } +} + +func (c *Client) setFiltersCC() { + if c.cfg.FT8 { c.write([]byte("set/ft8\r\n")) } else { c.write([]byte("set/noft8\r\n")) } + if c.cfg.FT4 { c.write([]byte("set/ft4\r\n")) } else { c.write([]byte("set/noft4\r\n")) } + if c.cfg.Skimmer { c.write([]byte("set/skimmer\r\n")) } else { c.write([]byte("set/noskimmer\r\n")) } + if c.cfg.Beacon { c.write([]byte("set/beacon\r\n")) } else { c.write([]byte("set/nobeacon\r\n")) } +} + +func (c *Client) setFiltersDXSpider() { + if c.cfg.Skimmer && c.cfg.FT8 && c.cfg.FT4 { + c.write([]byte("SET/SKIMMER CW FT8 FT4\r\n")) + } else if c.cfg.Skimmer && c.cfg.FT8 { + c.write([]byte("SET/SKIMMER CW FT8\r\n")) + } else if c.cfg.Skimmer { + c.write([]byte("SET/SKIMMER CW\r\n")) + } else { + c.write([]byte("UNSET/SKIMMER\r\n")) + } +} + +func (c *Client) setFiltersAR() { + if c.cfg.FT8 { c.write([]byte("set/ft8\r\n")) } else { c.write([]byte("set/noft8\r\n")) } + if c.cfg.FT4 { c.write([]byte("set/ft4\r\n")) } else { c.write([]byte("set/noft4\r\n")) } +} + +func (c *Client) write(data []byte) { + c.mu.Lock() + defer c.mu.Unlock() + if c.conn == nil || c.writer == nil { + return + } + c.writer.Write(data) + c.writer.Flush() +} + +func (c *Client) sendConsole(line string) { + select { + case c.ConsoleChan <- line: + default: + } +} + +func (c *Client) backoff() time.Duration { + d := time.Duration(float64(BaseReconnectDelay) * math.Pow(2, float64(c.reconnects))) + if d > MaxReconnectDelay { + return MaxReconnectDelay + } + return d +} diff --git a/internal/db/db.go b/internal/db/db.go new file mode 100644 index 0000000..26991a2 --- /dev/null +++ b/internal/db/db.go @@ -0,0 +1,103 @@ +package db + +import ( + "context" + "database/sql" + "fmt" + + _ "github.com/mattn/go-sqlite3" +) + +// DB est le wrapper principal autour de la connexion SQLite +type DB struct { + conn *sql.DB +} + +// Open ouvre (ou crĂ©e) la base SQLite et applique les migrations +func Open(path string) (*DB, error) { + conn, err := sql.Open("sqlite3", path+"?_journal_mode=WAL&_busy_timeout=5000") + if err != nil { + return nil, fmt.Errorf("open db: %w", err) + } + + conn.SetMaxOpenConns(1) + conn.SetMaxIdleConns(1) + + db := &DB{conn: conn} + if err := db.migrate(); err != nil { + conn.Close() + return nil, fmt.Errorf("migrate: %w", err) + } + + return db, nil +} + +func (db *DB) Close() error { + return db.conn.Close() +} + +func (db *DB) Conn() *sql.DB { + return db.conn +} + +// migrate crĂ©e les tables si elles n'existent pas +func (db *DB) migrate() error { + ctx := context.Background() + + _, err := db.conn.ExecContext(ctx, ` + CREATE TABLE IF NOT EXISTS spots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + dx TEXT NOT NULL, + spotter TEXT NOT NULL, + frequency_khz REAL NOT NULL, + frequency_mhz TEXT NOT NULL, + band TEXT NOT NULL, + mode TEXT NOT NULL, + comment TEXT DEFAULT '', + original_comment TEXT DEFAULT '', + time TEXT DEFAULT '', + timestamp INTEGER NOT NULL, + dxcc TEXT DEFAULT '', + country_name TEXT DEFAULT '', + new_dxcc INTEGER DEFAULT 0, + new_band INTEGER DEFAULT 0, + new_mode INTEGER DEFAULT 0, + new_slot INTEGER DEFAULT 0, + callsign_worked INTEGER DEFAULT 0, + in_watchlist INTEGER DEFAULT 0, + pota_ref TEXT DEFAULT '', + sota_ref TEXT DEFAULT '', + park_name TEXT DEFAULT '', + summit_name TEXT DEFAULT '', + flex_spot_number INTEGER DEFAULT 0, + command_number INTEGER DEFAULT 0, + color TEXT DEFAULT '#ffeaeaea', + background_color TEXT DEFAULT '#ff000000', + priority TEXT DEFAULT '5', + life_time TEXT DEFAULT '900', + cluster_name TEXT DEFAULT '', + source INTEGER DEFAULT 0 + )`) + if err != nil { + return fmt.Errorf("create spots table: %w", err) + } + + // Index pour les recherches frĂ©quentes + _, err = db.conn.ExecContext(ctx, ` + CREATE INDEX IF NOT EXISTS idx_spots_dx_band ON spots(dx, band); + CREATE INDEX IF NOT EXISTS idx_spots_timestamp ON spots(timestamp); + CREATE INDEX IF NOT EXISTS idx_spots_flex_number ON spots(flex_spot_number); + CREATE INDEX IF NOT EXISTS idx_spots_command_number ON spots(command_number); + `) + if err != nil { + return fmt.Errorf("create indexes: %w", err) + } + + return nil +} + +// DeleteAll supprime tous les spots (appelĂ© au dĂ©marrage comme l'ancien code) +func (db *DB) DeleteAll(ctx context.Context) error { + _, err := db.conn.ExecContext(ctx, `DELETE FROM spots`) + return err +} diff --git a/internal/db/spots_store.go b/internal/db/spots_store.go new file mode 100644 index 0000000..d5930f5 --- /dev/null +++ b/internal/db/spots_store.go @@ -0,0 +1,214 @@ +package db + +import ( + "context" + "database/sql" + "fmt" + "time" + + "github.com/user/flexdxcluster2/internal/spot" +) + +type SpotsStore struct { + db *DB +} + +func NewSpotsStore(db *DB) *SpotsStore { + return &SpotsStore{db: db} +} + +// Create insère un nouveau spot et retourne son ID +func (s *SpotsStore) Create(ctx context.Context, sp spot.Spot) (int64, error) { + res, err := s.db.conn.ExecContext(ctx, ` + INSERT INTO spots ( + dx, spotter, frequency_khz, frequency_mhz, band, mode, + comment, original_comment, time, timestamp, + dxcc, country_name, + new_dxcc, new_band, new_mode, new_slot, callsign_worked, in_watchlist, + pota_ref, sota_ref, park_name, summit_name, + flex_spot_number, command_number, + color, background_color, priority, life_time, + cluster_name, source + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, + sp.DX, sp.Spotter, sp.FrequencyKHz, sp.FrequencyMHz, sp.Band, sp.Mode, + sp.Comment, sp.OriginalComment, sp.Time, sp.Timestamp, + sp.DXCC, sp.CountryName, + boolToInt(sp.NewDXCC), boolToInt(sp.NewBand), boolToInt(sp.NewMode), + boolToInt(sp.NewSlot), boolToInt(sp.CallsignWorked), boolToInt(sp.InWatchlist), + sp.POTARef, sp.SOTARef, sp.ParkName, sp.SummitName, + sp.FlexSpotNumber, sp.CommandNumber, + sp.Color, sp.BackgroundColor, sp.Priority, sp.LifeTime, + sp.ClusterName, int(sp.Source), + ) + if err != nil { + return 0, fmt.Errorf("create spot: %w", err) + } + return res.LastInsertId() +} + +// GetAll retourne tous les spots, optionnellement filtrĂ©s par bande +func (s *SpotsStore) GetAll(ctx context.Context, band string) ([]spot.Spot, error) { + var rows *sql.Rows + var err error + + if band == "" || band == "0" || band == "ALL" { + rows, err = s.db.conn.QueryContext(ctx, + `SELECT * FROM spots ORDER BY timestamp DESC`) + } else { + rows, err = s.db.conn.QueryContext(ctx, + `SELECT * FROM spots WHERE band = ? ORDER BY timestamp DESC`, band) + } + if err != nil { + return nil, err + } + defer rows.Close() + return scanSpots(rows) +} + +// FindByDXAndBand cherche un spot existant pour le mĂŞme DX sur la mĂŞme bande +func (s *SpotsStore) FindByDXAndBand(ctx context.Context, dx, band string) (*spot.Spot, error) { + row := s.db.conn.QueryRowContext(ctx, + `SELECT * FROM spots WHERE dx = ? AND band = ? ORDER BY timestamp DESC LIMIT 1`, + dx, band) + sp, err := scanSpot(row) + if err == sql.ErrNoRows { + return nil, nil + } + return sp, err +} + +// FindByCommandNumber cherche un spot par son numĂ©ro de commande Flex +func (s *SpotsStore) FindByCommandNumber(ctx context.Context, cmdNum int) (*spot.Spot, error) { + row := s.db.conn.QueryRowContext(ctx, + `SELECT * FROM spots WHERE command_number = ? LIMIT 1`, cmdNum) + sp, err := scanSpot(row) + if err == sql.ErrNoRows { + return nil, nil + } + return sp, err +} + +// FindByFlexSpotNumber cherche un spot par son numĂ©ro de spot Flex +func (s *SpotsStore) FindByFlexSpotNumber(ctx context.Context, flexNum int) (*spot.Spot, error) { + row := s.db.conn.QueryRowContext(ctx, + `SELECT * FROM spots WHERE flex_spot_number = ? LIMIT 1`, flexNum) + sp, err := scanSpot(row) + if err == sql.ErrNoRows { + return nil, nil + } + return sp, err +} + +// UpdateFlexSpotNumber met Ă  jour le numĂ©ro de spot Flex après confirmation du Flex +func (s *SpotsStore) UpdateFlexSpotNumber(ctx context.Context, cmdNum, flexNum int) error { + _, err := s.db.conn.ExecContext(ctx, + `UPDATE spots SET flex_spot_number = ? WHERE command_number = ?`, + flexNum, cmdNum) + return err +} + +// DeleteByFlexSpotNumber supprime un spot par son numĂ©ro Flex +func (s *SpotsStore) DeleteByFlexSpotNumber(ctx context.Context, flexNum int) error { + _, err := s.db.conn.ExecContext(ctx, + `DELETE FROM spots WHERE flex_spot_number = ?`, flexNum) + return err +} + +// DeleteByID supprime un spot par son ID +func (s *SpotsStore) DeleteByID(ctx context.Context, id int64) error { + _, err := s.db.conn.ExecContext(ctx, + `DELETE FROM spots WHERE id = ?`, id) + return err +} + +// DeleteExpired supprime les spots expirĂ©s selon leur lifetime +func (s *SpotsStore) DeleteExpired(ctx context.Context, lifetimeSeconds int64) ([]spot.Spot, error) { + cutoff := time.Now().Unix() - lifetimeSeconds + rows, err := s.db.conn.QueryContext(ctx, + `SELECT * FROM spots WHERE timestamp < ?`, cutoff) + if err != nil { + return nil, err + } + expired, err := scanSpots(rows) + rows.Close() + if err != nil { + return nil, err + } + + _, err = s.db.conn.ExecContext(ctx, + `DELETE FROM spots WHERE timestamp < ?`, cutoff) + return expired, err +} + +// --- Helpers --- + +func boolToInt(b bool) int { + if b { + return 1 + } + return 0 +} + +func scanSpots(rows *sql.Rows) ([]spot.Spot, error) { + var spots []spot.Spot + for rows.Next() { + sp, err := scanSpotFromRows(rows) + if err != nil { + return nil, err + } + spots = append(spots, *sp) + } + return spots, rows.Err() +} + +func scanSpot(row *sql.Row) (*spot.Spot, error) { + var sp spot.Spot + var newDXCC, newBand, newMode, newSlot, worked, inWatchlist, source int + err := row.Scan( + &sp.ID, &sp.DX, &sp.Spotter, &sp.FrequencyKHz, &sp.FrequencyMHz, + &sp.Band, &sp.Mode, &sp.Comment, &sp.OriginalComment, &sp.Time, &sp.Timestamp, + &sp.DXCC, &sp.CountryName, + &newDXCC, &newBand, &newMode, &newSlot, &worked, &inWatchlist, + &sp.POTARef, &sp.SOTARef, &sp.ParkName, &sp.SummitName, + &sp.FlexSpotNumber, &sp.CommandNumber, + &sp.Color, &sp.BackgroundColor, &sp.Priority, &sp.LifeTime, + &sp.ClusterName, &source, + ) + if err != nil { + return nil, err + } + sp.NewDXCC = newDXCC == 1 + sp.NewBand = newBand == 1 + sp.NewMode = newMode == 1 + sp.NewSlot = newSlot == 1 + sp.CallsignWorked = worked == 1 + sp.InWatchlist = inWatchlist == 1 + sp.Source = spot.SpotSource(source) + return &sp, nil +} + +func scanSpotFromRows(rows *sql.Rows) (*spot.Spot, error) { + var sp spot.Spot + var newDXCC, newBand, newMode, newSlot, worked, inWatchlist, source int + err := rows.Scan( + &sp.ID, &sp.DX, &sp.Spotter, &sp.FrequencyKHz, &sp.FrequencyMHz, + &sp.Band, &sp.Mode, &sp.Comment, &sp.OriginalComment, &sp.Time, &sp.Timestamp, + &sp.DXCC, &sp.CountryName, + &newDXCC, &newBand, &newMode, &newSlot, &worked, &inWatchlist, + &sp.POTARef, &sp.SOTARef, &sp.ParkName, &sp.SummitName, + &sp.FlexSpotNumber, &sp.CommandNumber, + &sp.Color, &sp.BackgroundColor, &sp.Priority, &sp.LifeTime, + &sp.ClusterName, &source, + ) + if err != nil { + return nil, err + } + sp.NewDXCC = newDXCC == 1 + sp.NewBand = newBand == 1 + sp.NewMode = newMode == 1 + sp.NewSlot = newSlot == 1 + sp.CallsignWorked = worked == 1 + sp.InWatchlist = inWatchlist == 1 + sp.Source = spot.SpotSource(source) + return &sp, nil +} diff --git a/internal/modes/modes.go b/internal/modes/modes.go new file mode 100644 index 0000000..47b519f --- /dev/null +++ b/internal/modes/modes.go @@ -0,0 +1,229 @@ +package modes + +import ( + "regexp" + "strings" + + "github.com/user/flexdxcluster2/internal/bands" +) + +type ModeRange struct { + MinFreqMHz float64 + MaxFreqMHz float64 + Mode string +} + +var BandModeRanges = map[string][]ModeRange{ + "160M": { + {1.800, 1.838, "CW"}, + {1.838, 1.843, "FT8"}, + {1.843, 2.000, "LSB"}, + }, + "80M": { + {3.500, 3.560, "CW"}, + {3.560, 3.575, "FT8"}, + {3.575, 3.578, "FT4"}, + {3.578, 3.590, "RTTY"}, + {3.590, 3.800, "LSB"}, + }, + "60M": { + {5.330, 5.357, "CW"}, + {5.357, 5.359, "FT8"}, + {5.359, 5.405, "USB"}, + }, + "40M": { + {7.000, 7.040, "CW"}, + {7.040, 7.047, "RTTY"}, + {7.047, 7.050, "FT4"}, + {7.050, 7.100, "FT8"}, + {7.100, 7.300, "LSB"}, + }, + "30M": { + {10.100, 10.130, "CW"}, + {10.130, 10.142, "FT8"}, + {10.142, 10.150, "FT4"}, + }, + "20M": { + {14.000, 14.070, "CW"}, + {14.070, 14.078, "FT8"}, + {14.078, 14.083, "FT4"}, + {14.083, 14.100, "FT8"}, + {14.100, 14.112, "RTTY"}, + {14.112, 14.350, "USB"}, + }, + "17M": { + {18.068, 18.090, "CW"}, + {18.090, 18.104, "FT8"}, + {18.104, 18.106, "FT4"}, + {18.106, 18.110, "FT8"}, + {18.110, 18.168, "USB"}, + }, + "15M": { + {21.000, 21.070, "CW"}, + {21.070, 21.100, "FT8"}, + {21.100, 21.130, "RTTY"}, + {21.130, 21.143, "FT4"}, + {21.143, 21.450, "USB"}, + }, + "12M": { + {24.890, 24.910, "CW"}, + {24.910, 24.918, "FT8"}, + {24.918, 24.930, "FT4"}, + {24.930, 24.990, "USB"}, + }, + "10M": { + {28.000, 28.070, "CW"}, + {28.070, 28.110, "FT8"}, + {28.110, 28.179, "RTTY"}, + {28.179, 28.190, "FT4"}, + {28.190, 29.000, "USB"}, + {29.000, 29.700, "FM"}, + }, + "6M": { + {50.000, 50.100, "CW"}, + {50.100, 50.313, "USB"}, + {50.313, 50.318, "FT8"}, + {50.318, 50.323, "FT4"}, + {50.323, 51.000, "USB"}, + {51.000, 54.000, "FM"}, + }, + "QO-100": { + {10489.500, 10489.540, "CW"}, + {10489.540, 10489.650, "FT8"}, + {10489.650, 10489.990, "USB"}, + }, +} + +func GuessMode(freqMHz float64) string { + band := bands.FrequencyToBand(freqMHz) + if band == "N/A" { + if freqMHz < 10.0 { + return "LSB" + } + return "USB" + } + return GuessModeForBand(freqMHz, band) +} + +func GuessModeForBand(freqMHz float64, band string) string { + ranges, exists := BandModeRanges[band] + if !exists { + if bands.IsUSBBand(band) { + return "USB" + } + return "LSB" + } + for _, r := range ranges { + if freqMHz >= r.MinFreqMHz && freqMHz < r.MaxFreqMHz { + return r.Mode + } + } + if bands.IsUSBBand(band) { + return "USB" + } + return "LSB" +} + +func ExtractModeFromComment(comment string) string { + if comment == "" { + return "" + } + commentUpper := strings.ToUpper(comment) + + if strings.Contains(commentUpper, "FT8") || + (strings.Contains(commentUpper, "DB") && strings.Contains(commentUpper, "HZ")) { + return "FT8" + } + if strings.Contains(commentUpper, "FT4") { + return "FT4" + } + if strings.Contains(commentUpper, "WPM") || strings.Contains(commentUpper, " CW ") || + strings.HasSuffix(commentUpper, "CW") || strings.HasPrefix(commentUpper, "CW ") { + return "CW" + } + + digitalModes := []string{"RTTY", "PSK31", "PSK63", "PSK", "MFSK", "OLIVIA", "JT65", "JT9"} + for _, mode := range digitalModes { + if strings.Contains(commentUpper, mode) { + return mode + } + } + + voiceModes := []string{"USB", "LSB", "SSB", "FM", "AM"} + for _, mode := range voiceModes { + if strings.Contains(commentUpper, " "+mode+" ") || + strings.HasPrefix(commentUpper, mode+" ") || + strings.HasSuffix(commentUpper, " "+mode) || + commentUpper == mode { + return mode + } + } + return "" +} + +// DetermineMode — prioritĂ© : mode explicite > commentaire > frĂ©quence +func DetermineMode(explicitMode string, comment string, freqMHz float64) string { + if explicitMode != "" { + explicitMode = strings.ToUpper(explicitMode) + if explicitMode == "SSB" { + return bands.NormalizeSSBModeByFrequency(explicitMode, freqMHz) + } + return explicitMode + } + + modeFromComment := ExtractModeFromComment(comment) + if modeFromComment != "" { + if modeFromComment == "SSB" { + return bands.NormalizeSSBModeByFrequency(modeFromComment, freqMHz) + } + return modeFromComment + } + + return GuessMode(freqMHz) +} + +func IsCWMode(mode string) bool { return strings.ToUpper(mode) == "CW" } +func IsSSBMode(mode string) bool { + m := strings.ToUpper(mode) + return m == "SSB" || m == "USB" || m == "LSB" +} +func IsDigitalMode(mode string) bool { + m := strings.ToUpper(mode) + for _, dm := range []string{"FT8", "FT4", "RTTY", "PSK31", "PSK63", "PSK", "MFSK", "OLIVIA", "JT65", "JT9"} { + if m == dm { + return true + } + } + return false +} +func IsPhoneMode(mode string) bool { + m := strings.ToUpper(mode) + for _, pm := range []string{"SSB", "USB", "LSB", "FM", "AM"} { + if m == pm { + return true + } + } + return false +} + +func ParseModeFromRawSpot(rawSpot string) string { + re := regexp.MustCompile(`\b(CW|SSB|USB|LSB|FM|AM|FT8|FT4|RTTY|PSK\d*)\b`) + return re.FindString(strings.ToUpper(rawSpot)) +} + +func GetModeColor(mode string) string { + switch strings.ToUpper(mode) { + case "CW": + return "#10b981" + case "FT8", "FT4": + return "#8b5cf6" + case "RTTY": + return "#f59e0b" + case "FM": + return "#ec4899" + } + if IsSSBMode(mode) { + return "#3b82f6" + } + return "#6b7280" +} diff --git a/internal/solar/solar.go b/internal/solar/solar.go new file mode 100644 index 0000000..e5c5c38 --- /dev/null +++ b/internal/solar/solar.go @@ -0,0 +1,20 @@ +package solar + +import "encoding/xml" + +type Data struct { + SolarFlux string `xml:"solarflux" json:"solarFlux"` + Sunspots string `xml:"sunspots" json:"sunspots"` + AIndex string `xml:"aindex" json:"aIndex"` + KIndex string `xml:"kindex" json:"kIndex"` + SolarWind string `xml:"solarwind" json:"solarWind"` + HeliumLine string `xml:"heliumline" json:"heliumLine"` + ProtonFlux string `xml:"protonflux" json:"protonFlux"` + GeomagField string `xml:"geomagfield" json:"geomagField"` + Updated string `xml:"updated" json:"updated"` +} + +type XML struct { + XMLName xml.Name `xml:"solar"` + Data Data `xml:"solardata"` +} diff --git a/internal/spot/parser.go b/internal/spot/parser.go new file mode 100644 index 0000000..438415d --- /dev/null +++ b/internal/spot/parser.go @@ -0,0 +1,89 @@ +package spot + +import ( + "regexp" + "strconv" + "strings" +) + +// Regex pour les deux formats de spots cluster +var ( + // Format standard : DX de SPOTTER: FREQ DX MODE COMMENT TIME + SpotRe = regexp.MustCompile(`(?i)DX\sde\s([\w\d\/]+?)(?:-[#\d-]+)?\s*:\s*(\d+\.\d+)\s+([\w\d\/]+)\s+(?:(CW|SSB|FT8|FT4|RTTY|USB|LSB|FM)\s+)?(.+?)\s+(\d{4}Z)`) + + // Format court : FREQ DX DATE TIME COMMENT + SpotReShort = regexp.MustCompile(`^(\d+\.\d+)\s+([\w\d\/]+)\s+\d{2}-\w{3}-\d{4}\s+(\d{4}Z)\s+(.+?)\s*<([\w\d\/]+)>\s*$`) + + // DĂ©tection rapide du format court + ShortSpotDetectRe = regexp.MustCompile(`^\d+\.\d+\s+[\w\d\/]+\s+\d{2}-\w{3}-\d{4}`) +) + +// ParseResult contient le spot parsĂ© et une Ă©ventuelle erreur +type ParseResult struct { + Spot *Spot + Err error + Skipped bool // true si la ligne n'est pas un spot (pas une erreur) +} + +// ParseLine tente de parser une ligne brute du cluster en Spot +// Retourne nil si la ligne n'est pas un spot DX +func ParseLine(line string, clusterName string) *Spot { + // DĂ©tecter si c'est un spot + isSpot := strings.Contains(line, "DX de ") || ShortSpotDetectRe.MatchString(line) + if !isSpot { + return nil + } + + match := SpotRe.FindStringSubmatch(line) + if len(match) > 0 { + return parseStandardFormat(match, clusterName) + } + + match = SpotReShort.FindStringSubmatch(line) + if len(match) > 0 { + return parseShortFormat(match, clusterName) + } + + return nil +} + +func parseStandardFormat(match []string, clusterName string) *Spot { + freqKHz := parseFreq(match[2]) + return &Spot{ + Spotter: match[1], + FrequencyKHz: freqKHz, + DX: match[3], + Mode: match[4], + Comment: strings.TrimSpace(match[5]), + Time: match[6], + ClusterName: clusterName, + Source: SourceCluster, + } +} + +func parseShortFormat(match []string, clusterName string) *Spot { + freqKHz := parseFreq(match[1]) + return &Spot{ + FrequencyKHz: freqKHz, + DX: match[2], + Time: match[3], + Comment: strings.TrimSpace(match[4]), + Spotter: match[5], + ClusterName: clusterName, + Source: SourceCluster, + } +} + +// parseFreq parse une frĂ©quence string en kHz float64 +// Gère kHz (>1000) et MHz (<1000) automatiquement +func parseFreq(s string) float64 { + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return 0 + } + // Si < 1000 c'est en MHz, on convertit en kHz + if f < 1000 { + return f * 1000.0 + } + return f +} diff --git a/internal/spot/spot.go b/internal/spot/spot.go new file mode 100644 index 0000000..ad61dd8 --- /dev/null +++ b/internal/spot/spot.go @@ -0,0 +1,97 @@ +package spot + +import ( + "fmt" + "time" +) + +// SpotSource identifie l'origine d'un spot +type SpotSource int + +const ( + SourceCluster SpotSource = iota // Spot reçu depuis un cluster DX telnet + SourceManual // Spot ajoutĂ© manuellement depuis l'UI +) + +// Spot est la struct universelle — remplace TelnetSpot + FlexSpot +// Plus de conversion entre les deux, tout passe par cette struct unique +type Spot struct { + // --- Persistance --- + ID int64 + + // --- IdentitĂ© --- + DX string + Spotter string + + // --- FrĂ©quence --- + // FrequencyKHz est la source de vĂ©ritĂ© interne + // FrequencyMHz est le format string attendu par FlexRadio (ex: "14.195000") + FrequencyKHz float64 + FrequencyMHz string + Band string + + // --- Mode --- + Mode string + + // --- MĂ©tadonnĂ©es cluster --- + Comment string + Time string // Format "1234Z" + Timestamp int64 // Unix timestamp + ReceivedAt time.Time + ClusterName string + Source SpotSource + + // --- DXCC --- + DXCC string + CountryName string + + // --- Flags Log4OM (calculĂ©s depuis la DB Log4OM) --- + NewDXCC bool + NewBand bool + NewMode bool + NewSlot bool + CallsignWorked bool + + // --- Watchlist --- + InWatchlist bool + WatchlistNotify bool + + // --- POTA / SOTA --- + POTARef string + SOTARef string + ParkName string + SummitName string + + // --- FlexRadio panadapter --- + FlexSpotNumber int + CommandNumber int + Color string + BackgroundColor string + Priority string + LifeTime string + OriginalComment string +} + +// FreqMHzString retourne la frĂ©quence formatĂ©e pour FlexRadio +// ex: 14195.0 kHz → "14.195000" +func (s *Spot) FreqMHzString() string { + if s.FrequencyMHz != "" { + return s.FrequencyMHz + } + if s.FrequencyKHz > 1000 { + return formatMHz(s.FrequencyKHz / 1000.0) + } + return formatMHz(s.FrequencyKHz) +} + +// FreqKHz retourne la frĂ©quence en kHz depuis le champ disponible +func (s *Spot) FreqKHz() float64 { + if s.FrequencyKHz > 0 { + return s.FrequencyKHz + } + return 0 +} + +func formatMHz(mhz float64) string { + return fmt.Sprintf("%.6f", mhz) +} diff --git a/internal/stats/stats.go b/internal/stats/stats.go new file mode 100644 index 0000000..22bdfeb --- /dev/null +++ b/internal/stats/stats.go @@ -0,0 +1,57 @@ +package stats + +import "sync" + +type SpotStats struct { + mu sync.RWMutex + Received int64 + Processed int64 + Rejected int64 +} + +var Global = &SpotStats{} + +func (s *SpotStats) IncrReceived() { + s.mu.Lock() + s.Received++ + s.mu.Unlock() +} + +func (s *SpotStats) IncrProcessed() { + s.mu.Lock() + s.Processed++ + s.mu.Unlock() +} + +func (s *SpotStats) IncrRejected() { + s.mu.Lock() + s.Rejected++ + s.mu.Unlock() +} + +func (s *SpotStats) Get() (received, processed, rejected int64) { + s.mu.RLock() + defer s.mu.RUnlock() + return s.Received, s.Processed, s.Rejected +} + +func (s *SpotStats) SuccessRate() float64 { + s.mu.RLock() + defer s.mu.RUnlock() + if s.Received == 0 { + return 0.0 + } + return float64(s.Processed) / float64(s.Received) * 100.0 +} + +// Snapshot retourne un map JSON-friendly pour le frontend +func (s *SpotStats) Snapshot() map[string]interface{} { + s.mu.RLock() + defer s.mu.RUnlock() + return map[string]interface{}{ + "received": s.Received, + "processed": s.Processed, + "rejected": s.Rejected, + "successRate": s.SuccessRate(), + } +} diff --git a/internal/watchlist/watchlist.go b/internal/watchlist/watchlist.go new file mode 100644 index 0000000..5a9109e --- /dev/null +++ b/internal/watchlist/watchlist.go @@ -0,0 +1,260 @@ +package watchlist + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "sync" + "time" +) + +type Entry struct { + Callsign string `json:"callsign"` + LastSeen time.Time `json:"lastSeen"` + LastSeenStr string `json:"lastSeenStr"` + AddedAt time.Time `json:"addedAt"` + SpotCount int `json:"spotCount"` + IsContest bool `json:"isContest"` + Notify bool `json:"notify"` + + // ActiveSpotIDs n'est pas sĂ©rialisĂ© — reconstruit en mĂ©moire + activeSpotIDs map[int64]bool +} + +type Watchlist struct { + entries map[string]*Entry + filePath string + mu sync.RWMutex +} + +func New(filePath string) *Watchlist { + w := &Watchlist{ + entries: make(map[string]*Entry), + filePath: filePath, + } + w.load() + return w +} + +func (w *Watchlist) load() { + w.mu.Lock() + defer w.mu.Unlock() + + data, err := os.ReadFile(w.filePath) + if err != nil { + return + } + + var entries []Entry + if err := json.Unmarshal(data, &entries); err != nil { + return + } + + for i := range entries { + e := &entries[i] + e.activeSpotIDs = make(map[int64]bool) + if e.LastSeen.IsZero() { + e.LastSeenStr = "Never" + } else { + e.LastSeenStr = FormatLastSeen(e.LastSeen) + } + w.entries[e.Callsign] = e + } +} + +func (w *Watchlist) save() error { + entries := make([]Entry, 0, len(w.entries)) + for _, e := range w.entries { + entries = append(entries, *e) + } + data, err := json.MarshalIndent(entries, "", " ") + if err != nil { + return err + } + return os.WriteFile(w.filePath, data, 0644) +} + +func (w *Watchlist) Add(callsign string) error { + return w.add(callsign, false) +} + +func (w *Watchlist) AddContest(callsign string) error { + return w.add(callsign, true) +} + +func (w *Watchlist) add(callsign string, isContest bool) error { + w.mu.Lock() + defer w.mu.Unlock() + + callsign = strings.ToUpper(strings.TrimSpace(callsign)) + if callsign == "" { + return fmt.Errorf("callsign cannot be empty") + } + if _, exists := w.entries[callsign]; exists { + return fmt.Errorf("callsign already in watchlist") + } + + w.entries[callsign] = &Entry{ + Callsign: callsign, + AddedAt: time.Now(), + LastSeenStr: "Never", + activeSpotIDs: make(map[int64]bool), + IsContest: isContest, + } + + return w.save() +} + +func (w *Watchlist) Remove(callsign string) error { + w.mu.Lock() + defer w.mu.Unlock() + + callsign = strings.ToUpper(strings.TrimSpace(callsign)) + if _, exists := w.entries[callsign]; !exists { + return fmt.Errorf("callsign not in watchlist") + } + delete(w.entries, callsign) + return w.save() +} + +func (w *Watchlist) SetNotify(callsign string, notify bool) error { + w.mu.Lock() + defer w.mu.Unlock() + + callsign = strings.ToUpper(strings.TrimSpace(callsign)) + e, exists := w.entries[callsign] + if !exists { + return fmt.Errorf("callsign not found") + } + e.Notify = notify + return w.save() +} + +func (w *Watchlist) Matches(callsign string) bool { + w.mu.RLock() + defer w.mu.RUnlock() + _, ok := w.entries[strings.ToUpper(callsign)] + return ok +} + +func (w *Watchlist) GetEntry(callsign string) *Entry { + w.mu.RLock() + defer w.mu.RUnlock() + e, ok := w.entries[strings.ToUpper(callsign)] + if !ok { + return nil + } + copy := *e + return © +} + +func (w *Watchlist) GetAll() []Entry { + w.mu.RLock() + defer w.mu.RUnlock() + entries := make([]Entry, 0, len(w.entries)) + for _, e := range w.entries { + cp := *e + if !cp.LastSeen.IsZero() { + cp.LastSeenStr = FormatLastSeen(cp.LastSeen) + } + entries = append(entries, cp) + } + return entries +} + +func (w *Watchlist) GetAllCallsigns() []string { + w.mu.RLock() + defer w.mu.RUnlock() + callsigns := make([]string, 0, len(w.entries)) + for cs := range w.entries { + callsigns = append(callsigns, cs) + } + return callsigns +} + +func (w *Watchlist) MarkSeen(callsign string) { + w.mu.Lock() + defer w.mu.Unlock() + e, ok := w.entries[strings.ToUpper(callsign)] + if !ok { + return + } + e.LastSeen = time.Now() + e.LastSeenStr = FormatLastSeen(e.LastSeen) + e.SpotCount++ +} + +func (w *Watchlist) AddActiveSpot(callsign string, spotID int64) { + w.mu.Lock() + defer w.mu.Unlock() + e, ok := w.entries[strings.ToUpper(callsign)] + if !ok { + return + } + if e.activeSpotIDs == nil { + e.activeSpotIDs = make(map[int64]bool) + } + e.activeSpotIDs[spotID] = true +} + +func (w *Watchlist) RemoveActiveSpot(spotID int64) { + w.mu.Lock() + defer w.mu.Unlock() + for _, e := range w.entries { + delete(e.activeSpotIDs, spotID) + } +} + +func (w *Watchlist) GetAllWithActiveStatus() []map[string]interface{} { + w.mu.RLock() + defer w.mu.RUnlock() + result := make([]map[string]interface{}, 0, len(w.entries)) + for _, e := range w.entries { + lastSeenStr := "Never" + if !e.LastSeen.IsZero() { + lastSeenStr = FormatLastSeen(e.LastSeen) + } + result = append(result, map[string]interface{}{ + "callsign": e.Callsign, + "lastSeen": e.LastSeen, + "lastSeenStr": lastSeenStr, + "addedAt": e.AddedAt, + "spotCount": e.SpotCount, + "notify": e.Notify, + "isContest": e.IsContest, + "hasActiveSpots": len(e.activeSpotIDs) > 0, + "activeCount": len(e.activeSpotIDs), + }) + } + return result +} + +func FormatLastSeen(t time.Time) string { + if t.IsZero() { + return "Never" + } + d := time.Since(t) + switch { + case d < time.Minute: + return "Just now" + case d < time.Hour: + m := int(d.Minutes()) + if m == 1 { + return "1 minute ago" + } + return fmt.Sprintf("%d minutes ago", m) + case d < 24*time.Hour: + h := int(d.Hours()) + if h == 1 { + return "1 hour ago" + } + return fmt.Sprintf("%d hours ago", h) + default: + days := int(d.Hours() / 24) + if days == 1 { + return "1 day ago" + } + return fmt.Sprintf("%d days ago", days) + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..9934b6f --- /dev/null +++ b/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "FlexDXCluster2", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +}