From 4ac73d63cad27ba096a3c65707794053b922797c Mon Sep 17 00:00:00 2001 From: Pavel Loginov Date: Sun, 6 Sep 2026 16:09:53 +0300 Subject: [PATCH 01/15] Release 1.4.0: improve UX and update licensing --- .github/pull_request_template.md | 15 + .github/workflows/cla.yml | 468 +++++++++++++++ .github/workflows/tests.yml | 16 + .gitignore | 1 + CLA.md | 128 +++++ COMMERCIAL_LICENSE.md | 74 +++ CONTRIBUTING.md | 50 ++ LICENSE | 297 +++------- LICENSING.md | 110 ++++ NOTICE | 6 + README.md | 16 + app/modules/roxywi/auth.py | 2 +- app/static/css/ux.css | 121 ++++ app/static/js/metrics.js | 11 +- app/static/js/overview.js | 86 +-- app/static/js/rmon/status_page.js | 45 +- app/static/js/script.js | 98 +--- app/static/js/ux.js | 157 +++++ app/templates/ajax/smon/status_pages.html | 6 +- app/templates/base.html | 20 +- app/templates/include/input_macros.html | 4 +- app/templates/include/login.html | 15 +- app/templates/include/main_head.html | 8 +- app/templates/include/main_menu.html | 4 +- app/templates/languages/en.html | 32 ++ app/templates/languages/fr.html | 32 ++ app/templates/languages/pt-br.html | 32 ++ app/templates/languages/ru.html | 32 ++ app/templates/login.html | 91 +-- app/templates/ovw.html | 36 +- app/templates/smon/dashboard.html | 18 +- app/templates/smon/manage_status_page.html | 23 +- app/version.py | 2 +- licenses/legacy/LICENSE-before-1.4.0.txt | 203 +++++++ tests/frontend/README.md | 10 + tests/frontend/package-lock.json | 543 ++++++++++++++++++ tests/frontend/package.json | 6 + tests/frontend/ux.test.cjs | 209 +++++++ .../test_authentication_workflow.py | 15 +- tests/ui/test_pages.py | 62 ++ 40 files changed, 2601 insertions(+), 503 deletions(-) create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/cla.yml create mode 100644 CLA.md create mode 100644 COMMERCIAL_LICENSE.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSING.md create mode 100644 NOTICE create mode 100644 app/static/css/ux.css create mode 100644 app/static/js/ux.js create mode 100644 licenses/legacy/LICENSE-before-1.4.0.txt create mode 100644 tests/frontend/README.md create mode 100644 tests/frontend/package-lock.json create mode 100644 tests/frontend/package.json create mode 100644 tests/frontend/ux.test.cjs diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..16fa685 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ +## Summary + +Describe the change and why it is needed. + +## Testing + +Describe the tests or checks you ran. + +## Contributor License Agreement + +After opening this pull request, comment exactly: + +`/sign-cla` + +You only need to sign once per CLA version. If you already accepted the current `CLA.md`, the `CLA` status check will pass automatically. diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 0000000..5b1fa3a --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,468 @@ +name: CLA + +on: + pull_request_target: + branches: + - main + - "release/**" + types: + - opened + - synchronize + - reopened + - ready_for_review + issue_comment: + types: + - created + push: + paths: + - CLA.md + workflow_dispatch: + +permissions: {} + +concurrency: + group: rmon-cla-registry + cancel-in-progress: false + +jobs: + evaluate: + name: Update CLA status + if: github.event_name == 'pull_request_target' + runs-on: ubuntu-latest + permissions: + contents: read + issues: read + pull-requests: read + statuses: write + + steps: + - name: Evaluate contributor acceptance + uses: actions/github-script@v7 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const pr = context.payload.pull_request; + const registryTitle = "[CLA] Signature Registry"; + const markerPrefix = "", jsonStart); + if (jsonEnd === -1) { + continue; + } + + try { + const record = JSON.parse(body.slice(jsonStart, jsonEnd)); + if ( + record.version === 1 && + Number(record.github_id) === Number(githubId) && + record.cla_sha === claSha + ) { + return true; + } + } catch (error) { + core.warning(`Ignoring malformed CLA registry record in comment ${comment.id}`); + } + } + } + + return false; + } + + const { sha: claSha, defaultBranch } = await currentClaSha(); + const login = pr.user.login; + const isExempt = exemptLogins.has(login); + const accepted = isExempt || await hasSignature(pr.user.id, claSha); + + await github.rest.repos.createCommitStatus({ + owner, + repo, + sha: pr.head.sha, + state: accepted ? "success" : "failure", + context: "CLA", + description: isExempt + ? "Automated dependency account is exempt from CLA" + : accepted + ? "Contributor accepted the current CLA" + : "Comment /sign-cla on the PR to accept the current CLA", + target_url: `${context.serverUrl}/${owner}/${repo}/blob/${defaultBranch}/CLA.md`, + }); + + if (!accepted) { + core.notice(`@${login} has not accepted CLA ${claSha}. Comment /sign-cla on PR #${pr.number}.`); + } + + sign: + name: Record CLA acceptance + if: >- + github.event_name == 'issue_comment' && + github.event.issue.pull_request && + github.event.comment.body == '/sign-cla' + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + pull-requests: read + statuses: write + + steps: + - name: Record acceptance + uses: actions/github-script@v7 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const issueNumber = context.payload.issue.number; + const commenter = context.payload.comment.user; + const registryTitle = "[CLA] Signature Registry"; + const markerPrefix = "", jsonStart); + if (jsonEnd === -1) { + continue; + } + + try { + const record = JSON.parse(body.slice(jsonStart, jsonEnd)); + if ( + record.version === 1 && + Number(record.github_id) === Number(commenter.id) && + record.cla_sha === claSha + ) { + return true; + } + } catch (error) { + core.warning(`Ignoring malformed CLA registry record in comment ${comment.id}`); + } + } + } + + return false; + } + + let registries = await findRegistryIssues(); + const accepted = await alreadyAccepted(registries); + + if (!accepted) { + let registry = registries.find((issue) => issue.state === "open"); + + if (!registry) { + const created = await github.rest.issues.create({ + owner, + repo, + title: registryTitle, + body: [ + "This issue is an automated public registry of RMON CLA acceptances.", + "", + "Do not edit or manually add signature records. Only records posted by `github-actions[bot]` are trusted by the CLA workflow.", + "", + "Each record is bound to the contributor's numeric GitHub user ID and the Git blob SHA of the accepted `CLA.md` version.", + ].join("\n"), + }); + registry = created.data; + } + + const record = { + version: 1, + github_id: commenter.id, + github_login: commenter.login, + cla_sha: claSha, + accepted_at: context.payload.comment.created_at, + source_pr: issueNumber, + source_comment_id: context.payload.comment.id, + command: "/sign-cla", + }; + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: registry.number, + body: [ + `${markerPrefix}${JSON.stringify(record)} -->`, + `✅ @${commenter.login} accepted RMON CLA \`${claSha}\` on ${context.payload.comment.created_at}.`, + `Source: #${issueNumber}, comment ${context.payload.comment.id}.`, + ].join("\n"), + }); + } + + await github.rest.repos.createCommitStatus({ + owner, + repo, + sha: pr.head.sha, + state: "success", + context: "CLA", + description: "Contributor accepted the current CLA", + target_url: `${context.serverUrl}/${owner}/${repo}/blob/${defaultBranch}/CLA.md`, + }); + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: issueNumber, + body: accepted + ? `✅ @${commenter.login}, you already accepted the current RMON CLA (\`${claSha}\`).` + : `✅ @${commenter.login}, your RMON CLA acceptance has been recorded for version \`${claSha}\`.`, + }); + + refresh: + name: Refresh open PR CLA statuses + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: read + issues: read + pull-requests: read + statuses: write + + steps: + - name: Refresh statuses + uses: actions/github-script@v7 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const registryTitle = "[CLA] Signature Registry"; + const markerPrefix = "", jsonStart); + if (jsonEnd === -1) { + continue; + } + + try { + const record = JSON.parse(body.slice(jsonStart, jsonEnd)); + if (record.version === 1 && record.cla_sha === claSha) { + acceptedIds.add(Number(record.github_id)); + } + } catch (error) { + core.warning(`Ignoring malformed CLA registry record in comment ${comment.id}`); + } + } + } + + const pulls = await github.paginate( + github.rest.pulls.list, + { + owner, + repo, + state: "open", + per_page: 100, + } + ); + + for (const pr of pulls) { + const accepted = + exemptLogins.has(pr.user.login) || + acceptedIds.has(Number(pr.user.id)); + + await github.rest.repos.createCommitStatus({ + owner, + repo, + sha: pr.head.sha, + state: accepted ? "success" : "failure", + context: "CLA", + description: exemptLogins.has(pr.user.login) + ? "Automated dependency account is exempt from CLA" + : accepted + ? "Contributor accepted the current CLA" + : "Comment /sign-cla on the PR to accept the current CLA", + target_url: `${context.serverUrl}/${owner}/${repo}/blob/${defaultBranch}/CLA.md`, + }); + } + + core.notice(`Refreshed CLA status for ${pulls.length} open pull request(s).`); diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e697757..6e01c22 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,6 +11,22 @@ permissions: contents: read jobs: + frontend-tests: + runs-on: ubuntu-latest + timeout-minutes: 10 + defaults: + run: + working-directory: tests/frontend + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + cache-dependency-path: tests/frontend/package-lock.json + - run: npm ci --ignore-scripts + - run: npm test + unit-tests: runs-on: ubuntu-latest timeout-minutes: 20 diff --git a/.gitignore b/.gitignore index 1c0bc40..d6e34f2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ __pycache__/ /.pytest_cache/ /.coverage /.idea/ +/tests/frontend/node_modules/ diff --git a/CLA.md b/CLA.md new file mode 100644 index 0000000..f4dce2d --- /dev/null +++ b/CLA.md @@ -0,0 +1,128 @@ +# RMON Contributor License Agreement + +This Contributor License Agreement ("Agreement") applies to each contribution +that you submit to RMON after this Agreement is adopted by the project. + +By expressly accepting this Agreement in connection with a pull request or +other contribution, you agree to the terms below. + +## 1. Definitions + +"Contribution" means any original work of authorship, including source code, +documentation, tests, designs, or other material, that you intentionally submit +to the RMON project for inclusion in the project. + +"Project" means RMON and the repositories maintained as part of the +RMON project. + +"Project Owner" means the copyright holder or entity authorized to maintain +and license RMON. + +## 2. Your ownership + +You retain ownership of the copyright in your Contribution. This Agreement is a +license grant; it is not an assignment of your copyright. + +## 3. Copyright license + +You grant the Project Owner and recipients of software distributed by the +Project Owner a perpetual, worldwide, non-exclusive, royalty-free, +irrevocable copyright license to reproduce, prepare derivative works of, +publicly display, publicly perform, distribute, and otherwise use your +Contribution as part of RMON. + +You also grant the Project Owner the right to sublicense and relicense your +Contribution, including as part of RMON, under source-available, +open-source, proprietary, commercial, or other software-license terms chosen by +the Project Owner. + +This includes the right to offer RMON containing your Contribution +under both the project's public source license and separate commercial license +terms. + +## 4. Patent license + +To the extent you can grant patent rights, you grant the Project Owner and +recipients of RMON a perpetual, worldwide, non-exclusive, +royalty-free patent license under patent claims that you control and that are +necessarily infringed by your Contribution alone or by combination of your +Contribution with the Project as submitted. + +If a recipient institutes patent litigation alleging that the Contribution or +RMON infringes a patent, that recipient's patent license granted under +this section terminates to the extent permitted by applicable law. + +## 5. Authority to contribute + +You represent that: + +- you are legally entitled to grant the rights in this Agreement; +- the Contribution is your original work, or you have sufficient rights to + submit it under this Agreement; +- to your knowledge, the Contribution does not intentionally include material + that cannot legally be contributed to the Project; +- if your employer or another organization owns rights in the Contribution, + you have obtained authorization to submit it and grant the rights in this + Agreement. + +If you are contributing on behalf of a company or other organization, you +represent that you are authorized to accept this Agreement on its behalf, or +that an authorized representative has separately approved the contribution. + +## 6. Third-party material + +If a Contribution includes third-party material, identify that material and its +license clearly in the pull request or accompanying documentation. Do not submit +third-party code whose license is incompatible with RMON's licensing +model. + +## 7. No obligation + +The Project Owner is not required to use, merge, distribute, support, or retain +any Contribution. + +## 8. No warranties + +Unless required by applicable law or agreed separately in writing, your +Contribution is provided without warranties or conditions of any kind. + +## 9. Public licensing and commercial licensing + +You understand that RMON may be distributed under the Elastic License +2.0 or another public license selected by the Project Owner and may also be +licensed to other parties under separate commercial or proprietary terms. + +No revenue share, royalty, fee, or other payment is owed to you merely because +your Contribution is included in a commercially licensed version of +RMON. + +## 10. How to accept + +For a GitHub pull request, accept this Agreement by posting the following +comment on the pull request: + +`/sign-cla` + +The project's GitHub workflow records the acceptance together with your +numeric GitHub user ID, the accepted CLA version, the date of acceptance, +and the originating pull request/comment. + +You only need to accept a particular version of this Agreement once. If this +Agreement changes, the Project may require you to accept the new version before +additional Contributions are merged. + +A Contribution must not be merged unless the contributor has expressly +accepted the applicable version of this Agreement. + +## 11. Existing contributions + +This Agreement does not retroactively change the license under which a +Contribution was previously submitted and accepted before the Project adopted +this CLA. Existing contributions remain subject to the rights already granted +for those contributions unless the contributor separately agrees otherwise. + +## 12. Entire contributor agreement + +This Agreement governs the rights granted for Contributions submitted after its +adoption unless the contributor and Project Owner enter into a separate written +agreement covering those Contributions. diff --git a/COMMERCIAL_LICENSE.md b/COMMERCIAL_LICENSE.md new file mode 100644 index 0000000..b1a35c4 --- /dev/null +++ b/COMMERCIAL_LICENSE.md @@ -0,0 +1,74 @@ +# RMON commercial licensing + +RMON is available under the Elastic License 2.0 (ELv2) for uses +permitted by that license. Separate commercial licensing is available for +organizations that need additional rights. + +## Hosted and managed services + +If your organization wants to provide RMON, or a service exposing a +substantial set of RMON features or functionality, to third parties as +a hosted or managed service, obtain a commercial license before launching that +offering. + +Commercial licensing can cover use cases such as: + +- SaaS and RMON-as-a-Service; +- managed or multi-tenant RMON hosting; +- white-label hosted services; +- OEM or embedded commercial distribution; +- negotiated redistribution rights; +- commercial branding arrangements; +- enterprise support, warranty, indemnity, or other negotiated terms. + +## No commercial rights are granted by this file + +This document describes the availability of commercial licensing. It is not a +commercial license grant and does not itself give any organization additional +rights beyond the license that already applies to the software. + +Additional commercial rights exist only after the copyright holder and the +customer enter into a separate written agreement that expressly grants those +rights. + +## Expected commercial agreement + +A signed commercial agreement should normally identify at least: + +- the licensor's legal name and address; +- the customer's legal name and address; +- the RMON software and versions covered; +- whether SaaS, hosted, managed, OEM, white-label, or redistribution rights are + granted; +- permitted production environments, customers, tenants, or usage limits, if + applicable; +- fees and payment terms; +- agreement term and renewal terms; +- support and service obligations, if any; +- confidentiality terms, if any; +- intellectual-property ownership; +- warranty, liability, and indemnity terms; +- termination and post-termination rights; +- governing law and dispute-resolution terms. + +## Requesting a license + +Contact the RMON copyright holder through the project repository with: + +1. your organization's legal name; +2. a short description of the product or service; +3. whether customers will access the RMON UI, APIs, or monitoring checks; +4. whether the service is single-tenant or multi-tenant; +5. expected number of customers or deployments; +6. whether you need white-label, OEM, redistribution, support, or indemnity + rights. + +The copyright holder can then provide commercial terms appropriate for that +use case. + +## Legal identity + +A repository username or project name should not be used as a substitute for +the licensor's legal identity in a signed commercial agreement. Before signing +a commercial agreement, replace project-only identifiers with the correct legal +name, address, and other required contracting details. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1639130 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,50 @@ +# Contributing to RMON + +## Backend changes policy + +Any backend code change must be covered by tests. + +When changing backend logic: + +- add new tests for new behavior; +- update existing tests for changed behavior; +- do not remove failing tests unless the tested behavior was intentionally removed; +- if a backend change does not need tests, explain why in the pull request. + +## Contributor License Agreement + +RMON uses a Contributor License Agreement so that contributions can be +distributed under the project's public source license and, where applicable, +under separate commercial terms. + +Before a pull request can be merged, read [CLA.md](CLA.md) and accept it by +posting this exact comment on your pull request: + +`/sign-cla` + +Acceptance is recorded against your numeric GitHub user ID and the version of +`CLA.md` on the repository's default branch. You only need to sign once per CLA +version. A checked box or a statement in the pull request description does not +replace the signing comment. + +The `CLA` status check verifies the pull request author's acceptance. If a pull +request contains contributions from other people, maintainers must also ensure +that those contributors have accepted the applicable CLA. Identify third-party +material and its license in the pull request. + +Do not merge a contribution until the applicable contributors have expressly +accepted the CLA. + +## Maintainer setup + +The signing workflow reads `CLA.md` from the default branch. Publish the CLA and +`.github/workflows/cla.yml` there before relying on the signing workflow for +pull requests targeting `main` or `release/**`. + +Configure the repository's branch protection or rulesets to require the `CLA` +status check. Adding the workflow alone does not enforce a merge restriction. + +The workflow maintains a public `[CLA] Signature Registry` issue. Only signature +records written by the workflow's GitHub Actions bot are accepted. Changes to +the CLA on the default branch refresh open pull request statuses and require +contributors to accept the new version. diff --git a/LICENSE b/LICENSE index 3d84448..cdba7ad 100644 --- a/LICENSE +++ b/LICENSE @@ -1,203 +1,94 @@ -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - 10. Modification and distribution for commercial purposes is prohibited without the consent of the author. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [2024] [Pavel Loginov] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Elastic License 2.0 + +URL: https://www.elastic.co/licensing/elastic-license + +Acceptance + +By using the software, you agree to all of the terms and conditions below. + +Copyright License + +The licensor grants you a non-exclusive, royalty-free, worldwide, +non-sublicensable, non-transferable license to use, copy, distribute, make +available, and prepare derivative works of the software, in each case subject +to the limitations and conditions below. + +Limitations + +You may not provide the software to third parties as a hosted or managed +service, where the service provides users with access to any substantial set of +the features or functionality of the software. + +You may not move, change, disable, or circumvent the license key functionality +in the software, and you may not remove or obscure any functionality in the +software that is protected by the license key. + +You may not alter, remove, or obscure any licensing, copyright, or other notices +of the licensor in the software. Any use of the licensor's trademarks is +subject to applicable law. + +Patents + +The licensor grants you a license, under any patent claims the licensor can +license, or becomes able to license, to make, have made, use, sell, offer for +sale, import and have imported the software, in each case subject to the +limitations and conditions in this license. This license does not cover any +patent claims that you cause to be infringed by modifications or additions to +the software. + +If you or your company make any written claim that the software infringes or +contributes to infringement of any patent, your patent license for the software +granted under these terms ends immediately. If your company makes such a claim, +your patent license ends immediately for work on behalf of your company. + +Notices + +You must ensure that anyone who gets a copy of any part of the software from +you also gets a copy of these terms. + +If you modify the software, you must include in any modified copies of the +software prominent notices stating that you have modified the software. + +No Other Rights + +These terms do not imply any licenses other than those expressly granted in +these terms. + +Termination + +If you use the software in violation of these terms, such use is not licensed, +and your licenses will automatically terminate. If the licensor provides you +with a notice of your violation, and you cease all violation of this license no +later than 30 days after you receive that notice, your licenses will be +reinstated retroactively. However, if you violate these terms after such +reinstatement, any additional violation of these terms will cause your licenses +to terminate automatically and permanently. + +No Liability + +As far as the law allows, the software comes as is, without any warranty or +condition, and the licensor will not be liable to you for any damages arising +out of these terms or the use or nature of the software, under any kind of legal +claim. + +Definitions + +The licensor is the entity offering these terms, and the software is the +software the licensor makes available under these terms, including any portion +of it. + +you refers to the individual or entity agreeing to these terms. + +your company is any legal entity, sole proprietorship, or other kind of +organization that you work for, plus all organizations that have control over, +are under the control of, or are under common control with that organization. +control means ownership of substantially all the assets of an entity, or the +power to direct its management and policies by vote, contract, or otherwise. +Control can be direct or indirect. + +your licenses are all the licenses granted to you for the software under these +terms. + +use means anything you do with the software requiring one of your licenses. + +trademark means trademarks, service marks, and similar rights. diff --git a/LICENSING.md b/LICENSING.md new file mode 100644 index 0000000..6f370e4 --- /dev/null +++ b/LICENSING.md @@ -0,0 +1,110 @@ +# RMON licensing + +RMON uses a source-available licensing model designed to keep +self-hosting broadly available while reserving hosted/managed RMON +services for commercial licensing. + +## Community license + +Starting with the 1.4.0 release line, the RMON source distribution is offered +under the Elastic License 2.0 (ELv2), SPDX identifier `Elastic-2.0`. + +The canonical ELv2 text is published by Elastic at: +https://www.elastic.co/licensing/elastic-license + +An SPDX reference is available at: +https://spdx.org/licenses/Elastic-2.0.html + +ELv2 is source-available, but it is not an OSI-approved Open Source license. + +## Typical permitted uses + +Subject to the complete ELv2 terms, typical permitted uses include: + +- running RMON internally in your own organization; +- modifying RMON for your own internal use; +- evaluating, testing, and developing against RMON; +- redistributing RMON subject to ELv2 requirements; +- a consultant installing or operating RMON for a customer where the + customer is using it internally and the consultant is not offering + RMON itself as a hosted product to multiple third parties. + +## When a commercial license is required + +A separate commercial agreement is required when you want rights that are not +granted by ELv2. The primary example is offering RMON, or a service +that exposes a substantial set of RMON features or functionality, to +third parties as a hosted or managed service. + +Examples include: + +- RMON-as-a-Service; +- a hosted monitoring product that substantially exposes the + RMON UI, APIs, monitoring checks, or feature set to customers; +- a managed multi-tenant RMON offering; +- a white-label hosted RMON service; +- another hosted or managed offering where commercial rights beyond ELv2 are + needed. + +A commercial license may also be used for negotiated OEM, redistribution, +branding, support, indemnity, warranty, or other enterprise rights. + +See `COMMERCIAL_LICENSE.md` for the commercial licensing process. + +## What is not automatically a hosted RMON service + +Using RMON as an internal component of a larger service does not by +itself mean that the larger service is prohibited. The controlling terms are +the ELv2 terms, including whether third-party users are given access to a +substantial set of RMON features or functionality. + +When a proposed deployment is close to that boundary, obtain written licensing +confirmation before launching the service. + +## Historical releases + +RMON versions and copies distributed before this licensing change remain +subject to the terms that accompanied those copies. This change does not revoke +previously granted rights or retroactively apply ELv2 to earlier copies. + +Before this change, the repository's `LICENSE` contained the Apache License 2.0 +text with an additional clause restricting modification and distribution for +commercial purposes without the author's consent. The exact previous text is +preserved in [licenses/legacy/LICENSE-before-1.4.0.txt](licenses/legacy/LICENSE-before-1.4.0.txt) +for historical reference. It is not an alternative license for the current +source distribution. + +Consult the license and notices shipped with the particular historical copy; +the transition does not relabel those terms as MIT or unmodified Apache 2.0. + +## Contributions + +New contributions are accepted only under the contributor terms in `CLA.md`. +Those terms are intended to ensure that RMON can continue to be +published under ELv2 while also being offered under separate commercial terms. + +Contributors retain ownership of their contributions but grant the project the +rights described in the CLA, including the rights needed for sublicensing and +commercial licensing. + +## Third-party dependencies + +Third-party dependencies and vendored components retain their own licenses. +Nothing in RMON's licensing files changes the license of third-party +software. + +## Trademarks + +A software license does not automatically grant rights to project names, +logos, service marks, or other trademarks except to the extent required by +applicable law or expressly stated in a separate agreement. + +## Questions and commercial licensing + +For a proposed SaaS, hosted, managed, OEM, white-label, or other commercial use, +contact the RMON copyright holder through the project repository before +launching the offering. + +A signed commercial agreement should identify the licensor and licensee by +legal name and should state the exact rights granted, fees, term, support (if +any), liability terms, governing law, and termination terms. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..fc636f9 --- /dev/null +++ b/NOTICE @@ -0,0 +1,6 @@ +RMON +Copyright 2024 Pavel Loginov + +Third-party dependencies and vendored components retain their own copyright +notices and licenses. See LICENSING.md for the current licensing model and +historical release terms. diff --git a/README.md b/README.md index b315b76..98575ff 100644 --- a/README.md +++ b/README.md @@ -55,3 +55,19 @@ Subscription checks are centralized in `app/modules/subscription/access.py`, fol All cataloged features are currently free. `SUBSCRIPTION_ENFORCEMENT_ENABLED` is intentionally set to `False` in the subscription module, so stored license status and plan values do not restrict access. When paid enforcement is ready, change that code constant to `True`; the existing feature policies, route guards, business-service checks, UI visibility rules, and plan limits will become active together. ![alt text](https://rmon.io/static//images/rmon_checks.png "RMON checks") + +## License + +Starting with the 1.4.0 release line, RMON is source-available under the +[Elastic License 2.0](LICENSE) (`Elastic-2.0`). Self-hosted and internal use is +available subject to ELv2. Offering RMON, or a service exposing a substantial +set of its functionality, to third parties as a hosted or managed service +requires rights permitted by ELv2 or a separate commercial license. + +See [LICENSING.md](LICENSING.md) for usage examples and historical release +terms, and [COMMERCIAL_LICENSE.md](COMMERCIAL_LICENSE.md) for commercial +licensing. Contributions are subject to [CLA.md](CLA.md); see +[CONTRIBUTING.md](CONTRIBUTING.md) for the signing process. + +Historical versions and copies retain the terms that accompanied them. +Third-party dependencies and vendored components retain their own licenses. diff --git a/app/modules/roxywi/auth.py b/app/modules/roxywi/auth.py index 5882581..ccea617 100644 --- a/app/modules/roxywi/auth.py +++ b/app/modules/roxywi/auth.py @@ -97,7 +97,7 @@ def check_in_ldap(user, password): def do_login(user_params: dict, next_url: str): next_url = safe_next_url(next_url) - redirect_to = f'https://{request.host}{next_url}' + redirect_to = next_url response = jsonify({"status": "done", "next_url": redirect_to}) access_token = create_jwt_token(user_params) diff --git a/app/static/css/ux.css b/app/static/css/ux.css new file mode 100644 index 0000000..a62c9e4 --- /dev/null +++ b/app/static/css/ux.css @@ -0,0 +1,121 @@ +/* Layout and accessibility shared by existing RMON screens. */ +body { font-size: 14px; } +button, input, select, textarea { font: inherit; } +[hidden] { display: none !important; } +:focus-visible { outline: 3px solid #147eb3 !important; outline-offset: 3px; } +.skip-link { position: fixed; top: -100px; left: 16px; z-index: 2000; padding: 12px; background: white; color: #00243e; } +.skip-link:focus { top: 8px; } +.app-shell { --sidebar-width: 207px; min-height: 100vh; } +.app-shell .top-menu { position: fixed; width: var(--sidebar-width); height: 100dvh; min-height: 0; margin: 0; overflow-y: auto; overflow-x: hidden; } +.app-shell .top-menu .v_menu { display: none; position: static; width: auto; opacity: 1; border: 0; transition: none; } +.app-shell .top-menu .p_menu:hover .v_menu, .app-shell .top-menu .p_menu:focus-within .v_menu { display: block; } +.app-shell .top-menu .v_menu a { padding-left: 24px; } +.app-shell .container { box-sizing: border-box; min-width: 0; width: auto; max-width: none; margin-left: var(--sidebar-width); min-height: calc(100vh - 90px); } +.page-header { display: flex; align-items: center; gap: 10px; padding: 10px 16px; flex-wrap: wrap; } +.page-header h2 { padding: 0; margin: 0; height: auto; flex: 1; min-width: 0; } +.account-actions { display: flex; gap: 8px; align-items: center; margin-left: auto; } +.page-header .login { float: none; margin: 0; font-size: 14px; } +body .icon-button { display: inline-flex; align-items: center; justify-content: center; min-width: 36px; min-height: 36px; box-sizing: border-box; margin: 0; padding: 6px !important; border: 1px solid transparent; border-radius: 6px; background: transparent; color: inherit; cursor: pointer; text-decoration: none; } +body .icon-button:hover { background: rgba(49, 175, 225, .15); } +#menu-toggle { font-size: 22px; color: var(--color-blue); } +body.nav-collapsed { --sidebar-width: 0px; } +.nav-collapsed .top-menu { display: none; } +.menu-backdrop { position: fixed; inset: 0; z-index: 98; background: #001b30a6; border: 0; } +.app-shell .footer { box-sizing: border-box; display: flex; align-items: center; flex-wrap: wrap; gap: 12px; margin: 0 16px 0 calc(var(--sidebar-width) + 16px); padding: 12px 0; width: auto; min-width: 0; max-width: none; } +.footer #logo_footer, .footer .footer-div, .footer #version { float: none; margin: 0; padding: 0; width: auto; } +.footer .footer-div { flex: 1; } +.footer #useful-links { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px 14px; } +.footer .footer-link { padding: 4px 0; font-size: 13px; } +.footer #logo_footer_img { margin: 0; } +.footer #hide_menu, .footer .show_menu { display: none !important; } +.table-scroll { width: auto; max-width: 100%; min-width: 0; overflow-x: auto; overscroll-behavior-x: contain; } +.table-scroll > table { float: none !important; margin: 0 !important; width: 100% !important; max-width: none !important; } +.overview-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; padding: 16px; } +.overview-grid .overview-wi { width: 100%; min-width: 0; max-width: none; float: none; margin: 0; } +.overview-grid > .table-scroll { border: 1px solid #b8c3cc; border-radius: 8px; align-self: start; } +.overview-grid .overviewHead { height: auto; } +.overview-grid .overview-small-table { height: auto; min-height: 0; } +.overview-grid #cpu_div:has(.widget-state), .overview-grid #ram_div:has(.widget-state) { height: auto !important; min-height: 122px; } +.widget-state { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; min-height: 100px; padding: 12px; text-align: center; box-sizing: border-box; } +.retry-button { color: inherit; border: 1px solid #8295a5; padding: 7px 12px; border-radius: 5px; background: transparent; cursor: pointer; } +.empty-state { text-align: center; margin: 20px auto; padding: 24px; max-width: 650px; } +.empty-state h3 { background: transparent; border: 0; padding: 0; } +.empty-state p { font-size: 16px; line-height: 1.6; } +.empty-state img { max-width: min(240px, 70vw); height: auto; } +body button.add-button { background: #087fab; color: #fff; border: 1px solid transparent; font-weight: 600; min-height: 40px; } +body button.add-button:hover { background: #00668f; } +body .ui-dialog { box-sizing: border-box; max-width: calc(100vw - 24px); } +body .ui-dialog .ui-dialog-titlebar { flex: 0 0 auto; } +body .ui-dialog .ui-dialog-title { white-space: normal; } +body .ui-dialog .ui-dialog-buttonpane { flex: 0 0 auto; margin: 0; background: var(--color-white); border-top: 1px solid #b8c3cc; } +body .ui-dialog .ui-dialog-content { min-width: 0; } +body .ui-dialog .overview { max-width: 100%; } +body .ui-dialog .validateTips { width: auto; max-width: 100%; } +body .ui-dialog textarea { box-sizing: border-box; max-width: 100%; } +body .ui-selectmenu-menu .ui-menu { max-width: calc(100vw - 32px); } +body .field-help { font-size: 13px; line-height: 1.5; overflow-wrap: anywhere; margin: 8px 0; } +body #create-status-page-step-1-overview, body #create-status-page-step-2-overview { width: 100%; table-layout: fixed; } +body #create-status-page-step-1 .form-control, body #create-status-page-step-2 .form-control { width: 100%; box-sizing: border-box; } +#status-page-url-preview { color: var(--color-blue); } +#pages .page_div { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } +#pages .page_link { flex: 1; min-width: 0; overflow-wrap: anywhere; } +#pages .page_slug { font-size: 13px; } +body #admin-tabs { display: flex; overflow-x: auto; overflow-y: hidden; flex-wrap: nowrap; gap: 4px; } +body #admin-tabs li { flex: 0 0 auto; float: none; } +.ui-tabs-panel { min-width: 0; } +body #toast-container { max-width: calc(100vw - 24px); } + +/* Login keeps its existing form IDs, login flow and SSO buttons. */ +body.login-page { min-height: 100dvh; display: grid; place-items: center; background: #eef4f8; color: #172b3b; padding: 24px; box-sizing: border-box; } +.login-page #main_div { margin: 0; padding: 0; width: 100%; min-width: 0; max-width: 440px; background: white; border-radius: 14px; box-shadow: 0 8px 36px #00243e1a; } +.login-page #login-form { padding: 28px; min-height: 0; } +.login-page #logo_span { float: none; border: 0; margin: -28px -28px 28px; padding: 24px; width: auto; height: auto; background: #00243e; border-radius: 14px 14px 0 0; text-align: center; } +.login-page #logo_img { margin: 0; float: none; width: auto; } +.login-page #logo_span img { width: 170px; margin: 0; padding: 0; } +.login-page #logo_sign { float: none; color: white; margin: 6px 0 0; font-size: 14px; } +.login-page #auth { margin: 0; float: none; position: static; width: 100%; } +.login-page #auth > label { display: block; margin: 12px 0 6px; font-weight: 600; } +.login-page .fontuser, .login-page .fontpassword { position: relative; } +.login-page .fontuser > i, .login-page .fontpassword > i { position: absolute; top: 16px; left: 12px; color: #436078; } +.login-page .form-login { box-sizing: border-box; width: 100%; min-height: 46px; padding: 10px 44px 10px 34px; border: 1px solid #91a4b4 !important; border-radius: 6px; font-size: 16px; color: #172b3b; background: white; } +.login-page .password-toggle { position: absolute; top: 4px; right: 4px; color: #172b3b; } +.login-page #enter { width: 100%; margin: 20px 0 0; min-height: 46px; color: white; background: #007cab; border: 0; font-weight: 600; } +.login-page .wrong-login { box-sizing: border-box; width: 100%; margin: 16px 0 0; text-align: left; } +.login-page .oidc-login-options { width: 100%; margin: 20px 0 0; padding: 0; clear: both; } +.login-page .oidc-login-button { box-sizing: border-box; width: 100%; min-height: 44px; color: #172b3b; white-space: normal; } + +@media (max-width: 1100px) { + .overview-grid { grid-template-columns: minmax(0, 1fr); } + body .ui-dialog #smon-add-table > table { width: 100% !important; float: none !important; } +} +@media (max-width: 800px) { + .app-shell { --sidebar-width: 0px; } + .app-shell .top-menu { width: 260px; display: none; top: 62px; height: calc(100dvh - 62px); z-index: 99; } + .app-shell.nav-open .top-menu { display: block; } + .nav-open { overflow: hidden; } + .page-header { min-height: 62px; padding: 8px 12px; box-sizing: border-box; position: relative; z-index: 100; background: var(--color-white); } + .page-header h2 { font-size: 18px; } + .account-actions { gap: 2px; } + .account-actions #show-user-settings-button { max-width: 140px; overflow-wrap: anywhere; } + .app-shell .top-menu .menu a { min-height: 44px; box-sizing: border-box; } + .app-shell .top-menu .v_menu { display: block; position: static; width: auto; } + .app-shell .top-menu .v_menu a { padding-left: 30px; } + .app-shell .container { width: 100%; margin-left: 0; } + .app-shell .footer { margin-left: 12px; margin-right: 12px; } + .footer #useful-links { justify-content: flex-start; } + body .icon-button, body [role=button], body .ui-button { min-height: 44px; min-width: 44px; } + body .table-scroll { margin: 0; } + body .table-scroll table { min-width: 560px; } + .overview-grid { padding: 12px; gap: 12px; } + .overview-grid .table-scroll table { min-width: 0; } + body .ui-dialog table.overview { width: 100% !important; table-layout: fixed; } + body .ui-dialog table.overview td { display: block; box-sizing: border-box; width: 100% !important; padding: 6px 2px !important; } + body .ui-dialog input:not([type=checkbox]):not([type=radio]):not([type=hidden]), body .ui-dialog textarea, + body .ui-dialog .ui-selectmenu-button, body .ui-dialog .tagify { box-sizing: border-box; width: 100% !important; max-width: 100%; font-size: 16px; } + body .ui-dialog .checks { display: flex; gap: 12px; height: auto; max-height: 300px; } + body .ui-dialog .checks > div { width: 50%; min-width: 0; float: none; } + body .ui-dialog .all-checks { width: 100%; height: auto; min-height: 52px; display: flex; } + body .ui-dialog .check-name { width: auto; min-width: 0; overflow-wrap: anywhere; float: none; margin: 4px; flex: 1; } + body .ui-tabs-panel { padding: 10px !important; } + .empty-state { padding: 16px; margin: 12px; } +} diff --git a/app/static/js/metrics.js b/app/static/js/metrics.js index 1ea733d..5b1ff1a 100644 --- a/app/static/js/metrics.js +++ b/app/static/js/metrics.js @@ -5,11 +5,11 @@ function getChartDataHapWiRam(ip) { } $.ajax({ url: "/metrics/ram", + global: false, + error: () => RmonUI.widgetState("#ram_div", "error", () => getChartDataHapWiRam(ip)), data: JSON.stringify(jsonData), contentType: "application/json; charset=utf-8", - beforeSend: function() { - $('#ram').html('loading...') - }, + beforeSend: () => RmonUI.widgetState("#ram_div", "loading"), type: "POST", success: function (result) { let data = []; @@ -19,6 +19,7 @@ function getChartDataHapWiRam(ip) { // Преобразование значений в числа const formattedData = ramsData.map(value => parseFloat(value)); + RmonUI.widgetState("#ram_div", "ready"); renderChartHapWiRam(formattedData); } }); @@ -83,6 +84,9 @@ function getChartDataHapWiCpu(ip) { } $.ajax({ url: "/metrics/cpu", + global: false, + beforeSend: () => RmonUI.widgetState("#cpu_div", "loading"), + error: () => RmonUI.widgetState("#cpu_div", "error", () => getChartDataHapWiCpu(ip)), data: JSON.stringify(jsonData), contentType: "application/json; charset=utf-8", type: "POST", @@ -92,6 +96,7 @@ function getChartDataHapWiCpu(ip) { // Преобразование значений в числа const formattedData = ramsData.map(value => parseFloat(value)); + RmonUI.widgetState("#cpu_div", "ready"); renderChartHapWiCpu(formattedData); } }); diff --git a/app/static/js/overview.js b/app/static/js/overview.js index 44dde55..2291de6 100644 --- a/app/static/js/overview.js +++ b/app/static/js/overview.js @@ -5,47 +5,16 @@ function showOverview(serv, hostnamea) { showUsersOverview(); showSubOverview(); showServicesOverview(); - updatingCpuRamCharts(); var i; for (i = 0; i < serv.length; i++) { showOverviewCallBack(serv[i], hostnamea[i]) } } function showOverviewCallBack(serv, hostnamea) { - $.ajax( { - url: "/overview/server/"+serv, - beforeSend: function() { - $("#"+hostnamea).html(''); - }, - type: "GET", - success: function( data ) { - if (data.indexOf('error:') != '-1') { - toastr.error(data); - $("#"+hostnamea).html(""); - } else { - $("#" + hostnamea).empty(); - $("#" + hostnamea).html(data); - } - } - } ); + return RmonUI.loadFragment("#" + hostnamea, "/overview/server/" + encodeURIComponent(serv)); } function showServicesOverview() { - $.ajax( { - url: "/overview/services", - beforeSend: function() { - $("#services_ovw").html(''); - - }, - type: "GET", - success: function( data ) { - if (data.indexOf('error:') != '-1') { - toastr.error(data); - } else { - $("#services_ovw").empty(); - $("#services_ovw").html(data); - } - } - } ); + return RmonUI.loadFragment("#services_ovw", "/overview/services"); } function showOverviewServer(ip) { getChartDataHapWiRam(ip); @@ -87,12 +56,12 @@ $( function() { $("#show-all-groups").css("display", "block"); }); - $( "#show-all-haproxy-wi-log" ).click( function() { + $(document).on('click', '#show-all-haproxy-wi-log', function() { $(".show-haproxy-wi-log").show("fast"); $("#hide-all-haproxy-wi-log").css("display", "block"); $("#show-all-haproxy-wi-log").css("display", "none"); }); - $( "#hide-all-haproxy-wi-log" ).click( function() { + $(document).on('click', '#hide-all-haproxy-wi-log', function() { $(".show-haproxy-wi-log").hide("fast"); $("#hide-all-haproxy-wi-log").css("display", "none"); $("#show-all-haproxy-wi-log").css("display", "block"); @@ -103,52 +72,11 @@ $( function() { } }); function showUsersOverview() { - $.ajax( { - url: "overview/users", - type: "GET", - beforeSend: function() { - $("#users-table").html(''); - }, - success: function( data ) { - data = data.replace(/\s+/g,' '); - if (data.indexOf('error:') != '-1') { - toastr.error(data); - } else { - $("#users-table").html(data); - } - } - } ); + return RmonUI.loadFragment("#users-table", "/overview/users"); } function showSubOverview() { - $.ajax( { - url: "/overview/sub", - type: "GET", - beforeSend: function() { - $("#sub-table").html(''); - }, - success: function( data ) { - data = data.replace(/\s+/g,' '); - if (data.indexOf('error:') != '-1') { - toastr.error(data); - } else { - $("#sub-table").html(data); - } - } - } ); + return RmonUI.loadFragment("#sub-table", "/overview/sub"); } - function ShowOverviewLogs() { - $.ajax( { - url: "/overview/logs", - type: "GET", - beforeSend: function() { - $("#overview-logs").html(''); - }, - success: function( data ) { - data = data.replace(/\s+/g,' '); - $("#overview-logs").html(data); - $.getScript("/static/js/fontawesome.min.js") - $.getScript("/static/js/overview.js") - } - } ); + return RmonUI.loadFragment("#overview-logs", "/overview/logs"); } diff --git a/app/static/js/rmon/status_page.js b/app/static/js/rmon/status_page.js index 4c13edc..34abbf1 100644 --- a/app/static/js/rmon/status_page.js +++ b/app/static/js/rmon/status_page.js @@ -1,11 +1,29 @@ -function createStatusPageStep1(edited=false, page_id=0) { - clearStatusPageDialog(); +function updateStatusPagePreview() { + const preview = $('#status-page-url-preview'); + preview.text(window.location.origin + preview.attr('data-prefix') + $('#new-status-page-slug').val()); +} +$(function () { + $('#new-status-page-slug').attr('aria-describedby', 'status-page-slug-help status-page-url-preview') + .on('input', function () { $(this).data('manually-edited', true); updateStatusPagePreview(); }); + $('#new-status-page-name').on('input', function () { + if (!$('#new-status-page-slug').data('manually-edited')) { + $('#new-status-page-slug').val($(this).val().toLowerCase().trim() + .normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9_]+/g, '-').replace(/^-|-$/g, '')); + updateStatusPagePreview(); + } + }); +}); +function createStatusPageStep1(edited=false, page_id=0, preserveDraft=false) { + if (!preserveDraft) clearStatusPageDialog(); let next_word = $('#translate').attr('data-next'); let smon_add_tabel_title = $("#create-status-page-step-1-overview").attr('title'); if (edited) { smon_add_tabel_title = $("#create-status-page-step-1-overview").attr('data-edit'); + } + if (edited && !preserveDraft) { $('#new-status-page-name').val($('#page_name-' + page_id).text()); $('#new-status-page-slug').val($('#page_slug-' + page_id).text().split('/').pop()); + $('#new-status-page-slug').data('manually-edited', true); $('#new-status-page-desc').val($('#page_desc-' + page_id).text().replace('(', '').replace(')', '')); $.ajax({ url: api_v_prefix + '/rmon/status-page/' + page_id + '?recurse=true', @@ -43,19 +61,21 @@ function createStatusPageStep1(edited=false, page_id=0) { text: next_word, click: function () { if ($('#new-status-page-name').val() == '') { - toastr.error('error: Fill in the Name field'); + toastr.error(RmonUI.text('name_required')); + $('#new-status-page-name').trigger('focus'); return false; } if (!regx.test($('#new-status-page-slug').val())) { - toastr.error('error: Incorrect Slug'); + toastr.error(RmonUI.text('slug_invalid')); + $('#new-status-page-slug').trigger('focus'); return false; } if ($('#new-status-page-slug').val().indexOf('--') != '-1') { - toastr.error('error: "--" are prohibeted in Slug'); + toastr.error(RmonUI.text('slug_invalid')); return false; } if ($('#new-status-page-slug').val() == '') { - toastr.error('error: Fill in the Slug field'); + toastr.error(RmonUI.text('slug_invalid')); return false; } createStatusPageStep2(edited, page_id); @@ -70,6 +90,7 @@ function createStatusPageStep1(edited=false, page_id=0) { } }] }); + updateStatusPagePreview(); addSmonStatus.dialog('open'); } function createStatusPageStep2(edited, page_id) { @@ -106,7 +127,7 @@ function createStatusPageStep2(edited, page_id) { text: back_word, click: function () { $(this).dialog("close"); - createStatusPageStep1(edited, page_id); + createStatusPageStep1(edited, page_id, true); } }, { text: cancel_word, @@ -121,7 +142,7 @@ function createStatusPageStep2(edited, page_id) { function clearStatusPageDialog() { clearTips(); $('#new-status-page-name').val(''); - $('#new-status-page-slug').val(''); + $('#new-status-page-slug').val('').data('manually-edited', false); $('#new-status-page-desc').val(''); $('#new-status-page-style').val(''); $("#enabled-check > div").each((index, elem) => { @@ -150,10 +171,12 @@ function createStatusPage(dialog_id) { ]), elem("div", {"class":"page_slug","id":"page_slug-"+id}, "/rmon/status/"+json_data['slug']) ]), - elem("div", {"class":"edit status_page-edit","onclick":"createStatusPageStep1('true', '"+id+"')"}), - elem("div", {"class":"delete","onclick":"confirmDeleteStatusPage('"+id+"')"}) + elem("button", {"type":"button","aria-label":RmonUI.text("edit"),"class":"edit status_page-edit icon-button","onclick":"createStatusPageStep1('true', '"+id+"')"}), + elem("button", {"type":"button","aria-label":RmonUI.text("delete"),"class":"delete icon-button","onclick":"confirmDeleteStatusPage('"+id+"')"}) ]) $("#pages").append(new_page); + $("#status-pages-empty").prop("hidden", true); + clearStatusPageDialog(); smon_manage_status_page_avg_status(id); $(dialog_id).dialog('close'); $.getScript("/static/js/fontawesome.min.js"); @@ -246,9 +269,11 @@ function deleteStatusPage(page_id) { statusCode: { 204: function (xhr) { $("#page_" + page_id).remove(); + $("#status-pages-empty").prop("hidden", $("#pages .page_div").length > 0); }, 404: function (xhr) { $("#page_" + page_id).remove(); + $("#status-pages-empty").prop("hidden", $("#pages .page_div").length > 0); } }, success: function (data) { diff --git a/app/static/js/script.js b/app/static/js/script.js index 2328de8..753e154 100644 --- a/app/static/js/script.js +++ b/app/static/js/script.js @@ -46,9 +46,9 @@ $.ajaxSetup({ headers: {"X-CSRF-TOKEN": csrf_token}, }); $(document).ajaxError(function myErrorHandler(event, xhr, ajaxOptions, thrownError) { - if (xhr.status != 401 && xhr.status != 404) { - toastr.error(xhr.responseJSON.error); - } + if (xhr.statusText !== 'abort' && !ajaxOptions.error) { + RmonUI.notifyRequestError(xhr); + } }); function showLog() { @@ -171,9 +171,10 @@ $( function() { }); toastr.options.closeButton = true; toastr.options.progressBar = true; - toastr.options.positionClass = 'toast-bottom-full-width'; - toastr.options.timeOut = 25000; - toastr.options.extendedTimeOut = 50000; + toastr.options.positionClass = 'toast-bottom-right'; + toastr.options.timeOut = 8000; + toastr.options.preventDuplicates = true; + toastr.options.extendedTimeOut = 2000; $('#errorMess').click(function () { $('#error').remove(); }); @@ -198,45 +199,7 @@ $( function() { $("input[type=checkbox]").checkboxradio(); $(".controlgroup").controlgroup(); - $("#hide_menu").click(function () { - $(".top-menu").hide("drop", "fast"); - $(".container").css("max-width", "100%"); - $(".footer").css("max-width", "97%"); - $(".container").css("margin-left", "1%"); - $(".footer").css("margin-left", "1%"); - $(".show_menu").show(); - $("#hide_menu").hide(); - sessionStorage.setItem('hide_menu', 'hide'); - }); - $("#show_menu").click(function () { - $(".top-menu").show("drop", "fast"); - $(".container").css("max-width", "100%"); - $(".footer").css("max-width", "100%"); - $(".container").css("margin-left", "207px"); - $(".footer").css("margin-left", "207px"); - $(".show_menu").hide(); - $("#hide_menu").show(); - sessionStorage.setItem('hide_menu', 'show'); - }); - var hideMenu = sessionStorage.getItem('hide_menu'); - if (hideMenu === "show") { - $(".top-menu").show("drop", "fast"); - $(".container").css("max-width", "100%"); - $(".container").css("margin-left", "207px"); - $(".footer").css("margin-left", "207px"); - $(".footer").css("max-width", "100%"); - $("#hide_menu").show(); - $(".show_menu").hide(); - } - if (hideMenu === "hide") { - $(".top-menu").hide(); - $(".container").css("max-width", "97%"); - $(".container").css("margin-left", "1%"); - $(".footer").css("margin-left", "1%"); - $(".footer").css("max-width", "97%"); - $(".show_menu").show(); - $("#hide_menu").hide(); - } + RmonUI.initNavigation(); var now = new Date(Date.now()); if ($('#time_range_out_hour').val() != '' && $('#time_range_out_hour').val() != 'None') { @@ -319,29 +282,18 @@ $( function() { data: JSON.stringify(json_data), contentType: "application/json; charset=utf-8", type: "POST", - statusCode: { - 401: function (xhr) { - $('.alert').show(); - if (xhr.responseText.indexOf('disabled') != '-1') { - $('.alert').html('Your login is disabled') - } else { - $('.alert').html('Login or password is incorrect'); - ban(); - } - } - }, - success: function (data) { + error: function (xhr) { + $('#wrong-login').show(); + $('#login-error-message').text(xhr.status === 401 ? RmonUI.text('login_failed') : RmonUI.requestError(xhr)); + if (xhr.status === 401) { ban(); } + }, + success: function (data) { if (data.status === 'failed') { - if (data.error.indexOf('disabled') != '-1') { - $('.alert').show(); - $('.alert').html(data.error); - } else { - $('.alert').show(); - $('.alert').html(data.error); - ban(); - } - } else { - sessionStorage.removeItem('check-service'); + $('#wrong-login').show(); + $('#login-error-message').text(RmonUI.text('login_failed')); + ban(); + } else { + sessionStorage.removeItem('check-service'); window.location.replace(data.next_url); } } @@ -486,7 +438,7 @@ checkTheme(); async function ban() { $( '#login').attr('disabled', 'disabled'); $( '#pass').attr('disabled', 'disabled'); - $( "input[type=submit], button" ).button('disable'); + $('#enter').button('disable'); $('#wrong-login').show(); $('#ban_10').show(); $( '#ban_timer').text(10); @@ -500,7 +452,7 @@ async function ban() { $( '#login').removeAttr('disabled'); $( '#pass').removeAttr('disabled'); - $( "input[type=submit], button" ).button('enable'); + $('#enter').button('enable'); $('#ban_10').hide(); } function changeCurrentGroupF(user_id) { @@ -664,6 +616,8 @@ function show_version() { NProgress.configure({showSpinner: false}); $.ajax( { url: "/internal/show_version", + global: false, + error: function () { $("#version").text("RMON"); }, contentType: "application/json; charset=utf-8", success: function( data ) { if (data.need_update) { @@ -690,13 +644,17 @@ function show_version() { } ); NProgress.configure({showSpinner: true}); } -function showPassword(input) { +function showPassword(input, button) { let x = document.getElementById(input); if (x.type === "password") { x.type = "text"; } else { x.type = "password"; } + if (button) { + button.setAttribute('aria-pressed', String(x.type === 'text')); + button.setAttribute('aria-label', RmonUI.text(x.type === 'text' ? 'hide_password' : 'show_password')); + } } function removeData() { let chart; diff --git a/app/static/js/ux.js b/app/static/js/ux.js new file mode 100644 index 0000000..dc7473d --- /dev/null +++ b/app/static/js/ux.js @@ -0,0 +1,157 @@ +/* Shared interaction helpers. Existing editor IDs and event handlers stay stable. */ +window.RmonUI = (function () { + 'use strict'; + const catalog = document.getElementById('rmon-ui-strings'); + const strings = catalog ? JSON.parse(catalog.textContent) : {}; + const text = key => strings[key] || key; + function requestError(xhr) { + const keys = {0: 'network_error', 400: 'validation_error', 401: 'unauthorized', + 403: 'forbidden', 404: 'not_found', 409: 'conflict', 422: 'validation_error', 429: 'rate_limit'}; + return text(xhr.status >= 500 ? 'server_error' : (keys[xhr.status] || 'request_error')); + } + function notifyRequestError(xhr) { + toastr.error(requestError(xhr)); + } + function widgetState(target, state, retry) { + const host = $(target); + if (!host.length) return; + host.children('.widget-state, .widget-state-row').remove(); + host.attr('aria-busy', String(state === 'loading')); + host.children('canvas').toggle(state === 'ready'); + if (state === 'ready') return; + const message = $('
', {class: 'widget-state', role: 'status'}); + $('').text(text(state === 'loading' ? 'loading' : 'widget_error')).appendTo(message); + if (state === 'error' && retry) { + $(' +
{% endfor %} diff --git a/app/templates/base.html b/app/templates/base.html index 74d45d9..0bc039d 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -1,3 +1,4 @@ + {% set chosen_lang = lang|default('en') %} {% import 'languages/'+lang|default('en')+'.html' as lang %} @@ -21,17 +22,22 @@ data-unknown="{{ lang.smon_page.desc.UNKNOWN }}" data-warning="{{ lang.smon_page.desc.WARNING }}"/> {% include 'include/main_head.html' %} - + + {% if g.user_params['user'] %} - + {% include 'include/main_menu.html' %} -
+
+ +
    {% if g.user_params['role'] %} {% if g.user_params['role'] <= 2 %} @@ -44,7 +50,7 @@

    {% block content %}{% endblock %}
    -

    +