coderuday21 Cursor commited on
Commit
333dd2f
·
1 Parent(s): 4595db8

Fix local library scan: DDA mode in run.py, path encoding, gitignore large tifs

Browse files
.gitignore CHANGED
@@ -4,6 +4,12 @@ __pycache__/
4
  data/satellite_app.db
5
  data/overlays/*
6
  !data/overlays/.gitkeep
 
 
 
 
 
 
7
  .env
8
  *.egg-info/
9
  dist/
 
4
  data/satellite_app.db
5
  data/overlays/*
6
  !data/overlays/.gitkeep
7
+ # Local library images (too large for git — keep on disk only)
8
+ library_sources/**/*.tif
9
+ library_sources/**/*.tiff
10
+ library_sources/**/*.TIF
11
+ library_sources/**/*.TIFF
12
+ !library_sources/**/.gitkeep
13
  .env
14
  *.egg-info/
15
  dist/
Dockerfile CHANGED
@@ -21,7 +21,7 @@ WORKDIR /app
21
 
22
  # Build-time info + cache-bust:
23
  # Changing APP_BUILD forces Docker to re-run subsequent layers (including pip install).
24
- ARG APP_BUILD=27
25
  ENV APP_BUILD=${APP_BUILD}
26
  ENV GDAL_CONFIG=/usr/bin/gdal-config
27
  RUN echo "Docker build start: APP_BUILD=${APP_BUILD}" && python -V
 
21
 
22
  # Build-time info + cache-bust:
23
  # Changing APP_BUILD forces Docker to re-run subsequent layers (including pip install).
24
+ ARG APP_BUILD=28
25
  ENV APP_BUILD=${APP_BUILD}
26
  ENV GDAL_CONFIG=/usr/bin/gdal-config
27
  RUN echo "Docker build start: APP_BUILD=${APP_BUILD}" && python -V
app/dda/config.py CHANGED
@@ -1,5 +1,6 @@
1
  import os
2
  from pathlib import Path
 
3
 
4
  from ..database import DATA_DIR
5
 
@@ -9,10 +10,21 @@ IS_DDA_MODE = APP_MODE == "dda"
9
  # Project root: change_detection_webapp/
10
  PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
11
 
12
- # Local folder library — drop images into year subfolders here (no web upload required)
13
- LOCAL_LIBRARY_ROOT = Path(
14
- os.environ.get("LOCAL_LIBRARY_ROOT", str(PROJECT_ROOT / "library_sources"))
15
- ).resolve()
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  LIBRARY_DIR = DATA_DIR / "library"
18
  THUMBS_DIR = LIBRARY_DIR / "thumbs"
@@ -29,7 +41,12 @@ ALLOWED_EXTENSIONS = {".tif", ".tiff", ".png", ".jpg", ".jpeg"}
29
 
30
 
31
  def ensure_library_dirs() -> None:
32
- for d in (LIBRARY_DIR, THUMBS_DIR, PREVIEWS_DIR, LOCAL_THUMB_CACHE, LOCAL_LIBRARY_ROOT):
 
 
 
 
 
33
  try:
34
  d.mkdir(parents=True, exist_ok=True)
35
  except OSError:
@@ -37,14 +54,15 @@ def ensure_library_dirs() -> None:
37
 
38
 
39
  def ensure_local_year_folders() -> None:
40
- """Create default year folders under library_sources if missing."""
41
  from datetime import datetime
42
  current = datetime.now().year
43
- for year in (current - 1, current, current + 1):
44
- try:
45
- (LOCAL_LIBRARY_ROOT / str(year)).mkdir(parents=True, exist_ok=True)
46
- except OSError:
47
- pass
 
48
 
49
 
50
  def max_upload_bytes_for_extension(ext: str) -> int:
 
1
  import os
2
  from pathlib import Path
3
+ from typing import List
4
 
5
  from ..database import DATA_DIR
6
 
 
10
  # Project root: change_detection_webapp/
11
  PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
12
 
13
+
14
+ def get_library_roots() -> List[Path]:
15
+ """All folders scanned for year-based images (project + writable data copy)."""
16
+ roots: List[Path] = []
17
+ if os.environ.get("LOCAL_LIBRARY_ROOT"):
18
+ roots.append(Path(os.environ["LOCAL_LIBRARY_ROOT"]).resolve())
19
+ for candidate in (PROJECT_ROOT / "library_sources", DATA_DIR / "library_sources"):
20
+ resolved = candidate.resolve()
21
+ if resolved not in roots:
22
+ roots.append(resolved)
23
+ return roots
24
+
25
+
26
+ # Primary root (shown in UI) — first entry from get_library_roots()
27
+ LOCAL_LIBRARY_ROOT = get_library_roots()[0]
28
 
29
  LIBRARY_DIR = DATA_DIR / "library"
30
  THUMBS_DIR = LIBRARY_DIR / "thumbs"
 
41
 
42
 
43
  def ensure_library_dirs() -> None:
44
+ for root in get_library_roots():
45
+ try:
46
+ root.mkdir(parents=True, exist_ok=True)
47
+ except OSError:
48
+ pass
49
+ for d in (LIBRARY_DIR, THUMBS_DIR, PREVIEWS_DIR, LOCAL_THUMB_CACHE):
50
  try:
51
  d.mkdir(parents=True, exist_ok=True)
52
  except OSError:
 
54
 
55
 
56
  def ensure_local_year_folders() -> None:
57
+ """Create default year folders under each library root if missing."""
58
  from datetime import datetime
59
  current = datetime.now().year
60
+ for root in get_library_roots():
61
+ for year in (current - 1, current, current + 1):
62
+ try:
63
+ (root / str(year)).mkdir(parents=True, exist_ok=True)
64
+ except OSError:
65
+ pass
66
 
67
 
68
  def max_upload_bytes_for_extension(ext: str) -> int:
app/dda/library_routes.py CHANGED
@@ -65,7 +65,7 @@ def _image_to_dict(asset: ImageAsset, zone_name: str = "", village_name: str = "
65
  @router.get("/config")
66
  def dda_config():
67
  _require_dda()
68
- from .config import MAX_GEOTIFF_BYTES, MAX_IMAGE_BYTES, LOCAL_LIBRARY_ROOT
69
  max_gb = MAX_GEOTIFF_BYTES / (1024 ** 3)
70
  return {
71
  "mode": "dda",
@@ -77,7 +77,7 @@ def dda_config():
77
  "allowedExtensions": sorted(ALLOWED_EXTENSIONS),
78
  "hierarchyMode": "admin",
79
  "librarySource": "local_folder",
80
- "localLibraryPath": str(LOCAL_LIBRARY_ROOT),
81
  }
82
 
83
 
 
65
  @router.get("/config")
66
  def dda_config():
67
  _require_dda()
68
+ from .config import MAX_GEOTIFF_BYTES, MAX_IMAGE_BYTES, get_library_roots
69
  max_gb = MAX_GEOTIFF_BYTES / (1024 ** 3)
70
  return {
71
  "mode": "dda",
 
77
  "allowedExtensions": sorted(ALLOWED_EXTENSIONS),
78
  "hierarchyMode": "admin",
79
  "librarySource": "local_folder",
80
+ "localLibraryPaths": [str(r) for r in get_library_roots()],
81
  }
82
 
83
 
app/dda/local_library.py CHANGED
@@ -6,12 +6,10 @@ Folder layout (under library_sources/):
6
  library_sources/
7
  2024/
8
  image_a.tif
9
- site_1/
10
- image_b.tif
11
  2025/
12
  image_c.tif
13
 
14
- Copy or save files directly into the year folder on disk; the app scans on load / refresh.
15
  """
16
  from __future__ import annotations
17
 
@@ -20,10 +18,11 @@ import logging
20
  from dataclasses import dataclass
21
  from pathlib import Path
22
  from typing import List, Optional
 
23
 
24
  from fastapi import HTTPException
25
 
26
- from .config import ALLOWED_EXTENSIONS, LOCAL_LIBRARY_ROOT, LOCAL_THUMB_CACHE
27
  from .geotiff_io import inspect_image, raster_to_preview_png
28
 
29
  logger = logging.getLogger(__name__)
@@ -32,6 +31,7 @@ logger = logging.getLogger(__name__)
32
  @dataclass
33
  class LocalImageEntry:
34
  path: str # relative posix path, e.g. 2025/aerial.tif
 
35
  year: int
36
  filename: str
37
  file_size_bytes: int
@@ -41,73 +41,80 @@ def _is_year_dir(name: str) -> bool:
41
  return len(name) == 4 and name.isdigit() and 1990 <= int(name) <= 2100
42
 
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  def safe_resolve(relative_path: str) -> Path:
45
- """Resolve a library-relative path; block path traversal."""
46
  rel = relative_path.replace("\\", "/").lstrip("/")
47
  if not rel or ".." in rel.split("/"):
48
  raise HTTPException(status_code=400, detail="Invalid image path")
49
- root = LOCAL_LIBRARY_ROOT.resolve()
50
- full = (root / rel).resolve()
51
- try:
52
- full.relative_to(root)
53
- except ValueError:
54
- raise HTTPException(status_code=400, detail="Invalid image path")
55
- if not full.is_file():
56
- raise HTTPException(status_code=404, detail="Image file not found")
57
- if full.suffix.lower() not in ALLOWED_EXTENSIONS:
58
- raise HTTPException(status_code=400, detail="Unsupported image format")
59
- return full
60
 
61
 
62
  def scan_years() -> List[dict]:
63
- """List year folders and image counts."""
64
  ensure_root()
65
- years = []
66
- if not LOCAL_LIBRARY_ROOT.exists():
67
- return years
68
- for entry in sorted(LOCAL_LIBRARY_ROOT.iterdir()):
69
- if not entry.is_dir() or not _is_year_dir(entry.name):
70
  continue
71
- count = sum(
72
- 1
73
- for p in entry.rglob("*")
74
- if p.is_file() and p.suffix.lower() in ALLOWED_EXTENSIONS
75
- )
76
- years.append({"year": int(entry.name), "imageCount": count})
77
- return years
78
 
79
 
80
  def scan_images(year: Optional[int] = None, query: Optional[str] = None) -> List[LocalImageEntry]:
81
- """Scan library_sources for images, optionally filtered by year and filename."""
82
  ensure_root()
83
  results: List[LocalImageEntry] = []
84
- root = LOCAL_LIBRARY_ROOT
85
- if not root.exists():
86
- return results
87
-
88
  q = (query or "").strip().lower()
89
- year_dirs: List[Path]
90
- if year is not None:
91
- ydir = root / str(year)
92
- year_dirs = [ydir] if ydir.is_dir() else []
93
- else:
94
- year_dirs = [d for d in sorted(root.iterdir()) if d.is_dir() and _is_year_dir(d.name)]
95
 
96
- for ydir in year_dirs:
97
- y = int(ydir.name)
98
- for path in sorted(ydir.rglob("*")):
99
- if not path.is_file() or path.suffix.lower() not in ALLOWED_EXTENSIONS:
100
  continue
101
- rel = path.relative_to(root).as_posix()
102
  if q and q not in rel.lower():
103
  continue
104
  try:
105
  size = path.stat().st_size
106
  except OSError:
107
  continue
 
108
  results.append(
109
  LocalImageEntry(
110
  path=rel,
 
111
  year=y,
112
  filename=path.name,
113
  file_size_bytes=size,
@@ -117,13 +124,15 @@ def scan_images(year: Optional[int] = None, query: Optional[str] = None) -> List
117
 
118
 
119
  def entry_to_dict(entry: LocalImageEntry, include_meta: bool = False) -> dict:
 
120
  out = {
121
  "path": entry.path,
122
  "year": entry.year,
123
  "filename": entry.filename,
124
  "fileSizeBytes": entry.file_size_bytes,
125
- "thumbUrl": f"/api/dda/local/thumb?path={entry.path}",
126
  "source": "local_folder",
 
127
  }
128
  if include_meta:
129
  try:
@@ -148,7 +157,7 @@ def thumb_cache_path(relative_path: str) -> Path:
148
  def get_or_build_thumb(relative_path: str, max_side: int = 256) -> Path:
149
  full = safe_resolve(relative_path)
150
  cache = thumb_cache_path(relative_path)
151
- if cache.exists():
152
  return cache
153
  cache.parent.mkdir(parents=True, exist_ok=True)
154
  raster_to_preview_png(full, cache, max_side=max_side)
@@ -156,5 +165,33 @@ def get_or_build_thumb(relative_path: str, max_side: int = 256) -> Path:
156
 
157
 
158
  def ensure_root() -> None:
159
- LOCAL_LIBRARY_ROOT.mkdir(parents=True, exist_ok=True)
 
160
  LOCAL_THUMB_CACHE.mkdir(parents=True, exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  library_sources/
7
  2024/
8
  image_a.tif
 
 
9
  2025/
10
  image_c.tif
11
 
12
+ Copy files into library_sources/YEAR/ in the project folder (or data/library_sources/ on HF).
13
  """
14
  from __future__ import annotations
15
 
 
18
  from dataclasses import dataclass
19
  from pathlib import Path
20
  from typing import List, Optional
21
+ from urllib.parse import quote
22
 
23
  from fastapi import HTTPException
24
 
25
+ from .config import ALLOWED_EXTENSIONS, LOCAL_THUMB_CACHE, get_library_roots
26
  from .geotiff_io import inspect_image, raster_to_preview_png
27
 
28
  logger = logging.getLogger(__name__)
 
31
  @dataclass
32
  class LocalImageEntry:
33
  path: str # relative posix path, e.g. 2025/aerial.tif
34
+ root: Path
35
  year: int
36
  filename: str
37
  file_size_bytes: int
 
41
  return len(name) == 4 and name.isdigit() and 1990 <= int(name) <= 2100
42
 
43
 
44
+ def _iter_image_files(root: Path, year: Optional[int] = None):
45
+ if not root.exists():
46
+ return
47
+ if year is not None:
48
+ year_dirs = [root / str(year)] if (root / str(year)).is_dir() else []
49
+ else:
50
+ year_dirs = [d for d in sorted(root.iterdir()) if d.is_dir() and _is_year_dir(d.name)]
51
+ for ydir in year_dirs:
52
+ y = int(ydir.name)
53
+ for path in sorted(ydir.rglob("*")):
54
+ if not path.is_file():
55
+ continue
56
+ ext = path.suffix.lower()
57
+ if ext not in ALLOWED_EXTENSIONS:
58
+ logger.debug("Skipped (extension %s): %s", ext, path)
59
+ continue
60
+ yield root, y, path
61
+
62
+
63
  def safe_resolve(relative_path: str) -> Path:
64
+ """Resolve a library-relative path across all configured roots."""
65
  rel = relative_path.replace("\\", "/").lstrip("/")
66
  if not rel or ".." in rel.split("/"):
67
  raise HTTPException(status_code=400, detail="Invalid image path")
68
+ for root in get_library_roots():
69
+ full = (root / rel).resolve()
70
+ try:
71
+ full.relative_to(root.resolve())
72
+ except ValueError:
73
+ continue
74
+ if full.is_file() and full.suffix.lower() in ALLOWED_EXTENSIONS:
75
+ return full
76
+ raise HTTPException(status_code=404, detail="Image file not found")
 
 
77
 
78
 
79
  def scan_years() -> List[dict]:
80
+ """List year folders and image counts (merged across all roots)."""
81
  ensure_root()
82
+ counts: dict[int, int] = {}
83
+ for root in get_library_roots():
84
+ if not root.exists():
 
 
85
  continue
86
+ for entry in sorted(root.iterdir()):
87
+ if not entry.is_dir() or not _is_year_dir(entry.name):
88
+ continue
89
+ y = int(entry.name)
90
+ n = sum(1 for _ in _iter_image_files(root, year=y))
91
+ counts[y] = counts.get(y, 0) + n
92
+ return [{"year": y, "imageCount": counts[y]} for y in sorted(counts)]
93
 
94
 
95
  def scan_images(year: Optional[int] = None, query: Optional[str] = None) -> List[LocalImageEntry]:
96
+ """Scan all library roots for images."""
97
  ensure_root()
98
  results: List[LocalImageEntry] = []
99
+ seen_paths: set[str] = set()
 
 
 
100
  q = (query or "").strip().lower()
 
 
 
 
 
 
101
 
102
+ for root in get_library_roots():
103
+ for r, y, path in _iter_image_files(root, year=year):
104
+ rel = path.relative_to(r).as_posix()
105
+ if rel in seen_paths:
106
  continue
 
107
  if q and q not in rel.lower():
108
  continue
109
  try:
110
  size = path.stat().st_size
111
  except OSError:
112
  continue
113
+ seen_paths.add(rel)
114
  results.append(
115
  LocalImageEntry(
116
  path=rel,
117
+ root=r,
118
  year=y,
119
  filename=path.name,
120
  file_size_bytes=size,
 
124
 
125
 
126
  def entry_to_dict(entry: LocalImageEntry, include_meta: bool = False) -> dict:
127
+ encoded_path = quote(entry.path, safe="/")
128
  out = {
129
  "path": entry.path,
130
  "year": entry.year,
131
  "filename": entry.filename,
132
  "fileSizeBytes": entry.file_size_bytes,
133
+ "thumbUrl": f"/api/dda/local/thumb?path={encoded_path}",
134
  "source": "local_folder",
135
+ "rootPath": str(entry.root),
136
  }
137
  if include_meta:
138
  try:
 
157
  def get_or_build_thumb(relative_path: str, max_side: int = 256) -> Path:
158
  full = safe_resolve(relative_path)
159
  cache = thumb_cache_path(relative_path)
160
+ if cache.exists() and cache.stat().st_mtime >= full.stat().st_mtime:
161
  return cache
162
  cache.parent.mkdir(parents=True, exist_ok=True)
163
  raster_to_preview_png(full, cache, max_side=max_side)
 
165
 
166
 
167
  def ensure_root() -> None:
168
+ for root in get_library_roots():
169
+ root.mkdir(parents=True, exist_ok=True)
170
  LOCAL_THUMB_CACHE.mkdir(parents=True, exist_ok=True)
171
+
172
+
173
+ def library_debug_info() -> dict:
174
+ """Diagnostics for troubleshooting missing images."""
175
+ roots_info = []
176
+ for root in get_library_roots():
177
+ info = {
178
+ "path": str(root),
179
+ "exists": root.exists(),
180
+ "years": [],
181
+ "otherFiles": [],
182
+ }
183
+ if root.exists():
184
+ for entry in sorted(root.iterdir()):
185
+ if entry.is_dir() and _is_year_dir(entry.name):
186
+ files = []
187
+ for _, _, p in _iter_image_files(root, year=int(entry.name)):
188
+ files.append({"name": p.name, "size": p.stat().st_size})
189
+ info["years"].append({"year": entry.name, "files": files})
190
+ elif entry.is_file() and entry.name not in ("README.md", ".gitkeep"):
191
+ info["otherFiles"].append(entry.name)
192
+ roots_info.append(info)
193
+ return {
194
+ "roots": roots_info,
195
+ "allowedExtensions": sorted(ALLOWED_EXTENSIONS),
196
+ "totalImages": len(scan_images()),
197
+ }
app/dda/local_routes.py CHANGED
@@ -4,10 +4,11 @@ from typing import Optional
4
  from fastapi import APIRouter, HTTPException, Query
5
  from fastapi.responses import FileResponse
6
 
7
- from .config import IS_DDA_MODE, LOCAL_LIBRARY_ROOT, geotiff_io_available
8
  from .local_library import (
9
  entry_to_dict,
10
  get_or_build_thumb,
 
11
  safe_resolve,
12
  scan_images,
13
  scan_years,
@@ -24,21 +25,30 @@ def _require_dda():
24
  @router.get("/local/config")
25
  def local_library_config():
26
  _require_dda()
 
27
  return {
28
  "source": "local_folder",
29
- "rootPath": str(LOCAL_LIBRARY_ROOT),
 
30
  "instructions": (
31
- "Copy .tif / .tiff images into year folders under library_sources/ "
32
- "(e.g. library_sources/2025/my_image.tif), then click Refresh."
 
33
  ),
34
  "geotiffEnabled": geotiff_io_available(),
35
  }
36
 
37
 
 
 
 
 
 
 
38
  @router.get("/local/years")
39
  def local_years():
40
  _require_dda()
41
- return {"years": scan_years(), "rootPath": str(LOCAL_LIBRARY_ROOT)}
42
 
43
 
44
  @router.get("/local/images")
@@ -82,4 +92,4 @@ def local_rescan():
82
  _require_dda()
83
  years = scan_years()
84
  total = sum(y["imageCount"] for y in years)
85
- return {"ok": True, "years": years, "totalImages": total, "rootPath": str(LOCAL_LIBRARY_ROOT)}
 
4
  from fastapi import APIRouter, HTTPException, Query
5
  from fastapi.responses import FileResponse
6
 
7
+ from .config import IS_DDA_MODE, geotiff_io_available, get_library_roots
8
  from .local_library import (
9
  entry_to_dict,
10
  get_or_build_thumb,
11
+ library_debug_info,
12
  safe_resolve,
13
  scan_images,
14
  scan_years,
 
25
  @router.get("/local/config")
26
  def local_library_config():
27
  _require_dda()
28
+ roots = [str(r) for r in get_library_roots()]
29
  return {
30
  "source": "local_folder",
31
+ "rootPath": roots[0] if roots else "",
32
+ "rootPaths": roots,
33
  "instructions": (
34
+ "Copy .tif / .tiff images into library_sources/YEAR/ inside the project folder "
35
+ "(e.g. change_detection_webapp/library_sources/2025/), then click Refresh. "
36
+ "Run locally with: python run.py (DDA mode is enabled automatically)."
37
  ),
38
  "geotiffEnabled": geotiff_io_available(),
39
  }
40
 
41
 
42
+ @router.get("/local/debug")
43
+ def local_debug():
44
+ _require_dda()
45
+ return library_debug_info()
46
+
47
+
48
  @router.get("/local/years")
49
  def local_years():
50
  _require_dda()
51
+ return {"years": scan_years(), "rootPaths": [str(r) for r in get_library_roots()]}
52
 
53
 
54
  @router.get("/local/images")
 
92
  _require_dda()
93
  years = scan_years()
94
  total = sum(y["imageCount"] for y in years)
95
+ return {"ok": True, "years": years, "totalImages": total, "rootPaths": [str(r) for r in get_library_roots()]}
library_sources/README.md CHANGED
@@ -22,10 +22,25 @@ library_sources/
22
 
23
  ## How to use
24
 
25
- 1. Copy your images into the correct **year** folder (e.g. `library_sources/2025/`).
26
- 2. Run the app (`python run.py` or open the dev Space).
27
- 3. Open **Image Library** → click **Refresh** if you added files while the app was running.
28
- 4. Select a year in the sidebar to view images.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  ## Large files
31
 
 
22
 
23
  ## How to use
24
 
25
+ 1. Copy your images into the correct **year** folder:
26
+ ```
27
+ change_detection_webapp/library_sources/2025/your_image.tif
28
+ change_detection_webapp/library_sources/2026/other_image.tif
29
+ ```
30
+ 2. Start the app locally: `python run.py` (DDA mode is enabled automatically).
31
+ 3. Open **Image Library** → click **Refresh**.
32
+ 4. Select **2025** or **2026** in the sidebar to filter by year.
33
+
34
+ > **Hugging Face dev Space:** large `.tif` files are **not** uploaded via git. Run locally, or copy files into the Space persistent `data/library_sources/` folder.
35
+
36
+ ## Troubleshooting
37
+
38
+ | Problem | Fix |
39
+ |---------|-----|
40
+ | Images not showing | Use `python run.py` locally; click **Refresh** |
41
+ | Wrong page (simple upload UI) | Set `APP_MODE=dda` on HF, or run locally |
42
+ | Filename has spaces | Supported (e.g. `Grid no. 54_ORI-005.tif`) |
43
+ | Check scan | Open `/api/dda/local/debug` in browser |
44
 
45
  ## Large files
46
 
run.py CHANGED
@@ -35,6 +35,9 @@ def main():
35
  if here not in sys.path:
36
  sys.path.insert(0, here)
37
 
 
 
 
38
  try:
39
  import uvicorn
40
  except ImportError:
 
35
  if here not in sys.path:
36
  sys.path.insert(0, here)
37
 
38
+ # Local runs use DDA dev UI + folder library unless already set
39
+ os.environ.setdefault("APP_MODE", "dda")
40
+
41
  try:
42
  import uvicorn
43
  except ImportError:
static/js/dda/app.js CHANGED
@@ -77,10 +77,11 @@ async function initDda() {
77
  if (hint) {
78
  hint.textContent = localCfg.geotiffEnabled ? 'GeoTIFF ready' : 'GeoTIFF limited';
79
  }
 
80
  const pathEl = document.getElementById('lib-path-display');
81
- if (pathEl) pathEl.textContent = localCfg.rootPath || ddaConfig.localLibraryPath || '';
82
  const folderPath = document.getElementById('lib-folder-path');
83
- if (folderPath) folderPath.textContent = 'Folder: library_sources/';
84
 
85
  const yearsData = await ddaApi('GET', '/api/dda/local/years');
86
  localYears = yearsData.years || [];
@@ -108,15 +109,18 @@ async function loadLibraryImages() {
108
  grid.innerHTML = `<p class="dim">No images in ${selectedYear || 'library_sources'}. Copy .tif files into <code>library_sources/${selectedYear || 'YEAR'}/</code> and click Refresh.</p>`;
109
  return;
110
  }
111
- grid.innerHTML = items.map((img) => `
112
- <div class="dda-card-img" draggable="true" data-image-path="${img.path}" title="${img.filename}">
113
- ${img.thumbUrl ? `<img src="${img.thumbUrl}" alt="" loading="lazy" />` : '<div class="meta">No preview</div>'}
 
 
114
  <div class="meta">
115
  <strong>${img.year}</strong><br/>
116
  ${img.filename}<br/>
117
  <span class="dim">${formatBytes(img.fileSizeBytes)}</span>
118
  </div>
119
- </div>`).join('');
 
120
  grid.querySelectorAll('.dda-card-img').forEach((card) => {
121
  card.addEventListener('dragstart', (e) => {
122
  e.dataTransfer.setData('application/x-dda-image-path', card.dataset.imagePath);
 
77
  if (hint) {
78
  hint.textContent = localCfg.geotiffEnabled ? 'GeoTIFF ready' : 'GeoTIFF limited';
79
  }
80
+ const paths = (localCfg.rootPaths || [localCfg.rootPath || ddaConfig.localLibraryPaths?.[0]]).filter(Boolean);
81
  const pathEl = document.getElementById('lib-path-display');
82
+ if (pathEl) pathEl.textContent = paths.join('\n');
83
  const folderPath = document.getElementById('lib-folder-path');
84
+ if (folderPath) folderPath.textContent = paths.length ? `Scanning: ${paths[0]}` : '';
85
 
86
  const yearsData = await ddaApi('GET', '/api/dda/local/years');
87
  localYears = yearsData.years || [];
 
109
  grid.innerHTML = `<p class="dim">No images in ${selectedYear || 'library_sources'}. Copy .tif files into <code>library_sources/${selectedYear || 'YEAR'}/</code> and click Refresh.</p>`;
110
  return;
111
  }
112
+ grid.innerHTML = items.map((img) => {
113
+ const thumb = img.thumbUrl ? img.thumbUrl.replace(/path=[^&]+/, 'path=' + encodeURIComponent(img.path)) : '';
114
+ return `
115
+ <div class="dda-card-img" draggable="true" data-image-path="${img.path.replace(/"/g, '&quot;')}" title="${img.filename}">
116
+ ${thumb ? `<img src="${thumb}" alt="" loading="lazy" />` : '<div class="meta">No preview</div>'}
117
  <div class="meta">
118
  <strong>${img.year}</strong><br/>
119
  ${img.filename}<br/>
120
  <span class="dim">${formatBytes(img.fileSizeBytes)}</span>
121
  </div>
122
+ </div>`;
123
+ }).join('');
124
  grid.querySelectorAll('.dda-card-img').forEach((card) => {
125
  card.addEventListener('dragstart', (e) => {
126
  e.dataTransfer.setData('application/x-dda-image-path', card.dataset.imagePath);
templates/index_dda.html CHANGED
@@ -88,7 +88,7 @@
88
  </section>
89
  </div>
90
 
91
- <script src="/static/js/dda/app.js?v=3"></script>
92
- <script src="/static/js/dda/library.js?v=3"></script>
93
  </body>
94
  </html>
 
88
  </section>
89
  </div>
90
 
91
+ <script src="/static/js/dda/app.js?v=4"></script>
92
+ <script src="/static/js/dda/library.js?v=4"></script>
93
  </body>
94
  </html>