citrascope 0.3.0__py3-none-any.whl → 0.4.0__py3-none-any.whl

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.
citrascope/__main__.py CHANGED
@@ -1,24 +1,21 @@
1
1
  import click
2
2
 
3
3
  from citrascope.citra_scope_daemon import CitraScopeDaemon
4
+ from citrascope.constants import DEFAULT_WEB_PORT
4
5
  from citrascope.settings.citrascope_settings import CitraScopeSettings
5
6
 
6
7
 
7
- @click.group()
8
- @click.option("--dev", is_flag=True, default=False, help="Use the development API (dev.app.citra.space)")
9
- @click.option("--log-level", default="INFO", help="Set logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)")
10
- @click.option("--keep-images", is_flag=True, default=False, help="Keep image files after upload (do not delete)")
11
- @click.pass_context
12
- def cli(ctx, dev, log_level, keep_images):
13
- ctx.obj = {"settings": CitraScopeSettings(dev=dev, log_level=log_level, keep_images=keep_images)}
14
-
15
-
16
- @cli.command("start")
17
- @click.option("--web-host", default="0.0.0.0", help="Web server host address (default: 0.0.0.0)")
18
- @click.option("--web-port", default=24872, type=int, help="Web server port (default: 24872)")
19
- @click.pass_context
20
- def start(ctx, web_host, web_port):
21
- daemon = CitraScopeDaemon(ctx.obj["settings"], enable_web=True, web_host=web_host, web_port=web_port)
8
+ @click.command()
9
+ @click.option(
10
+ "--web-port",
11
+ default=DEFAULT_WEB_PORT,
12
+ type=int,
13
+ help=f"Web server port (default: {DEFAULT_WEB_PORT})",
14
+ )
15
+ def cli(web_port):
16
+ """CitraScope daemon - configure via web UI at http://localhost:24872"""
17
+ settings = CitraScopeSettings(web_port=web_port)
18
+ daemon = CitraScopeDaemon(settings)
22
19
  daemon.run()
23
20
 
24
21
 
@@ -41,7 +41,19 @@ class CitraApiClient(AbstractCitraApiClient):
41
41
  return resp.json()
42
42
  except httpx.HTTPStatusError as e:
43
43
  if self.logger:
44
- self.logger.error(f"HTTP error: {e.response.status_code} {e.response.text}")
44
+ # Check if response is HTML (e.g., Cloudflare error pages)
45
+ content_type = e.response.headers.get("content-type", "")
46
+ response_text = e.response.text
47
+
48
+ if "text/html" in content_type or response_text.strip().startswith("<"):
49
+ # Log only status and a brief message for HTML responses, sometimes we get Cloudflare error pages
50
+ self.logger.error(
51
+ f"HTTP error: {e.response.status_code} - "
52
+ f"Received HTML error page (likely Cloudflare or server error) for {method} {endpoint}"
53
+ )
54
+ else:
55
+ # Log full response for non-HTML errors (JSON, plain text, etc.)
56
+ self.logger.error(f"HTTP error: {e.response.status_code} {response_text}")
45
57
  return None
46
58
  except Exception as e:
47
59
  if self.logger:
@@ -17,9 +17,6 @@ class CitraScopeDaemon:
17
17
  settings: CitraScopeSettings,
18
18
  api_client: Optional[AbstractCitraApiClient] = None,
19
19
  hardware_adapter: Optional[AbstractAstroHardwareAdapter] = None,
20
- enable_web: bool = True,
21
- web_host: str = "0.0.0.0",
22
- web_port: int = 24872,
23
20
  ):
24
21
  self.settings = settings
25
22
  CITRASCOPE_LOGGER.setLevel(self.settings.log_level)
@@ -33,16 +30,14 @@ class CitraScopeDaemon:
33
30
 
34
31
  self.api_client = api_client
35
32
  self.hardware_adapter = hardware_adapter
36
- self.enable_web = enable_web
37
33
  self.web_server = None
38
34
  self.task_manager = None
39
35
  self.ground_station = None
40
36
  self.telescope_record = None
41
37
  self.configuration_error: Optional[str] = None
42
38
 
43
- # Create web server instance if enabled (always start web server)
44
- if self.enable_web:
45
- self.web_server = CitraScopeWebServer(daemon=self, host=web_host, port=web_port)
39
+ # Create web server instance (always enabled)
40
+ self.web_server = CitraScopeWebServer(daemon=self, host="0.0.0.0", port=self.settings.web_port)
46
41
 
47
42
  def _create_hardware_adapter(self) -> AbstractAstroHardwareAdapter:
48
43
  """Factory method to create the appropriate hardware adapter based on settings."""
@@ -73,12 +68,8 @@ class CitraScopeDaemon:
73
68
  try:
74
69
  if reload_settings:
75
70
  CITRASCOPE_LOGGER.info("Reloading configuration...")
76
- # Reload settings from file
77
- new_settings = CitraScopeSettings(
78
- dev=("dev.api" in self.settings.host),
79
- log_level=self.settings.log_level,
80
- keep_images=self.settings.keep_images,
81
- )
71
+ # Reload settings from file (preserving web_port)
72
+ new_settings = CitraScopeSettings(web_port=self.settings.web_port)
82
73
  self.settings = new_settings
83
74
  CITRASCOPE_LOGGER.setLevel(self.settings.log_level)
84
75
 
@@ -206,11 +197,10 @@ class CitraScopeDaemon:
206
197
  return False, error_msg
207
198
 
208
199
  def run(self):
209
- # Start web server FIRST if enabled, so users can monitor/configure
200
+ # Start web server FIRST, so users can monitor/configure
210
201
  # The web interface will remain available even if configuration is incomplete
211
- if self.enable_web:
212
- self.web_server.start()
213
- CITRASCOPE_LOGGER.info(f"Web interface available at http://{self.web_server.host}:{self.web_server.port}")
202
+ self.web_server.start()
203
+ CITRASCOPE_LOGGER.info(f"Web interface available at http://{self.web_server.host}:{self.web_server.port}")
214
204
 
215
205
  try:
216
206
  # Try to initialize components
@@ -238,6 +228,7 @@ class CitraScopeDaemon:
238
228
  """Clean up resources on shutdown."""
239
229
  if self.task_manager:
240
230
  self.task_manager.stop()
241
- if self.enable_web and self.web_server:
231
+ if self.web_server:
242
232
  CITRASCOPE_LOGGER.info("Stopping web server...")
243
- self.web_server.stop()
233
+ if self.web_server.web_log_handler:
234
+ CITRASCOPE_LOGGER.removeHandler(self.web_server.web_log_handler)
@@ -0,0 +1,23 @@
1
+ """Application-wide constants for CitraScope.
2
+
3
+ This module contains all shared constants used across different parts of the application.
4
+ Centralizing these values prevents duplication and circular import issues.
5
+ """
6
+
7
+ # ============================================================================
8
+ # API ENDPOINTS
9
+ # ============================================================================
10
+ PROD_API_HOST = "api.citra.space"
11
+ DEV_API_HOST = "dev.api.citra.space"
12
+ DEFAULT_API_PORT = 443
13
+
14
+ # ============================================================================
15
+ # WEB APP URLs
16
+ # ============================================================================
17
+ PROD_APP_URL = "https://app.citra.space"
18
+ DEV_APP_URL = "https://dev.app.citra.space"
19
+
20
+ # ============================================================================
21
+ # WEB SERVER
22
+ # ============================================================================
23
+ DEFAULT_WEB_PORT = 24872 # "CITRA" on phone keypad
@@ -422,8 +422,8 @@ class NinaAdvancedHttpAdapter(AbstractAstroHardwareAdapter):
422
422
 
423
423
  nina_sequence_name = f"Citra Target: {satellite_data['name']}, Task: {task_id}"
424
424
 
425
- # Replace basic placeholders
426
- tle_data = f"{elset['tle'][0]}\n{elset['tle'][1]}"
425
+ # Replace basic placeholders (use \r\n for Windows NINA compatibility)
426
+ tle_data = f"{elset['tle'][0]}\r\n{elset['tle'][1]}"
427
427
  sequence_json["Name"] = nina_sequence_name
428
428
 
429
429
  # Navigate to the TLE container (ID 20 in the template)
@@ -23,14 +23,15 @@ class WebLogHandler(logging.Handler):
23
23
  def emit(self, record):
24
24
  """Emit a log record."""
25
25
  try:
26
- # Filter out web-related logs from the web UI
27
- # (uvicorn.access, WebSocket messages, HTTP Request logs, etc.)
28
- if (
29
- record.name.startswith("uvicorn")
30
- or "WebSocket" in record.getMessage()
31
- or "HTTP Request:" in record.getMessage()
32
- ):
33
- return
26
+ # Filter out routine web-related logs from the web UI
27
+ # but keep ERROR and CRITICAL level messages
28
+ if record.levelno < logging.ERROR:
29
+ if (
30
+ record.name.startswith("uvicorn")
31
+ or "WebSocket" in record.getMessage()
32
+ or "HTTP Request:" in record.getMessage()
33
+ ):
34
+ return
34
35
 
35
36
  # Get the original levelname without color codes
36
37
  # Record.levelname might have ANSI codes from ColoredFormatter
@@ -10,6 +10,7 @@ import platformdirs
10
10
  APP_NAME = "citrascope"
11
11
  APP_AUTHOR = "citra-space"
12
12
 
13
+ from citrascope.constants import DEFAULT_API_PORT, DEFAULT_WEB_PORT, PROD_API_HOST
13
14
  from citrascope.logging import CITRASCOPE_LOGGER
14
15
  from citrascope.settings.settings_file_manager import SettingsFileManager
15
16
 
@@ -17,18 +18,11 @@ from citrascope.settings.settings_file_manager import SettingsFileManager
17
18
  class CitraScopeSettings:
18
19
  """Settings for CitraScope loaded from JSON configuration file."""
19
20
 
20
- def __init__(
21
- self,
22
- dev: bool = False,
23
- log_level: str = "INFO",
24
- keep_images: bool = False,
25
- ):
21
+ def __init__(self, web_port: int = DEFAULT_WEB_PORT):
26
22
  """Initialize settings from JSON config file.
27
23
 
28
24
  Args:
29
- dev: If True, use development API endpoint
30
- log_level: Logging level (DEBUG, INFO, WARNING, ERROR)
31
- keep_images: If True, preserve captured images
25
+ web_port: Port for web interface (default: 24872) - bootstrap option only
32
26
  """
33
27
  self.config_manager = SettingsFileManager()
34
28
 
@@ -38,9 +32,9 @@ class CitraScopeSettings:
38
32
  # Application data directories
39
33
  self._images_dir = Path(platformdirs.user_data_dir(APP_NAME, appauthor=APP_AUTHOR)) / "images"
40
34
 
41
- # API Settings
42
- self.host: str = config.get("host", "dev.api.citra.space" if dev else "api.citra.space")
43
- self.port: int = config.get("port", 443)
35
+ # API Settings (all loaded from config file)
36
+ self.host: str = config.get("host", PROD_API_HOST)
37
+ self.port: int = config.get("port", DEFAULT_API_PORT)
44
38
  self.use_ssl: bool = config.get("use_ssl", True)
45
39
  self.personal_access_token: str = config.get("personal_access_token", "")
46
40
  self.telescope_id: str = config.get("telescope_id", "")
@@ -51,9 +45,12 @@ class CitraScopeSettings:
51
45
  # Hardware adapter-specific settings stored as dict
52
46
  self.adapter_settings: Dict[str, Any] = config.get("adapter_settings", {})
53
47
 
54
- # Runtime settings (can be overridden by CLI flags)
55
- self.log_level: str = log_level if log_level != "INFO" else config.get("log_level", "INFO")
56
- self.keep_images: bool = keep_images if keep_images else config.get("keep_images", False)
48
+ # Runtime settings (all loaded from config file, configurable via web UI)
49
+ self.log_level: str = config.get("log_level", "INFO")
50
+ self.keep_images: bool = config.get("keep_images", False)
51
+
52
+ # Web port: CLI override if non-default, otherwise use config file
53
+ self.web_port: int = web_port if web_port != DEFAULT_WEB_PORT else config.get("web_port", DEFAULT_WEB_PORT)
57
54
 
58
55
  # Task retry configuration
59
56
  self.max_task_retries: int = config.get("max_task_retries", 3)
@@ -64,10 +61,6 @@ class CitraScopeSettings:
64
61
  self.file_logging_enabled: bool = config.get("file_logging_enabled", True)
65
62
  self.log_retention_days: int = config.get("log_retention_days", 30)
66
63
 
67
- if dev:
68
- self.host = "dev.api.citra.space"
69
- CITRASCOPE_LOGGER.info("Using development API endpoint.")
70
-
71
64
  def get_images_dir(self) -> Path:
72
65
  """Get the path to the images directory.
73
66
 
@@ -105,6 +98,7 @@ class CitraScopeSettings:
105
98
  "adapter_settings": self.adapter_settings,
106
99
  "log_level": self.log_level,
107
100
  "keep_images": self.keep_images,
101
+ "web_port": self.web_port,
108
102
  "max_task_retries": self.max_task_retries,
109
103
  "initial_retry_delay_seconds": self.initial_retry_delay_seconds,
110
104
  "max_retry_delay_seconds": self.max_retry_delay_seconds,
@@ -137,6 +131,7 @@ class CitraScopeSettings:
137
131
  settings.adapter_settings = config.get("adapter_settings", {})
138
132
  settings.log_level = config.get("log_level", "INFO")
139
133
  settings.keep_images = config.get("keep_images", False)
134
+ settings.web_port = config.get("web_port", DEFAULT_WEB_PORT)
140
135
  settings.max_task_retries = config.get("max_task_retries", 3)
141
136
  settings.initial_retry_delay_seconds = config.get("initial_retry_delay_seconds", 30)
142
137
  settings.max_retry_delay_seconds = config.get("max_retry_delay_seconds", 300)
@@ -11,6 +11,9 @@ from citrascope.tasks.scope.static_telescope_task import StaticTelescopeTask
11
11
  from citrascope.tasks.scope.tracking_telescope_task import TrackingTelescopeTask
12
12
  from citrascope.tasks.task import Task
13
13
 
14
+ # Task polling interval in seconds
15
+ TASK_POLL_INTERVAL_SECONDS = 15
16
+
14
17
 
15
18
  class TaskManager:
16
19
  def __init__(
@@ -44,8 +47,9 @@ class TaskManager:
44
47
  self._report_online()
45
48
  tasks = self.api_client.get_telescope_tasks(self.telescope_record["id"])
46
49
 
47
- # If API call failed (timeout, network error, etc.), skip this poll iteration
50
+ # If API call failed (timeout, network error, etc.), wait before retrying
48
51
  if tasks is None:
52
+ self._stop_event.wait(TASK_POLL_INTERVAL_SECONDS)
49
53
  continue
50
54
 
51
55
  added = 0
@@ -113,7 +117,7 @@ class TaskManager:
113
117
  except Exception as e:
114
118
  self.logger.error(f"Exception in poll_tasks loop: {e}", exc_info=True)
115
119
  time.sleep(5) # avoid tight error loop
116
- self._stop_event.wait(15)
120
+ self._stop_event.wait(TASK_POLL_INTERVAL_SECONDS)
117
121
 
118
122
  def _report_online(self):
119
123
  """
citrascope/web/app.py CHANGED
@@ -4,6 +4,7 @@ import asyncio
4
4
  import json
5
5
  import os
6
6
  from datetime import datetime, timezone
7
+ from importlib.metadata import PackageNotFoundError, version
7
8
  from pathlib import Path
8
9
  from typing import Any, Dict, List, Optional
9
10
 
@@ -13,15 +14,14 @@ from fastapi.responses import HTMLResponse, JSONResponse
13
14
  from fastapi.staticfiles import StaticFiles
14
15
  from pydantic import BaseModel
15
16
 
17
+ from citrascope.constants import (
18
+ DEV_API_HOST,
19
+ DEV_APP_URL,
20
+ PROD_API_HOST,
21
+ PROD_APP_URL,
22
+ )
16
23
  from citrascope.logging import CITRASCOPE_LOGGER
17
24
 
18
- # ============================================================================
19
- # URL CONSTANTS - Single source of truth for Citra web URLs
20
- # ============================================================================
21
- PROD_APP_URL = "https://app.citra.space"
22
- DEV_APP_URL = "https://dev.app.citra.space"
23
- # ============================================================================
24
-
25
25
 
26
26
  class SystemStatus(BaseModel):
27
27
  """Current system status."""
@@ -155,7 +155,7 @@ class CitraScopeWebApp:
155
155
 
156
156
  settings = self.daemon.settings
157
157
  # Determine app URL based on API host
158
- app_url = DEV_APP_URL if "dev." in settings.host else PROD_APP_URL
158
+ app_url = DEV_APP_URL if settings.host == DEV_API_HOST else PROD_APP_URL
159
159
 
160
160
  # Get config file path
161
161
  config_path = str(settings.config_manager.get_config_path())
@@ -198,6 +198,15 @@ class CitraScopeWebApp:
198
198
  "error": getattr(self.daemon, "configuration_error", None),
199
199
  }
200
200
 
201
+ @self.app.get("/api/version")
202
+ async def get_version():
203
+ """Get CitraScope version."""
204
+ try:
205
+ pkg_version = version("citrascope")
206
+ return {"version": pkg_version}
207
+ except PackageNotFoundError:
208
+ return {"version": "development"}
209
+
201
210
  @self.app.get("/api/hardware-adapters")
202
211
  async def get_hardware_adapters():
203
212
  """Get list of available hardware adapters."""
citrascope/web/server.py CHANGED
@@ -7,13 +7,14 @@ import time
7
7
 
8
8
  import uvicorn
9
9
 
10
+ from citrascope.constants import DEFAULT_WEB_PORT
10
11
  from citrascope.logging import CITRASCOPE_LOGGER, WebLogHandler
11
12
 
12
13
 
13
14
  class CitraScopeWebServer:
14
15
  """Manages the web server and its configuration."""
15
16
 
16
- def __init__(self, daemon, host: str = "0.0.0.0", port: int = 24872):
17
+ def __init__(self, daemon, host: str = "0.0.0.0", port: int = DEFAULT_WEB_PORT):
17
18
  self.daemon = daemon
18
19
  self.host = host
19
20
  self.port = port
@@ -101,6 +102,14 @@ class CitraScopeWebServer:
101
102
  asyncio.create_task(self._status_broadcast_loop())
102
103
 
103
104
  await server.serve()
105
+ except OSError as e:
106
+ if e.errno == 48: # Address already in use
107
+ CITRASCOPE_LOGGER.error(
108
+ f"Port {self.port} is already in use. Please stop any other services using this port "
109
+ f"or use --web-port to specify a different port."
110
+ )
111
+ else:
112
+ CITRASCOPE_LOGGER.error(f"Web server OS error: {e}", exc_info=True)
104
113
  except Exception as e:
105
114
  CITRASCOPE_LOGGER.error(f"Web server error: {e}", exc_info=True)
106
115
 
@@ -412,12 +412,14 @@ function updateLatestLogLine() {
412
412
 
413
413
  const timestamp = new Date(latestLog.timestamp).toLocaleTimeString();
414
414
  const cleanMessage = stripAnsiCodes(latestLog.message);
415
+ // Truncate message to ~150 chars for collapsed header (approx 2 lines)
416
+ const truncatedMessage = cleanMessage.length > 150 ? cleanMessage.substring(0, 150) + '...' : cleanMessage;
415
417
 
416
418
  content.querySelector('.log-timestamp').textContent = timestamp;
417
419
  const levelSpan = content.querySelector('.log-level');
418
420
  levelSpan.classList.add(`log-level-${latestLog.level}`);
419
421
  levelSpan.textContent = latestLog.level;
420
- content.querySelector('.log-message').textContent = cleanMessage;
422
+ content.querySelector('.log-message').textContent = truncatedMessage;
421
423
 
422
424
  latestLogLine.innerHTML = '';
423
425
  latestLogLine.appendChild(content);
@@ -2,12 +2,34 @@
2
2
 
3
3
  import { getConfig, saveConfig, getConfigStatus, getHardwareAdapters, getAdapterSchema } from './api.js';
4
4
 
5
+ // API Host constants - must match backend constants in app.py
6
+ const PROD_API_HOST = 'api.citra.space';
7
+ const DEV_API_HOST = 'dev.api.citra.space';
8
+ const DEFAULT_API_PORT = 443;
9
+
5
10
  let currentAdapterSchema = [];
6
11
  export let currentConfig = {};
7
12
 
8
13
  /**
9
14
  * Initialize configuration management
10
15
  */
16
+ async function fetchVersion() {
17
+ try {
18
+ const response = await fetch('/api/version');
19
+ const data = await response.json();
20
+ const versionEl = document.getElementById('citraScopeVersion');
21
+ if (versionEl && data.version) {
22
+ versionEl.textContent = data.version;
23
+ }
24
+ } catch (error) {
25
+ console.error('Error fetching version:', error);
26
+ const versionEl = document.getElementById('citraScopeVersion');
27
+ if (versionEl) {
28
+ versionEl.textContent = 'unknown';
29
+ }
30
+ }
31
+ }
32
+
11
33
  export async function initConfig() {
12
34
  // Populate hardware adapter dropdown
13
35
  await loadAdapterOptions();
@@ -25,6 +47,19 @@ export async function initConfig() {
25
47
  });
26
48
  }
27
49
 
50
+ // API endpoint selection change
51
+ const apiEndpointSelect = document.getElementById('apiEndpoint');
52
+ if (apiEndpointSelect) {
53
+ apiEndpointSelect.addEventListener('change', function(e) {
54
+ const customContainer = document.getElementById('customHostContainer');
55
+ if (e.target.value === 'custom') {
56
+ customContainer.style.display = 'block';
57
+ } else {
58
+ customContainer.style.display = 'none';
59
+ }
60
+ });
61
+ }
62
+
28
63
  // Config form submission
29
64
  const configForm = document.getElementById('configForm');
30
65
  if (configForm) {
@@ -34,6 +69,7 @@ export async function initConfig() {
34
69
  // Load initial config
35
70
  await loadConfiguration();
36
71
  checkConfigStatus();
72
+ fetchVersion();
37
73
  }
38
74
 
39
75
  /**
@@ -114,6 +150,27 @@ async function loadConfiguration() {
114
150
  imagesDirElement.textContent = config.images_dir_path;
115
151
  }
116
152
 
153
+ // API endpoint selector
154
+ const apiEndpointSelect = document.getElementById('apiEndpoint');
155
+ const customHostContainer = document.getElementById('customHostContainer');
156
+ const customHost = document.getElementById('customHost');
157
+ const customPort = document.getElementById('customPort');
158
+ const customUseSsl = document.getElementById('customUseSsl');
159
+
160
+ if (config.host === PROD_API_HOST) {
161
+ apiEndpointSelect.value = 'production';
162
+ customHostContainer.style.display = 'none';
163
+ } else if (config.host === DEV_API_HOST) {
164
+ apiEndpointSelect.value = 'development';
165
+ customHostContainer.style.display = 'none';
166
+ } else {
167
+ apiEndpointSelect.value = 'custom';
168
+ customHostContainer.style.display = 'block';
169
+ customHost.value = config.host || '';
170
+ customPort.value = config.port || DEFAULT_API_PORT;
171
+ customUseSsl.checked = config.use_ssl !== undefined ? config.use_ssl : true;
172
+ }
173
+
117
174
  // Core fields
118
175
  document.getElementById('personal_access_token').value = config.personal_access_token || '';
119
176
  document.getElementById('telescopeId').value = config.telescope_id || '';
@@ -274,6 +331,24 @@ async function saveConfiguration(event) {
274
331
  // Hide previous messages
275
332
  hideConfigMessages();
276
333
 
334
+ // Determine API host settings based on endpoint selection
335
+ const apiEndpoint = document.getElementById('apiEndpoint').value;
336
+ let host, port, use_ssl;
337
+
338
+ if (apiEndpoint === 'production') {
339
+ host = PROD_API_HOST;
340
+ port = DEFAULT_API_PORT;
341
+ use_ssl = true;
342
+ } else if (apiEndpoint === 'development') {
343
+ host = DEV_API_HOST;
344
+ port = DEFAULT_API_PORT;
345
+ use_ssl = true;
346
+ } else { // custom
347
+ host = document.getElementById('customHost').value;
348
+ port = parseInt(document.getElementById('customPort').value, 10);
349
+ use_ssl = document.getElementById('customUseSsl').checked;
350
+ }
351
+
277
352
  const config = {
278
353
  personal_access_token: document.getElementById('personal_access_token').value,
279
354
  telescope_id: document.getElementById('telescopeId').value,
@@ -282,10 +357,11 @@ async function saveConfiguration(event) {
282
357
  log_level: document.getElementById('logLevel').value,
283
358
  keep_images: document.getElementById('keep_images').checked,
284
359
  file_logging_enabled: document.getElementById('file_logging_enabled').checked,
285
- // Preserve API settings from loaded config
286
- host: currentConfig.host || 'api.citra.space',
287
- port: currentConfig.port || 443,
288
- use_ssl: currentConfig.use_ssl !== undefined ? currentConfig.use_ssl : true,
360
+ // API settings from endpoint selector
361
+ host: host,
362
+ port: port,
363
+ use_ssl: use_ssl,
364
+ // Preserve other settings from loaded config
289
365
  max_task_retries: currentConfig.max_task_retries || 3,
290
366
  initial_retry_delay_seconds: currentConfig.initial_retry_delay_seconds || 30,
291
367
  max_retry_delay_seconds: currentConfig.max_retry_delay_seconds || 300,
@@ -57,6 +57,29 @@ body {
57
57
  /* Log accordion customization */
58
58
  .log-accordion-button {
59
59
  border-bottom: 1px solid #444 !important;
60
+ position: relative;
61
+ }
62
+
63
+ .accordion-social-links {
64
+ position: absolute;
65
+ right: 50px; /* Position to the left of the accordion arrow */
66
+ top: 50%;
67
+ transform: translateY(-50%);
68
+ display: flex;
69
+ gap: 12px;
70
+ align-items: center;
71
+ }
72
+
73
+ .social-link {
74
+ color: #a0aec0;
75
+ transition: color 0.2s ease;
76
+ display: flex;
77
+ align-items: center;
78
+ text-decoration: none;
79
+ }
80
+
81
+ .social-link:hover {
82
+ color: #e2e8f0;
60
83
  }
61
84
 
62
85
  .log-accordion-body {
@@ -72,6 +95,15 @@ body {
72
95
  .log-latest-line {
73
96
  font-family: monospace;
74
97
  color: #e2e8f0;
98
+ display: block;
99
+ overflow: hidden;
100
+ text-overflow: ellipsis;
101
+ display: -webkit-box;
102
+ -webkit-line-clamp: 2;
103
+ -webkit-box-orient: vertical;
104
+ line-height: 1.4em;
105
+ max-height: 2.8em;
106
+ padding-right: 130px; /* Make room for 3 social icons and accordion arrow */
75
107
  }
76
108
 
77
109
  /* Task display */
@@ -34,6 +34,7 @@
34
34
  <li class="nav-item"><a href="#" class="nav-link px-2 text-white" data-section="monitoring"
35
35
  aria-current="page">Monitoring</a></li>
36
36
  <li class="nav-item"><a href="#" class="nav-link px-2" data-section="config">Config</a></li>
37
+ <li class="nav-item"><a href="https://docs.citra.space/citrascope/" class="nav-link px-2" target="_blank">Docs</a></li>
37
38
  <li class="nav-item"><a href="#" class="nav-link bg-success text-white btn" style="display: none;"
38
39
  id="taskScopeButton" target="_blank">Task my Scope</a></li>
39
40
  </ul>
@@ -165,6 +166,32 @@
165
166
  </div>
166
167
  <div class="card-body">
167
168
  <div class="row g-3">
169
+ <div class="col-12">
170
+ <label for="apiEndpoint" class="form-label">API Endpoint</label>
171
+ <select id="apiEndpoint" class="form-select">
172
+ <option value="production">Production (api.citra.space)</option>
173
+ <option value="development">Development (dev.api.citra.space)</option>
174
+ <option value="custom">Custom</option>
175
+ </select>
176
+ </div>
177
+ <div class="col-12" id="customHostContainer" style="display: none;">
178
+ <label for="customHost" class="form-label">Custom API Host</label>
179
+ <input type="text" id="customHost" class="form-control" placeholder="api.example.com">
180
+ <div class="row g-2 mt-2">
181
+ <div class="col-6">
182
+ <label for="customPort" class="form-label small">Port</label>
183
+ <input type="number" id="customPort" class="form-control" value="443" placeholder="443">
184
+ </div>
185
+ <div class="col-6 d-flex align-items-end">
186
+ <div class="form-check">
187
+ <input class="form-check-input" type="checkbox" id="customUseSsl" checked>
188
+ <label class="form-check-label" for="customUseSsl">
189
+ Use SSL
190
+ </label>
191
+ </div>
192
+ </div>
193
+ </div>
194
+ </div>
168
195
  <div class="col-12 col-md-6">
169
196
  <label for="personal_access_token" class="form-label">Personal Access Token <span class="text-danger">*</span></label>
170
197
  <input type="password" id="personal_access_token" class="form-control" placeholder="Enter your Citra API token" required>
@@ -277,6 +304,15 @@
277
304
  </small>
278
305
  </div>
279
306
  </div>
307
+
308
+ <!-- Version Info -->
309
+ <div class="row mt-3">
310
+ <div class="col">
311
+ <small class="text-muted">
312
+ CitraScope Version: <span id="citraScopeVersion" class="text-secondary">Loading...</span>
313
+ </small>
314
+ </div>
315
+ </div>
280
316
  </form>
281
317
  </div>
282
318
 
@@ -315,6 +351,23 @@
315
351
  data-bs-toggle="collapse" data-bs-target="#logAccordionCollapse" aria-expanded="false"
316
352
  aria-controls="logAccordionCollapse">
317
353
  <span id="latestLogLine" class="log-latest-line">Log Terminal</span>
354
+ <div class="accordion-social-links">
355
+ <a href="https://github.com/citra-space/citrascope/issues/new" target="_blank" class="social-link" title="Report an Issue" onclick="event.stopPropagation();">
356
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
357
+ <path d="M16 8c0 3.866-3.582 7-8 7a9 9 0 0 1-2.347-.306c-.584.296-1.925.864-4.181 1.234-.2.032-.352-.176-.273-.362.354-.836.674-1.95.77-2.966C.744 11.37 0 9.76 0 8c0-3.866 3.582-7 8-7s8 3.134 8 7M5 8a1 1 0 1 0-2 0 1 1 0 0 0 2 0m4 0a1 1 0 1 0-2 0 1 1 0 0 0 2 0m3 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/>
358
+ </svg>
359
+ </a>
360
+ <a href="https://discord.gg/STgJQkWe9y" target="_blank" class="social-link" title="Join Discord" onclick="event.stopPropagation();">
361
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
362
+ <path d="M13.545 2.907a13.227 13.227 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 0 0-3.658 0 8.258 8.258 0 0 0-.412-.833.051.051 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019c.308-.42.582-.863.818-1.329a.05.05 0 0 0-.01-.059.051.051 0 0 0-.018-.011 8.875 8.875 0 0 1-1.248-.595.05.05 0 0 1-.02-.066.051.051 0 0 1 .015-.019c.084-.063.168-.129.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 0 1 .053.007c.08.066.164.132.248.195a.051.051 0 0 1-.004.085 8.254 8.254 0 0 1-1.249.594.05.05 0 0 0-.03.03.052.052 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.235 13.235 0 0 0 4.001-2.02.049.049 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 0 0-.02-.019Zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612Zm5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612Z"/>
363
+ </svg>
364
+ </a>
365
+ <a href="https://github.com/citra-space/citrascope" target="_blank" class="social-link" title="View on GitHub" onclick="event.stopPropagation();">
366
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
367
+ <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
368
+ </svg>
369
+ </a>
370
+ </div>
318
371
  </button>
319
372
  </h2>
320
373
  <div id="logAccordionCollapse" class="accordion-collapse collapse" aria-labelledby="logAccordionHeader"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: citrascope
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Remotely control a telescope while it polls for tasks, collects and edge processes data, and delivers results and data for further processing.
5
5
  Author-email: Patrick McDavid <patrick@citra.space>
6
6
  License-Expression: MIT
@@ -9,13 +9,9 @@ Requires-Dist: click
9
9
  Requires-Dist: fastapi>=0.104.0
10
10
  Requires-Dist: httpx
11
11
  Requires-Dist: platformdirs>=4.0.0
12
- Requires-Dist: pydantic-settings
13
- Requires-Dist: pytest-cov
14
12
  Requires-Dist: python-dateutil
15
- Requires-Dist: python-json-logger
16
13
  Requires-Dist: requests
17
14
  Requires-Dist: skyfield
18
- Requires-Dist: types-python-dateutil
19
15
  Requires-Dist: uvicorn[standard]>=0.24.0
20
16
  Requires-Dist: websockets>=12.0
21
17
  Provides-Extra: all
@@ -38,10 +34,7 @@ Requires-Dist: mypy; extra == 'dev'
38
34
  Requires-Dist: pre-commit; extra == 'dev'
39
35
  Requires-Dist: pytest; extra == 'dev'
40
36
  Requires-Dist: pytest-cov; extra == 'dev'
41
- Provides-Extra: docs
42
- Requires-Dist: sphinx; extra == 'docs'
43
- Requires-Dist: sphinx-autodoc-typehints; extra == 'docs'
44
- Requires-Dist: sphinx-markdown-builder; extra == 'docs'
37
+ Requires-Dist: types-python-dateutil; extra == 'dev'
45
38
  Provides-Extra: indi
46
39
  Requires-Dist: pixelemon; extra == 'indi'
47
40
  Requires-Dist: plotly; extra == 'indi'
@@ -65,33 +58,46 @@ Remotely control a telescope while it polls for tasks, collects observations, an
65
58
  - Connects to configured telescope and camera hardware
66
59
  - Acts as a task daemon carrying out and remitting photography tasks
67
60
 
61
+ ## Documentation
62
+
63
+ Full documentation is available at [docs.citra.space](https://docs.citra.space/citrascope/).
64
+
65
+ Documentation source is maintained in the [citra-space/docs](https://github.com/citra-space/docs) repository.
66
+
68
67
  ## Installation
69
68
 
70
- ### Install with pipx
69
+ **Important:** CitraScope requires Python 3.10, 3.11, or 3.12.
71
70
 
72
- [pipx](https://pipx.pypa.io/) installs the CLI tool in an isolated environment while making it globally available:
71
+ ### Check Your Python Version
73
72
 
74
73
  ```sh
75
- pipx install citrascope
74
+ python3 --version
76
75
  ```
77
76
 
78
- ### Optional Dependencies
77
+ If you don't have a compatible version, install one with [pyenv](https://github.com/pyenv/pyenv):
79
78
 
80
- CitraScope supports different hardware adapters through optional dependency groups:
79
+ ```sh
80
+ pyenv install 3.12.0
81
+ pyenv local 3.12.0 # Sets Python 3.12.0 for the current directory
82
+ ```
81
83
 
82
- - **INDI adapter** (for Linux-based telescope control):
83
- ```sh
84
- pipx install citrascope[indi]
85
- # or with pip: pip install citrascope[indi]
86
- ```
84
+ ### Install CitraScope
87
85
 
88
- - **All optional dependencies**:
89
- ```sh
90
- pipx install citrascope[all]
91
- # or with pip: pip install citrascope[all]
92
- ```
86
+ **Recommended: Using pip in a virtual environment**
93
87
 
94
- The base installation without optional dependencies supports the NINA adapter, which works via HTTP API calls and does not require additional Python packages.
88
+ ```sh
89
+ python3 -m venv citrascope-env
90
+ source citrascope-env/bin/activate # On Windows: citrascope-env\Scripts\activate
91
+ pip install citrascope
92
+ ```
93
+
94
+ ### Optional Dependencies
95
+
96
+ For Linux-based telescope control (INDI):
97
+
98
+ ```sh
99
+ pip install citrascope[indi]
100
+ ```
95
101
 
96
102
  This provides the `citrascope` command-line tool. To see available commands:
97
103
 
@@ -101,16 +107,18 @@ citrascope --help
101
107
 
102
108
  ## Usage
103
109
 
104
- Run the CLI tool:
110
+ ### Starting the Daemon
111
+
112
+ Run the daemon with:
105
113
 
106
114
  ```sh
107
- citrascope start
115
+ citrascope
108
116
  ```
109
117
 
110
- To connect to the Citra Dev server:
118
+ By default, this starts the web interface on `http://localhost:24872`. You can customize the port:
111
119
 
112
120
  ```sh
113
- citrascope start --dev
121
+ citrascope --web-port 8080
114
122
  ```
115
123
 
116
124
  ## Developer Setup
@@ -174,10 +182,10 @@ Then create a release in the GitHub UI from the new tag. This triggers automatic
174
182
 
175
183
  If you are using Visual Studio Code, you can run or debug the project directly using the pre-configured launch options in `.vscode/launch.json`:
176
184
 
177
- - **Python: citrascope dev start** — Runs the main entry point with development options.
178
- - **Python: citrascope dev start DEBUG logging** — Runs with development options and sets log level to DEBUG for more detailed output.
185
+ - **Python: citrascope** — Runs the daemon with default settings
186
+ - **Python: citrascope (custom port)** — Runs with web interface on port 8080
179
187
 
180
- To use these, open the Run and Debug panel in VS Code, select the desired configuration, and click the Run or Debug button. This is a convenient way to start or debug the app without manually entering commands.
188
+ To use these, open the Run and Debug panel in VS Code, select the desired configuration, and click the Run or Debug button.
181
189
 
182
190
  ## Running Tests
183
191
 
@@ -1,38 +1,38 @@
1
1
  citrascope/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- citrascope/__main__.py,sha256=r8y3_mgY2fm5XOzPgkDjq-a8uQFgdrxLrzup11UEB-I,1091
3
- citrascope/citra_scope_daemon.py,sha256=e4PGeoBRzO14PN-g4bm_14WOugRhNQtUW-QzrBXvePQ,10598
2
+ citrascope/__main__.py,sha256=W7uY30MOuIlmhzVEoiZ_6BMmKrsqIvxcAxqw4-pOT3k,596
3
+ citrascope/citra_scope_daemon.py,sha256=mw7U2gEgfDc3TuXVUHZAkHjSwY2T259FW02LAjz7vWU,10315
4
+ citrascope/constants.py,sha256=Mc7SLzCelUMDV96BIwP684fFLGANCOEO_mM3GCeRDVY,968
4
5
  citrascope/api/abstract_api_client.py,sha256=gjmA9mw1O-TK16nYahOWClAwqPc_L1E3F2llZJeKTPw,624
5
- citrascope/api/citra_api_client.py,sha256=FBxqVLIhozBycFqHcx_348ZvYFjWTIFiVbIKkTrgEEU,5253
6
+ citrascope/api/citra_api_client.py,sha256=8rpz25Diy8YhuCiQ9HqMi4TIqxAc6BbrvqoFu8u-orQ,6007
6
7
  citrascope/hardware/abstract_astro_hardware_adapter.py,sha256=BdQrZkLSh2siszG8fGNEWpknC5wyZXZWgbaq7Zc7cAo,6131
7
8
  citrascope/hardware/adapter_registry.py,sha256=fFIZhXYphZ_p480c6hICpcx9fNOeX-EG2tvLHm372dM,3170
8
9
  citrascope/hardware/indi_adapter.py,sha256=uNrjkfxD0zjOPfar6J-frb6A87VkEjsL7SD9N9bEsC8,29903
9
10
  citrascope/hardware/kstars_dbus_adapter.py,sha256=Nv6ijVDvgTCTZUmRFh3Wh-YS7ChiztiXF17OWlzJwoo,7001
10
- citrascope/hardware/nina_adv_http_adapter.py,sha256=RCvCefwd4RWQke8AxAlafZX6vkAY26zHPO9rWOtrQB0,26217
11
+ citrascope/hardware/nina_adv_http_adapter.py,sha256=2McglZprEJQ8OKnqFXDrZCcl2AylkWO8Y1kGrS_nwtY,26261
11
12
  citrascope/hardware/nina_adv_http_survey_template.json,sha256=beg4H6Bzby-0x5uDc_eRJQ_rKs8VT64sDJyAzS_q1l4,14424
12
13
  citrascope/logging/__init__.py,sha256=YU38HLMWfbXh_H-s7W7Zx2pbCR4f_tRk7z0G8xqz4_o,179
13
14
  citrascope/logging/_citrascope_logger.py,sha256=GkqNpFJWiatqrBr8t4o2nHt7V9bBDJ8mysM0F4AXMa8,3479
14
- citrascope/logging/web_log_handler.py,sha256=PhltZ4l6kWQDL3ALrPeGmwMNZ1iEyWE_Lb5-F5CF82w,2599
15
+ citrascope/logging/web_log_handler.py,sha256=d0XQzHJZ5M1v3H351tdkBYg7EOwFzXpp7PA9nYejIV0,2659
15
16
  citrascope/settings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- citrascope/settings/citrascope_settings.py,sha256=hNy25dp7suh4F9kYsxIycSVBwpshjPLSHvytAPzzcpk,5930
17
+ citrascope/settings/citrascope_settings.py,sha256=yBDlM5uCnhUQgIrNDMTPX6cmgKnKl-lvra41-7XN40E,5966
17
18
  citrascope/settings/settings_file_manager.py,sha256=Yijb-I9hbbVJ2thkr7OrfkNknSPt1RDpsE7VvqAs0a8,4193
18
- citrascope/tasks/runner.py,sha256=Y_dySx0IXnhK6lzipzqRlC9ti5EZPEixri8FXYBWun0,12426
19
+ citrascope/tasks/runner.py,sha256=xVnGWe_iYutBYqIhgOOriPjPcYUJ8IRbYRYKiAbehAQ,12584
19
20
  citrascope/tasks/task.py,sha256=0u0oN56E6KaNz19ba_7WuY43Sk4CTXc8UPT7sdUpRXo,1287
20
21
  citrascope/tasks/scope/base_telescope_task.py,sha256=wIdyUxplFNhf_YMdCXOK6pG7HF7tZn_id59TvYyWZAY,9674
21
22
  citrascope/tasks/scope/static_telescope_task.py,sha256=XP53zYVcyLHLvebDU06Jx0ghPK3tb0c_XmO60yj_XSA,1132
22
23
  citrascope/tasks/scope/tracking_telescope_task.py,sha256=k5LEmEi_xnFHNjqPNYb8_tqDdCFD3YGe25Wh_brJXHk,1130
23
24
  citrascope/web/__init__.py,sha256=CgU36fyNSxGXjUy3hsHwx7UxF8UO4Qsb7PjC9-6tRmY,38
24
- citrascope/web/app.py,sha256=SPFtQTs8XJ_MijH8LMNO8SuIVg7AkibMEktJdIFnXbI,20146
25
- citrascope/web/server.py,sha256=9nyrjep2yajZzelWKgDUwowhRRvyo-X3nCNGt2nex8Y,4758
25
+ citrascope/web/app.py,sha256=OfBur9oM06NakrA30MOk9CgX0ewJ7IZl4kn0gtQgHJ4,20261
26
+ citrascope/web/server.py,sha256=IJJk4HgEwcsjHercL-Q5z39NmJRbkNk_51HIUKKhtRE,5242
26
27
  citrascope/web/static/api.js,sha256=s-b1FIw-pTo3A8kLlLINVqHhIvfHwTWA7cEvz4N8Gqc,1924
27
- citrascope/web/static/app.js,sha256=shzo_dtmTF7pu4S3bBtWo2EpIdFp5vzq8sefF8OFgl0,17620
28
- citrascope/web/static/config.js,sha256=fF9Hnd_TLJWogGP_ZnsvNzLlZBRjmmgDg9w13uMUCps,12814
29
- citrascope/web/static/style.css,sha256=wlxeWN4j5OKg9DDFnBX_WFlgNytAL61H2Mt3-NcXEZU,2155
28
+ citrascope/web/static/app.js,sha256=xwKBzLB2uWytyUDdzrT6qsQBwIfESypjf8jSLnrQRZg,17820
29
+ citrascope/web/static/config.js,sha256=4pjMH8yS24JkH62OpklEdPC0EAmd7e5c21WWETz1EO0,15701
30
+ citrascope/web/static/style.css,sha256=haVMnKlULZ-SL_qmLyqnrwHdX28s6KzVK9uE_R5nlLo,2877
30
31
  citrascope/web/static/websocket.js,sha256=UITw1DDfehOKpjlltn5MXhewZYGKzPFmaTtMFtC0-Ps,3931
31
32
  citrascope/web/static/img/citra.png,sha256=Bq8dPWB6fNz7a_H0FuEtNmZWcPHH2iV2OC-fMg4REbQ,205570
32
33
  citrascope/web/static/img/favicon.png,sha256=zrbUlpFXDB_zmsIdhhn8_klnc2Ma3N6Q8ouBMAxFjbM,24873
33
- citrascope/web/templates/dashboard.html,sha256=pRaD8Dzxo9BqDxMktBz24xbTkWpeC6_Wa41yJdJFV48,18293
34
- docs/index.md,sha256=YQDeVrN9AcbRzo88Jc4iRCO70gAh_4GSgImrJMwcSCo,1402
35
- citrascope-0.3.0.dist-info/METADATA,sha256=8tWhBpEf785kKyW5aPkA-MxNwof6-MWqDLQQWEp7-iQ,6650
36
- citrascope-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
37
- citrascope-0.3.0.dist-info/entry_points.txt,sha256=fP22Lt8bNZ_whBowDnOWSADf_FUrgAWnIhqqPf5Xo2g,55
38
- citrascope-0.3.0.dist-info/RECORD,,
34
+ citrascope/web/templates/dashboard.html,sha256=4ChTsnMMP6VyfdQT9bM4rRruYHJorAeZFOMDhI-j3fA,24155
35
+ citrascope-0.4.0.dist-info/METADATA,sha256=eombd_1rHdc9ubLY6PM7M2UKBqLMeT8aOQDUtqqWMP0,6481
36
+ citrascope-0.4.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
37
+ citrascope-0.4.0.dist-info/entry_points.txt,sha256=fP22Lt8bNZ_whBowDnOWSADf_FUrgAWnIhqqPf5Xo2g,55
38
+ citrascope-0.4.0.dist-info/RECORD,,
docs/index.md DELETED
@@ -1,47 +0,0 @@
1
- # CitraScope Documentation
2
-
3
- Welcome to the CitraScope documentation.
4
-
5
- ## Overview
6
-
7
- CitraScope is a Python application for remote telescope control, task automation, and data collection.
8
- It connects to the Citra.space API and INDI hardware to execute observation tasks.
9
-
10
- ## Architecture
11
-
12
- - **CLI Entrypoint:** `citrascope/__main__.py`
13
- Handles configuration, authentication, and starts the task daemon.
14
- - **API Client:** `citrascope/api/client.py`
15
- Communicates with Citra.space for authentication, telescope, satellite, and ground station data.
16
- - **Task Management:** `citrascope/tasks/runner.py`
17
- Polls for tasks, schedules, and executes observations.
18
- - **Settings:** `citrascope/settings/_citrascope_settings.py`
19
- Loads configuration from environment variables.
20
-
21
- ## Configuration
22
-
23
- See [README.md](../README.md) for installation and environment setup.
24
- Environment variables are documented in `.env.example`.
25
-
26
- ## Usage
27
-
28
- Run the CLI:
29
- ```sh
30
- python -m citrascope start
31
- ```
32
- Or use VS Code launch configurations for development and debugging.
33
-
34
- ## Testing
35
-
36
- - **Unit tests** are written using [pytest](https://pytest.org/) and are located in the `tests/` directory.
37
- - To run tests manually, use:
38
-
39
- ```bash
40
- pytest
41
- ```
42
-
43
- ## Further Documentation
44
-
45
- - [API Reference](https://api.citra.space/docs)
46
- - [Contributing Guide](contributing.md) (coming soon)
47
- - [Troubleshooting](troubleshooting.md) (coming soon)