Compare commits

..

138 Commits

Author SHA1 Message Date
Matt Hill
52c389e669 revert 2021-01-25 09:55:02 -07:00
Matt Hill
4d3d5fc94a remove bold style 2021-01-25 09:55:02 -07:00
Aaron Greenspan
5a4e980d31 ui: eject disks todo 2021-01-25 09:55:02 -07:00
Aaron Greenspan
8c79984e80 ui: default restore warnings 2021-01-25 09:55:02 -07:00
Aaron Greenspan
53db8fc4ec ui: preload bold, remove getdots from dom after load 2021-01-25 09:55:02 -07:00
Keagan McClelland
d2a70a782b wtf haskell 2021-01-25 09:55:02 -07:00
Aaron Greenspan
8096bef541 ui: copy 2021-01-25 09:55:02 -07:00
Aaron Greenspan
b8d84c5fc2 ui: if check fails, continue 2021-01-25 09:55:02 -07:00
Aaron Greenspan
d3dbdebbcf ui: no mutations, less hacky 2021-01-25 09:55:02 -07:00
Aaron Greenspan
55179e3ead ui: super coooool 2021-01-25 09:55:02 -07:00
Aaron Greenspan
bcbd972502 ui: coooool 2021-01-25 09:55:02 -07:00
Aaron Greenspan
fe7410c0fa ui: displayEmver on updates 2021-01-25 09:55:02 -07:00
Aaron Greenspan
4fba3a1d75 ui: remove log 2021-01-25 09:55:02 -07:00
Aaron Greenspan
c61c164c0f ui: pass toggle value to server object 2021-01-25 09:55:02 -07:00
Aaron Greenspan
ba04b7c431 ui: finalize PR 2021-01-25 09:55:02 -07:00
Aaron Greenspan
39accaa382 ui: cleanup 2021-01-25 09:55:02 -07:00
Aaron Greenspan
219f66ae8a ui: super clear conditionals 2021-01-25 09:55:02 -07:00
Aaron Greenspan
e482ccf7fd ui: fire autoCheck async 2021-01-25 09:55:02 -07:00
Aaron Greenspan
e96ef695a3 ui: revert to sync style 2021-01-25 09:55:02 -07:00
Aaron Greenspan
833941b031 save 2021-01-25 09:55:02 -07:00
Aiden McClelland
7417bfdbfa agent: invert autoCheckUpdates 2021-01-25 09:55:02 -07:00
Aaron Greenspan
df05fade25 ui: remove eject from backups 2021-01-25 09:55:02 -07:00
Aaron Greenspan
47e5951fc3 agent: serialize installAlert gdi 2021-01-25 09:55:02 -07:00
Aaron Greenspan
c1b6d5e1e4 ui: mocks off 2021-01-25 09:55:02 -07:00
Aaron Greenspan
832d7aaa6c ui: restore alert 2021-01-25 09:55:02 -07:00
Aaron Greenspan
db8a26c84c agent: adds restore alerts 2021-01-25 09:55:02 -07:00
Aaron Greenspan
18f18e3b95 agent: adds autoCheckUpdates to v0 2021-01-25 09:55:02 -07:00
Aaron Greenspan
7ed220dc51 ui: switch to concatMap 2021-01-25 09:55:02 -07:00
Aaron Greenspan
d224b7a114 ui: readability improvements 2021-01-25 09:55:02 -07:00
Aaron Greenspan
66ddf752ac save 2021-01-25 09:55:02 -07:00
Aaron Greenspan
b72252b437 ui: testing 2021-01-25 09:55:02 -07:00
Aaron Greenspan
e74ab3ce26 ui: remove interval checking 2021-01-25 09:55:02 -07:00
Aaron Greenspan
b7821576bb ui: factors auto checks 2021-01-25 09:55:02 -07:00
Aaron Greenspan
b717853759 agent: rip versionLatest 2021-01-25 09:55:02 -07:00
Aiden McClelland
ebd4cb8480 appmgr: restore-alert 2021-01-25 09:55:02 -07:00
Aiden McClelland
1188d67e30 appmgr: add install-alert to index 2021-01-25 09:55:02 -07:00
Aiden McClelland
25c55ea426 appmgr: sync after deleting hostname 2021-01-25 09:55:02 -07:00
Aiden McClelland
f993f19614 appmgr: handle tor address version change 2021-01-25 09:55:02 -07:00
Keagan McClelland
e38baa4779 removes unnecessary lib 2021-01-25 09:55:02 -07:00
Keagan McClelland
b3ab312088 more casing 2021-01-25 09:55:02 -07:00
Keagan McClelland
41b01efed3 consistent casing 2021-01-25 09:55:02 -07:00
Keagan McClelland
d211de9782 sledgehammer error catch, log and return nothing so that embassy remains live even if registry is unreachable 2021-01-25 09:55:02 -07:00
Aaron Greenspan
408cc45688 agent: adds versionLatest to V0 resilient to reg failures 2021-01-25 09:55:02 -07:00
Keagan McClelland
68a87c8c4f fixes timestamp parsing 2021-01-25 09:55:02 -07:00
Keagan McClelland
718d556080 rename stuff 2021-01-25 09:55:02 -07:00
Aaron Greenspan
71f2c88b8f installWarning ~> installAlert 2021-01-25 09:55:02 -07:00
Aaron Greenspan
49628f07e6 rocket ~> globe, move icons 2021-01-25 09:55:02 -07:00
Aaron Greenspan
9f45879c7f ui: remove drives 2021-01-25 09:55:02 -07:00
Aiden McClelland
fcb807eb42 agent: .autoCheckUpdates 2021-01-25 09:55:02 -07:00
Aaron Greenspan
d5d0ea3ade ui: fix PR comments 2021-01-25 09:55:02 -07:00
Matt Hill
50b887fcb9 remove lines 2021-01-25 09:55:02 -07:00
Matt Hill
fc9b3a6d69 enable and disable auto check for updates 2021-01-25 09:55:02 -07:00
Matt Hill
0c7eae7333 auto check for update on init 2021-01-25 09:55:02 -07:00
Aaron Greenspan
792a5cc429 ui: cleanup 2021-01-25 09:55:02 -07:00
Aaron Greenspan
8e46719b49 ui: clickable launch tabs 2021-01-25 09:55:02 -07:00
Aaron Greenspan
aef63a60c2 save 2021-01-25 09:55:02 -07:00
Aaron Greenspan
567ac9fa50 ui: rhs clickable 2021-01-25 09:55:02 -07:00
Aaron Greenspan
eb5473087b ui: change drives tab name 2021-01-25 09:55:02 -07:00
Aaron Greenspan
5d04f1f6b0 ui: preload rocket 2021-01-25 09:55:02 -07:00
Aiden McClelland
8bdb2e6f3b appmgr: metadata kebab case 2021-01-25 09:55:02 -07:00
Aaron Greenspan
a122c4a058 agent: cleanup 2021-01-25 09:55:02 -07:00
Aaron Greenspan
b908967ac4 agent: fetches server ack for v0 response 2021-01-25 09:55:02 -07:00
Aaron Greenspan
ad92660c76 agent: adds post to welcome 2021-01-25 09:55:02 -07:00
Keagan McClelland
070835c40e wip 2021-01-25 09:55:02 -07:00
Aaron Greenspan
16a4c41886 ui: adds version to path of welcome OS 2021-01-25 09:55:02 -07:00
Aaron Greenspan
da922f498e ui: simpler ui on welcome 2021-01-25 09:55:02 -07:00
Aaron Greenspan
c315dbaadf ui: adds 0.2.8 welcome content 2021-01-25 09:55:02 -07:00
Aaron Greenspan
939ad844e8 ui: completes welcome ack trigger 2021-01-25 09:55:02 -07:00
Matt Hill
778d22ab2b start adding welcome OS screen 2021-01-25 09:55:02 -07:00
Aiden McClelland
29d5e3b36e appmgr: %s/warning/alert/g 2021-01-25 09:55:02 -07:00
Aaron Greenspan
c4eef4db17 ui: default uninstall warning message 2021-01-25 09:55:02 -07:00
Aaron Greenspan
73d40c71ad ui: backup fin state ignore unreachable 2021-01-25 09:55:02 -07:00
Aiden McClelland
2022a7fc1d appmgr: backup restore metadata 2021-01-25 09:55:02 -07:00
Aiden McClelland
fb9b7d2a58 formatting 2021-01-25 09:55:02 -07:00
Aiden McClelland
c72b7425fc appmgr: fix tokio-tar 2021-01-25 09:55:02 -07:00
Aaron Greenspan
9066d77a70 ui: cleanup 2021-01-25 09:55:02 -07:00
Keagan McClelland
27f05a4588 adds timestamps 2021-01-25 09:55:02 -07:00
Keagan McClelland
ce8280b0ba serialize it 2021-01-25 09:55:02 -07:00
Keagan McClelland
7c3238cf8d install warning stuff complete 2021-01-25 09:55:02 -07:00
Aaron Greenspan
81d11842f0 ui: remove logs 2021-01-25 09:55:02 -07:00
Aaron Greenspan
238ede33b9 ui: adds install/uninstall warnings 2021-01-25 09:55:02 -07:00
Aaron Greenspan
071bd159ab ui: adds developer-notes component 2021-01-25 09:55:02 -07:00
Aiden McClelland
22da61b05a appmgr: add warnings to manifest 2021-01-25 09:55:02 -07:00
Aaron Greenspan
ee13552c21 ui: add latestVersionTimestamp to mocks 2021-01-25 09:55:02 -07:00
Aaron Greenspan
d7508345eb timestamp sorting 2021-01-25 09:55:02 -07:00
Keagan McClelland
90b412384a initialize record 2021-01-25 09:55:02 -07:00
Keagan McClelland
ebd74cca3c periodically restarts tor daemon when it fails to get a response from itself, rate limits restarts 2021-01-25 09:55:02 -07:00
Keagan McClelland
5fbf34a84e removes expiration verification for the setup flow to prevent clock skew from failing registration 2021-01-25 09:55:02 -07:00
Aaron Greenspan
271dd3e12d ui: external drives omits fully mounted drives 2021-01-25 09:55:02 -07:00
Aaron Greenspan
4f315e9958 agent: apt ~> apt-get, add eject sync 2021-01-25 09:55:02 -07:00
Aaron Greenspan
57955ef22e ui: fix path 2021-01-25 09:55:02 -07:00
Aaron Greenspan
3ecfb4e4eb ui, agent: POST to eject cause client DELETE doesnt do bodys like a jerk 2021-01-25 09:55:02 -07:00
Aaron Greenspan
90227c0606 agent, ui: move to post body to avoid encodeURI complications 2021-01-25 09:55:02 -07:00
Aaron Greenspan
234fc06f76 ui: emver rendering in OS banner 2021-01-25 09:55:02 -07:00
Aaron Greenspan
b9a3a0cb8d ui: encodeURIComponent > encodeURI 2021-01-25 09:55:02 -07:00
Aaron Greenspan
e2a1ac7033 ui: use new eject disk endpoint 2021-01-25 09:55:02 -07:00
Aaron Greenspan
23077c6c6b agent: errorT to errorC handling in eject disks 2021-01-25 09:55:02 -07:00
Aaron Greenspan
c25295500b agent: logicalName to queryparams instead of path 2021-01-25 09:55:02 -07:00
Aaron Greenspan
4da4fd66a3 ui: fix messaging for .local launches 2021-01-25 09:55:02 -07:00
Aaron Greenspan
1b1fd40e08 ui: adds logs, disables .local tabs 2021-01-25 09:55:02 -07:00
Aaron Greenspan
b80634df83 agent: remove redundant fn 2021-01-25 09:55:02 -07:00
Aaron Greenspan
b4d1db7e11 agent: removes exinst from appmanifest types 2021-01-25 09:55:02 -07:00
Aaron Greenspan
2d10220e52 agent: fixes manifest parsing 2021-01-25 09:55:02 -07:00
Aiden McClelland
34d52d063e appmgr: fix: restart policy 2021-01-25 09:55:02 -07:00
Aaron Greenspan
8b21ed13ce gpg test 2021-01-25 09:55:02 -07:00
Aaron Greenspan
4aaddb233a ui: finalize toast 2021-01-25 09:55:02 -07:00
Aaron Greenspan
d6dfdda061 ui: fix import in liveapi 2021-01-25 09:55:02 -07:00
Aaron Greenspan
da605e419b ui: fix import in liveapi 2021-01-25 09:55:02 -07:00
Aaron Greenspan
54e1acc6b6 ui: modal direction 2021-01-25 09:55:02 -07:00
Aaron Greenspan
a896f4c7a1 ui: beautiful checkbox for marking to eject drive after backup. broken 2021-01-25 09:55:02 -07:00
Aaron Greenspan
0cd2a32b24 drives page 2021-01-25 09:55:02 -07:00
Aaron Greenspan
31318687bf agent: fix appmgr version spec 2021-01-25 09:55:02 -07:00
Aaron Greenspan
f53581be8c ui: fix no launch css AIS 2021-01-25 09:55:02 -07:00
Aaron Greenspan
450ce68c8a ui: launch buttons, not on consulate 2021-01-25 09:55:02 -07:00
Aaron Greenspan
06b34d588e better css 2021-01-25 09:55:02 -07:00
Aaron Greenspan
66c08c730b adds ui tag and launch button in AIS 2021-01-25 09:55:02 -07:00
Keagan McClelland
5b248013e5 removed redundant code 2021-01-25 09:55:02 -07:00
Aaron Greenspan
865bb12f31 appmgr: fix 2021-01-25 09:55:02 -07:00
Aaron Greenspan
9f9d32e0db appmgr: mod.rs v0_2_8 current 2021-01-25 09:55:02 -07:00
Aaron Greenspan
0874d77403 appmgr: version to 0.2.8 2021-01-25 09:55:02 -07:00
Aaron Greenspan
0962a20852 agent: adds ui to app installed preview 2021-01-25 09:55:02 -07:00
Aaron Greenspan
3a63dab586 agent: bump to 0.2.8. Appmgr still at 0.2.7 2021-01-25 09:55:02 -07:00
Aaron Greenspan
4e0ad21384 agent: adds endpoint for disk ejection 2021-01-25 09:55:02 -07:00
Aaron Greenspan
89ff5de01b ui: adds a banner to the marketplace page when an OS update is detected 2021-01-25 09:55:02 -07:00
Aaron Greenspan
6da3c7e326 fin 2021-01-25 09:55:02 -07:00
Aaron Greenspan
7acfd2da5b 0.2.8 versioning 2021-01-25 09:55:02 -07:00
Lucy Cifferello
becb8c5c35 reverse mocks 2021-01-18 22:17:49 -07:00
Lucy Cifferello
0035a2062e add badges and caution message 2021-01-18 22:17:49 -07:00
Aiden McClelland
e8a693049e add lifeline 2020-12-24 14:33:38 -07:00
Aiden McClelland
69cfe3daf4 repo: update documentation 2020-12-24 14:31:36 -07:00
Aiden McClelland
83dbc0f0cc ui: update package json 2020-12-24 08:05:31 -07:00
Aiden McClelland
e5a844e0d1 repo: added docs for contributing 2020-12-24 08:05:31 -07:00
Aiden McClelland
626943f98c gnu 2020-12-24 08:05:31 -07:00
Aiden McClelland
3a4270bc85 run make_image.sh as root 2020-12-24 08:05:31 -07:00
Aiden McClelland
2f6bd4b1b6 make image 2020-12-24 08:05:31 -07:00
Matt Hill
1cce947846 Update LICENSE.md 2020-12-21 09:27:56 -08:00
Matt Hill
c4703ef50b Update LICENSE.md 2020-12-21 09:27:56 -08:00
Aiden McClelland
c98f7ebd34 appmgr: disable verify in pack 2020-12-15 11:34:33 -08:00
125 changed files with 2252 additions and 415 deletions

237
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,237 @@
<!-- omit in toc -->
# Contributing to Embassy OS
First off, thanks for taking the time to contribute! ❤️
All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
> - Star the project
> - Tweet about it
> - Refer this project in your project's readme
> - Mention the project at local meetups and tell your friends/colleagues
> - Buy an [Embassy](https://start9labs.com)
<!-- omit in toc -->
## Table of Contents
- [I Have a Question](#i-have-a-question)
- [I Want To Contribute](#i-want-to-contribute)
- [Reporting Bugs](#reporting-bugs)
- [Suggesting Enhancements](#suggesting-enhancements)
- [Your First Code Contribution](#your-first-code-contribution)
- [Setting Up Your Development Environment](#setting-up-your-development-environment)
- [Building The Image](#building-the-image)
- [Improving The Documentation](#improving-the-documentation)
- [Styleguides](#styleguides)
- [Formatting](#formatting)
- [Atomic Commits](#atomic-commits)
- [Commit Messages](#commit-messages)
- [Pull Requests](#pull-requests)
- [Rebasing Changes](#rebasing-changes)
- [Join The Discussion](#join-the-discussion)
- [Join The Project Team](#join-the-project-team)
## I Have a Question
> If you want to ask a question, we assume that you have read the available [Documentation](https://docs.start9labs.com).
Before you ask a question, it is best to search for existing [Issues](https://github.com/Start9Labs/embassy-os/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first.
If you then still feel the need to ask a question and need clarification, we recommend the following:
- Open an [Issue](https://github.com/Start9Labs/embassy-os/issues/new).
- Provide as much context as you can about what you're running into.
- Provide project and platform versions, depending on what seems relevant.
We will then take care of the issue as soon as possible.
<!--
You might want to create a separate issue tag for questions and include it in this description. People should then tag their issues accordingly.
Depending on how large the project is, you may want to outsource the questioning, e.g. to Stack Overflow or Gitter. You may add additional contact and information possibilities:
- IRC
- Slack
- Gitter
- Stack Overflow tag
- Blog
- FAQ
- Roadmap
- E-Mail List
- Forum
-->
## I Want To Contribute
> ### Legal Notice <!-- omit in toc -->
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.
### Reporting Bugs
<!-- omit in toc -->
#### Before Submitting a Bug Report
A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible.
- Make sure that you are using the latest version.
- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://docs.start9labs.com). If you are looking for support, you might want to check [this section](#i-have-a-question)).
- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/Start9Labs/embassy-osissues?q=label%3Abug).
- Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue.
- Collect information about the bug:
- Stack trace (Traceback)
- Client OS, Platform and Version (Windows/Linux/macOS/iOS/Android, Firefox/Tor Browser/Consulate)
- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.
- Possibly your input and the output
- Can you reliably reproduce the issue? And can you also reproduce it with older versions?
<!-- omit in toc -->
#### How Do I Submit a Good Bug Report?
> You must never report security related issues, vulnerabilities or bugs to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to <security@start9labs.com>.
<!-- You may add a PGP key to allow the messages to be sent encrypted as well. -->
We use GitHub issues to track bugs and errors. If you run into an issue with the project:
- Open an [Issue](https://github.com/Start9Labs/embassy-os/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.)
- Explain the behavior you would expect and the actual behavior.
- Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case.
- Provide the information you collected in the previous section.
Once it's filed:
- The project team will label the issue accordingly.
- A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced.
- If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be [implemented by someone](#your-first-code-contribution).
<!-- You might want to create an issue template for bugs and errors that can be used as a guide and that defines the structure of the information to be included. If you do so, reference it here in the description. -->
### Suggesting Enhancements
This section guides you through submitting an enhancement suggestion for Embassy OS, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions.
<!-- omit in toc -->
#### Before Submitting an Enhancement
- Make sure that you are using the latest version.
- Read the [documentation](https://docs.start9labs.com) carefully and find out if the functionality is already covered, maybe by an individual configuration.
- Perform a [search](https://github.com/Start9Labs/embassy-os/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library.
<!-- omit in toc -->
#### How Do I Submit a Good Enhancement Suggestion?
Enhancement suggestions are tracked as [GitHub issues](https://github.com/Start9Labs/embassy-os/issues).
- Use a **clear and descriptive title** for the issue to identify the suggestion.
- Provide a **step-by-step description of the suggested enhancement** in as many details as possible.
- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you.
- You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. <!-- this should only be included if the project has a GUI -->
- **Explain why this enhancement would be useful** to most Embassy OS users. You may also want to point out the other projects that solved it better and which could serve as inspiration.
<!-- You might want to create an issue template for enhancement suggestions that can be used as a guide and that defines the structure of the information to be included. If you do so, reference it here in the description. -->
### Project Structure
Embassy OS has 3 main components: `agent`, `appmgr`, and `ui`.
- The `ui` (Typescript Ionic Angular) is the code that is deployed to the browser to provide the user interface for Embassy OS
- The `agent` (Haskell) is a daemon that provides the interface for the `ui` to interact with the Embassy, as well as manage system state.
- `appmgr` (Rust) is a command line utility and (soon to be) daemon that sets up and manages services and their environments.
### Your First Code Contribution
#### Setting up your development environment
##### agent
There are two main workflows to consider when developing on the agent. During the development process you will spend
most of your time developing in an environment where you cannot actually run the agent. This is because we make heavy
platform specific assumptions (by nature of the project) around what folders get used and what package management tools
are used for the underlying system. If you are running this on a platform besides Linux you won't even be able to run
the agent effectively on your dev machine. Even if you are on Linux you may not want to turn administrative control over
to the software you are currently developing. So how do you know that anything you are doing is right? We make extensive
use of Haskell's type system and surrounding tooling. For this you will want to make sure you are using the [haskell-language-server](https://github.com/haskell/haskell-language-server)
and [stack](https://github.com/commercialhaskell/stack)
At some point though you will want to build the agent for the target platform (Raspberry Pi 4). This is the second build
flow that you will need to consider.
At Start9 we build the agent in two different ways. The primary way we have done it is on the Raspberry Pi itself. To do
this you will need stack built for the Raspberry Pi. Unfortunately, however, FPComplete no longer
distributes ARMv7 binaries for stack. Though hopefully soon we will be able to submit the binaries we've built for this
project back to them and have them hosted more visibly. The way we bootstrap through this problem is by downloading version
[2.1.3](https://github.com/commercialhaskell/stack/releases/download/v2.1.3/stack-2.1.3-linux-arm.tar.gz) and using that
to compile v2.5.1. Before you can successfully compile anything with GHC on the Raspberry Pi. You will need to tweak the
relevant GHC config. You will need to edit the file at `~/.stack/programs/arm-linux/ghc-8.10.2/lib/ghc-8.10.2/settings`
and change the line `("C compiler flags", " -marm -fno-stack-protector -mcpu=cortex-a7")` to include `-mcpu=cortex-a7`.
You will also need to make sure you've downloaded and installed LLVM 9.
Once you have done these things, you simply need to `cd` into the embassy-os project and then run `make agent`.
##### ui
- Requirements
- [Install nodejs](https://nodejs.org/en/)
- [Install npm](https://www.npmjs.com/get-npm)
- [Install ionic cli](https://ionicframework.com/docs/intro/cli)
- Scripts (run within ./ui directory)
- `npm i` installs ui node package dependencies
- `npm run build` compiles project, depositing build artifacts into ./ui/www
- `npm run build-prod` as above but customized for deployment to an Embassy
- `ionic serve` serves the ui on localhost:8100 for local development. Edit ./ui/use-mocks.json to 'true' to use mocks during local development
- `./build-send.sh <embassy .local address suffix>` builds the project and deploys it to the referenced Embassy
- Find your Embassy on the LAN using the Start9 Setup App or network tools. It's address will be of the form `start9-<suffix>.local`.
- For example `./build-send.sh abcdefgh` will deploy the ui to the Embassy with LAN address `start9-abcdefgh.local`.
- SSH keys must be installed on the Embassy prior to running this script.
##### appmgr
- [Install Rust](https://rustup.rs)
- Recommended: [rust-analyzer](https://rust-analyzer.github.io/)
#### Building The Image
- Requirements
- `ext4fs` (available if running on the Linux kernel)
- [Docker](https://docs.docker.com/get-docker/)
- GNU Make
- Building
- build the [agent](#agent)
- make sure resulting artifact is agent/dist/agent
- run `make`
### Improving The Documentation
You can find the repository for Start9's documentation [here](https://github.com/Start9Labs/documentation). If there is something you would like to see added, let us know, or create an issue yourself. Welcome are contributions for lacking or incorrect information, broken links, requested additions, or general style improvements.
Contributions in the form of setup guides for integrations with external applications are highly encouraged. If you struggled through a process and would like to share your steps with others, check out the docs for each [service](https://github.com/Start9Labs/documentation/blob/master/source/user-manuals/available-services/index.rst) we support. The wrapper repos contain sections for adding integration guides, such as this [one](https://github.com/Start9Labs/bitcoind-wrapper/tree/master/docs). These not only help out others in the community, but inform how we can create a more seamless and intuitive experience.
## Styleguides
### Formatting
Code must be formatted with the formatter designated for each component:
- `ui`: [tslint](https://palantir.github.io/tslint/)
- `agent`: [brittany](https://github.com/lspitzner/brittany)
- `appmgr`: [rustfmt](https://github.com/rust-lang/rustfmt)
### Atomic Commits
Commits [should be atomic](https://en.wikipedia.org/wiki/Atomic_commit#Atomic_commit_convention) and diffs should be easy to read.
Do not mix any formatting fixes or code moves with actual code changes.
### Commit Messages
If a commit touches only 1 component, prefix the message with the affected component. i.e. `appmgr: update to tokio v0.3`.
### Pull Requests
The body of a pull request should contain sufficient description of what the changes do, as well as a justification.
You should include references to any relevant [issues](https://github.com/Start9Labs/embassy-os/issues).
### Rebasing Changes
When a pull request conflicts with the target branch, you may be asked to rebase it on top of the current target branch. The git rebase command will take care of rebuilding your commits on top of the new base.
This project aims to have a clean git history, where code changes are only made in non-merge commits. This simplifies auditability because merge commits can be assumed to not contain arbitrary code changes.
## Join The Discussion
Current or aspiring contributors? Join our community developer Matrix channel: `#community-dev:matrix.start9labs.com`.
Just interested in or using the project? Join our community [Telegram](https://t.me/start9_labs) or Matrix channel: `#community:matrix.start9labs.com`.
## Join The Project Team
Interested in becoming a part of the Start9 Labs team? Send an email to <jobs@start9labs.com>
<!-- omit in toc -->
## Attribution
This guide is based on the **contributing-gen**. [Make your own](https://github.com/bttger/contributing-gen)!

View File

@@ -2,17 +2,24 @@
This license governs the use of the accompanying Software. If you use the Software, you accept this license. If you do not accept the license, do not use the Software.
1. **Definitions**
1. "Licensor" means the copyright owner, Start9 Labs, Inc, or its successor(s) in interest, or a future assignee of the copyright.
2. "Source Code" means the code made available to you by the Licensor pursuant to the terms of this license and any derivative works based thereon.
3. "Object Code" means any non-source form of the Source Code, including the machine-language output by a compiler or assembler.
4. "Distribute" means to convey or to publish and generally has the same meaning here as under U.S. Copyright law.
5. "Personal Use" means accessing, copying, reviewing, auditing, running, testing, or modifying the Source Code.
2. **Grant of Rights**
1. Subject to the terms of this license, the Licensor grants you, the licensee, a non-exclusive, worldwide, royalty-free copyright license to the Source Code for Personal Use only.
2. Subject to the terms of this license, the Licensor grants you, the licensee, the right to Distribute the Source Code or modifications to the Source Code.
3. Distributing the Object Code, or any Object Code created based on your modifications to the Source Code, is not permitted under the terms of this license without express written consent of the Licensor.
4. If you Distribute the Source Code, or if permission is granted to Distribute the Object Code, you expressly undertake not to remove, or modify, in any manner, the copyright notices attached to the Source Code, and displayed in any output of the Object Code when run, and to reproduce these notices, in an identical manner, in any distributed copies of the Source Code or Object Code together with a copy of this license. If you Distribute a modified copy of the Software or a derivative work based thereon, the work must carry prominent notices stating that you modified it, and giving a relevant date.
5. The terms of this license will apply to anyone who comes into possession of a copy of the Source Code or Object Code, and any modifications or derivative works based thereon, made by anyone.
3. **Disclaimer.** THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Licensor has no obligation to support recipients of the Source Code or Object Code.
1. **Definitions.**
1. Licensor means the copyright owner, Start9 Labs, Inc, or its successor(s) in interest, or a future assignee of the copyright.
2. Source Code means the preferred form of the Software for making modifications to it.
3. Object Code means any non-source form of the Software, including the machine-language output by a compiler or assembler.
4. Distribute means to convey or to publish and generally has the same meaning here as under U.S. Copyright law.
5. “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software.
2. **Grant of Rights.** Subject to the terms of this license, the Licensor grants you, the licensee, a non-exclusive, worldwide, royalty-free copyright license to:
1. Access, audit, copy, modify, compile, or distribute the Source Code or modifications to the Source Code.
2. Run, test, or otherwise use the Object Code.
3. **Limitations.**
1. The grant of rights under the License will NOT include, and the License does NOT grant you the right to:
1. Sell the Software or any derivative works based thereon.
2. Distribute the Object Code.
2. If you Distribute the Source Code, or if permission is separately granted to Distribute the Object Code, you expressly undertake not to remove, or modify, in any manner, the copyright notices attached to the Source Code, and displayed in any output of the Object Code when run, and to reproduce these notices, in an identical manner, in any distributed copies of the Software together with a copy of this license. If you Distribute a modified copy of the Software, or a derivative work based thereon, the work must carry prominent notices stating that you modified it, and giving a relevant date.
3. The terms of this license will apply to anyone who comes into possession of a copy of the Software, and any modifications or derivative works based thereon, made by anyone.
4. **Contributions.** You hereby grant to Licensor a perpetual, irrevocable, worldwide, non-exclusive, royalty-free license to use and exploit any modifications or derivative works based on the Source Code of which you are the author.
5. **Disclaimer.** THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. LICENSOR HAS NO OBLIGATION TO SUPPORT RECIPIENTS OF THE SOFTWARE.

View File

@@ -1,12 +1,20 @@
APPMGR_SRC := $(shell find appmgr/src) appmgr/Cargo.toml appmgr/Cargo.lock
LIFELINE_SRC := $(shell find lifeline/src) lifeline/Cargo.toml lifeline/Cargo.lock
AGENT_SRC := $(shell find agent/src) $(shell find agent/config) agent/stack.yaml agent/package.yaml agent/build.sh
.DELETE_ON_ERROR:
UI_SRC := $(shell find ui/src) \
ui/angular.json \
ui/browserslist \
ui/client-manifest.yaml \
ui/ionic.config.json \
ui/postprocess.ts \
ui/tsconfig.json \
ui/tslint.json \
ui/use-mocks.json
all: embassy.img
embassy.img: buster.img product_key appmgr/target/armv7-unknown-linux-musleabihf/release/appmgr ui/www agent/dist/agent agent/config/agent.service
./make_image.sh
embassy.img: buster.img product_key appmgr/target/armv7-unknown-linux-gnueabihf/release/appmgr ui/www agent/dist/agent agent/config/agent.service lifeline/target/armv7-unknown-linux-gnueabihf/release/lifeline lifeline/lifeline.service setup.sh setup.service docker-daemon.json
sudo ./make_image.sh
buster.img:
wget -O buster.zip https://downloads.raspberrypi.org/raspios_lite_armhf/images/raspios_lite_armhf-2020-08-24/2020-08-20-raspios-buster-armhf-lite.zip
@@ -18,13 +26,28 @@ product_key:
echo "X\c" > product_key
cat /dev/random | base32 | head -c11 | tr '[:upper:]' '[:lower:]' >> product_key
appmgr/target/armv7-unknown-linux-musleabihf/release/appmgr: $(APPMGR_SRC)
appmgr/target/armv7-unknown-linux-gnueabihf/release/appmgr: $(APPMGR_SRC)
docker run --rm -it -v ~/.cargo/registry:/root/.cargo/registry -v "$(shell pwd)":/home/rust/src start9/rust-arm-cross:latest sh -c "(cd appmgr && cargo build --release --features=production)"
docker run --rm -it -v ~/.cargo/registry:/root/.cargo/registry -v "$(shell pwd)":/home/rust/src start9/rust-arm-cross:latest arm-linux-gnueabi-strip appmgr/target/armv7-unknown-linux-gnueabihf/release/appmgr
appmgr: appmgr/target/armv7-unknown-linux-musleabihf/release/appmgr
appmgr: appmgr/target/armv7-unknown-linux-gnueabihf/release/appmgr
agent/dist/agent: $(AGENT_SRC)
(cd agent; ./build.sh)
(cd agent && ./build.sh)
agent: agent/dist/agent
ui/node_modules: ui/package.json
npm --prefix ui install
ui/www: $(UI_SRC) ui/node_modules
npm --prefix ui run build-prod
ui: ui/www
lifeline/target/armv7-unknown-linux-gnueabihf/release/lifeline: $(LIFELINE_SRC)
docker run --rm -it -v ~/.cargo/registry:/root/.cargo/registry -v "$(shell pwd)":/home/rust/src start9/rust-arm-cross:latest sh -c "(cd lifeline && cargo build --release)"
docker run --rm -it -v ~/.cargo/registry:/root/.cargo/registry -v "$(shell pwd)":/home/rust/src start9/rust-arm-cross:latest arm-linux-gnueabi-strip lifeline/target/armv7-unknown-linux-gnueabihf/release/lifeline
lifeline: lifeline/target/armv7-unknown-linux-gnueabihf/release/lifeline

View File

@@ -1 +1,35 @@
# Embassy OS
# EmbassyOS
[![Version](https://img.shields.io/github/v/tag/Start9Labs/embassy-os?color=success)](https://github.com/Start9Labs/embassy-os/releases)
[![community](https://img.shields.io/badge/community-telegram-informational)](https://t.me/start9_labs)
[![support](https://img.shields.io/badge/support-docs-important)](https://docs.start9labs.com)
[![developer](https://img.shields.io/badge/developer-matrix-blueviolet)](https://matrix.to/#/#community-dev:matrix.start9labs.com)
[![website](https://img.shields.io/website?down_color=lightgrey&down_message=offline&up_color=success&up_message=online&url=https%3A%2F%2Fstart9labs.com)](https://start9labs.com)
[![twitter](https://img.shields.io/twitter/follow/start9labs?label=Follow)](https://twitter.com/start9labs)
### _Anyone can do it. No one can stop it._ ###
EmbassyOS is a mass-market, graphical operating system designed to facilitate the discovery, installation, configuration, private self-hosting, and reliable operation of open-source software services and applications. It aims to eliminate trust and custodianship from personal computing.
![EmbassyOS image](https://sesoodan.sirv.com/eos.png?w=600)
## ⚠️ Caution
Some technologies supported by this software, such as [Lightning](https://lightning.network/), are considered in active development and might experience issues. Do not commit any funds you are not willing to loose. Be #reckless at your own risk.
## Running EmbassyOS
There are multiple ways to obtain and begin using EmbassyOS.
### Buy an Embassy
This is the most convenient option. Simply [buy an Embassy](https://start9labs.com) from Start9 Labs and plug it in. Depending on where you live, shipping costs and import duties may vary.
### Build your own Embassy
While not as convenient as buying an Embassy, this option is easier than you might imagine, and there are 4 reasons why you might prefer it:
1. You already have a Raspberry Pi and would like to re-purpose it.
1. You want to save on shipping costs.
1. You prefer not to divulge your physical shipping address.
1. You just like building things.
To pursue this option, follow this [guide](https://docs.start9labs.com/getting-started/diy.html).
## Contributing
To build EmbassyOS from source, or to contribute to its development, see [here](https://github.com/Start9Labs/embassy-os/blob/master/CONTRIBUTING.md#building-the-image).

View File

@@ -7,7 +7,9 @@
/v0 ServerR GET PATCH
/v0/name NameR PATCH
/v0/autoCheckUpdates AutoCheckUpdatesR PATCH
/v0/welcome/#Version WelcomeR POST
/v0/specs SpecsR GET
/v0/metrics MetricsR GET
@@ -37,7 +39,8 @@
/v0/apps/#AppId/backup/restore RestoreBackupR POST
/v0/apps/#AppId/autoconfig/#AppId AutoconfigureR POST
/v0/disks ListDisksR GET
/v0/disks DisksR GET
/v0/disks/eject EjectR POST
/v0/update UpdateAgentR POST
/v0/wifi WifiR GET POST

View File

@@ -33,6 +33,6 @@ database:
database: "start9_agent.sqlite3"
poolsize: "_env:YESOD_SQLITE_POOLSIZE:10"
app-mgr-version-spec: "=0.2.7"
app-mgr-version-spec: "=0.2.8"
#analytics: UA-YOURCODE

View File

@@ -0,0 +1 @@
SELECT TRUE;

View File

@@ -1,5 +1,5 @@
name: ambassador-agent
version: 0.2.7
version: 0.2.8
default-extensions:
- NoImplicitPrelude
@@ -42,6 +42,7 @@ dependencies:
- comonad
- conduit
- conduit-extra
- connection
- containers
- cryptonite
- cryptonite-conduit
@@ -72,6 +73,7 @@ dependencies:
- mime-types
- monad-control
- monad-logger
- network
- persistent
- persistent-sqlite
- persistent-template

View File

@@ -67,6 +67,8 @@ import Model
import Settings
import Lib.Background
import qualified Daemon.SslRenew as SSLRenew
import Lib.Tor (newTorManager)
import Daemon.TorHealth
appMain :: IO ()
appMain = do
@@ -106,13 +108,16 @@ makeFoundation appSettings = do
-- subsite.
appLogger <- newStdoutLoggerSet defaultBufSize >>= makeYesodLogger
appHttpManager <- getGlobalManager
appTorManager <- newTorManager (appTorSocksPort appSettings)
appWebServerThreadId <- newIORef Nothing
appSelfUpdateSpecification <- newEmptyMVar
appIsUpdating <- newIORef Nothing
appIsUpdateFailed <- newIORef Nothing
appOsVersionLatest <- newIORef Nothing
appBackgroundJobs <- newTVarIO (JobCache HM.empty)
def <- getDefaultProcDevMetrics
appProcDevMomentCache <- newIORef (now, mempty, def)
appLastTorRestart <- newIORef now
-- We need a log function to create a connection pool. We need a connection
-- pool to create our foundation. And we need our foundation to get a
@@ -193,6 +198,10 @@ startupSequence foundation = do
void . forkIO . forever $ forkIO (SSLRenew.renewSslLeafCert foundation) *> sleep 86_400
withAgentVersionLog_ "SSL Renewal daemon started"
withAgentVersionLog_ "Initializing Tor health check loop"
void . forkIO . forever $ forkIO (runReaderT torHealth foundation) *> sleep 300
withAgentVersionLog_ "Tor health check loop running"
-- reloading avahi daemon
-- DRAGONS! make sure this step happens AFTER system synchronization
withAgentVersionLog_ "Publishing Agent to Avahi Daemon"

View File

@@ -0,0 +1,50 @@
{-# LANGUAGE QuasiQuotes #-}
module Daemon.TorHealth where
import Startlude
import Data.String.Interpolate.IsString
import Foundation
import Lib.SystemPaths
import Lib.Tor
import Yesod ( RenderRoute(renderRoute) )
import Network.HTTP.Simple ( getResponseBody )
import Network.HTTP.Client ( parseRequest )
import Network.HTTP.Client ( httpLbs )
import Data.ByteString.Lazy ( toStrict )
import qualified UnliftIO.Exception as UnliftIO
import Settings
import Data.IORef ( writeIORef
, readIORef
)
import Lib.SystemCtl
torHealth :: ReaderT AgentCtx IO ()
torHealth = do
settings <- asks appSettings
host <- injectFilesystemBaseFromContext settings getAgentHiddenServiceUrl
let url = mappend [i|http://#{host}:5959|] . fold $ mappend "/" <$> fst (renderRoute VersionR)
response <- UnliftIO.try @_ @SomeException $ torGet (toS url)
case response of
Left _ -> do
putStrLn @Text "Failed Tor health check"
lastRestart <- asks appLastTorRestart >>= liftIO . readIORef
cooldown <- asks $ appTorRestartCooldown . appSettings
now <- liftIO getCurrentTime
if now > addUTCTime cooldown lastRestart
then do
ec <- liftIO $ systemCtl RestartService "tor"
case ec of
ExitSuccess -> asks appLastTorRestart >>= liftIO . flip writeIORef now
ExitFailure _ -> do
putStrLn @Text "Failed to restart tor daemon after failed tor health check"
else do
putStrLn @Text "Failed tor healthcheck inside of cooldown window, tor will not be restarted"
Right _ -> pure ()
torGet :: String -> ReaderT AgentCtx IO ByteString
torGet url = do
manager <- asks appTorManager
req <- parseRequest url
liftIO $ toStrict . getResponseBody <$> httpLbs req manager

View File

@@ -58,19 +58,23 @@ import Settings
-- keep settings and values requiring initialization before your application
-- starts running, such as database connections. Every handler will have
-- access to the data present here.
data OsVersionCache = OsVersionCache { osVersion :: Version, lastChecked :: UTCTime }
data AgentCtx = AgentCtx
{ appSettings :: AppSettings
, appHttpManager :: Manager
, appTorManager :: Manager
, appConnPool :: ConnectionPool -- ^ Database connection pool.
, appLogger :: Logger
, appWebServerThreadId :: IORef (Maybe ThreadId)
, appIsUpdating :: IORef (Maybe Version)
, appIsUpdateFailed :: IORef (Maybe S9Error)
, appOsVersionLatest :: IORef (Maybe OsVersionCache)
, appProcDevMomentCache :: IORef (UTCTime, ProcDevMomentStats, ProcDevMetrics)
, appSelfUpdateSpecification :: MVar VersionRange
, appBackgroundJobs :: TVar JobCache
, appIconTags :: TVar (HM.HashMap AppId (Digest MD5))
, appLastTorRestart :: IORef UTCTime
}
setWebProcessThreadId :: ThreadId -> AgentCtx -> IO ()

View File

@@ -68,6 +68,7 @@ import Lib.Background
import Lib.Error
import qualified Lib.External.AppMgr as AppMgr
import qualified Lib.External.Registry as Reg
import qualified Lib.External.AppManifest as AppManifest
import Lib.IconCache
import qualified Lib.Notifications as Notifications
import Lib.SystemPaths
@@ -209,6 +210,7 @@ getAvailableAppByIdLogic appId = do
, appAvailableFullReleaseNotes = storeAppVersionInfoReleaseNotes latest
, appAvailableFullDependencyRequirements = HM.elems dependencyRequirements
, appAvailableFullVersions = storeAppVersionInfoVersion <$> storeAppVersions
, appAvailableFullInstallAlert = storeAppVersionInfoInstallAlert latest
}
getAppLogsByIdR :: AppId -> Handler (JSONResponse [Text])
@@ -230,7 +232,7 @@ getInstalledAppsLogic :: (Has (Reader AgentCtx) sig m, Has AppMgr2.AppMgr sig m,
getInstalledAppsLogic = do
jobCache <- asks appBackgroundJobs >>= liftIO . readTVarIO
let installCache = installInfo . fst <$> inspect SInstalling jobCache
serverApps <- AppMgr2.list [AppMgr2.flags|-s -d|]
serverApps <- AppMgr2.list [AppMgr2.flags|-s -d -m|]
let remapped = remapAppMgrInfo jobCache serverApps
installingPreviews = flip
HM.mapWithKey
@@ -242,6 +244,7 @@ getInstalledAppsLogic = do
, appInstalledPreviewStatus = AppStatusTmp Installing
, appInstalledPreviewVersionInstalled = storeAppVersionInfoVersion
, appInstalledPreviewTorAddress = Nothing
, appInstalledPreviewUi = False
}
installedPreviews = flip
HML.mapWithKey
@@ -251,6 +254,7 @@ getInstalledAppsLogic = do
, appInstalledPreviewStatus = s
, appInstalledPreviewVersionInstalled = v
, appInstalledPreviewTorAddress = infoResTorAddress
, appInstalledPreviewUi = AppManifest.uiAvailable infoResManifest
}
pure $ HML.elems $ HML.union installingPreviews installedPreviews
@@ -283,6 +287,8 @@ getInstalledAppByIdLogic appId = do
, appInstalledFullLastBackup = backupTime
, appInstalledFullTorAddress = Nothing
, appInstalledFullConfiguredRequirements = []
, appInstalledFullUninstallAlert = Nothing
, appInstalledFullRestoreAlert = Nothing
}
serverApps <- AppMgr2.list [AppMgr2.flags|-s -d|]
let remapped = remapAppMgrInfo jobCache serverApps
@@ -290,6 +296,7 @@ getInstalledAppByIdLogic appId = do
let
installed = do
(status, version, AppMgr2.InfoRes {..}) <- hoistMaybe (HM.lookup appId remapped)
manifest' <- lift $ LAsync.async $ AppMgr2.infoResManifest <<$>> AppMgr2.info [AppMgr2.flags|-M|] appId
instructions' <- lift $ LAsync.async $ AppMgr2.instructions appId
requirements <- LAsync.runConcurrently $ flip
HML.traverseWithKey
@@ -309,6 +316,7 @@ getInstalledAppByIdLogic appId = do
(HM.lookup depId installCache $> AppStatusTmp Installing)
<|> (view _1 <$> HM.lookup depId remapped)
pure $ dependencyInfoToDependencyRequirement (AsInstalled STrue) (base, depStatus, depInfo)
manifest <- lift $ LAsync.wait manifest'
instructions <- lift $ LAsync.wait instructions'
backupTime <- lift $ LAsync.wait backupTime'
pure AppInstalledFull { appInstalledFullBase = AppBase appId infoResTitle (iconUrl appId version)
@@ -318,6 +326,8 @@ getInstalledAppByIdLogic appId = do
, appInstalledFullLastBackup = backupTime
, appInstalledFullTorAddress = infoResTorAddress
, appInstalledFullConfiguredRequirements = HM.elems requirements
, appInstalledFullUninstallAlert = manifest >>= AppManifest.appManifestUninstallAlert
, appInstalledFullRestoreAlert = manifest >>= AppManifest.appManifestRestoreAlert
}
runMaybeT (installing <|> installed) `orThrowM` NotFoundE "appId" (show appId)
@@ -642,6 +652,7 @@ getAvailableAppVersionInfoLogic appId appVersionSpec = do
pure AppVersionInfo { appVersionInfoVersion = storeAppVersionInfoVersion
, appVersionInfoReleaseNotes = storeAppVersionInfoReleaseNotes
, appVersionInfoDependencyRequirements = HM.elems requirements
, appVersionInfoInstallAlert = storeAppVersionInfoInstallAlert
}
postAutoconfigureR :: AppId -> AppId -> Handler (JSONResponse (WithBreakages AutoconfigureChangesRes))
@@ -730,6 +741,7 @@ storeAppToAvailablePreview s@StoreApp {..} installed = AppAvailablePreview
(storeAppVersionInfoVersion $ extract storeAppVersions)
storeAppDescriptionShort
installed
storeAppTimestamp
type AsInstalled :: Bool -> Type
newtype AsInstalled a = AsInstalled { unAsInstalled :: SBool a }

View File

@@ -57,6 +57,14 @@ instance FromJSON RestoreBackupReq where
restoreBackupPassword <- o .:? "password" .!= Nothing
pure RestoreBackupReq { .. }
data EjectDiskReq = EjectDiskReq
{ ejectDiskLogicalName :: Text
} deriving (Eq, Show)
instance FromJSON EjectDiskReq where
parseJSON = withObject "Eject Disk Req" $ \o -> do
ejectDiskLogicalName <- o .: "logicalName"
pure EjectDiskReq { .. }
-- Handlers
postCreateBackupR :: AppId -> Handler ()
@@ -95,9 +103,11 @@ postRestoreBackupR appId = disableEndpointOnFailedUpdate $ do
& handleS9ErrC
& runM
getListDisksR :: Handler (JSONResponse [AppMgr.DiskInfo])
getListDisksR = fmap JSONResponse . runM . handleS9ErrC $ listDisksLogic
getDisksR :: Handler (JSONResponse [AppMgr.DiskInfo])
getDisksR = fmap JSONResponse . runM . handleS9ErrC $ listDisksLogic
postEjectR :: Handler ()
postEjectR = runM . handleS9ErrC $ requireCheckJsonBody >>= ejectDiskLogic . ejectDiskLogicalName
-- Logic
@@ -203,6 +213,13 @@ restoreBackupLogic appId RestoreBackupReq {..} = do
listDisksLogic :: (Has (Error S9Error) sig m, MonadIO m) => m [AppMgr.DiskInfo]
listDisksLogic = runExceptT AppMgr.diskShow >>= liftEither
ejectDiskLogic :: (Has (Error S9Error) sig m, MonadIO m) => Text -> m ()
ejectDiskLogic t = do
(ec, _) <- AppMgr.readProcessInheritStderr "eject" [toS t] ""
case ec of
ExitSuccess -> pure ()
ExitFailure n -> throwError $ EjectE n
insertBackupResult :: MonadIO m => AppId -> Version -> Bool -> SqlPersistT m (Entity BackupRecord)
insertBackupResult appId appVersion succeeded = do
uuid <- liftIO nextRandom

View File

@@ -7,7 +7,6 @@ import Startlude hiding ( ask )
import Control.Carrier.Lift ( runM )
import Data.Conduit
import qualified Data.Conduit.Binary as CB
import Data.Time.ISO8601
import Yesod.Core hiding ( expiresAt )
import Foundation
@@ -32,7 +31,6 @@ getHostsR = handleS9ErrT $ do
hostParams <- extractHostsQueryParams
verifyHmac productKey hostParams
verifyTimestampNotExpired $ hostsParamsExpiration hostParams
mClaimedAt <- checkExistingPasswordRegistration rootAccountName
case mClaimedAt of
@@ -50,15 +48,6 @@ verifyHmac productKey params = do
HostsParams { hostsParamsHmac, hostsParamsExpiration, hostsParamsSalt } = params
unauthorizedHmac = ClientCryptographyE "Unauthorized hmac"
verifyTimestampNotExpired :: MonadIO m => Text -> S9ErrT m ()
verifyTimestampNotExpired expirationTimestamp = do
now <- liftIO getCurrentTime
case parseISO8601 . toS $ expirationTimestamp of
Nothing -> throwE $ TTLExpirationE "invalid timestamp"
Just expiration -> when (expiration < now) (throwE $ TTLExpirationE "expired")
getCertificateR :: Handler TypedContent
getCertificateR = do
base <- getsYesod $ appFilesystemBase . appSettings

View File

@@ -28,6 +28,7 @@ data AppAvailablePreview = AppAvailablePreview
, appAvailablePreviewVersionLatest :: Version
, appAvailablePreviewDescriptionShort :: Text
, appAvailablePreviewInstallInfo :: Maybe (Version, AppStatus)
, appAvailablePreviewTimestamp :: UTCTime
}
deriving (Eq, Show)
instance ToJSON AppAvailablePreview where
@@ -36,6 +37,7 @@ instance ToJSON AppAvailablePreview where
, "descriptionShort" .= appAvailablePreviewDescriptionShort
, "versionInstalled" .= (fst <$> appAvailablePreviewInstallInfo)
, "status" .= (snd <$> appAvailablePreviewInstallInfo)
, "latestVersionTimestamp" .= appAvailablePreviewTimestamp
]
data AppInstalledPreview = AppInstalledPreview
@@ -43,6 +45,7 @@ data AppInstalledPreview = AppInstalledPreview
, appInstalledPreviewStatus :: AppStatus
, appInstalledPreviewVersionInstalled :: Version
, appInstalledPreviewTorAddress :: Maybe TorAddress
, appInstalledPreviewUi :: Bool
}
deriving (Eq, Show)
instance ToJSON AppInstalledPreview where
@@ -50,6 +53,7 @@ instance ToJSON AppInstalledPreview where
[ "status" .= appInstalledPreviewStatus
, "versionInstalled" .= appInstalledPreviewVersionInstalled
, "torAddress" .= (unTorAddress <$> appInstalledPreviewTorAddress)
, "ui" .= appInstalledPreviewUi
]
data InstallNewAppReq = InstallNewAppReq
@@ -70,6 +74,7 @@ data AppAvailableFull = AppAvailableFull
, appAvailableFullDescriptionShort :: Text
, appAvailableFullDescriptionLong :: Text
, appAvailableFullReleaseNotes :: Text
, appAvailableFullInstallAlert :: Maybe Text
, appAvailableFullDependencyRequirements :: [Full AppDependencyRequirement]
, appAvailableFullVersions :: NonEmpty Version
}
@@ -86,6 +91,7 @@ instance ToJSON AppAvailableFull where
, "versions" .= appAvailableFullVersions
, "releaseNotes" .= appAvailableFullReleaseNotes
, "serviceRequirements" .= appAvailableFullDependencyRequirements
, "installAlert" .= appAvailableFullInstallAlert
]
)
@@ -126,6 +132,8 @@ data AppInstalledFull = AppInstalledFull
, appInstalledFullInstructions :: Maybe Text
, appInstalledFullLastBackup :: Maybe UTCTime
, appInstalledFullConfiguredRequirements :: [Stripped AppDependencyRequirement]
, appInstalledFullUninstallAlert :: Maybe Text
, appInstalledFullRestoreAlert :: Maybe Text
}
instance ToJSON AppInstalledFull where
toJSON AppInstalledFull {..} = object
@@ -138,18 +146,22 @@ instance ToJSON AppInstalledFull where
, "iconURL" .= appBaseIconUrl appInstalledFullBase
, "versionInstalled" .= appInstalledFullVersionInstalled
, "status" .= appInstalledFullStatus
, "uninstallAlert" .= appInstalledFullUninstallAlert
, "restoreAlert" .= appInstalledFullRestoreAlert
]
data AppVersionInfo = AppVersionInfo
{ appVersionInfoVersion :: Version
, appVersionInfoReleaseNotes :: Text
, appVersionInfoDependencyRequirements :: [Full AppDependencyRequirement]
, appVersionInfoInstallAlert :: Maybe Text
}
instance ToJSON AppVersionInfo where
toJSON AppVersionInfo {..} = object
[ "version" .= appVersionInfoVersion
, "releaseNotes" .= appVersionInfoReleaseNotes
, "serviceRequirements" .= appVersionInfoDependencyRequirements
, "installAlert" .= appVersionInfoInstallAlert
]
data ApiDependencyViolation

View File

@@ -31,13 +31,14 @@ data ServerRes = ServerRes
, serverStatus :: Maybe AppStatus
, serverStatusAt :: UTCTime
, serverVersionInstalled :: Version
, serverNotifications :: [Entity Notification]
, serverNotifications :: [ Entity Notification ]
, serverWifi :: WifiList
, serverSsh :: [SshKeyFingerprint]
, serverSsh :: [ SshKeyFingerprint ]
, serverAlternativeRegistryUrl :: Maybe Text
, serverSpecs :: SpecsRes
}
deriving (Eq, Show)
, serverWelcomeAck :: Bool
, serverAutoCheckUpdates :: Bool
} deriving (Eq, Show)
type JsonEncoding a = Encoding
jsonEncode :: (Monad m, ToJSON a) => a -> m (JsonEncoding a)
@@ -51,12 +52,13 @@ instance ToJSON ServerRes where
Nothing -> String "UPDATING"
Just stat -> toJSON stat
, "versionInstalled" .= serverVersionInstalled
, "versionLatest" .= Null
, "notifications" .= serverNotifications
, "wifi" .= serverWifi
, "ssh" .= serverSsh
, "alternativeRegistryUrl" .= serverAlternativeRegistryUrl
, "specs" .= serverSpecs
, "welcomeAck" .= serverWelcomeAck
, "autoCheckUpdates" .= serverAutoCheckUpdates
]
instance ToTypedContent ServerRes where
toTypedContent = toTypedContent . toJSON

View File

@@ -8,7 +8,7 @@ import Control.Carrier.Lift ( runM )
import Data.Aeson
import Data.IORef
import qualified Data.Text as T
import Database.Persist
import Database.Persist as Persist
import Yesod.Core.Handler
import Yesod.Persist.Core
import Yesod.Core.Json
@@ -30,6 +30,7 @@ import Lib.SystemPaths
import Lib.Ssh
import Lib.Tor
import Lib.Types.Core
import Lib.Types.Emver
import Model
import Settings
import Util.Function
@@ -37,7 +38,8 @@ import Util.Function
getServerR :: Handler (JsonEncoding ServerRes)
getServerR = handleS9ErrT $ do
settings <- getsYesod appSettings
agentCtx <- getYesod
let settings = appSettings agentCtx
now <- liftIO getCurrentTime
isUpdating <- getsYesod appIsUpdating >>= liftIO . readIORef
@@ -53,8 +55,11 @@ getServerR = handleS9ErrT $ do
alternativeRegistryUrl <- runM $ injectFilesystemBaseFromContext settings $ readSystemPath altRegistryUrlPath
name <- runM $ injectFilesystemBaseFromContext settings $ readSystemPath serverNamePath
ssh <- readFromPath settings sshKeysFilePath >>= parseSshKeys
wifi <- WpaSupplicant.runWlan0 $ liftA2 WifiList WpaSupplicant.getCurrentNetwork WpaSupplicant.listNetworks
wifi <- WpaSupplicant.runWlan0 $ liftA2 WifiList WpaSupplicant.getCurrentNetwork WpaSupplicant.listNetworks
specs <- getSpecs settings
welcomeAck <- fmap isJust . lift . runDB . Persist.get $ WelcomeAckKey agentVersion
autoCheckUpdates <- runM $ injectFilesystemBaseFromContext settings $ fmap not (existsSystemPath disableAutoCheckUpdatesPath)
let sid = T.drop 7 $ specsNetworkId specs
jsonEncode ServerRes { serverId = specsNetworkId specs
@@ -67,6 +72,8 @@ getServerR = handleS9ErrT $ do
, serverSsh = ssh
, serverAlternativeRegistryUrl = alternativeRegistryUrl
, serverSpecs = specs
, serverWelcomeAck = welcomeAck
, serverAutoCheckUpdates = autoCheckUpdates
}
where
parseSshKeys :: Text -> S9ErrT Handler [SshKeyFingerprint]
@@ -76,6 +83,9 @@ getServerR = handleS9ErrT $ do
Left e -> throwE $ InvalidSshKeyE (toS e)
Right as -> pure $ uncurry3 SshKeyFingerprint <$> as
postWelcomeR :: Version -> Handler ()
postWelcomeR version = runDB $ repsert (WelcomeAckKey version) WelcomeAck
getSpecs :: MonadIO m => AppSettings -> S9ErrT m SpecsRes
getSpecs settings = do
specsCPU <- liftIO getCpuInfo
@@ -102,9 +112,22 @@ newtype NullablePatchReq = NullablePatchReq { mpatchValue :: Maybe Text } derivi
instance FromJSON NullablePatchReq where
parseJSON = withObject "Nullable Patch Request" $ \o -> NullablePatchReq <$> o .:? "value"
newtype BoolPatchReq = BoolPatchReq { bpatchValue :: Bool } deriving (Eq, Show)
instance FromJSON BoolPatchReq where
parseJSON = withObject "Patch Request" $ \o -> BoolPatchReq <$> o .: "value"
patchNameR :: Handler ()
patchNameR = patchFile serverNamePath
patchAutoCheckUpdatesR :: Handler ()
patchAutoCheckUpdatesR = do
settings <- getsYesod appSettings
BoolPatchReq val <- requireCheckJsonBody
runM $ injectFilesystemBaseFromContext settings $ if val
then deleteSystemPath disableAutoCheckUpdatesPath
else writeSystemPath disableAutoCheckUpdatesPath ""
patchFile :: SystemPath -> Handler ()
patchFile path = do
settings <- getsYesod appSettings

View File

@@ -25,7 +25,6 @@ import qualified Data.HashMap.Strict as HM
import Data.Singletons.Prelude hiding ( Error )
import Data.Singletons.Prelude.Either
import qualified Data.String as String
import Exinst
import Lib.Algebra.Domain.AppMgr.Types
import Lib.Algebra.Domain.AppMgr.TH
@@ -67,7 +66,7 @@ data InfoRes a = InfoRes
(Either_ (DefaultEqSym1 'OnlyDependencies) (ElemSym1 'IncludeDependencies) a)
(HM.HashMap AppId DependencyInfo)
, infoResManifest
:: Include (Either_ (DefaultEqSym1 'OnlyManifest) (ElemSym1 'IncludeManifest) a) (Some1 AppManifest)
:: Include (Either_ (DefaultEqSym1 'OnlyManifest) (ElemSym1 'IncludeManifest) a) AppManifest
, infoResStatus :: Include (Either_ (DefaultEqSym1 'OnlyStatus) (ElemSym1 'IncludeStatus) a) AppContainerStatus
}
instance SingI (a :: Either OnlyInfoFlag [IncludeInfoFlag]) => FromJSON (InfoRes a) where

View File

@@ -31,6 +31,7 @@ data S9Error =
| AppMgrParseE Text Text String
| AppMgrInvalidConfigE Text
| AppMgrE Text Int
| EjectE Int
| AvahiE Text
| MetricE Text
| AppMgrVersionE Version VersionRange
@@ -51,6 +52,7 @@ data S9Error =
| WifiOrphaningE
| NoPasswordExistsE
| HostsParamsE Text
| ParamsE Text
| MissingFileE SystemPath
| ClientCryptographyE Text
| TTLExpirationE Text
@@ -86,6 +88,7 @@ toError = \case
AppMgrParseE cmd result e ->
ErrorResponse APPMGR_PARSE_ERROR [i|"appmgr #{cmd}" yielded an unparseable result:#{result}\nError: #{e}|]
AppMgrE cmd code -> ErrorResponse APPMGR_ERROR [i|"appmgr #{cmd}" exited with #{code}|]
EjectE code -> ErrorResponse EJECT_ERROR [i|"eject" command exited with #{code}|]
AppMgrVersionE av avs ->
ErrorResponse APPMGR_ERROR [i|"appmgr version #{av}" fails to satisfy requisite spec #{avs}|]
AvahiE e -> ErrorResponse AVAHI_ERROR [i|#{e}|]
@@ -136,6 +139,7 @@ toError = \case
TTLExpirationE desc -> ErrorResponse REGISTRATION_ERROR [i|TTL Expiration failure: #{desc}|]
EnvironmentValE appId -> ErrorResponse SYNCHRONIZATION_ERROR [i|Could not read environment values for #{appId}|]
HostsParamsE key -> ErrorResponse REGISTRATION_ERROR [i|Missing or invalid parameter #{key}|]
ParamsE key -> ErrorResponse INVALID_REQUEST [i|Missing or invalid parameter #{key}|]
InternalE msg -> ErrorResponse INTERNAL_ERROR msg
BackupE appId reason -> ErrorResponse BACKUP_ERROR [i|Backup failed for #{appId}: #{reason}|]
BackupPassInvalidE -> ErrorResponse BACKUP_ERROR [i|Password provided for backups is invalid|]
@@ -151,6 +155,7 @@ data ErrorCode =
| APPMGR_CONFIG_ERROR
| APPMGR_PARSE_ERROR
| APPMGR_ERROR
| EJECT_ERROR
| AVAHI_ERROR
| REGISTRY_ERROR
| APP_NOT_INSTALLED
@@ -201,6 +206,7 @@ toStatus = \case
AppMgrParseE{} -> status500
AppMgrInvalidConfigE _ -> status400
AppMgrE _ _ -> status500
EjectE _ -> status500
AppMgrVersionE _ _ -> status500
AvahiE _ -> status500
MetricE _ -> status500
@@ -238,6 +244,7 @@ toStatus = \case
TTLExpirationE _ -> status403
EnvironmentValE _ -> status500
HostsParamsE _ -> status400
ParamsE _ -> status400
BackupE _ _ -> status500
BackupPassInvalidE -> status403
InternalE _ -> status500

View File

@@ -6,10 +6,8 @@ import Startlude hiding ( ask )
import Control.Effect.Reader.Labelled
import Data.Aeson
import Data.Singletons.TypeLits
import qualified Data.HashMap.Strict as HM
import qualified Data.Yaml as Yaml
import Exinst
import Lib.Error
import Lib.SystemPaths
@@ -49,52 +47,49 @@ instance FromJSON AssetMapping where
assetMappingOverwrite <- o .: "overwrite"
pure $ AssetMapping { .. }
data AppManifest (n :: Nat) where
AppManifestV0 ::{ appManifestV0Id :: AppId
, appManifestV0Version :: Version
, appManifestV0Title :: Text
, appManifestV0DescShort :: Text
, appManifestV0DescLong :: Text
, appManifestV0ReleaseNotes :: Text
, appManifestV0PortMapping :: HM.HashMap Word16 Word16
, appManifestV0ImageType :: ImageType
, appManifestV0Mount :: FilePath
, appManifestV0Assets :: [AssetMapping]
, appManifestV0OnionVersion :: OnionVersion
, appManifestV0Dependencies :: HM.HashMap AppId VersionRange
} -> AppManifest 0
data AppManifest where
AppManifest ::{ appManifestId :: AppId
, appManifestVersion :: Version
, appManifestTitle :: Text
, appManifestDescShort :: Text
, appManifestDescLong :: Text
, appManifestReleaseNotes :: Text
, appManifestPortMapping :: HM.HashMap Word16 Word16
, appManifestImageType :: ImageType
, appManifestMount :: FilePath
, appManifestAssets :: [AssetMapping]
, appManifestOnionVersion :: OnionVersion
, appManifestDependencies :: HM.HashMap AppId VersionRange
, appManifestUninstallAlert :: Maybe Text
, appManifestRestoreAlert :: Maybe Text
} -> AppManifest
instance FromJSON (Some1 AppManifest) where
parseJSON = withObject "App Manifest" $ \o -> do
o .: "compat" >>= \case
("v0" :: Text) -> Some1 (SNat @0) <$> parseJSON (Object o)
compat -> fail $ "Unknown Manifest Version: " <> toS compat
uiAvailable :: AppManifest -> Bool
uiAvailable AppManifest {..} = isJust $ HM.lookup 80 appManifestPortMapping
instance FromJSON (AppManifest 0) where
parseJSON = withObject "App Manifest V0" $ \o -> do
appManifestV0Id <- o .: "id"
appManifestV0Version <- o .: "version"
appManifestV0Title <- o .: "title"
appManifestV0DescShort <- o .: "description" >>= (.: "short")
appManifestV0DescLong <- o .: "description" >>= (.: "long")
appManifestV0ReleaseNotes <- o .: "release-notes"
appManifestV0PortMapping <- o .: "ports" >>= fmap HM.fromList . traverse parsePortMapping
appManifestV0ImageType <- o .: "image" >>= (.: "type")
appManifestV0Mount <- o .: "mount"
appManifestV0Assets <- o .: "assets" >>= traverse parseJSON
appManifestV0OnionVersion <- o .: "hidden-service-version"
appManifestV0Dependencies <- o .:? "dependencies" .!= HM.empty >>= traverse parseDepInfo
pure $ AppManifestV0 { .. }
instance FromJSON AppManifest where
parseJSON = withObject "App Manifest " $ \o -> do
appManifestId <- o .: "id"
appManifestVersion <- o .: "version"
appManifestTitle <- o .: "title"
appManifestDescShort <- o .: "description" >>= (.: "short")
appManifestDescLong <- o .: "description" >>= (.: "long")
appManifestReleaseNotes <- o .: "release-notes"
appManifestPortMapping <- o .: "ports" >>= fmap HM.fromList . traverse parsePortMapping
appManifestImageType <- o .: "image" >>= (.: "type")
appManifestMount <- o .: "mount"
appManifestAssets <- o .: "assets" >>= traverse parseJSON
appManifestOnionVersion <- o .: "hidden-service-version"
appManifestDependencies <- o .:? "dependencies" .!= HM.empty >>= traverse parseDepInfo
appManifestUninstallAlert <- o .:? "uninstall-alert"
appManifestRestoreAlert <- o .:? "restore-alert"
pure $ AppManifest { .. }
where
parsePortMapping = withObject "Port Mapping" $ \o -> liftA2 (,) (o .: "tor") (o .: "internal")
parseDepInfo = withObject "Dep Info" $ (.: "version")
getAppManifest :: (MonadIO m, HasFilesystemBase sig m) => AppId -> S9ErrT m (Maybe (Some1 AppManifest))
getAppManifest :: (MonadIO m, HasFilesystemBase sig m) => AppId -> S9ErrT m (Maybe AppManifest)
getAppManifest appId = do
base <- ask @"filesystemBase"
ExceptT $ first (ManifestParseE appId) <$> liftIO
(Yaml.decodeFileEither . toS $ (appMgrAppPath appId <> "manifest.yaml") `relativeTo` base)
uiAvailable :: AppManifest n -> Bool
uiAvailable = \case
AppManifestV0 { appManifestV0PortMapping } -> elem 80 (HM.keys appManifestV0PortMapping)

View File

@@ -36,6 +36,7 @@ import Lib.SystemPaths
import Lib.Types.Core
import Lib.Types.Emver
import Lib.Types.ServerApp
import Data.Time.ISO8601 ( parseISO8601 )
newtype AppManifestRes = AppManifestRes
{ storeApps :: [StoreApp] } deriving (Eq, Show)
@@ -135,6 +136,7 @@ parseAppData = do
storeAppVersions <- ad .: "version-info" >>= \case
[] -> fail "No Valid Version Info"
(x : xs) -> pure $ x :| xs
storeAppTimestamp <- ad .: "timestamp" >>= maybe (fail "Invalid ISO8601 Timestamp") pure . parseISO8601
pure StoreApp { .. }
getAppVersionForSpec :: (Has RegistryUrl sig m, Has (Error S9Error) sig m, MonadIO m)

View File

@@ -96,12 +96,12 @@ parseKernelVersion = do
pure $ KernelVersion (Version (major', minor', patch', 0)) arch
synchronizer :: Synchronizer
synchronizer = sync_0_2_7
synchronizer = sync_0_2_8
{-# INLINE synchronizer #-}
sync_0_2_7 :: Synchronizer
sync_0_2_7 = Synchronizer
"0.2.7"
sync_0_2_8 :: Synchronizer
sync_0_2_8 = Synchronizer
"0.2.8"
[ syncCreateAgentTmp
, syncCreateSshDir
, syncRemoveAvahiSystemdDependency
@@ -121,6 +121,7 @@ sync_0_2_7 = Synchronizer
, syncPersistLogs
, syncConvertEcdsaCerts
, syncRestarterService
, syncInstallEject
]
syncCreateAgentTmp :: SyncOp
@@ -169,8 +170,8 @@ syncFullUpgrade = SyncOp "Full Upgrade" check migrate True
Just (Done _ (KernelVersion (Version av) _)) -> if av < (4, 19, 118, 0) then pure True else pure False
_ -> pure False
migrate = liftIO . run $ do
shell "apt update"
shell "apt full-upgrade -y"
shell "apt-get update"
shell "apt-get full-upgrade -y"
sync32BitKernel :: SyncOp
sync32BitKernel = SyncOp "32 Bit Kernel Switch" check migrate True
@@ -194,16 +195,24 @@ syncInstallNginx = SyncOp "Install Nginx" check migrate False
where
check = liftIO . run $ fmap isNothing (shell [i|which nginx || true|] $| conduit await)
migrate = liftIO . run $ do
apt "update"
apt "install" "nginx" "-y"
shell "apt-get update"
shell "apt-get install nginx -y"
syncInstallEject :: SyncOp
syncInstallEject = SyncOp "Install Eject" check migrate False
where
check = liftIO . run $ fmap isNothing (shell [i|which eject || true|] $| conduit await)
migrate = liftIO . run $ do
shell "apt-get update"
shell "apt-get install eject -y"
syncInstallDuplicity :: SyncOp
syncInstallDuplicity = SyncOp "Install duplicity" check migrate False
where
check = liftIO . run $ fmap isNothing (shell [i|which duplicity || true|] $| conduit await)
migrate = liftIO . run $ do
apt "update"
apt "install" "-y" "duplicity"
shell "apt-get update"
shell "apt-get install -y duplicity"
syncInstallExfatFuse :: SyncOp
syncInstallExfatFuse = SyncOp "Install exfat-fuse" check migrate False
@@ -215,8 +224,8 @@ syncInstallExfatFuse = SyncOp "Install exfat-fuse" check migrate False
ProcessException _ (ExitFailure 1) -> pure True
_ -> throwIO e
migrate = liftIO . run $ do
apt "update"
apt "install" "-y" "exfat-fuse"
shell "apt-get update"
shell "apt-get install -y exfat-fuse"
syncInstallExfatUtils :: SyncOp
syncInstallExfatUtils = SyncOp "Install exfat-utils" check migrate False
@@ -228,8 +237,8 @@ syncInstallExfatUtils = SyncOp "Install exfat-utils" check migrate False
ProcessException _ (ExitFailure 1) -> pure True
_ -> throwIO e
migrate = liftIO . run $ do
apt "update"
apt "install" "-y" "exfat-utils"
shell "apt-get update"
shell "apt-get install -y exfat-utils"
syncWriteConf :: Text -> ByteString -> SystemPath -> SyncOp
syncWriteConf name contents' confLocation = SyncOp [i|Write #{name} Conf|] check migrate False

View File

@@ -80,6 +80,11 @@ readSystemPath path = do
$ (Just <$> readFile (toS loadPath))
`catch` (\(e :: IOException) -> if isDoesNotExistError e then pure Nothing else throwIO e)
existsSystemPath :: (HasFilesystemBase sig m, MonadIO m) => SystemPath -> m Bool
existsSystemPath path = do
checkPath <- getAbsoluteLocationFor path
liftIO . doesPathExist $ toS checkPath
-- like the above, but throws IO error if file not found
readSystemPath' :: (HasFilesystemBase sig m, MonadIO m) => SystemPath -> m Text
readSystemPath' path = do
@@ -188,6 +193,9 @@ agentTorHiddenServicePrivateKeyPath = agentTorHiddenServiceDirectory <> "/hs_ed2
serverNamePath :: SystemPath
serverNamePath = "/root/agent/name.txt"
disableAutoCheckUpdatesPath :: SystemPath
disableAutoCheckUpdatesPath = "/root/agent/.disableAutoCheckUpdates"
altRegistryUrlPath :: SystemPath
altRegistryUrlPath = "/root/agent/alt_registry_url.txt"

View File

@@ -3,11 +3,22 @@ module Lib.Tor where
import Startlude
import qualified Data.Text as T
import Network.HTTP.Client
import Network.Connection
import Lib.SystemPaths
import Network.HTTP.Client.TLS ( mkManagerSettings
, newTlsManagerWith
)
import Data.Default
getAgentHiddenServiceUrl :: (HasFilesystemBase sig m, MonadIO m) => m Text
getAgentHiddenServiceUrl = T.strip <$> readSystemPath' agentTorHiddenServiceHostnamePath
getAgentHiddenServiceUrlMaybe :: (HasFilesystemBase sig m, MonadIO m) => m (Maybe Text)
getAgentHiddenServiceUrlMaybe = fmap T.strip <$> readSystemPath agentTorHiddenServiceHostnamePath
-- | 'newTorManager' currently assumes the tor client lives on the localhost. The port comes in over an argument.
-- If this is insufficient in the future, feel free to parameterize the host.
newTorManager :: Word16 -> IO Manager
newTorManager = newManager . mkManagerSettings def . Just . SockSettingsSimple "127.0.0.1" . fromIntegral

View File

@@ -56,6 +56,10 @@ instance Show Version where
let postfix = if q == 0 then "" else '.' : show q in show x <> "." <> show y <> "." <> show z <> postfix
instance IsString Version where
fromString s = either error id $ Atto.parseOnly parseVersion (T.pack s)
instance Read Version where
readsPrec _ s = case Atto.parseOnly parseVersion (T.pack s) of
Left _ -> []
Right a -> [(a, "")]
-- | A change in the value found at 'major' implies a breaking change in the API that this version number describes
major :: Version -> Word

View File

@@ -3,16 +3,17 @@ module Lib.Types.Emver.Orphans where
import Startlude
import Control.Monad.Fail
import Data.Aeson
import Lib.Types.Emver
import qualified Data.Attoparsec.Text as Atto
import qualified Data.Text as T
import Database.Persist
import Database.Persist.Sql
import qualified Data.Attoparsec.Text as Atto
import Control.Monad.Fail
import qualified Data.Text as T
import Web.HttpApiData
import Yesod.Core.Dispatch
import Lib.Types.Emver
instance ToJSON Version where
toJSON = String . show
instance FromJSON Version where
@@ -31,9 +32,16 @@ instance FromJSON VersionRange where
instance PersistField Version where
toPersistValue = toPersistValue @Text . show
fromPersistValue = first T.pack . Atto.parseOnly parseVersion <=< fromPersistValue
instance PersistFieldSql Version where
sqlType _ = SqlString
instance FromHttpApiData Version where
parseUrlPiece = first toS . Atto.parseOnly parseVersion
instance ToHttpApiData Version where
toUrlPiece = show
instance PathPiece Version where
toPathPiece = show
fromPathPiece = hush . Atto.parseOnly parseVersion
instance PathPiece VersionRange where
toPathPiece = show

View File

@@ -20,12 +20,14 @@ data StoreApp = StoreApp
, storeAppDescriptionLong :: Text
, storeAppIconUrl :: Text
, storeAppVersions :: NonEmpty StoreAppVersionInfo
, storeAppTimestamp :: UTCTime
}
deriving (Eq, Show)
data StoreAppVersionInfo = StoreAppVersionInfo
{ storeAppVersionInfoVersion :: Version
, storeAppVersionInfoReleaseNotes :: Text
, storeAppVersionInfoInstallAlert :: Maybe Text
}
deriving (Eq, Show)
instance Ord StoreAppVersionInfo where
@@ -34,6 +36,7 @@ instance FromJSON StoreAppVersionInfo where
parseJSON = withObject "Store App Version Info" $ \o -> do
storeAppVersionInfoVersion <- o .: "version"
storeAppVersionInfoReleaseNotes <- o .: "release-notes"
storeAppVersionInfoInstallAlert <- o .:? "install-alert"
pure StoreAppVersionInfo { .. }
instance ToJSON StoreAppVersionInfo where
toJSON StoreAppVersionInfo {..} =

View File

@@ -59,4 +59,7 @@ BackupRecord sql=backup
IconDigest
Id AppId
tag (Digest MD5)
WelcomeAck
Id Version
|]

View File

@@ -41,6 +41,9 @@ data AppSettings = AppSettings
-- ^ Should all log messages be displayed?
, appMgrVersionSpec :: VersionRange
, appFilesystemBase :: Text
, appTorSocksPort :: Word16
-- ^ Port on localhost where the tor client is listening, defaults to 9050
, appTorRestartCooldown :: NominalDiffTime
}
deriving Show
@@ -63,6 +66,8 @@ instance FromJSON AppSettings where
appMgrVersionSpec <- o .: "app-mgr-version-spec"
appFilesystemBase <- o .: "filesystem-base"
appTorSocksPort <- o .:? "tor-socks-port" .!= 9050
appTorRestartCooldown <- o .:? "tor-restart-cooldown" .!= (secondsToNominalDiffTime 600)
return AppSettings { .. }
-- | Raw bytes at compile time of @config/settings.yml@

View File

@@ -69,12 +69,12 @@ instance MonadResource m => MonadResource (FE.ReaderC r m) where
instance MonadResource m => MonadResource (FE.ErrorC e m) where
liftResourceT = lift . liftResourceT
instance MonadThrow (sub m) => MonadThrow (FE.Labelled label sub m) where
throwM = FE.Labelled . throwM
instance MonadThrow m => MonadThrow (FE.LiftC m) where
throwM = FE.LiftC . throwM
instance MonadLogger m => MonadLogger (FE.ErrorC e m) where
instance MonadLogger m => MonadLogger (FE.LiftC m) where
instance MonadLogger (sub m) => MonadLogger (FE.Labelled label sub m) where
monadLoggerLog a b c d = FE.Labelled $ monadLoggerLog a b c d
@@ -91,6 +91,13 @@ instance MonadHandler (sub m) => MonadHandler (FE.Labelled label sub m) where
liftHandler = FE.Labelled . liftHandler
liftSubHandler = FE.Labelled . liftSubHandler
instance MonadHandler m => MonadHandler (FE.ErrorC e m) where
type HandlerSite (FE.ErrorC e m) = HandlerSite m
type SubHandlerSite (FE.ErrorC e m) = SubHandlerSite m
liftHandler = lift . liftHandler
liftSubHandler = lift . liftSubHandler
instance MonadTransControl t => MonadTransControl (FE.Labelled k t) where
type StT (FE.Labelled k t) a = StT t a
liftWith f = FE.Labelled $ liftWith $ \run -> f (run . FE.runLabelled)

19
appmgr/Cargo.lock generated
View File

@@ -35,7 +35,7 @@ dependencies = [
[[package]]
name = "appmgr"
version = "0.2.7"
version = "0.2.8"
dependencies = [
"argonautica",
"async-trait",
@@ -51,6 +51,7 @@ dependencies = [
"lazy_static",
"linear-map",
"log",
"nix 0.19.1",
"openssl",
"pest",
"pest_derive",
@@ -601,7 +602,7 @@ dependencies = [
"gcc",
"libc",
"mktemp",
"nix",
"nix 0.11.1",
]
[[package]]
@@ -1310,6 +1311,18 @@ dependencies = [
"void",
]
[[package]]
name = "nix"
version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ccba0cfe4fdf15982d1674c69b1fd80bad427d293849982668dfe454bd61f2"
dependencies = [
"bitflags",
"cc",
"cfg-if 1.0.0",
"libc",
]
[[package]]
name = "nom"
version = "4.2.3"
@@ -2344,7 +2357,7 @@ dependencies = [
[[package]]
name = "tokio-tar"
version = "0.3.0"
source = "git+https://github.com/dr-bonez/tokio-tar.git#1ba710f344ddb2a5b4d98bb96c905195c3cd9d43"
source = "git+https://github.com/dr-bonez/tokio-tar.git?rev=1ba710f3#1ba710f344ddb2a5b4d98bb96c905195c3cd9d43"
dependencies = [
"filetime",
"futures-core",

View File

@@ -1,8 +1,8 @@
[package]
name = "appmgr"
version = "0.2.7"
authors = ["Aiden McClelland <me@drbonez.dev>"]
edition = "2018"
name = "appmgr"
version = "0.2.8"
[lib]
name = "appmgrlib"
@@ -18,12 +18,12 @@ portable = []
production = []
[dependencies]
emver = { version = "0.1.0", features = ["serde"] }
argonautica = "0.2.0"
async-trait = "0.1.42"
base32 = "0.4.0"
clap = "2.33"
ed25519-dalek = "1.0.1"
emver = { version = "0.1.0", features = ["serde"] }
failure = "0.1.8"
file-lock = "1.1"
futures = "0.3.8"
@@ -32,6 +32,7 @@ itertools = "0.9.0"
lazy_static = "1.4"
linear-map = { version = "1.2", features = ["serde_impl"] }
log = "0.4.11"
nix = "0.19.1"
openssl = "0.10.30"
pest = "2.1"
pest_derive = "2.1"
@@ -41,10 +42,10 @@ regex = "1.4.2"
reqwest = { version = "0.10.9", features = ["stream", "json"] }
rpassword = "5.0.0"
serde = { version = "1.0.118", features = ["derive", "rc"] }
serde_yaml = "0.8.14"
serde_cbor = "0.11.1"
serde_json = "1.0.59"
serde_yaml = "0.8.14"
simple-logging = "2.0"
tokio = { version = "0.3.5", features = ["full"] }
tokio-compat-02 = "0.1.2"
tokio-tar = { version = "0.3.0", git = "https://github.com/dr-bonez/tokio-tar.git" }
tokio-tar = { version = "0.3.0", git = "https://github.com/dr-bonez/tokio-tar.git", rev = "1ba710f3" }

View File

@@ -1,15 +1,25 @@
use std::path::Path;
use argonautica::{Hasher, Verifier};
use emver::Version;
use futures::try_join;
use futures::TryStreamExt;
use serde::Serialize;
use crate::apps;
use crate::util::from_yaml_async_reader;
use crate::util::to_yaml_async_writer;
use crate::util::Invoke;
use crate::version::VersionT;
use crate::Error;
use crate::ResultExt;
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Metadata {
pub app_version: Version,
pub os_version: &'static Version,
}
pub async fn create_backup<P: AsRef<Path>>(
path: P,
app_id: &str,
@@ -21,6 +31,7 @@ pub async fn create_backup<P: AsRef<Path>>(
crate::error::FILESYSTEM_ERROR,
"Backup Path Must Be Directory"
);
let metadata_path = path.join("metadata.yaml");
let pw_path = path.join("password");
let data_path = path.join("data");
let tor_path = path.join("tor");
@@ -56,6 +67,16 @@ pub async fn create_backup<P: AsRef<Path>>(
f.flush().await?;
}
let info = crate::apps::info(app_id).await?;
to_yaml_async_writer(
tokio::fs::File::create(metadata_path).await?,
&Metadata {
app_version: info.version,
os_version: crate::version::Current::new().semver(),
},
)
.await?;
let status = crate::apps::status(app_id, false).await?;
let exclude = if volume_path.is_dir() {
let ignore_path = volume_path.join(".backupignore");
@@ -124,6 +145,7 @@ pub async fn restore_backup<P: AsRef<Path>>(
crate::error::FILESYSTEM_ERROR,
"Backup Path Must Be Directory"
);
let metadata_path = path.join("metadata.yaml");
let pw_path = path.join("password");
let data_path = path.join("data");
let tor_path = path.join("tor");
@@ -181,12 +203,21 @@ pub async fn restore_backup<P: AsRef<Path>>(
);
// Fix the tor address in apps.yaml
let mut yhdl = apps::list_info_mut().await?;
let mut yhdl = crate::apps::list_info_mut().await?;
if let Some(app_info) = yhdl.get_mut(app_id) {
app_info.tor_address = Some(crate::tor::read_tor_address(app_id, None).await?);
}
yhdl.commit().await?;
tokio::fs::copy(
metadata_path,
Path::new(crate::VOLUMES)
.join(app_id)
.join("start9")
.join("restore.yaml"),
)
.await?;
// Attempt to configure the service with the config coming from restoration
let cfg_path = Path::new(crate::VOLUMES)
.join(app_id)

View File

@@ -30,6 +30,7 @@ pub struct VersionInfo {
pub release_notes: String,
pub os_version_required: VersionRange,
pub os_version_recommended: VersionRange,
pub install_alert: Option<String>,
}
const NULL_VERSION: Version = Version::new(0, 0, 0, 0);
@@ -52,6 +53,7 @@ impl AppIndex {
release_notes: manifest.release_notes,
os_version_required: manifest.os_version_required,
os_version_recommended: manifest.os_version_recommended,
install_alert: manifest.install_alert,
});
entry
.version_info
@@ -68,6 +70,7 @@ impl AppIndex {
release_notes: manifest.release_notes,
os_version_required: manifest.os_version_required,
os_version_recommended: manifest.os_version_recommended,
install_alert: manifest.install_alert,
}],
icon_type: "png".to_owned(), // TODO
},

View File

@@ -478,7 +478,7 @@ pub async fn install_v0<R: AsyncRead + Unpin + Send + Sync>(
let mut args = vec![
Cow::Borrowed(OsStr::new("create")),
Cow::Borrowed(OsStr::new("--restart")),
Cow::Borrowed(OsStr::new("on-failure")),
Cow::Borrowed(OsStr::new("no")),
Cow::Borrowed(OsStr::new("--name")),
Cow::Borrowed(OsStr::new(&manifest.id)),
Cow::Borrowed(OsStr::new("--mount")),

View File

@@ -37,6 +37,12 @@ pub struct ManifestV0 {
pub description: Description,
pub release_notes: String,
#[serde(default)]
pub install_alert: Option<String>,
#[serde(default)]
pub uninstall_alert: Option<String>,
#[serde(default)]
pub restore_alert: Option<String>,
#[serde(default)]
pub has_instructions: bool,
#[serde(default = "emver::VersionRange::any")]
pub os_version_required: emver::VersionRange,

View File

@@ -25,14 +25,6 @@ pub enum Error {
pub async fn pack(path: &str, output: &str) -> Result<(), failure::Error> {
let path = Path::new(path.trim_end_matches("/"));
let output = Path::new(output);
ensure!(
output
.extension()
.and_then(|a| a.to_str())
.ok_or_else(|| Error::InvalidOutputPath(format!("{}", output.display())))?
== "s9pk",
"Extension Must Be '.s9pk'"
);
log::info!(
"Starting pack of {} to {}.",
path.file_name()
@@ -74,9 +66,6 @@ pub async fn pack(path: &str, output: &str) -> Result<(), failure::Error> {
.with_context(|e| format!("{}: config_spec.yaml", e))?,
)
.await?;
config_spec.validate(&manifest)?;
let config = config_spec.gen(&mut rand::rngs::StdRng::from_entropy(), &None)?;
config_spec.matches(&config)?;
log::info!("Writing config spec to archive.");
let bin_config_spec = serde_cbor::to_vec(&config_spec)?;
let mut config_spec_header = tar::Header::new_gnu();
@@ -94,12 +83,6 @@ pub async fn pack(path: &str, output: &str) -> Result<(), failure::Error> {
.with_context(|e| format!("{}: config_rules.yaml", e))?,
)
.await?;
let mut cfgs = LinearMap::new();
cfgs.insert(manifest.id.as_str(), Cow::Borrowed(&config));
for rule in &config_rules {
rule.check(&config, &cfgs)
.with_context(|e| format!("Default Config does not satisfy: {}", e))?;
}
log::info!("Writing config rules to archive.");
let bin_config_rules = serde_cbor::to_vec(&config_rules)?;
let mut config_rules_header = tar::Header::new_gnu();

View File

@@ -217,7 +217,7 @@ pub async fn read_tor_key(
version: HiddenServiceVersion,
timeout: Option<Duration>,
) -> Result<String, Error> {
log::info!("Retrieving Tor hidden service address for {}.", name);
log::info!("Retrieving Tor hidden service key for {}.", name);
let addr_path = Path::new(HIDDEN_SERVICE_DIR_ROOT)
.join(format!("app-{}", name))
.join(match version {
@@ -277,6 +277,17 @@ pub async fn set_svc(
let ip = hidden_services.add(name.to_owned(), service);
log::info!("Adding Tor hidden service {} to {}.", name, ETC_TOR_RC);
write_services(&hidden_services).await?;
let addr_path = Path::new(HIDDEN_SERVICE_DIR_ROOT)
.join(format!("app-{}", name))
.join("hostname");
tokio::fs::remove_file(addr_path).await.or_else(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
Ok(())
} else {
Err(e)
}
})?;
nix::unistd::sync();
hidden_services.commit().await?;
log::info!("Reloading Tor.");
let svc_exit = std::process::Command::new("service")

View File

@@ -23,8 +23,9 @@ mod v0_2_4;
mod v0_2_5;
mod v0_2_6;
mod v0_2_7;
mod v0_2_8;
pub use v0_2_7::Version as Current;
pub use v0_2_8::Version as Current;
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
@@ -44,6 +45,7 @@ enum Version {
V0_2_5(Wrapper<v0_2_5::Version>),
V0_2_6(Wrapper<v0_2_6::Version>),
V0_2_7(Wrapper<v0_2_7::Version>),
V0_2_8(Wrapper<v0_2_8::Version>),
Other(emver::Version),
}
@@ -153,6 +155,7 @@ pub async fn init() -> Result<(), failure::Error> {
Version::V0_2_5(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_6(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_7(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_8(v) => v.0.migrate_to(&Current::new()).await?,
Version::Other(_) => (),
// TODO find some way to automate this?
}
@@ -240,6 +243,7 @@ pub async fn self_update(requirement: emver::VersionRange) -> Result<(), Error>
Version::V0_2_5(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_6(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_7(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_8(v) => Current::new().migrate_to(&v.0).await?,
Version::Other(_) => (),
// TODO find some way to automate this?
};

View File

@@ -0,0 +1,36 @@
use super::*;
use crate::util::Invoke;
const V0_2_8: emver::Version = emver::Version::new(0, 2, 8, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_2_7::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_2_8
}
async fn up(&self) -> Result<(), Error> {
for (app_id, _) in crate::apps::list_info().await? {
tokio::process::Command::new("docker")
.arg("stop")
.arg(&app_id)
.invoke("Docker")
.await?;
tokio::process::Command::new("docker")
.arg("update")
.arg("--restart")
.arg("no")
.arg(&app_id)
.invoke("Docker")
.await?;
}
Ok(())
}
async fn down(&self) -> Result<(), Error> {
Ok(())
}
}

30
appmgr/taplo.toml Normal file
View File

@@ -0,0 +1,30 @@
include = ["Cargo.toml"]
[formatting]
# Align consecutive entries vertically.
align_entries = false
# Append trailing commas for multi-line arrays.
array_trailing_comma = true
# Expand arrays to multiple lines that exceed the maximum column width.
array_auto_expand = true
# Collapse arrays that don't exceed the maximum column width and don't contain comments.
array_auto_collapse = true
# Omit white space padding from single-line arrays
compact_arrays = true
# Omit white space padding from the start and end of inline tables.
compact_inline_tables = false
# Maximum column width in characters, affects array expansion and collapse, this doesn't take whitespace into account.
# Note that this is not set in stone, and works on a best-effort basis.
column_width = 80
# Indent based on tables and arrays of tables and their subtables, subtables out of order are not indented.
indent_tables = false
# The substring that is used for indentation, should be tabs or spaces (but technically can be anything).
indent_string = ' '
# Add trailing newline at the end of the file if not present.
trailing_newline = true
# Alphabetically reorder keys that are not separated by empty lines.
reorder_keys = true
# Maximum amount of allowed consecutive blank lines. This does not affect the whitespace at the end of the document, as it is always stripped.
allowed_blank_lines = 2
# Use CRLF for line endings.
crlf = false

3
docker-daemon.json Normal file
View File

@@ -0,0 +1,3 @@
{
"log-driver": "journald"
}

2
lifeline/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target
**/*.rs.bk

6
lifeline/Cargo.lock generated Normal file
View File

@@ -0,0 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "lifeline"
version = "0.1.0"

9
lifeline/Cargo.toml Normal file
View File

@@ -0,0 +1,9 @@
[package]
name = "lifeline"
version = "0.1.0"
authors = ["Keagan McClelland <keagan.mcclelland@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

10
lifeline/lifeline.service Normal file
View File

@@ -0,0 +1,10 @@
[Unit]
Description=Boot process for system reset.
[Service]
Type=oneshot
ExecStart=/usr/local/bin/lifeline
RemainAfterExit=true
[Install]
WantedBy=multi-user.target

1
lifeline/rustfmt.toml Normal file
View File

@@ -0,0 +1 @@
max_width = 120

14
lifeline/src/main.rs Normal file
View File

@@ -0,0 +1,14 @@
use std::time::Duration;
mod sound;
use sound::{notes::*, Song};
const SUCCESS_SONG: Song = Song(&[(Some(A_4), Duration::from_millis(100))]);
fn main() {
std::fs::write("/sys/class/pwm/pwmchip0/export", "0").unwrap();
let res = SUCCESS_SONG.play();
std::fs::write("/sys/class/pwm/pwmchip0/unexport", "0").unwrap();
res.unwrap();
}

93
lifeline/src/sound.rs Normal file
View File

@@ -0,0 +1,93 @@
#![allow(dead_code)]
use std::io::Error;
use std::time::Duration;
pub mod notes {
pub const A_4: f64 = 440.0;
pub const B_4: f64 = 493.88;
pub const C_5: f64 = 523.25;
pub const D_5: f64 = 587.33;
pub const E_5: f64 = 659.25;
pub const F_5: f64 = 698.46;
pub const G_5: f64 = 783.99;
pub const A_5: f64 = 880.00;
pub const B_5: f64 = 987.77;
pub const E_6: f64 = 1318.51;
}
pub fn freq_to_period(freq: f64) -> Duration {
Duration::from_secs(1).div_f64(freq)
}
pub fn play(freq: f64) -> Result<(), Error> {
// set freq
let period = freq_to_period(freq);
let period_bytes = std::fs::read("/sys/class/pwm/pwmchip0/pwm0/period")?;
if period_bytes == b"0\n" {
std::fs::write("/sys/class/pwm/pwmchip0/pwm0/period", format!("{}", 1000))?;
}
std::fs::write("/sys/class/pwm/pwmchip0/pwm0/duty_cycle", "0")?;
std::fs::write("/sys/class/pwm/pwmchip0/pwm0/period", format!("{}", period.as_nanos()))?;
std::fs::write(
"/sys/class/pwm/pwmchip0/pwm0/duty_cycle",
format!("{}", (period / 2).as_nanos()),
)?;
// enable the thing
std::fs::write("/sys/class/pwm/pwmchip0/pwm0/enable", "1")?;
Ok(())
}
pub fn stop() -> Result<(), Error> {
// disable the thing
std::fs::write("/sys/class/pwm/pwmchip0/pwm0/enable", "0")?;
// sleep small amount
std::thread::sleep(Duration::from_micros(30));
Ok(())
}
pub fn play_for_duration(freq: f64, duration: Duration) -> Result<(), Error> {
play(freq)?;
std::thread::sleep(duration);
stop()
}
#[derive(Clone, Debug)]
pub struct Song<'a>(pub &'a [(Option<f64>, Duration)]);
impl<'a> Song<'a> {
pub fn play(&self) -> Result<(), Error> {
for (note, duration) in self.0 {
if let Some(note) = note {
play_for_duration(*note, *duration)?;
} else {
std::thread::sleep(*duration);
}
}
Ok(())
}
}
impl Song<'static> {
pub fn play_while<T, F: FnOnce() -> T>(&'static self, f: F) -> T {
let run = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
let t_run = run.clone();
let handle = std::thread::spawn(move || -> Result<(), Error> {
while t_run.load(std::sync::atomic::Ordering::SeqCst) {
self.play()?;
}
Ok(())
});
let res = f();
run.store(false, std::sync::atomic::Ordering::SeqCst);
let e = handle.join().unwrap().err();
if let Some(e) = e {
eprintln!("ERROR PLAYING SOUND: {}\n{:?}", e, e);
}
res
}
}
impl<'a> From<&'a [(Option<f64>, Duration)]> for Song<'a> {
fn from(t: &'a [(Option<f64>, Duration)]) -> Self {
Song(t)
}
}

View File

@@ -1,5 +1,37 @@
#!/bin/sh
#!/bin/bash
>&2 echo "As of 0.2.5, it is not possible to programmatically generate an Embassy image."
>&2 echo "The image must be setup manually by copying over the artifacts, and installing the necessary dependencies."
exit 1
mv buster.img embassy.img
product_key=$(cat product_key)
loopdev=$(losetup -f -P embassy.img --show)
root_mountpoint="/mnt/start9-${product_key}-root"
boot_mountpoint="/mnt/start9-${product_key}-boot"
mkdir -p "${root_mountpoint}"
mkdir -p "${boot_mountpoint}"
mount "${loopdev}p2" "${root_mountpoint}"
mount "${loopdev}p1" "${boot_mountpoint}"
echo "${product_key}" > "${root_mountpoint}/root/agent/product_key"
echo -n "start9-" > "${root_mountpoint}/etc/hostname"
echo -n "${product_key}" | shasum -t -a 256 | cut -c1-8 >> "${root_mountpoint}/etc/hostname"
cat "${root_mountpoint}/etc/hosts" | grep -v "127.0.1.1" > "${root_mountpoint}/etc/hosts.tmp"
echo -ne "127.0.1.1\tstart9-" >> "${root_mountpoint}/etc/hosts.tmp"
echo -n "${product_key}" | shasum -t -a 256 | cut -c1-8 >> "${root_mountpoint}/etc/hosts.tmp"
mv "${root_mountpoint}/etc/hosts.tmp" "${root_mountpoint}/etc/hosts"
cp agent/dist/agent "${root_mountpoint}/usr/local/bin/agent"
chmod 700 "${root_mountpoint}/usr/local/bin/agent"
cp appmgr/target/armv7-unknown-linux-musleabihf/release/appmgr "${root_mountpoint}/usr/local/bin/appmgr"
chmod 700 "${root_mountpoint}/usr/local/bin/appmgr"
cp lifeline/target/armv7-unknown-linux-musleabihf/release/lifeline "${root_mountpoint}/usr/local/bin/lifeline"
chmod 700 "${root_mountpoint}/usr/local/bin/lifeline"
cp docker-daemon.json "${root_mountpoint}/etc/docker/daemon.json"
cp setup.sh "${root_mountpoint}/root/setup.sh"
chmod 700 "${root_mountpoint}/root/setup.sh"
cp setup.service /etc/systemd/system/setup.service
cp lifeline/lifeline.service /etc/systemd/system/lifeline.service
cp agent/config/agent.service /etc/systemd/system/agent.service
cat "${boot_mountpoint}/config.txt" | grep -v "dtoverlay=pwm-2chan" > "${boot_mountpoint}/config.txt.tmp"
echo "dtoverlay=pwm-2chan" >> "${boot_mountpoint}/config.txt.tmp"
umount "${root_mountpoint}"
rm -r "${root_mountpoint}"
umount "${boot_mountpoint}"
rm -r "${boot_mountpoint}"
losetup -d ${loopdev}

10
setup.service Normal file
View File

@@ -0,0 +1,10 @@
[Unit]
Description=Boot process for system setup.
[Service]
Type=oneshot
ExecStart=/root/setup.sh
RemainAfterExit=true
[Install]
WantedBy=multi-user.target

21
setup.sh Normal file
View File

@@ -0,0 +1,21 @@
#!/bin/bash
apt update
apt install -y libsecp256k1-0
apt install -y tor
apt install -y docker.io
apt install -y iotop
apt install -y bmon
apt autoremove -y
mkdir -p /root/volumes
mkdir -p /root/tmp/appmgr
mkdir -p /root/agent
mkdir -p /root/appmgr/tor
systemctl enable lifeline
systemctl enable agent
systemctl enable ssh
systemctl enable avahi-daemon
passwd -l root
passwd -l pi
sync
systemctl disable setup.service
reboot

View File

@@ -29,6 +29,11 @@
"glob": "**/*.svg",
"input": "node_modules/ionicons/dist/ionicons/svg",
"output": "./svg"
},
{
"glob": "**/*.svg",
"input": "src/assets/icon",
"output": "./svg"
}
],
"styles": [

View File

@@ -10,14 +10,6 @@ rm -rf www
echo "FILTER: ionic build"
npm run build-prod
echo "FILTER: cp client-manifest.yaml www"
cp client-manifest.yaml www
echo "FILTER: git hash"
touch git-hash.txt
git log | head -n1 > git-hash.txt
mv git-hash.txt www
echo "FILTER: ssh + rm -rf /var/www/html/start9-ambassador/"
ssh root@start9-$1.local "rm -rf /var/www/html/start9-ambassador"

View File

@@ -1,6 +1,6 @@
manifest-version: 0
app-id: start9-ambassador
app-version: 0.2.7
app-version: 0.2.8
uri-rewrites:
- =/api -> http://{{start9-ambassador}}:5959/authenticate
- /api/ -> http://{{start9-ambassador}}:5959/

151
ui/package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "embassy-ui",
"version": "0.2.7",
"version": "0.2.8",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -375,79 +375,35 @@
}
},
"@angular-devkit/core": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-10.2.0.tgz",
"integrity": "sha512-XAszFhSF3mZw1VjoOsYGbArr5NJLcStjOvcCGjBPl1UBM2AKpuCQXHxI9XJGYKL3B93Vp5G58d8qkHvamT53OA==",
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-11.0.5.tgz",
"integrity": "sha512-hwV8fjF8JNPJkiVWw8MNzeIfDo01aD/OAOlC4L5rQnVHn+i2EiU3brSDmFqyeHPPV3h/QjuBkS3tkN7gSnVWaQ==",
"dev": true,
"requires": {
"ajv": "6.12.4",
"ajv": "6.12.6",
"fast-json-stable-stringify": "2.1.0",
"magic-string": "0.25.7",
"rxjs": "6.6.2",
"rxjs": "6.6.3",
"source-map": "0.7.3"
},
"dependencies": {
"ajv": {
"version": "6.12.4",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz",
"integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==",
"dev": true,
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
},
"rxjs": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz",
"integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==",
"dev": true,
"requires": {
"tslib": "^1.9.0"
}
},
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"dev": true
},
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"dev": true
}
}
},
"@angular-devkit/schematics": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-10.2.0.tgz",
"integrity": "sha512-TQI5NnE6iM3ChF5gZQ9qb+lZgMWa7aLoF5ksOyT3zrmOuICiQYJhA6SsjV95q7J4M55qYymwBib8KTqU/xuQww==",
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-11.0.5.tgz",
"integrity": "sha512-0NKGC8Nf/4vvDpWKB7bwxIazvNnNHnZBX6XlyBXNl+fW8tpTef3PNMJMSErTz9LFnuv61vsKbc36u/Ek2YChWg==",
"dev": true,
"requires": {
"@angular-devkit/core": "10.2.0",
"ora": "5.0.0",
"rxjs": "6.6.2"
},
"dependencies": {
"rxjs": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz",
"integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==",
"dev": true,
"requires": {
"tslib": "^1.9.0"
}
},
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"dev": true
}
"@angular-devkit/core": "11.0.5",
"ora": "5.1.0",
"rxjs": "6.6.3"
}
},
"@angular/cli": {
@@ -1919,16 +1875,17 @@
}
},
"@ionic/angular-toolkit": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/@ionic/angular-toolkit/-/angular-toolkit-2.3.3.tgz",
"integrity": "sha512-r87mApDLWbLaUtd5LvNHrRlZWxjQhaBBM1yPlk9M98dHOxcX3jy7kv60ZurGZutuvbhXISGvHcvvR90yywDC1A==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@ionic/angular-toolkit/-/angular-toolkit-3.0.0.tgz",
"integrity": "sha512-H6SX8k+uPTdcsZAHEOaH5oIuQWSeIqEqEjjPqiD0e5+wmqc94RANNC/cRX/3cnVsWQiqcUY75CbpGec4xRvWoA==",
"dev": true,
"requires": {
"@schematics/angular": ">=8.0.0",
"@schematics/angular": "^11.0.0",
"cheerio": "1.0.0-rc.3",
"colorette": "1.1.0",
"copy-webpack-plugin": "^6.0.3",
"tslib": "^1.9.0",
"copy-webpack-plugin": "^6.2.1",
"tapable": "^2.1.1",
"tslib": "^2.0.3",
"ws": "^7.0.1"
},
"dependencies": {
@@ -1938,16 +1895,10 @@
"integrity": "sha512-6S062WDQUXi6hOfkO/sBPVwE5ASXY4G2+b4atvhJfSsuUUhIaUKlkjLe9692Ipyt5/a+IPF5aVTu3V5gvXq5cg==",
"dev": true
},
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"dev": true
},
"ws": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz",
"integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==",
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.1.tgz",
"integrity": "sha512-pTsP8UAfhy3sk1lSk/O/s4tjD0CRwvMnzvwr4OKGX7ZvqZtUyx4KIJB5JWbkykPoc55tixMGgTNoh3k4FkNGFQ==",
"dev": true
}
}
@@ -2490,14 +2441,14 @@
}
},
"@schematics/angular": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-10.2.0.tgz",
"integrity": "sha512-rJRTTTL8CMMFb3ebCvAVHKHxuNzRqy/HtbXhJ82l5Xo/jXcm74eV2Q0RBUrNo1yBKWFIR+FIwiXLJaGcC/R9Pw==",
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-11.0.5.tgz",
"integrity": "sha512-7p2wweoJYhim8YUy3ih1SrPGqRsa6+aEFbYgo9v4zt7b3tOva8SvkbC2alayK74fclzQ7umqa6xAwvWhy8ORvg==",
"dev": true,
"requires": {
"@angular-devkit/core": "10.2.0",
"@angular-devkit/schematics": "10.2.0",
"jsonc-parser": "2.3.0"
"@angular-devkit/core": "11.0.5",
"@angular-devkit/schematics": "11.0.5",
"jsonc-parser": "2.3.1"
}
},
"@schematics/update": {
@@ -4449,21 +4400,21 @@
"dev": true
},
"copy-webpack-plugin": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-6.0.3.tgz",
"integrity": "sha512-q5m6Vz4elsuyVEIUXr7wJdIdePWTubsqVbEMvf1WQnHGv0Q+9yPRu7MtYFPt+GBOXRav9lvIINifTQ1vSCs+eA==",
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-6.4.1.tgz",
"integrity": "sha512-MXyPCjdPVx5iiWyl40Va3JGh27bKzOTNY3NjUTrosD2q7dR/cLD0013uqJ3BpFbUjyONINjb6qI7nDIJujrMbA==",
"dev": true,
"requires": {
"cacache": "^15.0.4",
"cacache": "^15.0.5",
"fast-glob": "^3.2.4",
"find-cache-dir": "^3.3.1",
"glob-parent": "^5.1.1",
"globby": "^11.0.1",
"loader-utils": "^2.0.0",
"normalize-path": "^3.0.0",
"p-limit": "^3.0.1",
"schema-utils": "^2.7.0",
"serialize-javascript": "^4.0.0",
"p-limit": "^3.0.2",
"schema-utils": "^3.0.0",
"serialize-javascript": "^5.0.1",
"webpack-sources": "^1.4.3"
},
"dependencies": {
@@ -4475,6 +4426,26 @@
"requires": {
"yocto-queue": "^0.1.0"
}
},
"schema-utils": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz",
"integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.6",
"ajv": "^6.12.5",
"ajv-keywords": "^3.5.2"
}
},
"serialize-javascript": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz",
"integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==",
"dev": true,
"requires": {
"randombytes": "^2.1.0"
}
}
}
},
@@ -7516,9 +7487,9 @@
}
},
"jsonc-parser": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.0.tgz",
"integrity": "sha512-b0EBt8SWFNnixVdvoR2ZtEGa9ZqLhbJnOjezn+WP+8kspFm+PFYDN8Z4Bc7pRlDjvuVcADSUkroIuTWWn/YiIA==",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz",
"integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==",
"dev": true
},
"jsonfile": {
@@ -8815,9 +8786,9 @@
}
},
"ora": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ora/-/ora-5.0.0.tgz",
"integrity": "sha512-s26qdWqke2kjN/wC4dy+IQPBIMWBJlSU/0JZhk30ZDBLelW25rv66yutUWARMigpGPzcXHb+Nac5pNhN/WsARw==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ora/-/ora-5.1.0.tgz",
"integrity": "sha512-9tXIMPvjZ7hPTbk8DFq1f7Kow/HU/pQYB60JbNq+QnGwcyhWVZaQ4hM9zQDEsPxw/muLpgiHSaumUZxCAmod/w==",
"dev": true,
"requires": {
"chalk": "^4.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "embassy-ui",
"version": "0.2.7",
"version": "0.2.8",
"description": "GUI for EmbassyOS",
"author": "Start9 Labs",
"homepage": "https://github.com/Start9Labs/embassy-ui",
@@ -8,7 +8,7 @@
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"build-prod": "ng build --prod && tsc postprocess.ts && node postprocess.js",
"build-prod": "ng build --prod && tsc postprocess.ts && node postprocess.js && cp client-manifest.yaml www && git log | head -n1 > www/git-hash.txt",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"

View File

@@ -33,6 +33,11 @@ const routes: Routes = [
canActivateChild: [AuthGuard],
loadChildren: () => import('./pages/apps-routes/apps-routing.module').then(m => m.AppsRoutingModule),
},
// {
// path: 'drives',
// canActivate: [AuthGuard],
// loadChildren: () => import('./pages/server-routes/external-drives/external-drives.module').then( m => m.ExternalDrivesPageModule),
// },
]
@NgModule({

View File

@@ -96,6 +96,7 @@
<ion-icon name="power"></ion-icon>
<ion-icon name="pulse"></ion-icon>
<ion-icon name="qr-code-outline"></ion-icon>
<ion-icon name="globe-outline"></ion-icon>
<ion-icon name="reload-outline"></ion-icon>
<ion-icon name="refresh-outline"></ion-icon>
<ion-icon name="save-outline"></ion-icon>
@@ -128,7 +129,7 @@
<ion-infinite-scroll-content loadingSpinner="lines"></ion-infinite-scroll-content>
</ion-content>
<ion-input></ion-input>
<ion-input type="password" value="getdots"></ion-input>
<ion-input *ngIf="untilLoaded" type="password" value="getdots"></ion-input>
<ion-item></ion-item>
<ion-item-divider></ion-item-divider>
<ion-item-group></ion-item-group>
@@ -147,6 +148,7 @@
<ion-spinner name="dots"></ion-spinner>
<ion-spinner name="lines"></ion-spinner>
<ion-text></ion-text>
<ion-text style="font-weight: bold">load bold</ion-text>
<ion-textarea></ion-textarea>
<ion-title></ion-title>
<ion-toast></ion-toast>

View File

@@ -13,6 +13,7 @@ import { LoaderService } from './services/loader.service'
import { Emver } from './services/emver.service'
import { SplitPaneTracker } from './services/split-pane.service'
import { LoadingOptions } from '@ionic/core'
import { pauseFor } from './util/misc.util'
@Component({
selector: 'app-root',
@@ -26,6 +27,7 @@ export class AppComponent {
serverName$ : Observable<string>
serverBadge$: Observable<number>
selectedIndex = 0
untilLoaded = true
appPages = [
{
title: 'Services',
@@ -47,6 +49,11 @@ export class AppComponent {
url: '/notifications',
icon: 'notifications-outline',
},
// {
// title: 'Backup drives',
// url: '/drives',
// icon: 'albums-outline',
// },
]
constructor (
@@ -69,6 +76,12 @@ export class AppComponent {
this.init()
}
ionViewDidEnter () {
// weird bug where a browser grabbed the value 'getdots' from the app.component.html preload input field.
// this removes that field after prleloading occurs.
pauseFor(500).then(() => this.untilLoaded = false)
}
async init () {
let fromFresh = true
await this.storage.ready()
@@ -175,7 +188,7 @@ export class AppComponent {
await alert.present()
}
splitPaneVisible (e) {
splitPaneVisible (e: any) {
this.splitPane.$menuFixedOpenOnLeft$.next(e.detail.visible)
}
}

View File

@@ -15,6 +15,7 @@ import { ConfigService } from './services/config.service'
import { QRCodeModule } from 'angularx-qrcode'
import { APP_CONFIG_COMPONENT_MAPPING } from './modals/app-config-injectable/modal-injectable-token'
import { appConfigComponents } from './modals/app-config-injectable/modal-injectable-value';
import { OSWelcomePageModule } from './modals/os-welcome/os-welcome.module'
@NgModule({
declarations: [AppComponent],
@@ -26,6 +27,7 @@ import { appConfigComponents } from './modals/app-config-injectable/modal-inject
AppRoutingModule,
IonicStorageModule.forRoot(),
QRCodeModule,
OSWelcomePageModule,
],
providers: [
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },

View File

@@ -0,0 +1,26 @@
<!-- TODO: EJECT-DISKS, add a check box to allow a user to eject a disk on backup completion. -->
<ion-content>
<div style="height: 85%; margin: 20px; display: flex; flex-direction: column; justify-content: space-between;">
<div>
<h4><ion-text color="dark">Ready to Backup</ion-text></h4>
<p><ion-text color="medium">Enter your master password to create an encrypted backup.</ion-text></p>
</div>
<div>
<ion-item lines="none" style="--background: var(--ion-background-color); --border-color: var(--ion-color-medium);">
<ion-label style="font-size: small" position="floating">Master Password</ion-label>
<ion-input style="border-style: solid; border-width: 0px 0px 1px 0px; border-color: var(--ion-color-dark);" [(ngModel)]="password" type="password" (ionChange)="$error$.next('')"></ion-input>
</ion-item>
<ion-item *ngIf="$error$ | async as e" lines="none" style="--background: var(--ion-background-color);">
<ion-label style="font-size: small" color="danger" class="ion-text-wrap">{{e}}</ion-label>
</ion-item>
</div>
<div style="display: flex; justify-content: flex-end; align-items: center;">
<ion-button fill="clear" color="medium" (click)="cancel()">
Cancel
</ion-button>
<ion-button fill="clear" color="primary" (click)="submit()">
Create Backup
</ion-button>
</div>
</div>
</ion-content>

View File

@@ -0,0 +1,22 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { AppBackupConfirmationComponent } from './app-backup-confirmation.component'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { SharingModule } from 'src/app/modules/sharing.module'
import { FormsModule } from '@angular/forms';
@NgModule({
declarations: [
AppBackupConfirmationComponent,
],
imports: [
CommonModule,
IonicModule,
RouterModule.forChild([]),
SharingModule,
FormsModule,
],
exports: [AppBackupConfirmationComponent],
})
export class AppBackupConfirmationComponentModule { }

View File

@@ -0,0 +1,45 @@
import { Component, Input, OnInit } from '@angular/core'
import { ModalController } from '@ionic/angular'
import { BehaviorSubject } from 'rxjs'
import { AppInstalledFull } from 'src/app/models/app-types'
import { DiskPartition } from 'src/app/models/server-model'
@Component({
selector: 'app-backup-confirmation',
templateUrl: './app-backup-confirmation.component.html',
styleUrls: ['./app-backup-confirmation.component.scss'],
})
export class AppBackupConfirmationComponent implements OnInit {
unmasked = false
password: string
$error$: BehaviorSubject<string> = new BehaviorSubject('')
// TODO: EJECT-DISKS pass this through the modalCtrl once ejecting disks is an option in the UI.
eject = true
message: string
@Input() app: AppInstalledFull
@Input() partition: DiskPartition
constructor (private readonly modalCtrl: ModalController) { }
ngOnInit () {
this.message = `Enter your master password to create an encrypted backup of ${this.app.title} to "${this.partition.label || this.partition.logicalname}".`
}
toggleMask () {
this.unmasked = !this.unmasked
}
cancel () {
this.modalCtrl.dismiss({ cancel: true })
}
submit () {
if (!this.password || this.password.length < 12) {
this.$error$.next('Password must be at least 12 characters in length.')
return
}
const { password } = this
this.modalCtrl.dismiss({ password })
}
}

View File

@@ -0,0 +1,12 @@
<div class="slide-content">
<div style="margin-top: 25px;">
<div style="margin: 15px; display: flex; justify-content: center; align-items: center;">
<ion-label [color]="$color$ | async" style="font-size: xx-large; font-weight: bold;">
Warning
</ion-label>
</div>
<div class="long-message">
{{params.developerNotes}}
</div>
</div>
</div>

View File

@@ -0,0 +1,20 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { DeveloperNotesComponent } from './developer-notes.component'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { SharingModule } from 'src/app/modules/sharing.module'
@NgModule({
declarations: [
DeveloperNotesComponent,
],
imports: [
CommonModule,
IonicModule,
RouterModule.forChild([]),
SharingModule,
],
exports: [DeveloperNotesComponent],
})
export class DeveloperNotesComponentModule { }

View File

@@ -0,0 +1,27 @@
import { Component, Input, OnInit } from '@angular/core'
import { BehaviorSubject, Subject } from 'rxjs'
import { Colorable, Loadable } from '../loadable'
import { WizardAction } from '../wizard-types'
@Component({
selector: 'developer-notes',
templateUrl: './developer-notes.component.html',
styleUrls: ['../install-wizard.component.scss'],
})
export class DeveloperNotesComponent implements OnInit, Loadable, Colorable {
@Input() params: {
action: WizardAction
developerNotes: string
}
$loading$ = new BehaviorSubject(false)
$color$ = new BehaviorSubject('warning')
$cancel$ = new Subject<void>()
load () { }
constructor () { }
ngOnInit () {
console.log('Developer Notes', this.params)
}
}

View File

@@ -11,6 +11,7 @@
<ion-slides *ngIf="!($error$ | async)" id="slide-show" style="--bullet-background: white" pager="false">
<ion-slide *ngFor="let slide of params.slideDefinitions">
<dependencies #components *ngIf="slide.selector === 'dependencies'" [params]="slide.params"></dependencies>
<developer-notes #components *ngIf="slide.selector === 'developer-notes'" [params]="slide.params"></developer-notes>
<dependents #components *ngIf="slide.selector === 'dependents'" [params]="slide.params" [finished]="finished"></dependents>
<complete #components *ngIf="slide.selector === 'complete'" [params]="slide.params" [finished]="finished"></complete>
</ion-slide>

View File

@@ -7,6 +7,7 @@ import { SharingModule } from 'src/app/modules/sharing.module'
import { DependenciesComponentModule } from './dependencies/dependencies.component.module'
import { DependentsComponentModule } from './dependents/dependents.component.module'
import { CompleteComponentModule } from './complete/complete.component.module'
import { DeveloperNotesComponentModule } from './developer-notes/developer-notes.component.module'
@NgModule({
declarations: [
@@ -20,6 +21,7 @@ import { CompleteComponentModule } from './complete/complete.component.module'
DependenciesComponentModule,
DependentsComponentModule,
CompleteComponentModule,
DeveloperNotesComponentModule,
],
exports: [InstallWizardComponent],
})

View File

@@ -7,6 +7,7 @@ import { capitalizeFirstLetter } from 'src/app/util/misc.util'
import { CompleteComponent } from './complete/complete.component'
import { DependenciesComponent } from './dependencies/dependencies.component'
import { DependentsComponent } from './dependents/dependents.component'
import { DeveloperNotesComponent } from './developer-notes/developer-notes.component'
import { Colorable, Loadable } from './loadable'
import { WizardAction } from './wizard-types'
@@ -80,22 +81,26 @@ export class InstallWizardComponent extends Cleanup implements OnInit {
private async slide () {
if (this.slideComponents[this.slideIndex + 1] === undefined) { return this.finished({ final: true }) }
this.slideIndex += 1
await this.slideContainer.lockSwipes(false)
await Promise.all([this.contentContainer.scrollToTop(), this.slideContainer.slideNext()])
await this.slideContainer.lockSwipes(true)
this.currentSlide.load()
await this.slideContainer.lockSwipes(false)
await Promise.all([
this.contentContainer.scrollToTop(),
this.slideContainer.slideNext(500),
])
await this.slideContainer.lockSwipes(true)
this.slideContainer.update()
}
}
export interface SlideCommon {
selector: string
selector: string // component http selector
cancelButton: {
// indicates the existence of a cancel button, and whether to have text or an icon 'x' by default.
afterLoading?: { text?: string },
whileLoading?: { text?: string }
}
nextButton?: string,
finishButton?: string
nextButton?: string, // existence and content of next button
finishButton?: string // existence and content of finish button
}
export type SlideDefinition = SlideCommon & (
@@ -108,6 +113,9 @@ export type SlideDefinition = SlideCommon & (
} | {
selector: 'complete',
params: CompleteComponent['params']
} | {
selector: 'developer-notes',
params: DeveloperNotesComponent['params']
}
)

View File

@@ -1,5 +1,6 @@
import { Injectable } from '@angular/core'
import { AppModel, AppStatus } from 'src/app/models/app-model'
import { exists } from 'src/app/util/misc.util'
import { AppDependency, DependentBreakage, AppInstalledPreview } from '../../models/app-types'
import { ApiService } from '../../services/api/api.service'
import { InstallWizardComponent, SlideDefinition, TopbarParams } from './install-wizard.component'
@@ -9,9 +10,9 @@ export class WizardBaker {
constructor (private readonly apiService: ApiService, private readonly appModel: AppModel) { }
install (values: {
id: string, title: string, version: string, serviceRequirements: AppDependency[]
id: string, title: string, version: string, serviceRequirements: AppDependency[], installAlert?: string
}): InstallWizardComponent['params'] {
const { id, title, version, serviceRequirements } = values
const { id, title, version, serviceRequirements, installAlert } = values
validate(id, exists, 'missing id')
validate(title, exists, 'missing title')
@@ -22,6 +23,9 @@ export class WizardBaker {
const toolbar: TopbarParams = { action, title, version }
const slideDefinitions: SlideDefinition[] = [
installAlert ? { selector: 'developer-notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
action, developerNotes: installAlert,
}} : undefined,
{ selector: 'dependencies', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Install', params: {
action, title, version, serviceRequirements,
}},
@@ -31,13 +35,14 @@ export class WizardBaker {
}),
}},
]
return { toolbar, slideDefinitions }
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
}
update (values: {
id: string, title: string, version: string, serviceRequirements: AppDependency[]
id: string, title: string, version: string, serviceRequirements: AppDependency[], installAlert?: string
}): InstallWizardComponent['params'] {
const { id, title, version, serviceRequirements } = values
const { id, title, version, serviceRequirements, installAlert } = values
validate(id, exists, 'missing id')
validate(title, exists, 'missing title')
validate(version, exists, 'missing version')
@@ -47,6 +52,9 @@ export class WizardBaker {
const toolbar: TopbarParams = { action, title, version }
const slideDefinitions: SlideDefinition[] = [
installAlert ? { selector: 'developer-notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
action, developerNotes: installAlert,
}} : undefined,
{ selector: 'dependencies', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Update', params: {
action, title, version, serviceRequirements,
}},
@@ -59,13 +67,13 @@ export class WizardBaker {
}),
}},
]
return { toolbar, slideDefinitions }
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
}
downgrade (values: {
id: string, title: string, version: string, serviceRequirements: AppDependency[]
id: string, title: string, version: string, serviceRequirements: AppDependency[], installAlert?: string
}): InstallWizardComponent['params'] {
const { id, title, version, serviceRequirements } = values
const { id, title, version, serviceRequirements, installAlert } = values
validate(id, exists, 'missing id')
validate(title, exists, 'missing title')
@@ -76,6 +84,9 @@ export class WizardBaker {
const toolbar: TopbarParams = { action, title, version }
const slideDefinitions: SlideDefinition[] = [
installAlert ? { selector: 'developer-notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
action, developerNotes: installAlert,
}} : undefined,
{ selector: 'dependencies', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Downgrade', params: {
action, title, version, serviceRequirements,
}},
@@ -88,13 +99,13 @@ export class WizardBaker {
}),
}},
]
return { toolbar, slideDefinitions }
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
}
uninstall (values: {
id: string, title: string, version: string
id: string, title: string, version: string, uninstallAlert?: string
}): InstallWizardComponent['params'] {
const { id, title, version } = values
const { id, title, version, uninstallAlert } = values
validate(id, exists, 'missing id')
validate(title, exists, 'missing title')
@@ -104,6 +115,9 @@ export class WizardBaker {
const toolbar: TopbarParams = { action, title, version }
const slideDefinitions: SlideDefinition[] = [
{ selector: 'developer-notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Continue', params: {
action, developerNotes: uninstallAlert || defaultUninstallationWarning(title) },
},
{ selector: 'dependents', cancelButton: { whileLoading: { }, afterLoading: { text: 'Cancel' } }, nextButton: 'Uninstall', params: {
action, verb: 'uninstalling', title, fetchBreakages: () => this.apiService.uninstallApp(id, true).then( ({ breakages }) => breakages ),
}},
@@ -111,7 +125,7 @@ export class WizardBaker {
action, verb: 'uninstalling', title, executeAction: () => this.apiService.uninstallApp(id).then(() => this.appModel.delete(id)),
}},
]
return { toolbar, slideDefinitions }
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
}
stop (values: {
@@ -158,4 +172,5 @@ function validate<T> (t: T, test: (t: T) => Boolean, desc: string) {
}
}
const exists = t => !!t
const defaultUninstallationWarning = serviceName => `Uninstalling ${ serviceName } will result in the deletion of its data.`

View File

@@ -0,0 +1,5 @@
<ion-item button lines="none" *ngIf="updateAvailable$ | async as version" (click)="confirmUpdate(version)">
<ion-label>
New EmbassyOS Version {{version | displayEmver}} Available!
</ion-label>
</ion-item>

View File

@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { UpdateOsBannerComponent } from './update-os-banner.component'
import { IonicModule } from '@ionic/angular'
import { SharingModule } from 'src/app/modules/sharing.module'
@NgModule({
declarations: [
UpdateOsBannerComponent,
],
imports: [
CommonModule,
IonicModule,
SharingModule,
],
exports: [UpdateOsBannerComponent],
})
export class UpdateOsBannerComponentModule { }

View File

@@ -0,0 +1,11 @@
ion-item {
--background: linear-gradient(90deg, var(--ion-color-light), var(--ion-color-primary));
--min-height: 0px;
ion-label {
font-family: 'Open Sans';
font-size: small;
text-align: center;
font-weight: bold;
}
}

View File

@@ -0,0 +1,48 @@
import { Component } from '@angular/core'
import { OsUpdateService } from 'src/app/services/os-update.service'
import { Observable } from 'rxjs'
import { AlertController } from '@ionic/angular'
import { LoaderService } from 'src/app/services/loader.service'
import { displayEmver } from 'src/app/pipes/emver.pipe'
@Component({
selector: 'update-os-banner',
templateUrl: './update-os-banner.component.html',
styleUrls: ['./update-os-banner.component.scss'],
})
export class UpdateOsBannerComponent {
updateAvailable$: Observable<undefined | string>
constructor (
private readonly osUpdateService: OsUpdateService,
private readonly alertCtrl: AlertController,
private readonly loader: LoaderService,
) {
this.updateAvailable$ = this.osUpdateService.watchForUpdateAvailable$()
}
ngOnInit () { }
async confirmUpdate (versionLatest: string) {
const alert = await this.alertCtrl.create({
header: `Update EmbassyOS`,
message: `Update EmbassyOS to version ${displayEmver(versionLatest)}?`,
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Update',
handler: () => this.update(versionLatest),
},
],
})
await alert.present()
}
private async update (versionLatest: string) {
return this.loader.displayDuringP(
this.osUpdateService.updateEmbassyOS(versionLatest),
)
}
}

View File

@@ -2,12 +2,14 @@ import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { AppBackupPage } from './app-backup.page'
import { AppBackupConfirmationComponentModule } from 'src/app/components/app-backup-confirmation/app-backup-confirmation.component.module'
@NgModule({
declarations: [AppBackupPage],
imports: [
CommonModule,
IonicModule,
AppBackupConfirmationComponentModule,
],
entryComponents: [AppBackupPage],
exports: [AppBackupPage],

View File

@@ -27,14 +27,25 @@
<ion-spinner *ngIf="loading" class="center" name="lines" color="warning"></ion-spinner>
<ng-container *ngIf="!loading">
<ion-item *ngIf="type === 'restore' && (app.restoreAlert || defaultRestoreAlert) as restore" class="notifier-item" style="box-shadow: 0 0 5px 1px var(--ion-color-danger); margin-bottom: 40px">
<ion-label class="ion-text-wrap">
<h2 style="display: flex; align-items: center; margin-bottom: 3px;">
<ion-icon style="margin-right: 5px;" slot="start" color="danger" slot="start" name="warning-outline"></ion-icon>
<ion-text color="danger" style="font-size: medium; font-weight: bold">Warning</ion-text>
</h2>
<p style="font-size: small">{{restore}}</p>
</ion-label>
</ion-item>
<ion-item *ngIf="allPartitionsMounted">
<ion-text *ngIf="type === 'create'" class="ion-text-wrap" color="warning">No partitions available. To begin a backup, insert a storage device into your Embassy.</ion-text>
<ion-text *ngIf="type === 'restore'" class="ion-text-wrap" color="warning">No partitions available. Insert the storage device containing the backup you wish to restore.</ion-text>
</ion-item>
<ion-item-group *ngFor="let d of disks">
<ion-item-divider>{{ d.logicalname }} ({{ d.size }})</ion-item-divider>
<ion-item-group>
<ion-item button [disabled]="p.isMounted" *ngFor="let p of d.partitions" (click)="presentAlert(p)">
<ion-item button [disabled]="p.isMounted" *ngFor="let p of d.partitions" (click)="presentAlert(d, p)">
<ion-icon slot="start" name="save-outline"></ion-icon>
<ion-label>
<h2>{{ p.label || p.logicalname }}</h2>

View File

@@ -0,0 +1,3 @@
.toast-close-button {
color: var(--ion-color-primary) !important;
}

View File

@@ -1,10 +1,12 @@
import { Component, Input } from '@angular/core'
import { ModalController, AlertController, LoadingController, IonicSafeString } from '@ionic/angular'
import { ModalController, AlertController, LoadingController, ToastController } from '@ionic/angular'
import { AppModel, AppStatus } from 'src/app/models/app-model'
import { AppInstalledFull } from 'src/app/models/app-types'
import { ApiService } from 'src/app/services/api/api.service'
import { DiskInfo, DiskPartition } from 'src/app/models/server-model'
import { pauseFor } from 'src/app/util/misc.util'
import { concatMap } from 'rxjs/operators'
import { AppBackupConfirmationComponent } from 'src/app/components/app-backup-confirmation/app-backup-confirmation.component'
@Component({
selector: 'app-backup',
@@ -18,6 +20,7 @@ export class AppBackupPage {
loading = true
error: string
allPartitionsMounted: boolean
defaultRestoreAlert: string
constructor (
private readonly modalCtrl: ModalController,
@@ -25,9 +28,11 @@ export class AppBackupPage {
private readonly loadingCtrl: LoadingController,
private readonly apiService: ApiService,
private readonly appModel: AppModel,
private readonly toastCtrl: ToastController,
) { }
ngOnInit () {
this.defaultRestoreAlert = `Restoring ${this.app.title} will overwrite its current data.`
return this.getExternalDisks().then(() => this.loading = false)
}
@@ -73,46 +78,33 @@ export class AppBackupPage {
await alert.present()
}
async presentAlert (partition: DiskPartition): Promise<void> {
async presentAlert (disk: DiskInfo, partition: DiskPartition): Promise<void> {
if (this.type === 'create') {
this.presentAlertCreateEncrypted(partition)
this.presentAlertCreateEncrypted(disk, partition)
} else {
this.presentAlertWarn(partition)
}
}
private async presentAlertCreateEncrypted (partition: DiskPartition): Promise<void> {
const alert = await this.alertCtrl.create({
private async presentAlertCreateEncrypted (disk: DiskInfo, partition: DiskPartition): Promise<void> {
const m = await this.modalCtrl.create({
componentProps: {
app: this.app,
partition,
},
cssClass: 'alertlike-modal',
component: AppBackupConfirmationComponent,
backdropDismiss: false,
header: `Encrypt Backup`,
message: `Enter your master password to create an encrypted backup of ${this.app.title} to "${partition.label || partition.logicalname}".`,
inputs: [
{
name: 'password',
label: 'Password',
type: 'password',
placeholder: 'Master Password',
},
],
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Create Backup',
handler: (data) => {
if (!data.password || data.password.length < 12) {
alert.message = new IonicSafeString(alert.message + '<br /><br /><ion-text color="danger">Password must be at least 12 characters in length.</ion-text>')
return false
} else {
this.create(partition, data.password)
}
},
},
],
})
await alert.present()
m.onWillDismiss().then(res => {
const data = res.data
if (data.cancel) return
// TODO: EJECT-DISKS we hard code the 'eject' last argument to be false, until ejection is an option in the UI. When it is, add it to the data object above ^
return this.create(disk, partition, data.password, false)
})
return await m.present()
}
private async presentAlertWarn (partition: DiskPartition): Promise<void> {
@@ -183,7 +175,7 @@ export class AppBackupPage {
}
}
private async create (partition: DiskPartition, password?: string): Promise<void> {
private async create (disk: DiskInfo, partition: DiskPartition, password: string, eject: boolean): Promise<void> {
this.error = ''
const loader = await this.loadingCtrl.create({
@@ -195,6 +187,14 @@ export class AppBackupPage {
try {
await this.apiService.createAppBackup(this.app.id, partition.logicalname, password)
this.appModel.update({ id: this.app.id, status: AppStatus.CREATING_BACKUP })
if (eject) {
this.appModel.watchForBackup(this.app.id).pipe(concatMap(
() => this.apiService.ejectExternalDisk(disk.logicalname),
)).subscribe({
next: () => this.toastEjection(disk, true),
error: () => this.toastEjection(disk, false),
})
}
await this.dismiss()
} catch (e) {
console.error(e)
@@ -203,4 +203,23 @@ export class AppBackupPage {
await loader.dismiss()
}
}
private async toastEjection (disk: DiskInfo, success: boolean) {
const { header, message, cssClass } = success ? {
header: 'Success',
message: `Drive ${disk.logicalname} ejected successfully`,
cssClass: 'notification-toast',
} : {
header: 'Error',
message: `Drive ${disk.logicalname} did not eject successfully`,
cssClass: 'alert-error-message',
}
const t = await this.toastCtrl.create({
header,
message,
cssClass,
duration: 2000,
})
await t.present()
}
}

View File

@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { OSWelcomePage } from './os-welcome.page'
import { SharingModule } from 'src/app/modules/sharing.module'
import { FormsModule } from '@angular/forms'
@NgModule({
imports: [
CommonModule,
IonicModule,
FormsModule,
SharingModule,
],
declarations: [OSWelcomePage],
})
export class OSWelcomePageModule { }

View File

@@ -0,0 +1,42 @@
<ion-header>
<ion-toolbar>
<ion-title >
<ion-label style="font-size: 20px;" class="ion-text-wrap">Welcome to {{ version }}!</ion-label>
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div style="display: flex; flex-direction: column; justify-content: space-between; height: 100%">
<div>
<h2>Highlights</h2>
<p>
0.2.8 is a small but important update designed to enhance awareness around potential pitfalls of using certain services.
It introduces warnings for installing, uninstalling, backing up, and restoring backups of stateful services such as LND or c-lightning.
Additionally, it draws a distinction between services that are designed to be launched inside the browser and those that are designed to run in the background
</p>
<p>
0.2.8 also introduces automatic update checks. With this enabled, each time you visit your embassy you will be notified if new Embassy OS or service versions are available. This setting can be edited in your Embassy Config page.
<ion-item lines="none" style="--border-radius: var(--icon-border-radius); margin-top: 15px">
<ion-label>Auto Check for Updates</ion-label>
<ion-toggle slot="end" [(ngModel)]="autoCheckUpdates"></ion-toggle>
</ion-item>
</p>
<div style="margin-top: 30px">
<h5 style="color: var(--ion-color-danger)">Important</h5>
<p>
If you have LND or c-lightning installed, please update them to the latest versions.
An oversight in Start9s USB backups system has created a situation where <b>restoring</b> a LND or c-lightning backup could potentially result in permanent loss of channel funds.
To be clear, <ion-text style="font-weight: 'bold';">DO NOT</ion-text> attempt to <b>restore</b> a LND or c-lightning backup until you have updated to the latest versions.
</p>
</div>
</div>
<div class="close-button">
<ion-button fill="outline" (click)="dismiss()">
Begin
</ion-button>
</div>
</div>
</ion-content>

View File

@@ -0,0 +1,8 @@
.close-button {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
min-height: 100px;
}

View File

@@ -0,0 +1,35 @@
import { Component, Input } from '@angular/core'
import { ModalController } from '@ionic/angular'
import { ServerModel } from 'src/app/models/server-model'
import { ApiService } from 'src/app/services/api/api.service'
import { ConfigService } from 'src/app/services/config.service'
@Component({
selector: 'os-welcome',
templateUrl: './os-welcome.page.html',
styleUrls: ['./os-welcome.page.scss'],
})
export class OSWelcomePage {
@Input() version: string
autoCheckUpdates = true
constructor (
private readonly modalCtrl: ModalController,
private readonly apiService: ApiService,
private readonly serverModel: ServerModel,
private readonly config: ConfigService,
) { }
async dismiss () {
this.apiService
.patchServerConfig('autoCheckUpdates', this.autoCheckUpdates)
.then(() => this.serverModel.update({ autoCheckUpdates: this.autoCheckUpdates }))
.then(() => this.apiService.acknowledgeOSWelcome(this.config.version))
.catch(console.error)
// return false to skip subsequent alert modals (e.g. check for updates modals)
// return true to show subsequent alert modals
return this.modalCtrl.dismiss(this.autoCheckUpdates)
}
}

View File

@@ -53,6 +53,7 @@ export class AppModel extends MapSubject<AppInstalledFull> {
if (!toWatch) return of(undefined)
return toWatch.status.pipe(
filter(s => s !== AppStatus.UNREACHABLE && s !== AppStatus.UNKNOWN),
pairwise(),
filter( ([old, _]) => old === AppStatus.INSTALLING ),
take(1),
@@ -60,6 +61,20 @@ export class AppModel extends MapSubject<AppInstalledFull> {
)
}
// TODO: EJECT-DISKS: we can use this to watch for an app completing its backup process.
watchForBackup (appId: string): Observable<string | undefined> {
const toWatch = super.watch(appId)
if (!toWatch) return of(undefined)
return toWatch.status.pipe(
filter(s => s !== AppStatus.UNREACHABLE && s !== AppStatus.UNKNOWN),
pairwise(),
filter( ([old, _]) => old === AppStatus.CREATING_BACKUP),
take(1),
mapTo(appId),
)
}
watchForInstallations (appIds: { id: string }[]): Observable<string> {
return merge(...appIds.map(({ id }) => this.watchForInstallation(id))).pipe(
filter(t => !!t),

View File

@@ -14,6 +14,7 @@ export interface BaseApp {
export interface AppAvailablePreview extends BaseApp {
versionLatest: string
descriptionShort: string
latestVersionTimestamp: Date //used for sorting AAL
}
export type AppAvailableFull =
@@ -28,12 +29,14 @@ export interface AppAvailableVersionSpecificInfo {
releaseNotes: string
serviceRequirements: AppDependency[]
versionViewing: string
installAlert?: string
}
// installed
export interface AppInstalledPreview extends BaseApp {
torAddress: string
versionInstalled: string
ui: boolean
}
export interface AppInstalledFull extends AppInstalledPreview {
@@ -41,6 +44,8 @@ export interface AppInstalledFull extends AppInstalledPreview {
lastBackup: string | null
configuredRequirements: AppDependency[] | null // null if not yet configured
hasFetchedFull: boolean
uninstallAlert?: string
restoreAlert?: string
}
// dependencies

View File

@@ -105,10 +105,11 @@ export class ServerModel {
wifi: { ssids: [], current: undefined },
ssh: [],
notifications: [],
welcomeAck: true,
autoCheckUpdates: true,
})
}
}
export interface S9Server {
serverId: string
name: string
@@ -122,6 +123,8 @@ export interface S9Server {
wifi: { ssids: string[], current: string }
ssh: SSHFingerprint[]
notifications: S9Notification[]
welcomeAck: boolean
autoCheckUpdates: boolean
}
export interface S9Notification {
@@ -161,7 +164,7 @@ export interface DiskInfo {
export interface DiskPartition {
logicalname: string,
isMounted: boolean, // Do not let them back up to this if true
isMounted: boolean, // We do not allow backups to mounted partitions
size: string | null,
label: string | null,
}

View File

@@ -7,6 +7,7 @@ import { SharingModule } from '../../../modules/sharing.module'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
import { UpdateOsBannerComponentModule } from 'src/app/components/update-os-banner/update-os-banner.component.module'
const routes: Routes = [
@@ -25,6 +26,7 @@ const routes: Routes = [
SharingModule,
PwaBackComponentModule,
BadgeMenuComponentModule,
UpdateOsBannerComponentModule,
],
declarations: [AppAvailableListPage],
})

View File

@@ -5,10 +5,10 @@
<badge-menu-button></badge-menu-button>
</ion-buttons>
</ion-toolbar>
<update-os-banner></update-os-banner>
</ion-header>
<ion-content class="ion-padding-bottom">
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
</ion-refresher>

View File

@@ -7,6 +7,7 @@ import { PropertySubjectId, initPropertySubject } from 'src/app/util/property-su
import { Subscription, BehaviorSubject, combineLatest } from 'rxjs'
import { take } from 'rxjs/operators'
import { markAsLoadingDuringP } from 'src/app/services/loader.service'
import { OsUpdateService } from 'src/app/services/os-update.service'
@Component({
selector: 'app-available-list',
@@ -24,6 +25,7 @@ export class AppAvailableListPage {
private readonly apiService: ApiService,
private readonly appModel: AppModel,
private readonly zone: NgZone,
private readonly osUpdateService: OsUpdateService,
) { }
async ngOnInit () {
@@ -33,6 +35,7 @@ export class AppAvailableListPage {
markAsLoadingDuringP(this.$loading$, Promise.all([
this.getApps(),
this.osUpdateService.checkWhenNotAvailable$().toPromise(), // checks for an os update, banner component renders conditionally
pauseFor(600),
]))
}
@@ -71,7 +74,9 @@ export class AppAvailableListPage {
async getApps (): Promise<void> {
try {
this.apps = await this.apiService.getAvailableApps().then(apps =>
apps.map(a => ({ id: a.id, subject: initPropertySubject(a) })),
apps
.sort( (a1, a2) => a2.latestVersionTimestamp.getTime() - a1.latestVersionTimestamp.getTime())
.map(a => ({ id: a.id, subject: initPropertySubject(a) })),
)
this.appModel.getContents().forEach(appInstalled => this.mergeInstalledProps(appInstalled.id))
} catch (e) {

View File

@@ -11,22 +11,22 @@
</ion-header>
<ion-content class="ion-padding-bottom" *ngIf="{
id: app$.id | async,
status: app$.status | async,
title: app$.title | async,
versionInstalled: app$.versionInstalled | async,
versionViewing: app$.versionViewing | async,
descriptionLong: app$.descriptionLong | async,
serviceRequirements: app$.serviceRequirements | async,
iconURL: app$.iconURL | async,
releaseNotes: app$.releaseNotes | async
id: $app$.id | async,
status: $app$.status | async,
title: $app$.title | async,
versionInstalled: $app$.versionInstalled | async,
versionViewing: $app$.versionViewing | async,
descriptionLong: $app$.descriptionLong | async,
serviceRequirements: $app$.serviceRequirements | async,
iconURL: $app$.iconURL | async,
releaseNotes: $app$.releaseNotes | async
} as vars"
>
<ion-spinner *ngIf="($loading$ | async)" class="center" name="lines" color="warning"></ion-spinner>
<error-message [$error$]="$error$" [dismissable]="vars.id"></error-message>
<ng-container *ngIf="!($loading$ | async) && vars.id && (app$ | compareInstalledAndViewing | async) as installedStatus">
<ng-container *ngIf="!($loading$ | async) && vars.id && ($app$ | compareInstalledAndViewing | async) as installedStatus">
<ion-item-group>
<ion-item lines="none">
<ion-avatar slot="start">
@@ -91,10 +91,10 @@
</ion-item>
<ion-item-divider>Release Notes</ion-item-divider>
<ion-item lines="none" button details="true" [disabled]="" (click)="presentModalReleaseNotes()" [disabled]="$versionSpecificLoading$ | async">
<ion-item lines="none" button details="true" [disabled]="" (click)="presentModalReleaseNotes()" [disabled]="$newVersionLoading$ | async">
<ion-icon slot="start" name="newspaper-outline" color="medium"></ion-icon>
<ion-label *ngIf="!($versionSpecificLoading$ | async)"><ion-text color="medium">New in {{ vars.versionViewing | displayEmver }}</ion-text></ion-label>
<ion-spinner style="display: block; margin: auto;" name="lines" color="dark" *ngIf="$versionSpecificLoading$ | async"></ion-spinner>
<ion-label *ngIf="!($newVersionLoading$ | async)"><ion-text color="medium">New in {{ vars.versionViewing | displayEmver }}</ion-text></ion-label>
<ion-spinner style="display: block; margin: auto;" name="lines" color="dark" *ngIf="$newVersionLoading$ | async"></ion-spinner>
</ion-item>
<ng-container *ngIf="(vars.serviceRequirements)?.length">
@@ -103,7 +103,7 @@
<ion-icon name="help-circle-outline"></ion-icon>
</ion-button>
</ion-item-divider>
<dependency-list [$loading$]="$versionSpecificLoading$" [hostApp]="app$ | peekProperties" [dependencies]="vars.serviceRequirements"></dependency-list>
<dependency-list [$loading$]="$dependenciesLoading$" [hostApp]="$app$ | peekProperties" [dependencies]="vars.serviceRequirements"></dependency-list>
</ng-container>
<ion-item-divider></ion-item-divider>
<ion-item lines="none" button (click)="presentAlertVersions()">

View File

@@ -25,9 +25,14 @@ import { displayEmver } from 'src/app/pipes/emver.pipe'
})
export class AppAvailableShowPage extends Cleanup {
$loading$ = new BehaviorSubject(true)
$versionSpecificLoading$ = new BehaviorSubject(false)
// When a new version is selected
$newVersionLoading$ = new BehaviorSubject(false)
// When dependencies are refreshing
$dependenciesLoading$ = new BehaviorSubject(false)
$error$ = new BehaviorSubject(undefined)
app$: PropertySubject<AppAvailableFull> = { } as any
$app$: PropertySubject<AppAvailableFull> = { } as any
appId: string
openRecommendation = false
@@ -56,28 +61,22 @@ export class AppAvailableShowPage extends Cleanup {
this.appId = this.route.snapshot.paramMap.get('appId') as string
this.cleanup(
// new version always includes dependencies, but not vice versa
this.$newVersionLoading$.subscribe(this.$dependenciesLoading$),
markAsLoadingDuring$(this.$loading$,
from(this.apiService.getAvailableApp(this.appId)).pipe(
tap(app => this.app$ = initPropertySubject(app)),
tap(app => this.$app$ = initPropertySubject(app)),
concatMap(() => this.fetchRecommendation()),
),
).pipe(
concatMap(() => this.syncWhenDependencyInstalls()), //must be final in stack
catchError(e => of(this.setError(e))),
).subscribe(),
merge(this.$loading$, this.$versionSpecificLoading$).pipe(concatMap(l => {
if (l) {
this.showMoreReleaseNotes = false
}
return pauseFor(125)
})).subscribe(
() => this.setMoreReleaseNotes(),
),
)
}
ionViewDidEnter () {
markAsLoadingDuring$(this.$versionSpecificLoading$, this.fetchAppVersionInfo()).subscribe({
markAsLoadingDuring$(this.$dependenciesLoading$, this.syncVersionSpecificInfo()).subscribe({
error: e => this.setError(e),
})
}
@@ -96,25 +95,25 @@ export class AppAvailableShowPage extends Cleanup {
return await popover.present()
}
fetchAppVersionInfo (versionSpec?: string): Observable<any> {
if (!this.app$.versionViewing) return of({ })
const specToFetch = versionSpec || `=${this.app$.versionViewing.getValue()}`
syncVersionSpecificInfo (versionSpec?: string): Observable<any> {
if (!this.$app$.versionViewing) return of({ })
const specToFetch = versionSpec || `=${this.$app$.versionViewing.getValue()}`
return from(this.apiService.getAvailableAppVersionSpecificInfo(this.appId, specToFetch)).pipe(
tap(versionInfo => this.syncVersionSpecificInfo(versionInfo)),
tap(versionInfo => this.mergeInfo(versionInfo)),
)
}
private syncVersionSpecificInfo (versionSpecificInfo: AppAvailableVersionSpecificInfo) {
private mergeInfo (versionSpecificInfo: AppAvailableVersionSpecificInfo) {
this.zone.run(() => {
Object.entries(versionSpecificInfo).forEach( ([k, v]) => {
if (!this.app$[k]) this.app$[k] = new BehaviorSubject(undefined)
if (v !== this.app$[k].getValue()) this.app$[k].next(v)
if (!this.$app$[k]) this.$app$[k] = new BehaviorSubject(undefined)
if (v !== this.$app$[k].getValue()) this.$app$[k].next(v)
})
})
}
async presentAlertVersions () {
const app = peekProperties(this.app$)
const app = peekProperties(this.$app$)
const alert = await this.alertCtrl.create({
header: 'Versions',
backdropDismiss: false,
@@ -133,13 +132,15 @@ export class AppAvailableShowPage extends Cleanup {
}, {
text: 'Ok',
handler: (version: string) => {
const previousVersion = this.app$.versionViewing.getValue()
this.app$.versionViewing.next(version)
markAsLoadingDuring$(this.$versionSpecificLoading$, this.fetchAppVersionInfo(`=${version}`))
const previousVersion = this.$app$.versionViewing.getValue()
this.$app$.versionViewing.next(version)
markAsLoadingDuring$(
this.$newVersionLoading$, this.syncVersionSpecificInfo(`=${version}`),
)
.subscribe({
error: e => {
this.setError(e)
this.app$.versionViewing.next(previousVersion)
this.$app$.versionViewing.next(previousVersion)
},
})
},
@@ -151,7 +152,7 @@ export class AppAvailableShowPage extends Cleanup {
}
async install () {
const app = peekProperties(this.app$)
const app = peekProperties(this.$app$)
const { cancelled } = await wizardModal(
this.modalCtrl,
this.wizardBaker.install({
@@ -159,6 +160,7 @@ export class AppAvailableShowPage extends Cleanup {
title: app.title,
version: app.versionViewing,
serviceRequirements: app.serviceRequirements,
installAlert: app.installAlert,
}),
)
if (cancelled) return
@@ -166,13 +168,14 @@ export class AppAvailableShowPage extends Cleanup {
}
async update (action: 'update' | 'downgrade') {
const app = peekProperties(this.app$)
const app = peekProperties(this.$app$)
const value = {
id: app.id,
title: app.title,
version: app.versionViewing,
serviceRequirements: app.serviceRequirements,
installAlert: app.installAlert,
}
switch (action) {
@@ -190,7 +193,7 @@ export class AppAvailableShowPage extends Cleanup {
}
async presentModalReleaseNotes () {
const { releaseNotes, versionViewing } = peekProperties(this.app$)
const { releaseNotes, versionViewing } = peekProperties(this.$app$)
const modal = await this.modalCtrl.create({
component: AppReleaseNotesPage,
@@ -207,17 +210,17 @@ export class AppAvailableShowPage extends Cleanup {
this.recommendation = history.state && history.state.installationRecommendation
if (this.recommendation) {
return from(this.fetchAppVersionInfo(this.recommendation.versionSpec))
return from(this.syncVersionSpecificInfo(this.recommendation.versionSpec))
} else {
return of({ })
}
}
private syncWhenDependencyInstalls (): Observable<void> {
return this.app$.serviceRequirements.pipe(
return this.$app$.serviceRequirements.pipe(
filter(deps => !!deps),
switchMap(deps => this.appModel.watchForInstallations(deps)),
concatMap(() => markAsLoadingDuring$(this.$versionSpecificLoading$, this.fetchAppVersionInfo())),
concatMap(() => markAsLoadingDuring$(this.$dependenciesLoading$, this.syncVersionSpecificInfo())),
catchError(e => of(console.error(e))),
)
}
@@ -226,24 +229,4 @@ export class AppAvailableShowPage extends Cleanup {
console.error(e)
this.$error$.next(e.message)
}
private setMoreReleaseNotes () {
const releaseNotes = document.getElementById(`release-notes-${this.appId}`)
if (releaseNotes) {
this.showMoreReleaseNotes = isTextOverflow(releaseNotes)
}
}
@HostListener('window:resize', ['$event'])
onResize () {
this.setMoreReleaseNotes()
}
}
function isTextOverflow (elem: any): boolean {
if (elem) {
return (elem.offsetWidth < elem.scrollWidth)
}
return false
}

View File

@@ -26,7 +26,6 @@
<p style="color: var(--ion-color-danger)">{{error.text}}</p>
<p><a style="color: var(--ion-color-danger); text-decoration: underline; font-weight: bold;" *ngIf="error.moreInfo && !openErrorMoreInfo" (click)="openErrorMoreInfo = true">{{error.moreInfo.buttonText}}</a></p>
<!-- presentPopover(error.moreInfo.title, error.moreInfo.description, $event) -->
<ng-container *ngIf="openErrorMoreInfo">
<p style="margin-top: 10px; color: var(--ion-color-medium);" [innerHTML]="error.moreInfo.title"></p>
<p style="margin-top: 10px; color: var(--ion-color-medium); font-size: small" [innerHTML]="error.moreInfo.description"></p>

View File

@@ -22,18 +22,27 @@
<ion-grid>
<ion-row>
<ion-col *ngFor="let app of apps" sizeXs="4" sizeSm="3" sizeMd="2" sizeLg="2">
<ion-card class="installed-card" [class.installed-card-on]="(app.subject.status | async) === 'RUNNING'" style="position:relative" [routerLink]="['/services', 'installed', app.id]">
<img style="position: absolute" class="main-img" [src]="app.subject.iconURL | async | iconParse" [alt]="app.subject.title | async" />
<ng-container *ngIf="{ tor: app.subject.torAddress | async, status: app.subject.status | async, ui: app.subject.ui | async, iconURL: app.subject.iconURL | async | iconParse, title: app.subject.title | async } as vars" >
<ion-card class="installed-card" [class.installed-card-on]="vars.status === 'RUNNING'" style="position:relative" [routerLink]="['/services', 'installed', app.id]">
<div class="launch-container" *ngIf="vars.ui && !isConsulate">
<div class="launch-button-triangle" (click)="launchUiTab(vars.tor, $event)" [class.disabled]="vars.status !== AppStatus.RUNNING || !isTor">
<ion-icon class="launch-button-triangle-icon" name="globe-outline"></ion-icon>
</div>
</div>
<img style="position: absolute" class="main-img" [src]="vars.iconURL" [alt]="app.subject.title | async" />
<img class="main-img" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">
<img class="bulb-on" *ngIf="app.subject.status | async | displayBulb: 'green'" src="assets/img/running-bulb.png"/>
<img class="bulb-on" *ngIf="app.subject.status | async | displayBulb: 'red'" src="assets/img/issue-bulb.png"/>
<img class="bulb-on" *ngIf="app.subject.status | async | displayBulb: 'yellow'" src="assets/img/warning-bulb.png"/>
<img class="bulb-off" *ngIf="app.subject.status | async | displayBulb: 'off'" src="assets/img/off-bulb.png"/>
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'green'" src="assets/img/running-bulb.png"/>
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'red'" src="assets/img/issue-bulb.png"/>
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'yellow'" src="assets/img/warning-bulb.png"/>
<img class="bulb-off" *ngIf="vars.status | displayBulb: 'off'" src="assets/img/off-bulb.png"/>
<ion-card-header>
<status [appStatus]="app.subject.status | async" size="small"></status>
<p>{{ app.subject.title | async }}</p>
<status [appStatus]="vars.status" size="small"></status>
<p>{{ vars.title }}</p>
</ion-card-header>
</ion-card>
</ng-container>
</ion-col>
</ion-row>
</ion-grid>
@@ -48,6 +57,5 @@
Marketplace
</ion-button>
</div>
</ng-container>
</ion-content>

View File

@@ -51,3 +51,41 @@
width: 13px !important;
margin: 9px;
}
.launch-button-triangle {
width: 0px;
height: 0px;
right: 0px;
margin: 0px;
border-style: solid;
border-width: 22px;
border-color: rgb(70 193 255 / 75%) rgb(70 193 255 / 75%) transparent transparent;
&:hover {
border-color: rgb(70 193 255) rgb(70 193 255) transparent transparent;
}
ion-icon {
position: absolute;
right: 7px;
top: 7px;
color: white;
}
}
.disabled {
pointer-events: none;
border-color: transparent;
&:hover {
border-color: transparent;
}
ion-icon {
color: var(--ion-color-medium);
}
}
.launch-container {
position: absolute;
right: 0px;
top: 0px;
margin: 0px;
}

View File

@@ -1,5 +1,5 @@
import { Component } from '@angular/core'
import { AppModel } from 'src/app/models/app-model'
import { AppModel, AppStatus } from 'src/app/models/app-model'
import { AppInstalledPreview } from 'src/app/models/app-types'
import { ModelPreload } from 'src/app/models/model-preload'
import { doForAtLeast } from 'src/app/util/misc.util'
@@ -9,6 +9,7 @@ import { BehaviorSubject, Observable, Subscription } from 'rxjs'
import { S9Server, ServerModel, ServerStatus } from 'src/app/models/server-model'
import { SyncDaemon } from 'src/app/services/sync.service'
import { Cleanup } from 'src/app/util/cleanup'
import { ConfigService } from 'src/app/services/config.service'
@Component({
selector: 'app-installed-list',
@@ -21,6 +22,8 @@ export class AppInstalledListPage extends Cleanup {
$loading$ = new BehaviorSubject(true)
s9Host$: Observable<string>
AppStatus = AppStatus
server: PropertySubject<S9Server>
currentServer: S9Server
apps: PropertySubjectId<AppInstalledPreview>[] = []
@@ -32,14 +35,19 @@ export class AppInstalledListPage extends Cleanup {
segmentValue: 'services' | 'embassy' = 'services'
showCertDownload : boolean
isConsulate: boolean
isTor: boolean
constructor (
private readonly serverModel: ServerModel,
private readonly appModel: AppModel,
private readonly preload: ModelPreload,
private readonly syncDaemon: SyncDaemon,
config: ConfigService,
) {
super()
this.isConsulate = config.isConsulateAndroid || config.isConsulateIos
this.isTor = config.isTor()
}
ngOnDestroy () {
@@ -50,6 +58,7 @@ export class AppInstalledListPage extends Cleanup {
this.server = this.serverModel.watch()
this.apps = []
this.cleanup(
// serverUpdateSubscription
this.server.status.subscribe(status => {
if (status === ServerStatus.UPDATING) {
@@ -87,6 +96,14 @@ export class AppInstalledListPage extends Cleanup {
this.error = e.message
},
})
}
async launchUiTab (address: string, event: Event) {
event.preventDefault()
event.stopPropagation()
address = address.startsWith('http') ? address : `http://${address}`
return window.open(address, '_blank')
}
async doRefresh (event: any) {

View File

@@ -40,3 +40,4 @@ const routes: Routes = [
declarations: [AppInstalledShowPage],
})
export class AppInstalledShowPageModule { }

View File

@@ -19,7 +19,8 @@
lastBackup: app.lastBackup | async,
hasFetchedFull: app.hasFetchedFull | async,
iconURL: app.iconURL | async,
title: app.title | async
title: app.title | async,
ui: app.ui | async
} as vars" class="ion-padding-bottom">
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
@@ -52,7 +53,7 @@
</ion-label>
</ion-item>
<ion-item class="no-cushion-item" lines=none>
<ion-item class="no-cushion-item" lines=none style="margin-bottom: 10px">
<ion-label class="status-readout">
<status size="bold-large" [appStatus]="vars.status"></status>
<ion-button *ngIf="vars.status === AppStatus.NEEDS_CONFIG" expand="block" fill="outline" [routerLink]="['config']">
@@ -75,6 +76,19 @@
</ion-button>
</ion-label>
</ion-item>
<ion-item class="no-cushion-item" *ngIf="vars.ui && !isConsulate" lines="none">
<ion-label style="margin-bottom: 10px; margin-top: 0px; display: flex; justify-content: left; align-items: center;" class="ion-text-wrap">
<ion-button fill="clear" size="small" class="launch-explanation-button" (click)="presentLaunchPopover(vars.status, $event)">
<ion-icon color="medium" name="information-circle-outline">
</ion-icon>
</ion-button>
<ion-button [disabled]="vars.status !== 'RUNNING' || !isTor" class="launch-button" [class.launch-button-off]="vars.status !== 'RUNNING' || !isTor" (click)="launchUiTab()">
<ion-icon style="position: absolute; z-index: 1; left: 0;" name="globe-outline"></ion-icon>
<ion-text>LAUNCH</ion-text>
</ion-button>
</ion-label>
</ion-item>
</div>
<ng-container *ngIf="app && app.id && vars.status !== 'INSTALLING'">

View File

@@ -15,7 +15,6 @@
}
.top-plate {
// margin-top: 20px;
background: var(--ion-item-background);
margin: 20px 10px;
border-radius: 10px;
@@ -30,7 +29,7 @@
border-radius: 10px;
align-items: center;
background: var(--ion-background-color);
margin: 10px 10px 15px 10px;
margin: 10px 10px 0px 10px;
border-style: solid;
border-width: 1px;
border-color: #404040;
@@ -39,3 +38,24 @@
.no-cushion-item {
--background: transparent; --padding-start: 0px; --inner-padding-end: 0px; --padding-end: 0px;
}
.launch-button {
width: 100%;
padding: 0px 10px;
--background: linear-gradient(200deg, rgb(70 193 255), rgb(70 193 255 / 45%));
width: calc(100% - 32px);
border-radius: 8px;
--border-radius: 8px;
}
.launch-button-off {
--background: #383838;
color: var(--ion-color-medium)
}
.launch-explanation-button {
position: absolute;
z-index: 1;
right: -2px;
--border-radius: 100px;
}

View File

@@ -18,6 +18,7 @@ import { Cleanup } from 'src/app/util/cleanup'
import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component'
import { Emver } from 'src/app/services/emver.service'
import { displayEmver } from 'src/app/pipes/emver.pipe'
import { ConfigService } from 'src/app/services/config.service'
@Component({
selector: 'app-installed-show',
@@ -33,8 +34,13 @@ export class AppInstalledShowPage extends Cleanup {
appId: string
AppStatus = AppStatus
showInstructions = false
isConsulate: boolean
isTor: boolean
dependencyDefintion = () => `<span style="font-style: italic">Dependencies</span> are other services which must be installed, configured appropriately, and started in order to start ${this.app.title.getValue()}`
launchDefinition = `<span style="font-style: italic">Launch A Service</span> <p>This button appears only for services that can be accessed inside the browser. If a service does not have this button, you must access it using another interface, such as a mobile app, desktop app, or another service on the Embassy. Please view the instructions for a service for details on how to use it.</p>`
launchOffDefinition = `<span style="font-style: italic">Launch A Service</span> <p>This button appears only for services that can be accessed inside the browser. Get your service running in order to launch!</p>`
launchLocalDefinition = `<span style="font-style: italic">Launch A Service</span> <p>This button appears only for services that can be accessed inside the browser. Visit your Embassy at its Tor address to launch this service!</p>`
@ViewChild(IonContent) content: IonContent
@@ -51,8 +57,11 @@ export class AppInstalledShowPage extends Cleanup {
private readonly appModel: AppModel,
private readonly popoverController: PopoverController,
private readonly emver: Emver,
config: ConfigService,
) {
super()
this.isConsulate = config.isConsulateIos || config.isConsulateAndroid
this.isTor = config.isTor()
}
async ngOnInit () {
@@ -93,6 +102,12 @@ export class AppInstalledShowPage extends Cleanup {
}
}
async launchUiTab () {
let uiAddress = this.app.torAddress.getValue()
uiAddress = uiAddress.startsWith('http') ? uiAddress : `http://${uiAddress}`
return window.open(uiAddress, '_blank')
}
async checkForUpdates () {
const app = peekProperties(this.app)
@@ -146,7 +161,7 @@ export class AppInstalledShowPage extends Cleanup {
async copyTor () {
const app = peekProperties(this.app)
let message = ''
await copyToClipboard(app.torAddress || '').then(success => { message = success ? 'copied to clipboard!' : 'failed to copy'})
await copyToClipboard(app.torAddress || '').then(success => { message = success ? 'copied to clipboard!' : 'failed to copy' })
const toast = await this.toastCtrl.create({
header: message,
@@ -252,6 +267,7 @@ export class AppInstalledShowPage extends Cleanup {
id: app.id,
title: app.title,
version: app.versionInstalled,
uninstallAlert: app.uninstallAlert,
}),
)
@@ -259,6 +275,18 @@ export class AppInstalledShowPage extends Cleanup {
return this.navCtrl.navigateRoot('/services/installed')
}
async presentLaunchPopover (status: AppStatus, ev: any) {
let desc: string
if (!this.isTor) {
desc = this.launchLocalDefinition
} else if (status !== AppStatus.RUNNING) {
desc = this.launchOffDefinition
} else {
desc = this.launchDefinition
}
return this.presentPopover(desc, ev)
}
async presentPopover (information: string, ev: any) {
const popover = await this.popoverController.create({
component: InformationPopoverComponent,

Some files were not shown because too many files have changed in this diff Show More