scorpio-cli 0.1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scorpio_cli-0.1.0.0/LICENSE +21 -0
- scorpio_cli-0.1.0.0/PKG-INFO +9 -0
- scorpio_cli-0.1.0.0/README.md +2 -0
- scorpio_cli-0.1.0.0/pyproject.toml +24 -0
- scorpio_cli-0.1.0.0/scorpio/__init__.py +0 -0
- scorpio_cli-0.1.0.0/scorpio/cli/__init__.py +0 -0
- scorpio_cli-0.1.0.0/scorpio/cli/check_last_release.py +59 -0
- scorpio_cli-0.1.0.0/scorpio/cli/main.py +26 -0
- scorpio_cli-0.1.0.0/scorpio/server/__init__.py +0 -0
- scorpio_cli-0.1.0.0/scorpio/server/config.py +9 -0
- scorpio_cli-0.1.0.0/scorpio/server/main.py +165 -0
- scorpio_cli-0.1.0.0/scorpio/server/system.json +22 -0
- scorpio_cli-0.1.0.0/scorpio/ui/__init__.py +0 -0
- scorpio_cli-0.1.0.0/scorpio/ui/app.js +113 -0
- scorpio_cli-0.1.0.0/scorpio/ui/index.html +39 -0
- scorpio_cli-0.1.0.0/scorpio/ui/style.css +238 -0
- scorpio_cli-0.1.0.0/scorpio_cli.egg-info/PKG-INFO +9 -0
- scorpio_cli-0.1.0.0/scorpio_cli.egg-info/SOURCES.txt +20 -0
- scorpio_cli-0.1.0.0/scorpio_cli.egg-info/dependency_links.txt +1 -0
- scorpio_cli-0.1.0.0/scorpio_cli.egg-info/entry_points.txt +2 -0
- scorpio_cli-0.1.0.0/scorpio_cli.egg-info/top_level.txt +1 -0
- scorpio_cli-0.1.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ScorpioIoTUC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scorpio-cli
|
|
3
|
+
Version: 0.1.0.0
|
|
4
|
+
Summary: Scorpio IoT command-line interface
|
|
5
|
+
Author-email: Pedro Zavala <pedropablozavalatejos@gmail.com>, IoT-UC Laboratory <scorpioiotuc@gmail.com>
|
|
6
|
+
Maintainer-email: Pedro Zavala <pedropablozavalatejos@gmail.com>
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "scorpio-cli"
|
|
7
|
+
version = "0.1.0.0"
|
|
8
|
+
description = "Scorpio IoT command-line interface"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Pedro Zavala", email = "pedropablozavalatejos@gmail.com" },
|
|
12
|
+
{ name = "IoT-UC Laboratory", email = "scorpioiotuc@gmail.com" },
|
|
13
|
+
]
|
|
14
|
+
maintainers = [
|
|
15
|
+
{ name = "Pedro Zavala", email = "pedropablozavalatejos@gmail.com" },
|
|
16
|
+
]
|
|
17
|
+
dependencies = []
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
scorpio = "scorpio.cli.main:main"
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.package-data]
|
|
23
|
+
"scorpio.ui" = ["*.html", "*.css", "*.js"]
|
|
24
|
+
"scorpio.server" = ["*.json"]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import shutil
|
|
4
|
+
import tempfile
|
|
5
|
+
import urllib.request
|
|
6
|
+
import zipfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
REPOSITORY = "ScorpioIoTUC/Scorpio-Project"
|
|
13
|
+
RELEASE_API = f"https://api.github.com/repos/{REPOSITORY}/releases/latest"
|
|
14
|
+
|
|
15
|
+
INSTALL_DIR = Path.home() / ".local" / "share" / "scorpio" / "Scorpio-Project"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_latest_release():
|
|
19
|
+
logger.info("Checking latest Scorpio release from %s", RELEASE_API)
|
|
20
|
+
request = urllib.request.Request(
|
|
21
|
+
RELEASE_API, headers={"User-Agent": "scorpio-iotuc"}
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
with urllib.request.urlopen(request) as response:
|
|
25
|
+
release = json.load(response)
|
|
26
|
+
|
|
27
|
+
logger.info("Latest Scorpio release is %s", release["tag_name"])
|
|
28
|
+
return release
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def install_latest_release():
|
|
32
|
+
release = get_latest_release()
|
|
33
|
+
version = release["tag_name"]
|
|
34
|
+
download_url = release["zipball_url"]
|
|
35
|
+
|
|
36
|
+
logger.info("Downloading Scorpio %s", version)
|
|
37
|
+
|
|
38
|
+
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
39
|
+
archive = Path(temporary_directory) / "scorpio.zip"
|
|
40
|
+
|
|
41
|
+
urllib.request.urlretrieve(download_url, archive)
|
|
42
|
+
logger.info("Release downloaded successfully")
|
|
43
|
+
|
|
44
|
+
extracted = Path(temporary_directory) / "extracted"
|
|
45
|
+
with zipfile.ZipFile(archive) as zip_file:
|
|
46
|
+
zip_file.extractall(extracted)
|
|
47
|
+
logger.info("Release extracted successfully")
|
|
48
|
+
|
|
49
|
+
source_directory = next(extracted.iterdir())
|
|
50
|
+
|
|
51
|
+
if INSTALL_DIR.exists():
|
|
52
|
+
logger.warning("Replacing existing Scorpio installation at %s", INSTALL_DIR)
|
|
53
|
+
shutil.rmtree(INSTALL_DIR)
|
|
54
|
+
|
|
55
|
+
INSTALL_DIR.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
shutil.move(source_directory, INSTALL_DIR)
|
|
57
|
+
|
|
58
|
+
logger.info("Scorpio %s installed at %s", version, INSTALL_DIR)
|
|
59
|
+
return INSTALL_DIR, version
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import threading
|
|
3
|
+
import webbrowser
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from scorpio.server.main import run_server
|
|
7
|
+
from scorpio.server.config import SERVER_URL
|
|
8
|
+
from scorpio.cli.check_last_release import install_latest_release
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
parser = argparse.ArgumentParser(prog="scorpio")
|
|
13
|
+
subcommands = parser.add_subparsers(dest="command", required=True)
|
|
14
|
+
subcommands.add_parser("ui", help="Starts the web UI for Scorpio")
|
|
15
|
+
|
|
16
|
+
args = parser.parse_args()
|
|
17
|
+
|
|
18
|
+
if args.command == "ui":
|
|
19
|
+
project_directory, version = install_latest_release()
|
|
20
|
+
print(f"Scorpio {version} instalado en {project_directory}")
|
|
21
|
+
threading.Timer(0.5, lambda: webbrowser.open(SERVER_URL)).start()
|
|
22
|
+
run_server()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import subprocess
|
|
3
|
+
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
|
4
|
+
from http import HTTPStatus
|
|
5
|
+
import logging
|
|
6
|
+
from scorpio.server.config import PORT, SERVER_URL, UI_URL, UI_DIR, SYSTEM_JSON_PATH
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Logger configs
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
logging.basicConfig(level=logging.INFO)
|
|
12
|
+
|
|
13
|
+
class Handler(SimpleHTTPRequestHandler):
|
|
14
|
+
def __init__(self, *args, **kwargs):
|
|
15
|
+
self.system_json = self._load_system_json()
|
|
16
|
+
super().__init__(*args, directory=str(UI_DIR), **kwargs)
|
|
17
|
+
|
|
18
|
+
def _load_system_json(self) -> dict:
|
|
19
|
+
# Loading system json from its path
|
|
20
|
+
with open(SYSTEM_JSON_PATH, "r") as f:
|
|
21
|
+
return json.load(f)
|
|
22
|
+
|
|
23
|
+
def _update_system_json(self):
|
|
24
|
+
# Update the system json with new data
|
|
25
|
+
current_data = self.system_json
|
|
26
|
+
with open(SYSTEM_JSON_PATH, "w") as f:
|
|
27
|
+
json.dump(current_data, f, indent=4)
|
|
28
|
+
|
|
29
|
+
def _get_steps_index(self) -> dict:
|
|
30
|
+
# Create a dictionary with the index of each step in the steps array
|
|
31
|
+
return {
|
|
32
|
+
item["name"]: index for index, item in enumerate(self.system_json["steps"])
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
def _check_previous_steps(self, step_name: str) -> bool:
|
|
36
|
+
# first we get the index of the step in the steps array
|
|
37
|
+
steps_index = self._get_steps_index()
|
|
38
|
+
step_index = steps_index[step_name]
|
|
39
|
+
# then we get the steps array
|
|
40
|
+
steps = self.system_json["steps"]
|
|
41
|
+
# then we check if all the previous steps are completed
|
|
42
|
+
previous_steps = [
|
|
43
|
+
step for step in steps[:step_index] if step["status"] == False
|
|
44
|
+
]
|
|
45
|
+
if previous_steps:
|
|
46
|
+
logging.warning("Previous steps not completed: %s", previous_steps)
|
|
47
|
+
return False
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
def _body(self) -> dict:
|
|
51
|
+
content_length = int(self.headers.get("Content-Length", 0))
|
|
52
|
+
body = self.rfile.read(content_length)
|
|
53
|
+
return json.loads(body) if body else {}
|
|
54
|
+
|
|
55
|
+
def _get_default(self) -> dict:
|
|
56
|
+
return {"message": "Welcome to the Scorpio setup server."}
|
|
57
|
+
|
|
58
|
+
def _get_system_state(self) -> dict:
|
|
59
|
+
logging.info("Getting system state")
|
|
60
|
+
|
|
61
|
+
steps = [item["status"] for item in self.system_json["steps"]]
|
|
62
|
+
if all(step for step in steps):
|
|
63
|
+
# The system is fully set up, return completed state
|
|
64
|
+
return {"completed": True, "current_step": None}
|
|
65
|
+
# The system is not fully set up, return the current step
|
|
66
|
+
current_step = self._get_current_step()
|
|
67
|
+
return {"completed": False, **current_step}
|
|
68
|
+
|
|
69
|
+
def _get_current_step(self) -> dict:
|
|
70
|
+
logging.info("Getting current step")
|
|
71
|
+
steps = [(item["name"], item["status"]) for item in self.system_json["steps"]]
|
|
72
|
+
# find the first step that is not completed
|
|
73
|
+
for step in steps:
|
|
74
|
+
if not step[1]:
|
|
75
|
+
return {"current_step": step[0]}
|
|
76
|
+
return {"current_step": "All steps completed"}
|
|
77
|
+
|
|
78
|
+
def do_GET(self):
|
|
79
|
+
if self.path == "/api/status":
|
|
80
|
+
return self.send_json(self._get_system_state(), HTTPStatus.OK)
|
|
81
|
+
elif self.path == "/api/current_step":
|
|
82
|
+
return self.send_json(self._get_current_step(), HTTPStatus.OK)
|
|
83
|
+
else:
|
|
84
|
+
return super().do_GET()
|
|
85
|
+
|
|
86
|
+
def do_POST(self):
|
|
87
|
+
if self.path != "/api/confirm_step":
|
|
88
|
+
self.send_json({"error": "Invalid route."}, HTTPStatus.NOT_FOUND)
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
body = self._body()
|
|
92
|
+
step = body.get("step", "")
|
|
93
|
+
steps_names = [step["name"] for step in self.system_json["steps"]]
|
|
94
|
+
|
|
95
|
+
# Review if the step exist
|
|
96
|
+
if step not in steps_names:
|
|
97
|
+
self.send_json(
|
|
98
|
+
{"error": "Invalid step.", "steps": steps_names}, HTTPStatus.NOT_FOUND
|
|
99
|
+
)
|
|
100
|
+
return
|
|
101
|
+
# Review if the step is already completed
|
|
102
|
+
step_data = next(
|
|
103
|
+
item for item in self.system_json["steps"] if item["name"] == step
|
|
104
|
+
)
|
|
105
|
+
if step_data["status"]:
|
|
106
|
+
next_step = self._get_current_step().get("current_step")
|
|
107
|
+
self.send_json(
|
|
108
|
+
{"error": "Step is already completed.", "next_step": next_step},
|
|
109
|
+
HTTPStatus.BAD_REQUEST,
|
|
110
|
+
)
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
# Review if previous steps are completed
|
|
114
|
+
if not self._check_previous_steps(step):
|
|
115
|
+
self.send_json(
|
|
116
|
+
{"error": "Previous steps are not completed."},
|
|
117
|
+
HTTPStatus.BAD_REQUEST,
|
|
118
|
+
)
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
# Execute the command associated with the step
|
|
122
|
+
command = step_data["command"]
|
|
123
|
+
try:
|
|
124
|
+
logging.info("Executing command for step '%s': %s", step, command)
|
|
125
|
+
logging.info("Mocking command ...")
|
|
126
|
+
# subprocess.run(command, shell=True, check=True)
|
|
127
|
+
|
|
128
|
+
# Extract the step index and update the system json with the new status
|
|
129
|
+
step_index = self._get_steps_index()[step]
|
|
130
|
+
step_data["status"] = True
|
|
131
|
+
self.system_json["steps"][step_index] = step_data
|
|
132
|
+
self._update_system_json()
|
|
133
|
+
self.send_json(
|
|
134
|
+
{"message": f"Step '{step}' executed successfully."}, HTTPStatus.OK
|
|
135
|
+
)
|
|
136
|
+
except subprocess.CalledProcessError as e:
|
|
137
|
+
logging.error("Error executing command for step '%s': %s", step, e)
|
|
138
|
+
self.send_json(
|
|
139
|
+
{"error": f"Error executing step '{step}': {e}"},
|
|
140
|
+
HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
141
|
+
)
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
def send_json(self, payload, status=200):
|
|
145
|
+
body = json.dumps(payload).encode()
|
|
146
|
+
self.send_response(status)
|
|
147
|
+
self.send_header("Content-Type", "application/json")
|
|
148
|
+
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
149
|
+
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
|
150
|
+
self.send_header("Content-Length", str(len(body)))
|
|
151
|
+
self.end_headers()
|
|
152
|
+
self.wfile.write(body)
|
|
153
|
+
|
|
154
|
+
def end_headers(self):
|
|
155
|
+
self.send_header("Access-Control-Allow-Origin", UI_URL)
|
|
156
|
+
super().end_headers()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def run_server():
|
|
160
|
+
logging.info("Server started at %s", SERVER_URL)
|
|
161
|
+
ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
run_server()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"steps": [
|
|
3
|
+
{
|
|
4
|
+
"name": "initial_dependencies",
|
|
5
|
+
"status": false,
|
|
6
|
+
"command": "make setup-all"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"name": "system_reboot",
|
|
10
|
+
"status": false,
|
|
11
|
+
"command": "sudo reboot"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "docker_dependencies",
|
|
15
|
+
"status": false,
|
|
16
|
+
"command": "make setup-docker"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"system_is_up": {
|
|
20
|
+
"status": false
|
|
21
|
+
}
|
|
22
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const API_URL = '';
|
|
2
|
+
|
|
3
|
+
const steps = [
|
|
4
|
+
{
|
|
5
|
+
name: 'initial_dependencies',
|
|
6
|
+
title: 'Instalar dependencias',
|
|
7
|
+
description: 'Prepara Scorpio y sus dependencias iniciales.'
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
name: 'system_reboot',
|
|
11
|
+
title: 'Reiniciar el sistema',
|
|
12
|
+
description: 'Reinicia la Raspberry Pi para aplicar los cambios.'
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'docker_dependencies',
|
|
16
|
+
title: 'Iniciar infraestructura',
|
|
17
|
+
description: 'Configura Docker y levanta los servicios de Scorpio.'
|
|
18
|
+
}
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
async function refresh() {
|
|
22
|
+
const response = await fetch(`${API_URL}/api/status`);
|
|
23
|
+
if (!response.ok) throw new Error(`Error HTTP: ${response.status}`);
|
|
24
|
+
|
|
25
|
+
const state = await response.json();
|
|
26
|
+
const currentStep = state.completed ? null : state.current_step;
|
|
27
|
+
const currentIndex = state.completed
|
|
28
|
+
? steps.length
|
|
29
|
+
: steps.findIndex(step => step.name === currentStep);
|
|
30
|
+
|
|
31
|
+
const completedCount = Math.max(0, currentIndex);
|
|
32
|
+
const progress = state.completed ? 100 : (completedCount / steps.length) * 100;
|
|
33
|
+
|
|
34
|
+
document.querySelector('#progress-bar').style.width = `${progress}%`;
|
|
35
|
+
document.querySelector('#progress-label').textContent = state.completed
|
|
36
|
+
? 'Instalación completa'
|
|
37
|
+
: `${completedCount} de ${steps.length} completados`;
|
|
38
|
+
|
|
39
|
+
document.querySelector('#steps').innerHTML = steps.map((step, index) => {
|
|
40
|
+
const done = state.completed || index < currentIndex;
|
|
41
|
+
const active = !state.completed && step.name === currentStep;
|
|
42
|
+
const statusClass = done ? 'done' : active ? 'active' : 'pending';
|
|
43
|
+
|
|
44
|
+
return `<li class="step ${statusClass}">
|
|
45
|
+
<span class="step-icon">${done ? '✓' : index + 1}</span>
|
|
46
|
+
<div>
|
|
47
|
+
<h3 class="step-title">${step.title}</h3>
|
|
48
|
+
<p class="step-description">${done ? 'Paso completado correctamente.' : step.description}</p>
|
|
49
|
+
</div>
|
|
50
|
+
${active ? `<button type="button" onclick="runStep('${step.name}')">${step.name === 'system_reboot' ? 'Reiniciar' : 'Iniciar'}</button>` : ''}
|
|
51
|
+
</li>`;
|
|
52
|
+
}).join('');
|
|
53
|
+
|
|
54
|
+
if (state.completed) showMessage('Scorpio está configurado y listo para utilizar.', 'success');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function runStep(stepName) {
|
|
58
|
+
const step = steps.find(item => item.name === stepName);
|
|
59
|
+
const confirmation = stepName === 'system_reboot'
|
|
60
|
+
? 'La Raspberry Pi se reiniciará y perderás la conexión temporalmente. ¿Confirmas que deseas continuar?'
|
|
61
|
+
: `Se ejecutará el paso “${step.title}”. ¿Confirmas que deseas continuar?`;
|
|
62
|
+
|
|
63
|
+
if (!confirm(confirmation)) return;
|
|
64
|
+
|
|
65
|
+
const button = document.querySelector('.step.active button');
|
|
66
|
+
if (button) {
|
|
67
|
+
button.disabled = true;
|
|
68
|
+
button.textContent = 'Procesando…';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
showMessage('Ejecutando el paso. Esto puede tardar unos minutos…', 'info');
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const response = await fetch(`${API_URL}/api/confirm_step`, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
body: JSON.stringify({ step: stepName })
|
|
77
|
+
});
|
|
78
|
+
const result = await response.json();
|
|
79
|
+
if (!response.ok) throw new Error(result.error || 'No se pudo ejecutar el paso.');
|
|
80
|
+
|
|
81
|
+
showMessage(result.message || 'Paso completado correctamente.', 'success');
|
|
82
|
+
await refresh();
|
|
83
|
+
} catch (error) {
|
|
84
|
+
showMessage(error.message || 'No se pudo conectar con el servidor.', 'error');
|
|
85
|
+
if (button) {
|
|
86
|
+
button.disabled = false;
|
|
87
|
+
button.textContent = stepName === 'system_reboot' ? 'Reiniciar' : 'Iniciar';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function showMessage(text, type) {
|
|
93
|
+
const message = document.querySelector('#message');
|
|
94
|
+
message.textContent = text;
|
|
95
|
+
message.className = `message ${type}`;
|
|
96
|
+
message.hidden = false;
|
|
97
|
+
sessionStorage.setItem('scorpio-message', JSON.stringify({ text, type }));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function restoreMessage() {
|
|
101
|
+
const savedMessage = sessionStorage.getItem('scorpio-message');
|
|
102
|
+
if (!savedMessage) return;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const { text, type } = JSON.parse(savedMessage);
|
|
106
|
+
showMessage(text, type);
|
|
107
|
+
} catch {
|
|
108
|
+
sessionStorage.removeItem('scorpio-message');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
restoreMessage();
|
|
113
|
+
refresh().catch(() => showMessage('No se pudo conectar con el servidor de Scorpio.', 'error'));
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="es">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Instalación de Scorpio</title>
|
|
7
|
+
<link rel="stylesheet" href="style.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main class="container">
|
|
11
|
+
<header class="header">
|
|
12
|
+
<div class="brand">SCORPIO</div>
|
|
13
|
+
<h1>Configura tu sistema</h1>
|
|
14
|
+
<p>Completa los pasos en orden para preparar Scorpio.</p>
|
|
15
|
+
</header>
|
|
16
|
+
|
|
17
|
+
<div id="message" class="message" role="status" aria-live="polite" hidden></div>
|
|
18
|
+
|
|
19
|
+
<section class="card" aria-labelledby="progress-title">
|
|
20
|
+
<div class="card-header">
|
|
21
|
+
<div>
|
|
22
|
+
<span class="eyebrow">INSTALACIÓN</span>
|
|
23
|
+
<h2 id="progress-title">Progreso de configuración</h2>
|
|
24
|
+
</div>
|
|
25
|
+
<span id="progress-label" class="progress-label">Cargando…</span>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="progress-track" aria-hidden="true">
|
|
29
|
+
<div id="progress-bar" class="progress-bar"></div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<ol id="steps" class="steps" aria-label="Pasos de instalación"></ol>
|
|
33
|
+
</section>
|
|
34
|
+
|
|
35
|
+
<p class="help">Mantén esta ventana abierta durante la instalación. El reinicio puede interrumpir la conexión temporalmente.</p>
|
|
36
|
+
</main>
|
|
37
|
+
<script src="app.js"></script>
|
|
38
|
+
</body>
|
|
39
|
+
</html>
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light;
|
|
3
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
4
|
+
color: #19182b;
|
|
5
|
+
background: #f5f5fb;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
* {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
body {
|
|
13
|
+
margin: 0;
|
|
14
|
+
min-height: 100vh;
|
|
15
|
+
background: linear-gradient(180deg, #ffffff 0, #f3f2fb 68%, #eeeefe 100%);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.container {
|
|
19
|
+
width: min(720px, calc(100% - 32px));
|
|
20
|
+
margin: 0 auto;
|
|
21
|
+
padding: 64px 0 40px;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.header {
|
|
25
|
+
margin-bottom: 28px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.brand {
|
|
29
|
+
color: #6754d9;
|
|
30
|
+
font-size: .75rem;
|
|
31
|
+
font-weight: 800;
|
|
32
|
+
letter-spacing: .16em;
|
|
33
|
+
margin-bottom: 14px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
h1 {
|
|
37
|
+
font-size: clamp(2rem, 6vw, 3rem);
|
|
38
|
+
line-height: 1.05;
|
|
39
|
+
letter-spacing: -.04em;
|
|
40
|
+
margin: 0 0 12px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.header p,
|
|
44
|
+
.help {
|
|
45
|
+
color: #6f6d82;
|
|
46
|
+
line-height: 1.6;
|
|
47
|
+
margin: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.card {
|
|
51
|
+
background: #fff;
|
|
52
|
+
border: 1px solid #e2e0f1;
|
|
53
|
+
border-radius: 20px;
|
|
54
|
+
padding: 26px;
|
|
55
|
+
box-shadow: 0 18px 50px rgba(65, 52, 130, .10);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.card-header {
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: flex-end;
|
|
61
|
+
justify-content: space-between;
|
|
62
|
+
gap: 20px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.eyebrow {
|
|
66
|
+
color: #8781a3;
|
|
67
|
+
font-size: .68rem;
|
|
68
|
+
font-weight: 800;
|
|
69
|
+
letter-spacing: .14em;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
h2 {
|
|
73
|
+
font-size: 1.2rem;
|
|
74
|
+
margin: 6px 0 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.progress-label {
|
|
78
|
+
color: #74708a;
|
|
79
|
+
font-size: .85rem;
|
|
80
|
+
white-space: nowrap;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.progress-track {
|
|
84
|
+
height: 6px;
|
|
85
|
+
margin: 22px 0 8px;
|
|
86
|
+
border-radius: 999px;
|
|
87
|
+
overflow: hidden;
|
|
88
|
+
background: #eceaf5;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.progress-bar {
|
|
92
|
+
width: 0;
|
|
93
|
+
height: 100%;
|
|
94
|
+
border-radius: inherit;
|
|
95
|
+
background: linear-gradient(90deg, #3478e5, #7758dc);
|
|
96
|
+
transition: width .35s ease;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.steps {
|
|
100
|
+
list-style: none;
|
|
101
|
+
padding: 0;
|
|
102
|
+
margin: 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.step {
|
|
106
|
+
display: grid;
|
|
107
|
+
grid-template-columns: 38px 1fr auto;
|
|
108
|
+
align-items: center;
|
|
109
|
+
gap: 14px;
|
|
110
|
+
padding: 22px 0;
|
|
111
|
+
border-bottom: 1px solid #eeecf5;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.step:last-child {
|
|
115
|
+
border-bottom: 0;
|
|
116
|
+
padding-bottom: 4px;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.step-icon {
|
|
120
|
+
display: grid;
|
|
121
|
+
place-items: center;
|
|
122
|
+
width: 34px;
|
|
123
|
+
height: 34px;
|
|
124
|
+
border-radius: 50%;
|
|
125
|
+
border: 1px solid #dcd9e8;
|
|
126
|
+
color: #aaa6ba;
|
|
127
|
+
background: #f8f7fb;
|
|
128
|
+
font-weight: 700;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.step-title {
|
|
132
|
+
margin: 0 0 4px;
|
|
133
|
+
font-size: 1rem;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.step-description {
|
|
137
|
+
margin: 0;
|
|
138
|
+
color: #918da2;
|
|
139
|
+
font-size: .86rem;
|
|
140
|
+
line-height: 1.45;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.step.done .step-icon {
|
|
144
|
+
color: #fff;
|
|
145
|
+
background: #5d61d9;
|
|
146
|
+
border-color: #5d61d9;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.step.done .step-title {
|
|
150
|
+
color: #5355be;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.step.active .step-icon {
|
|
154
|
+
color: #6656d7;
|
|
155
|
+
background: #eeebff;
|
|
156
|
+
border-color: #bcb2f1;
|
|
157
|
+
box-shadow: 0 0 0 4px #f3f1ff;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.step.pending {
|
|
161
|
+
opacity: .62;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
button {
|
|
165
|
+
border: 0;
|
|
166
|
+
border-radius: 10px;
|
|
167
|
+
padding: 10px 16px;
|
|
168
|
+
color: #fff;
|
|
169
|
+
background: linear-gradient(135deg, #3478e5, #7354d6);
|
|
170
|
+
box-shadow: 0 6px 16px rgba(91, 78, 200, .22);
|
|
171
|
+
font: inherit;
|
|
172
|
+
font-size: .86rem;
|
|
173
|
+
font-weight: 700;
|
|
174
|
+
cursor: pointer;
|
|
175
|
+
transition: transform .15s ease, opacity .15s ease;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
button:hover {
|
|
179
|
+
transform: translateY(-1px);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
button:disabled {
|
|
183
|
+
cursor: wait;
|
|
184
|
+
opacity: .55;
|
|
185
|
+
transform: none;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.message {
|
|
189
|
+
margin-bottom: 18px;
|
|
190
|
+
padding: 13px 15px;
|
|
191
|
+
border-radius: 11px;
|
|
192
|
+
font-size: .9rem;
|
|
193
|
+
border: 1px solid;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.message.success {
|
|
197
|
+
color: #126d34;
|
|
198
|
+
background: #edf9f1;
|
|
199
|
+
border-color: #bde5ca;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.message.error {
|
|
203
|
+
color: #a72c2c;
|
|
204
|
+
background: #fff1f1;
|
|
205
|
+
border-color: #efc1c1;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.message.info {
|
|
209
|
+
color: #514a89;
|
|
210
|
+
background: #f1efff;
|
|
211
|
+
border-color: #d4cef5;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.help {
|
|
215
|
+
margin: 18px 4px 0;
|
|
216
|
+
font-size: .82rem;
|
|
217
|
+
text-align: center;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@media (max-width: 560px) {
|
|
221
|
+
.container {
|
|
222
|
+
padding-top: 36px;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.card {
|
|
226
|
+
padding: 20px;
|
|
227
|
+
border-radius: 16px;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.step {
|
|
231
|
+
grid-template-columns: 34px 1fr;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.step button {
|
|
235
|
+
grid-column: 2;
|
|
236
|
+
justify-self: start;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scorpio-cli
|
|
3
|
+
Version: 0.1.0.0
|
|
4
|
+
Summary: Scorpio IoT command-line interface
|
|
5
|
+
Author-email: Pedro Zavala <pedropablozavalatejos@gmail.com>, IoT-UC Laboratory <scorpioiotuc@gmail.com>
|
|
6
|
+
Maintainer-email: Pedro Zavala <pedropablozavalatejos@gmail.com>
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
scorpio/__init__.py
|
|
5
|
+
scorpio/cli/__init__.py
|
|
6
|
+
scorpio/cli/check_last_release.py
|
|
7
|
+
scorpio/cli/main.py
|
|
8
|
+
scorpio/server/__init__.py
|
|
9
|
+
scorpio/server/config.py
|
|
10
|
+
scorpio/server/main.py
|
|
11
|
+
scorpio/server/system.json
|
|
12
|
+
scorpio/ui/__init__.py
|
|
13
|
+
scorpio/ui/app.js
|
|
14
|
+
scorpio/ui/index.html
|
|
15
|
+
scorpio/ui/style.css
|
|
16
|
+
scorpio_cli.egg-info/PKG-INFO
|
|
17
|
+
scorpio_cli.egg-info/SOURCES.txt
|
|
18
|
+
scorpio_cli.egg-info/dependency_links.txt
|
|
19
|
+
scorpio_cli.egg-info/entry_points.txt
|
|
20
|
+
scorpio_cli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
scorpio
|