diff --git a/.dockerignore b/.dockerignore old mode 100644 new mode 100755 diff --git a/.env.example b/.env.example old mode 100644 new mode 100755 diff --git a/.gitattributes b/.gitattributes old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index 6d39070..aa8501e --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ .public-release-local-patterns data/config/config.json data/config/APPINFO.json +docker-compose_old.yml # Runtime and user data data/postgres/* diff --git a/.public-release-local-patterns.example b/.public-release-local-patterns.example old mode 100644 new mode 100755 diff --git a/APPINFO.example.json b/APPINFO.example.json old mode 100644 new mode 100755 diff --git a/APPINFO.json b/APPINFO.json old mode 100644 new mode 100755 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md old mode 100644 new mode 100755 diff --git a/Dockerfile b/Dockerfile old mode 100644 new mode 100755 diff --git a/LICENSE.txt b/LICENSE.txt old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 27060e3..370daba --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# AssetManager 0.5.5.65 +# AssetManager 0.5.5.68 AssetManager is a self-hosted web application for managing IT equipment and other organizational assets. It provides asset inventory, software inventory, remote job execution, reporting, privacy/retention documentation, and optional integration with MeshCentral. diff --git a/SECURITY.md b/SECURITY.md old mode 100644 new mode 100755 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md old mode 100644 new mode 100755 diff --git a/VERSION b/VERSION old mode 100644 new mode 100755 index f8c741a..5e110a5 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.5.65 +0.5.5.68 diff --git a/app/__init__.py b/app/__init__.py old mode 100644 new mode 100755 diff --git a/app/backup.py b/app/backup.py old mode 100644 new mode 100755 diff --git a/app/config.py b/app/config.py old mode 100644 new mode 100755 diff --git a/app/database.py b/app/database.py old mode 100644 new mode 100755 diff --git a/app/fields.py b/app/fields.py old mode 100644 new mode 100755 diff --git a/app/i18n.py b/app/i18n.py old mode 100644 new mode 100755 index ffbc862..0c01d3c --- a/app/i18n.py +++ b/app/i18n.py @@ -201,6 +201,7 @@ BASE_TRANSLATIONS = { "lists.row_number": ("Row number", "Zeilennummer"), "lists.row_number_resize": ("Drag to change the row-number column width", "Ziehen, um die Breite der Nummernspalte zu ändern"), "lists.column_resize": ("Drag to change the column width", "Ziehen, um die Spaltenbreite zu ändern"), +"lists.multi_value_filter_hint": ("Paste multiple search values one per line. Values within one column are OR-linked.", "Mehrere Suchwerte können zeilenweise eingefügt werden. Innerhalb einer Spalte gilt ODER."), "appearance.colors_title": ("System colors", "Systemfarben"), "appearance.colors_help": ("Configure the central colors used throughout the application.", "Konfigurieren Sie die zentral im gesamten System verwendeten Farben."), "appearance.primary_color": ("Primary color", "Primärfarbe"), @@ -1314,6 +1315,15 @@ BASE_TRANSLATIONS.update({ "software.compare.comparison_name": ("Comparison name", "Vergleichsname"), "software.compare.status": ("Comparison status", "Vergleichsstatus"), "software.compare.empty": ("No software inventory is available for these assets.", "Für diese Assets sind keine Softwareinventuren vorhanden."), + "software.compare.select_second_title": ("Select second asset", "Zweites Asset auswählen"), + "software.compare.select_second_help": ("Choose the second asset for the comparison. The selection shows only values that actually exist for the selected field.", "Wähle das zweite Asset für den Vergleich. Das Auswahlfeld zeigt nur tatsächlich vorhandene Werte des gewählten Feldes."), + "software.compare.first_asset": ("First asset", "Erstes Asset"), + "software.compare.select_by": ("Select by", "Auswahl nach"), + "software.compare.second_asset": ("Second asset", "Zweites Asset"), + "software.compare.choose_value": ("Select value …", "Wert auswählen …"), + "software.compare.no_selectable_values": ("No matching values available", "Keine passenden Werte vorhanden"), + "software.compare.available_values": ("{count} values available", "{count} Werte verfügbar"), + "software.compare.load_error": ("Selection could not be loaded", "Auswahl konnte nicht geladen werden"), }) BASE_TRANSLATIONS.update({ diff --git a/app/job_state.py b/app/job_state.py old mode 100644 new mode 100755 diff --git a/app/main.py b/app/main.py old mode 100644 new mode 100755 index d53c08a..47a7ada --- a/app/main.py +++ b/app/main.py @@ -1840,21 +1840,41 @@ def save_upload(upload: UploadFile | None) -> str | None: return f"/static/uploads/{filename}" -def _standard_image_url(filename: str) -> str: - return "/static/uploads/library/" + urllib.parse.quote(filename) +def _standard_image_url(relative_path: str | Path) -> str: + """Build a public URL for a file inside the persistent image library.""" + relative = Path(relative_path) + encoded = "/".join(urllib.parse.quote(part, safe="") for part in relative.parts) + return "/static/uploads/library/" + encoded def list_standard_images() -> list[dict[str, str]]: - """Return reusable images stored in the persistent standard image library.""" + """Read reusable images directly from the persistent filesystem library. + + The directory is scanned on every form request so files copied to the Docker + host become available after a browser reload. Subdirectories are supported + as well; symlinks are ignored so the library cannot escape its configured + root directory. + """ images: list[dict[str, str]] = [] try: - candidates = sorted(STANDARD_IMAGE_DIR.iterdir(), key=lambda item: item.name.casefold()) + candidates = sorted( + STANDARD_IMAGE_DIR.rglob("*"), + key=lambda item: item.relative_to(STANDARD_IMAGE_DIR).as_posix().casefold(), + ) except OSError: return images + for path in candidates: - if not path.is_file() or path.suffix.lower() not in STANDARD_IMAGE_EXTENSIONS: + try: + if path.is_symlink() or not path.is_file() or path.suffix.lower() not in STANDARD_IMAGE_EXTENSIONS: + continue + relative = path.relative_to(STANDARD_IMAGE_DIR) + except (OSError, ValueError): continue - images.append({"name": path.name, "path": _standard_image_url(path.name)}) + images.append({ + "name": relative.as_posix(), + "path": _standard_image_url(relative), + }) return images @@ -2313,7 +2333,11 @@ def _seed_default_charts(db: Session) -> None: @app.get("/") def dashboard(request: Request, db: Session = Depends(get_db)): - categories = db.query(Category).options(joinedload(Category.visible_fields)).all() + categories = _sort_localized( + db.query(Category).options(joinedload(Category.visible_fields)).all(), + "name", + request, + ) category_asset_counts = dict( _apply_asset_access( db.query(Asset.category_id, func.count(Asset.id)), @@ -2639,13 +2663,161 @@ def _filter_assets_by_job_states( return filtered_assets, views +def _software_compare_field_options(db: Session, request: Request) -> list[dict[str, str]]: + """Return scalar, filterable asset fields that can identify a comparison asset.""" + definitions = ( + db.query(FieldDefinition) + .filter( + FieldDefinition.is_active.is_(True), + FieldDefinition.filterable.is_(True), + ) + .order_by(FieldDefinition.sort_order, func.lower(FieldDefinition.label), FieldDefinition.id) + .all() + ) + language_code = _request_language(request) + own = translation_dictionary(db, language_code) + english = translation_dictionary(db, "en") if language_code != "en" else own + options: list[dict[str, str]] = [] + for definition in definitions: + if definition.data_type in {"long_text", "json"}: + continue + if definition.field_name == "parent_asset_id": + continue + key = definition.translation_key or f"field.{definition.field_name}" + label = own.get(key) or english.get(key) or definition.label or definition.field_name + options.append({"field_name": definition.field_name, "label": label}) + + options.sort( + key=lambda item: ( + 0 if item["field_name"] == "name" else 1, + _localized_text_key(item["label"], language_code), + item["field_name"], + ) + ) + return options + + +def _software_compare_custom_value(row: AssetFieldValue, definition: FieldDefinition) -> Any: + if definition.data_type == "boolean": + return row.value_boolean + if definition.data_type == "datetime": + return row.value_datetime + if definition.data_type == "date": + return row.value_date + if definition.data_type in {"integer", "decimal"}: + return row.value_number + if definition.data_type == "json": + return row.value_json + return row.value_text + + +def _software_compare_display_value(request: Request, value: Any, definition: FieldDefinition) -> str: + if value is None: + return "" + if definition.data_type == "boolean": + return _translate_request(request, "common.yes", "Yes") if bool(value) else _translate_request(request, "common.no", "No") + if isinstance(value, datetime): + return value.isoformat(timespec="seconds") + if isinstance(value, date): + return value.isoformat() + if isinstance(value, (dict, list)): + return json.dumps(value, ensure_ascii=False, sort_keys=True) + return str(value).strip() + + +@app.get("/api/assets/software-compare/options") +def assets_software_compare_options( + request: Request, + field_name: str = "name", + exclude_asset_id: int | None = None, + db: Session = Depends(get_db), +): + """Return accessible second-asset choices for the requested selection field.""" + _require_admin(request) + definition = ( + db.query(FieldDefinition) + .filter( + FieldDefinition.field_name == field_name, + FieldDefinition.is_active.is_(True), + FieldDefinition.filterable.is_(True), + ) + .first() + ) + if not definition or definition.data_type in {"long_text", "json"} or definition.field_name == "parent_asset_id": + raise HTTPException(400, "Unsupported comparison selection field") + + query = _apply_asset_access(db.query(Asset), request) + if exclude_asset_id is not None: + query = query.filter(Asset.id != exclude_asset_id) + assets = query.order_by(func.lower(Asset.name), Asset.id).all() + if not assets: + return {"field_name": field_name, "options": []} + + values_by_asset: dict[int, Any] = {} + if definition.is_system and hasattr(Asset, definition.field_name): + for asset in assets: + values_by_asset[asset.id] = getattr(asset, definition.field_name, None) + else: + rows = ( + db.query(AssetFieldValue) + .filter( + AssetFieldValue.field_definition_id == definition.id, + AssetFieldValue.asset_id.in_([asset.id for asset in assets]), + ) + .all() + ) + for row in rows: + values_by_asset[row.asset_id] = _software_compare_custom_value(row, definition) + + candidates: list[dict[str, Any]] = [] + counts: dict[str, int] = {} + for asset in assets: + display_value = _software_compare_display_value( + request, + values_by_asset.get(asset.id), + definition, + ) + if not display_value: + continue + normalized = _normalize_filter_text(display_value) + counts[normalized] = counts.get(normalized, 0) + 1 + candidates.append({ + "asset_id": asset.id, + "asset_name": asset.name, + "value": display_value, + "normalized": normalized, + }) + + language_code = _request_language(request) + candidates.sort( + key=lambda item: ( + _localized_text_key(item["value"], language_code), + _localized_text_key(item["asset_name"], language_code), + item["asset_id"], + ) + ) + options = [] + for item in candidates: + label = item["value"] + if counts.get(item["normalized"], 0) > 1: + label = f"{label} — {item['asset_name']}" + options.append({ + "asset_id": item["asset_id"], + "value": item["value"], + "label": label, + "asset_name": item["asset_name"], + }) + return {"field_name": field_name, "options": options} + + @app.get("/assets") def assets_list(request: Request, category_id: int | None = None, db: Session = Depends(get_db)): - categories = ( + categories = _sort_localized( db.query(Category) .options(selectinload(Category.visible_fields).joinedload(CategoryField.definition)) - .order_by(Category.name) - .all() + .all(), + "name", + request, ) requested_exclusions = request.query_params.getlist("job_filter_exclude") @@ -2715,6 +2887,7 @@ def assets_list(request: Request, category_id: int | None = None, db: Session = "job_filter_statuses": ASSET_JOB_FILTER_STATUSES, "job_state_by_asset": job_state_by_asset, "asset_platforms": {asset.id: detect_platform(asset) for asset in assets}, + "compare_field_options": _software_compare_field_options(db, request) if _is_admin(request) else [], }, ) @@ -2949,7 +3122,11 @@ def chart_delete(chart_id: int, request: Request, db: Session = Depends(get_db)) @app.get("/assets/new") def asset_new(request: Request, category_id: int | None = None, db: Session = Depends(get_db)): _require_admin(request) - categories = db.query(Category).options(joinedload(Category.visible_fields)).all() + categories = _sort_localized( + db.query(Category).options(joinedload(Category.visible_fields)).all(), + "name", + request, + ) selected = next((c for c in categories if c.id == category_id), categories[0] if categories else None) return templates.TemplateResponse("asset_form.html", {"request": request, "asset": None, "categories": categories, "selected": selected, "fields": ASSET_FIELDS, "status_options": _active_status_options(db, request), "duplicate_mode": False, "parent_candidates": db.query(Asset).order_by(Asset.name).all(), "custom_values": {}, "standard_images": list_standard_images()}) @@ -2960,7 +3137,11 @@ def asset_duplicate(asset_id: int, request: Request, db: Session = Depends(get_d original = db.query(Asset).options(joinedload(Asset.category).joinedload(Category.visible_fields)).filter(Asset.id == asset_id).first() if not original: raise HTTPException(404, "Asset nicht gefunden") - categories = db.query(Category).options(joinedload(Category.visible_fields)).all() + categories = _sort_localized( + db.query(Category).options(joinedload(Category.visible_fields)).all(), + "name", + request, + ) class DuplicateValues: pass duplicate = DuplicateValues() @@ -3431,7 +3612,7 @@ def asset_tree( or bool(by_parent.get(asset.id)) ] - categories = db.query(Category).order_by(Category.name).all() + categories = _sort_localized(db.query(Category).all(), "name", request) actual_roots = [ asset for asset in all_assets if asset.parent_asset_id is None or asset.parent_asset_id not in all_by_id @@ -3821,7 +4002,11 @@ def asset_mesh_inventory_json(asset_id: int, request: Request, db: Session = Dep def asset_edit(asset_id: int, request: Request, category_id: int | None = None, db: Session = Depends(get_db)): _require_admin(request) asset = db.query(Asset).options(joinedload(Asset.category).joinedload(Category.visible_fields)).filter(Asset.id == asset_id).first() - categories = db.query(Category).options(joinedload(Category.visible_fields)).all() + categories = _sort_localized( + db.query(Category).options(joinedload(Category.visible_fields)).all(), + "name", + request, + ) if not asset: raise HTTPException(404, "Asset nicht gefunden") selected = next((category for category in categories if category.id == category_id), None) if category_id else asset.category @@ -4429,11 +4614,12 @@ def status_option_delete(option_id: int, request: Request, db: Session = Depends @app.get("/categories") def categories_list(request: Request, db: Session = Depends(get_db)): _require_admin(request) - categories = ( + categories = _sort_localized( db.query(Category) .options(selectinload(Category.visible_fields)) - .order_by(Category.name) - .all() + .all(), + "name", + request, ) asset_counts = dict( db.query(Asset.category_id, func.count(Asset.id)) @@ -5115,7 +5301,7 @@ _SOFTWARE_JOB_FILTER_KEYS = ( def _software_job_filter_values(request: Request) -> dict[str, str]: """Read and normalize the server-side filters of the recent-job table.""" return { - key: str(request.query_params.get(f"job_filter_{key}") or "").strip()[:200] + key: str(request.query_params.get(f"job_filter_{key}") or "").strip()[:12000] for key in _SOFTWARE_JOB_FILTER_KEYS } @@ -5240,115 +5426,157 @@ def _software_job_date_filter(column: Any, search_value: str): return or_(*conditions) +def _multi_filter_terms(value: Any, *, max_terms: int = 200) -> list[str]: + """Split a table filter into unique non-empty line-based OR terms.""" + terms: list[str] = [] + seen: set[str] = set() + for raw in str(value or "").replace("\r\n", "\n").replace("\r", "\n").split("\n"): + term = raw.strip() + if not term: + continue + key = term.casefold() + if key in seen: + continue + seen.add(key) + terms.append(term[:300]) + if len(terms) >= max_terms: + break + return terms + + def _apply_software_job_filters(query: Any, request: Request, filters: dict[str, str]): """Apply all recent-job filters before LIMIT is evaluated. - This keeps an old job searchable even when the visible result size is only - 25 rows. The large script, result and log columns are still excluded from - the final SELECT by ``load_only`` below. + Multiple lines within one column are OR-linked. Different columns remain + AND-linked. This allows users to paste a column copied from Excel directly + into a table filter without changing the normal single-value behavior. """ parameters_text = cast(SoftwareJob.parameters, String) - if filters["job_id"]: - value = filters["job_id"] - if value.isdigit(): - query = query.filter(SoftwareJob.id == int(value)) - else: - query = query.filter(_sql_text_contains(SoftwareJob.id, value)) - - if filters["created_at"]: - query = query.filter( - _software_job_date_filter(SoftwareJob.created_at, filters["created_at"]) - ) - - if filters["asset"]: - query = query.filter(_sql_text_contains(Asset.name, filters["asset"])) - - if filters["job_type"]: - value = filters["job_type"] - conditions = [ - _sql_text_contains(SoftwareJob.job_type, value), - _sql_text_contains(parameters_text, value), - ] - job_type_codes = _translated_filter_codes( - request, - "jobs.type", - ("software_inventory", "job_definition"), - value, - ) - if job_type_codes: - conditions.append(SoftwareJob.job_type.in_(job_type_codes)) - job_kind_codes = _translated_filter_codes( - request, - "jobdefs.kind", - ("script", "registry"), - value, - ) - for kind in job_kind_codes: - conditions.append( - and_( - SoftwareJob.job_type == "job_definition", - _sql_text_contains(parameters_text, kind), - ) - ) + terms = _multi_filter_terms(filters["job_id"]) + if terms: + conditions = [] + for value in terms: + if value.isdigit(): + conditions.append(SoftwareJob.id == int(value)) + else: + conditions.append(_sql_text_contains(SoftwareJob.id, value)) query = query.filter(or_(*conditions)) - if filters["package"]: - value = filters["package"] - query = query.filter( + terms = _multi_filter_terms(filters["created_at"]) + if terms: + query = query.filter(or_(*[ + _software_job_date_filter(SoftwareJob.created_at, value) + for value in terms + ])) + + terms = _multi_filter_terms(filters["asset"]) + if terms: + query = query.filter(or_(*[ + _sql_text_contains(Asset.name, value) + for value in terms + ])) + + terms = _multi_filter_terms(filters["job_type"]) + if terms: + term_conditions = [] + for value in terms: + conditions = [ + _sql_text_contains(SoftwareJob.job_type, value), + _sql_text_contains(parameters_text, value), + ] + job_type_codes = _translated_filter_codes( + request, + "jobs.type", + ("software_inventory", "job_definition"), + value, + ) + if job_type_codes: + conditions.append(SoftwareJob.job_type.in_(job_type_codes)) + job_kind_codes = _translated_filter_codes( + request, + "jobdefs.kind", + ("script", "registry"), + value, + ) + for kind in job_kind_codes: + conditions.append( + and_( + SoftwareJob.job_type == "job_definition", + _sql_text_contains(parameters_text, kind), + ) + ) + term_conditions.append(or_(*conditions)) + query = query.filter(or_(*term_conditions)) + + terms = _multi_filter_terms(filters["package"]) + if terms: + query = query.filter(or_(*[ or_( _sql_text_contains(SoftwarePackage.name, value), _sql_text_contains(parameters_text, value), ) - ) + for value in terms + ])) - if filters["action"]: - value = filters["action"] - conditions = [_sql_text_contains(SoftwareJob.action, value)] - action_codes = _translated_filter_codes( - request, - "jobs.action", - ("run", "install", "uninstall", "update"), - value, - ) - if action_codes: - conditions.append(SoftwareJob.action.in_(action_codes)) + terms = _multi_filter_terms(filters["action"]) + if terms: + term_conditions = [] + for value in terms: + conditions = [_sql_text_contains(SoftwareJob.action, value)] + action_codes = _translated_filter_codes( + request, + "jobs.action", + ("run", "install", "uninstall", "update"), + value, + ) + if action_codes: + conditions.append(SoftwareJob.action.in_(action_codes)) + term_conditions.append(or_(*conditions)) + query = query.filter(or_(*term_conditions)) + + terms = _multi_filter_terms(filters["priority"]) + if terms: + conditions = [] + for value in terms: + if re.fullmatch(r"[-+]?\d+", value): + conditions.append(SoftwareJob.priority == int(value)) + else: + conditions.append(_sql_text_contains(SoftwareJob.priority, value)) query = query.filter(or_(*conditions)) - if filters["priority"]: - value = filters["priority"] - if re.fullmatch(r"[-+]?\d+", value): - query = query.filter(SoftwareJob.priority == int(value)) - else: - query = query.filter(_sql_text_contains(SoftwareJob.priority, value)) + terms = _multi_filter_terms(filters["platform"]) + if terms: + query = query.filter(or_(*[ + _sql_text_contains(SoftwareJob.platform, value) + for value in terms + ])) - if filters["platform"]: - query = query.filter( - _sql_text_contains(SoftwareJob.platform, filters["platform"]) - ) - - if filters["status"]: - value = filters["status"] - conditions = [_sql_text_contains(SoftwareJob.status, value)] - status_codes = _translated_filter_codes( - request, - "software.status", - ( - "created", - "sending", - "sent", - "running", - "success", - "partial", - "failed", - "timeout", - "cancelled", - ), - value, - ) - if status_codes: - conditions.append(SoftwareJob.status.in_(status_codes)) - query = query.filter(or_(*conditions)) + terms = _multi_filter_terms(filters["status"]) + if terms: + term_conditions = [] + for value in terms: + conditions = [_sql_text_contains(SoftwareJob.status, value)] + status_codes = _translated_filter_codes( + request, + "software.status", + ( + "created", + "sending", + "sent", + "running", + "success", + "partial", + "failed", + "timeout", + "cancelled", + ), + value, + ) + if status_codes: + conditions.append(SoftwareJob.status.in_(status_codes)) + term_conditions.append(or_(*conditions)) + query = query.filter(or_(*term_conditions)) return query @@ -7269,7 +7497,7 @@ async def translations_import(request: Request, file: UploadFile = File(...), ig @app.get("/settings/appearance") def settings_appearance_page(request: Request, db: Session = Depends(get_db)): _require_admin(request) - categories = db.query(Category).order_by(Category.name.asc()).all() + categories = _sort_localized(db.query(Category).all(), "name", request) return templates.TemplateResponse( "settings.html", { @@ -7286,7 +7514,7 @@ def settings_appearance_page(request: Request, db: Session = Depends(get_db)): @app.get("/settings/authentication") def settings_authentication_page(request: Request, db: Session = Depends(get_db)): _require_admin(request) - categories = db.query(Category).order_by(Category.name.asc()).all() + categories = _sort_localized(db.query(Category).all(), "name", request) return templates.TemplateResponse( "settings.html", { @@ -7303,7 +7531,7 @@ def settings_authentication_page(request: Request, db: Session = Depends(get_db) @app.get("/settings/system") def settings_system_page(request: Request, db: Session = Depends(get_db)): _require_admin(request) - categories = db.query(Category).order_by(Category.name.asc()).all() + categories = _sort_localized(db.query(Category).all(), "name", request) return templates.TemplateResponse( "settings.html", { @@ -8050,7 +8278,7 @@ def settings_backup_restore(request: Request, backup_name: str, confirmation: st @app.get("/settings") def settings_page(request: Request, db: Session = Depends(get_db)): _require_admin(request) - categories = db.query(Category).order_by(Category.name.asc()).all() + categories = _sort_localized(db.query(Category).all(), "name", request) return templates.TemplateResponse( "settings.html", { @@ -8217,7 +8445,7 @@ def meshcentral_sync_page(request: Request, db: Session = Depends(get_db)): missing = db.query(Asset).filter(Asset.mesh_sync_status == "missing").count() definitions = db.query(FieldDefinition).filter(FieldDefinition.is_active.is_(True)).order_by(FieldDefinition.sort_order, FieldDefinition.label).all() mappings = db.query(MeshFieldMapping).order_by(MeshFieldMapping.priority, MeshFieldMapping.id).all() - categories = db.query(Category).order_by(Category.name.asc()).all() + categories = _sort_localized(db.query(Category).all(), "name", request) return templates.TemplateResponse("meshcentral_sync.html", { "request": request, "runs": runs, "config": public_config(), "linked": linked, "conflicts": conflicts, "missing": missing, diff --git a/app/meshcentral.py b/app/meshcentral.py old mode 100644 new mode 100755 diff --git a/app/migrations.py b/app/migrations.py old mode 100644 new mode 100755 diff --git a/app/models.py b/app/models.py old mode 100644 new mode 100755 diff --git a/app/presence.py b/app/presence.py old mode 100644 new mode 100755 diff --git a/app/presence_state.py b/app/presence_state.py old mode 100644 new mode 100755 diff --git a/app/privacy.py b/app/privacy.py old mode 100644 new mode 100755 diff --git a/app/privacy_retention.py b/app/privacy_retention.py old mode 100644 new mode 100755 diff --git a/app/software_control.py b/app/software_control.py old mode 100644 new mode 100755 diff --git a/app/static/app.css b/app/static/app.css old mode 100644 new mode 100755 diff --git a/app/static/css/app.css b/app/static/css/app.css old mode 100644 new mode 100755 index 17d20c7..3942811 --- a/app/static/css/app.css +++ b/app/static/css/app.css @@ -5,7 +5,7 @@ .table-scroll{overflow:auto}.table-hint{color:#667;font-size:13px}.multiline{white-space:pre-line;min-width:140px}#asset-table th[draggable=true]{cursor:grab;user-select:none}#asset-table th.dragging{opacity:.45}#asset-table{min-width:900px} @media(max-width:700px){main{padding:0 10px}} -.button-secondary{background:#546675}.column-filter-row th{padding:6px;background:#f5f7f9}.column-filter-row input{margin:0;min-width:110px;padding:6px;font-size:13px}.sortable-column{cursor:pointer;user-select:none;padding-right:24px;position:relative}.sortable-column.sort-asc::after{content:"▲";position:absolute;right:7px;font-size:10px}.sortable-column.sort-desc::after{content:"▼";position:absolute;right:7px;font-size:10px}tr[hidden]{display:none}.history-old,.history-new{white-space:pre-wrap} +.button-secondary{background:#546675}.column-filter-row th{padding:6px;background:#f5f7f9}.column-filter-row input,.column-filter-row textarea{margin:0;min-width:110px;padding:6px;font-size:13px}.sortable-column{cursor:pointer;user-select:none;padding-right:24px;position:relative}.sortable-column.sort-asc::after{content:"▲";position:absolute;right:7px;font-size:10px}.sortable-column.sort-desc::after{content:"▼";position:absolute;right:7px;font-size:10px}tr[hidden]{display:none}.history-old,.history-new{white-space:pre-wrap} /* Tabellen: fixierte Kopfzeilen und kompakte Aktionsschaltflächen */ .sticky-table{max-height:calc(100vh - 170px);overflow:auto;border:1px solid #d8dde2;background:#fff} @@ -511,9 +511,17 @@ body.single-scroll-list .sticky-table + .muted{ #asset-table tbody tr.duplicate-hidden{display:none!important} #asset-table tbody tr.duplicate-match td{ background:#fff8d8; + color:#1f2937; } #asset-table tbody tr.duplicate-match:hover td{ background:#fff1ad; + color:#1f2937; +} +html[data-theme="dark"] #asset-table tbody tr.duplicate-match td, +html[data-theme="dark"] #asset-table tbody tr.duplicate-match td a, +html[data-theme="dark"] #asset-table tbody tr.duplicate-match td .muted, +html[data-theme="dark"] #asset-table tbody tr.duplicate-match td small{ + color:#1f2937!important; } @@ -1094,12 +1102,22 @@ body.asset-list-scroll-page .asset-list-page #asset-table-scroll{ display:table-cell !important; } -.column-filter-row input[type="search"]{ +.column-filter-row input[type="search"], +.column-filter-row textarea{ display:block; width:100%; min-width:90px; box-sizing:border-box; } +.column-filter-row textarea{ + height:32px; + min-height:32px; + max-height:96px; + resize:vertical; + line-height:18px; + overflow:auto; + white-space:pre; +} /* Management pages use browser scrolling; avoid clipped sticky rows. */ .management-table-scroll thead tr:first-child th, @@ -1123,7 +1141,8 @@ body.asset-list-scroll-page .asset-list-page #asset-table-scroll{ /* v0.4.0.8: clearly visible active table filters */ -.column-filter-row input[type="search"]{ +.column-filter-row input[type="search"], +.column-filter-row textarea{ transition: background-color .15s ease, border-color .15s ease, @@ -1134,7 +1153,8 @@ body.asset-list-scroll-page .asset-list-page #asset-table-scroll{ background:#fff2c7; } -.column-filter-row input[type="search"].filter-input-active{ +.column-filter-row input[type="search"].filter-input-active, +.column-filter-row textarea.filter-input-active{ background:#fff7d8; border-color:#cf9b16; box-shadow: @@ -1143,7 +1163,8 @@ body.asset-list-scroll-page .asset-list-page #asset-table-scroll{ font-weight:600; } -.column-filter-row input[type="search"].filter-input-active:focus{ +.column-filter-row input[type="search"].filter-input-active:focus, +.column-filter-row textarea.filter-input-active:focus{ background:#fff9e5; border-color:#a87500; box-shadow: @@ -3075,3 +3096,29 @@ table th.table-resizable-column-header > .table-column-resizer:focus-visible::af .image-upload-label{display:flex;flex-direction:column;align-items:flex-start;gap:5px;margin-top:8px} html[data-theme="dark"] .standard-image-library{border-color:#46515d} html[data-theme="dark"] .standard-image-choice{border-color:#46515d} + +/* v0.5.5.67: line-based multi-value table filters and second-asset selector */ +.software-compare-select-dialog{ + width:min(560px,92vw); +} +.software-compare-select-dialog form{ + padding:0; + border:0; +} +.software-compare-select-dialog label{ + display:grid; + gap:6px; +} +.software-compare-select-dialog input, +.software-compare-select-dialog select{ + width:100%; +} +.software-compare-selection-status{ + min-height:1.25em; + margin:0; +} +html[data-theme="dark"] .column-filter-row textarea.filter-input-active{ + background:#493d14; + color:#fff2b3; + border-color:#d4a735; +} diff --git a/app/static/img/manufacturer-logo.png b/app/static/img/manufacturer-logo.png old mode 100644 new mode 100755 diff --git a/app/static/img/meshcentral-device-icons/icons256-1-1.png b/app/static/img/meshcentral-device-icons/icons256-1-1.png old mode 100644 new mode 100755 diff --git a/app/static/img/meshcentral-device-icons/icons256-2-1.png b/app/static/img/meshcentral-device-icons/icons256-2-1.png old mode 100644 new mode 100755 diff --git a/app/static/img/meshcentral-device-icons/icons256-3-1.png b/app/static/img/meshcentral-device-icons/icons256-3-1.png old mode 100644 new mode 100755 diff --git a/app/static/img/meshcentral-device-icons/icons256-4-1.png b/app/static/img/meshcentral-device-icons/icons256-4-1.png old mode 100644 new mode 100755 diff --git a/app/static/img/meshcentral-device-icons/icons256-5-1.png b/app/static/img/meshcentral-device-icons/icons256-5-1.png old mode 100644 new mode 100755 diff --git a/app/static/img/meshcentral-device-icons/icons256-6-1.png b/app/static/img/meshcentral-device-icons/icons256-6-1.png old mode 100644 new mode 100755 diff --git a/app/static/img/meshcentral-device-icons/icons256-7-1.png b/app/static/img/meshcentral-device-icons/icons256-7-1.png old mode 100644 new mode 100755 diff --git a/app/static/img/meshcentral-device-icons/icons256-8-1.png b/app/static/img/meshcentral-device-icons/icons256-8-1.png old mode 100644 new mode 100755 diff --git a/app/static/js/asset-columns.js b/app/static/js/asset-columns.js old mode 100644 new mode 100755 diff --git a/app/static/js/asset-duplicates.js b/app/static/js/asset-duplicates.js old mode 100644 new mode 100755 diff --git a/app/static/js/asset-inventory.js b/app/static/js/asset-inventory.js old mode 100644 new mode 100755 diff --git a/app/static/js/asset-page-boot.js b/app/static/js/asset-page-boot.js old mode 100644 new mode 100755 diff --git a/app/static/js/asset-perf.js b/app/static/js/asset-perf.js old mode 100644 new mode 100755 diff --git a/app/static/js/browser-state.js b/app/static/js/browser-state.js old mode 100644 new mode 100755 index 87d8b38..cc35b09 --- a/app/static/js/browser-state.js +++ b/app/static/js/browser-state.js @@ -24,7 +24,7 @@ } function filterControls(table) { - return [...table.querySelectorAll('thead input, thead select')].filter(control => { + return [...table.querySelectorAll('thead input, thead select, thead textarea')].filter(control => { const type = (control.type || '').toLowerCase(); return !['checkbox', 'radio', 'button', 'submit'].includes(type); }); @@ -44,6 +44,12 @@ try { state = JSON.parse(api.get(key) || 'null'); } catch (_) {} if (state?.field && (state.direction === 'asc' || state.direction === 'desc')) { table.assetApplySort(state.field, state.direction, {silent: true}); + } else if (table.dataset.defaultSortField) { + table.assetApplySort( + table.dataset.defaultSortField, + table.dataset.defaultSortDirection === 'desc' ? 'desc' : 'asc', + {silent: true} + ); } else { table.assetUpdateRowNumbers?.(); } diff --git a/app/static/js/image-editor.js b/app/static/js/image-editor.js old mode 100644 new mode 100755 diff --git a/app/static/js/issue-dialog.js b/app/static/js/issue-dialog.js old mode 100644 new mode 100755 diff --git a/app/static/js/list-tools.js b/app/static/js/list-tools.js old mode 100644 new mode 100755 diff --git a/app/static/js/local-time.js b/app/static/js/local-time.js old mode 100644 new mode 100755 diff --git a/app/static/js/presence.js b/app/static/js/presence.js old mode 100644 new mode 100755 diff --git a/app/static/js/software-inventory-selection.js b/app/static/js/software-inventory-selection.js old mode 100644 new mode 100755 diff --git a/app/static/js/software-job-selection.js b/app/static/js/software-job-selection.js old mode 100644 new mode 100755 diff --git a/app/static/js/software-job-status.js b/app/static/js/software-job-status.js old mode 100644 new mode 100755 diff --git a/app/static/js/table-tools.js b/app/static/js/table-tools.js old mode 100644 new mode 100755 index c0466bb..e591107 --- a/app/static/js/table-tools.js +++ b/app/static/js/table-tools.js @@ -714,9 +714,12 @@ const active = filterDefinitions .map(definition => ({ definition, - term: definition.input?.value.trim() || '' + terms: String(definition.input?.value || '') + .split(/\r?\n/) + .map(term => term.trim()) + .filter(Boolean) })) - .filter(item => item.term); + .filter(item => item.terms.length); if (usesServerFilters) { const serverTotal = Number.parseInt(table.dataset.serverFilterTotal || '', 10); @@ -737,13 +740,16 @@ let visibleRows = 0; [...tbody.rows].forEach(row => { - const hidden = active.some(({ definition, term }) => { + const hidden = active.some(({ definition, terms }) => { const cell = cellForDefinition(row, definition); const value = textValue(cell); - if (definition.numericFilter) { - return !numericFilterMatches(value, term); - } - return !searchTextMatches(value, term); + const matchesAnyTerm = terms.some(term => { + if (definition.numericFilter) { + return numericFilterMatches(value, term); + } + return searchTextMatches(value, term); + }); + return !matchesAnyTerm; }); row.hidden = hidden; @@ -824,11 +830,13 @@ } if (!header.dataset.noFilter && (!usesServerFilters || serverName)) { - const input = document.createElement('input'); - input.type = 'search'; + const input = document.createElement('textarea'); + input.rows = 1; input.placeholder = header.dataset.filterPlaceholder || 'Filtern …'; input.setAttribute('aria-label', `Filter ${textValue(header)}`); + input.title = document.body.dataset.multiValueFilterHint || ''; input.autocomplete = 'off'; + input.spellcheck = false; if (usesServerFilters) { input.name = `job_filter_${serverName}`; input.value = header.dataset.serverFilterValue || ''; @@ -836,18 +844,8 @@ applyFilters(); submitServerFilters(false); }); - input.addEventListener('search', () => { - applyFilters(); - submitServerFilters(false); - }); - input.addEventListener('keydown', event => { - if (event.key !== 'Enter') return; - event.preventDefault(); - submitServerFilters(true); - }); } else { input.addEventListener('input', applyFilters); - input.addEventListener('search', applyFilters); } filterCell.appendChild(input); diff --git a/app/static/js/toasts.js b/app/static/js/toasts.js old mode 100644 new mode 100755 diff --git a/app/static/uploads/.gitkeep b/app/static/uploads/.gitkeep old mode 100644 new mode 100755 diff --git a/app/templates/_chart.html b/app/templates/_chart.html old mode 100644 new mode 100755 diff --git a/app/templates/asset_detail.html b/app/templates/asset_detail.html old mode 100644 new mode 100755 diff --git a/app/templates/asset_form.html b/app/templates/asset_form.html old mode 100644 new mode 100755 diff --git a/app/templates/asset_history.html b/app/templates/asset_history.html old mode 100644 new mode 100755 diff --git a/app/templates/asset_mesh_link.html b/app/templates/asset_mesh_link.html old mode 100644 new mode 100755 diff --git a/app/templates/asset_software.html b/app/templates/asset_software.html old mode 100644 new mode 100755 diff --git a/app/templates/asset_tree.html b/app/templates/asset_tree.html old mode 100644 new mode 100755 diff --git a/app/templates/assets.html b/app/templates/assets.html old mode 100644 new mode 100755 index 3f703e0..8b90574 --- a/app/templates/assets.html +++ b/app/templates/assets.html @@ -99,6 +99,33 @@ {% if selected %}{% endif %} {% endif %} + +
+
+

{{ t('software.compare.select_second_title', 'Zweites Asset auswählen') }}

+ +
+

{{ t('software.compare.select_second_help', 'Wähle das zweite Asset für den Vergleich. Das Auswahlfeld zeigt nur vorhandene Werte des gewählten Feldes.') }}

+ + + +

+
+ + +
+
+
{% endif %}
@@ -120,7 +147,7 @@ {{ t('common.actions', 'Aktionen') }} - {% for a in assets %} + {% for a in assets %} {% set visual_source = a.visual_source or 'auto' %} @@ -387,6 +414,14 @@ const deleteButton = document.getElementById('bulk-delete-button'); const mergeButton = document.getElementById('merge-duplicates-button'); const softwareCompareButton = document.getElementById('software-compare-button'); + const softwareCompareDialog = document.getElementById('software-compare-select-dialog'); + const softwareCompareField = document.getElementById('software-compare-field-select'); + const softwareCompareSecond = document.getElementById('software-compare-second-select'); + const softwareCompareFirstName = document.getElementById('software-compare-first-name'); + const softwareCompareStatus = document.getElementById('software-compare-selection-status'); + const softwareCompareStart = document.getElementById('software-compare-dialog-start'); + const softwareCompareClose = document.getElementById('software-compare-dialog-close'); + const softwareCompareCancel = document.getElementById('software-compare-dialog-cancel'); const jobTypeSelect = document.getElementById('bulk-job-type'); const jobStartButton = document.getElementById('bulk-job-start-button'); const isVisible = box => { @@ -424,8 +459,10 @@ mergeButton.textContent = count >= 2 ? `⧉ {{ t('duplicates.merge') }} (${count})` : '⧉ {{ t('duplicates.merge') }}'; } if (softwareCompareButton) { - softwareCompareButton.disabled = count !== 2; - softwareCompareButton.textContent = count === 2 ? `⇄ {{ t('software.compare.button', 'Software vergleichen') }} (2)` : '⇄ {{ t('software.compare.button', 'Software vergleichen') }}'; + softwareCompareButton.disabled = count < 1 || count > 2; + softwareCompareButton.textContent = (count === 1 || count === 2) + ? `⇄ {{ t('software.compare.button', 'Software vergleichen') }} (${count})` + : '⇄ {{ t('software.compare.button', 'Software vergleichen') }}'; } if (jobTypeSelect) { jobTypeSelect.disabled = count === 0; filterJobDefinitions(); } if (jobStartButton) { @@ -436,13 +473,70 @@ } if (all) { all.checked = visible.length > 0 && count === visible.length; all.indeterminate = count > 0 && count < visible.length; } } - function submit(formId, containerId) { + function submitIds(formId, containerId, ids) { const form = document.getElementById(formId); const container = document.getElementById(containerId); + if (!form || !container) return; container.innerHTML = ''; - selected().forEach(id => { const input = document.createElement('input'); input.type='hidden'; input.name='asset_ids'; input.value=id; container.appendChild(input); }); + ids.forEach(id => { const input = document.createElement('input'); input.type='hidden'; input.name='asset_ids'; input.value=id; container.appendChild(input); }); form.requestSubmit(); } + function submit(formId, containerId) { + submitIds(formId, containerId, selected()); + } + function selectedAssetName(assetId) { + const box = boxes().find(item => item.value === String(assetId)); + return box?.closest('tr')?.dataset.assetName || ''; + } + async function loadSoftwareCompareOptions() { + const ids = selected(); + if (ids.length !== 1 || !softwareCompareField || !softwareCompareSecond) return; + const firstId = ids[0]; + softwareCompareSecond.disabled = true; + softwareCompareSecond.innerHTML = ``; + if (softwareCompareStatus) softwareCompareStatus.textContent = ''; + if (softwareCompareStart) softwareCompareStart.disabled = true; + const target = new URL('/api/assets/software-compare/options', window.location.origin); + target.searchParams.set('field_name', softwareCompareField.value || 'name'); + target.searchParams.set('exclude_asset_id', firstId); + try { + const response = await fetch(target.toString(), {headers: {'Accept': 'application/json'}, cache: 'no-store'}); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const payload = await response.json(); + const options = Array.isArray(payload.options) ? payload.options : []; + softwareCompareSecond.innerHTML = ''; + const placeholder = document.createElement('option'); + placeholder.value = ''; + placeholder.textContent = options.length + ? {{ t('software.compare.choose_value', 'Wert auswählen …')|tojson }} + : {{ t('software.compare.no_selectable_values', 'Keine passenden Werte vorhanden')|tojson }}; + softwareCompareSecond.appendChild(placeholder); + options.forEach(item => { + const option = document.createElement('option'); + option.value = String(item.asset_id); + option.textContent = item.label || item.value || item.asset_name || String(item.asset_id); + option.dataset.assetName = item.asset_name || ''; + softwareCompareSecond.appendChild(option); + }); + softwareCompareSecond.disabled = options.length === 0; + if (softwareCompareStatus) { + softwareCompareStatus.textContent = options.length + ? {{ t('software.compare.available_values', '{count} Werte verfügbar')|tojson }}.replace('{count}', String(options.length)) + : {{ t('software.compare.no_selectable_values', 'Keine passenden Werte vorhanden')|tojson }}; + } + } catch (error) { + softwareCompareSecond.innerHTML = ``; + if (softwareCompareStatus) softwareCompareStatus.textContent = {{ t('software.compare.load_error', 'Auswahl konnte nicht geladen werden')|tojson }}; + } + } + async function openSoftwareCompareDialog() { + const ids = selected(); + if (ids.length !== 1 || !softwareCompareDialog) return; + if (softwareCompareFirstName) softwareCompareFirstName.value = selectedAssetName(ids[0]); + if (softwareCompareField) softwareCompareField.value = 'name'; + softwareCompareDialog.showModal(); + await loadSoftwareCompareOptions(); + } table.addEventListener('change', event => { if (event.target.matches('.asset-select')) refresh(); }); @@ -450,7 +544,27 @@ editButton?.addEventListener('click', () => submit('bulk-edit-form', 'bulk-edit-ids')); deleteButton?.addEventListener('click', () => submit('bulk-delete-form', 'bulk-delete-ids')); mergeButton?.addEventListener('click', () => submit('merge-duplicates-form', 'merge-duplicate-ids')); - softwareCompareButton?.addEventListener('click', () => submit('software-compare-form', 'software-compare-ids')); + softwareCompareButton?.addEventListener('click', () => { + const ids = selected(); + if (ids.length === 2) { + submitIds('software-compare-form', 'software-compare-ids', ids); + return; + } + if (ids.length === 1) openSoftwareCompareDialog(); + }); + softwareCompareField?.addEventListener('change', loadSoftwareCompareOptions); + softwareCompareSecond?.addEventListener('change', () => { + if (softwareCompareStart) softwareCompareStart.disabled = !softwareCompareSecond.value; + }); + softwareCompareStart?.addEventListener('click', () => { + const ids = selected(); + const secondId = softwareCompareSecond?.value || ''; + if (ids.length !== 1 || !secondId) return; + softwareCompareDialog?.close(); + submitIds('software-compare-form', 'software-compare-ids', [ids[0], secondId]); + }); + softwareCompareClose?.addEventListener('click', () => softwareCompareDialog?.close()); + softwareCompareCancel?.addEventListener('click', () => softwareCompareDialog?.close()); jobTypeSelect?.addEventListener('change', refresh); jobStartButton?.addEventListener('click', () => { const count = selected().length; diff --git a/app/templates/assets_bulk_edit.html b/app/templates/assets_bulk_edit.html old mode 100644 new mode 100755 diff --git a/app/templates/assets_import.html b/app/templates/assets_import.html old mode 100644 new mode 100755 diff --git a/app/templates/assets_merge_order.html b/app/templates/assets_merge_order.html old mode 100644 new mode 100755 diff --git a/app/templates/assets_merge_preview.html b/app/templates/assets_merge_preview.html old mode 100644 new mode 100755 diff --git a/app/templates/base.html b/app/templates/base.html old mode 100644 new mode 100755 index 34b91b3..b67f931 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -14,7 +14,7 @@ --am-warning:{{ colors.get('warning_color', '#d97706') }}; --am-danger:{{ colors.get('danger_color', '#b42318') }}; } -{{ title or general.get('title', 'AssetManager') }}{% set favicon_path = general.get('favicon') or general.get('logo') %}{% if favicon_path %}{% endif %} +{{ title or general.get('title', 'AssetManager') }}{% set favicon_path = general.get('favicon') or general.get('logo') %}{% if favicon_path %}{% endif %}
{% if general.get('logo') %}Logo{% endif %}{{ general.get('title', 'AssetManager') }}{% if general.get('company_name') %}– {{ general.get('company_name') }}{% endif %} diff --git a/app/templates/categories.html b/app/templates/categories.html old mode 100644 new mode 100755 index 234193b..5959f0d --- a/app/templates/categories.html +++ b/app/templates/categories.html @@ -122,7 +122,7 @@
- +
diff --git a/app/templates/category_form.html b/app/templates/category_form.html old mode 100644 new mode 100755 diff --git a/app/templates/chart_form.html b/app/templates/chart_form.html old mode 100644 new mode 100755 diff --git a/app/templates/charts.html b/app/templates/charts.html old mode 100644 new mode 100755 diff --git a/app/templates/charts_manage.html b/app/templates/charts_manage.html old mode 100644 new mode 100755 diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html old mode 100644 new mode 100755 diff --git a/app/templates/field_definition_form.html b/app/templates/field_definition_form.html old mode 100644 new mode 100755 diff --git a/app/templates/field_definitions.html b/app/templates/field_definitions.html old mode 100644 new mode 100755 diff --git a/app/templates/job_definition_form.html b/app/templates/job_definition_form.html old mode 100644 new mode 100755 diff --git a/app/templates/job_definitions.html b/app/templates/job_definitions.html old mode 100644 new mode 100755 diff --git a/app/templates/jobs/bash_test.txt b/app/templates/jobs/bash_test.txt old mode 100644 new mode 100755 diff --git a/app/templates/jobs/bash_wrapper.sh b/app/templates/jobs/bash_wrapper.sh old mode 100644 new mode 100755 diff --git a/app/templates/jobs/cmd_test.txt b/app/templates/jobs/cmd_test.txt old mode 100644 new mode 100755 diff --git a/app/templates/jobs/cmd_wrapper.cmd b/app/templates/jobs/cmd_wrapper.cmd old mode 100644 new mode 100755 diff --git a/app/templates/jobs/powershell_test.txt b/app/templates/jobs/powershell_test.txt old mode 100644 new mode 100755 diff --git a/app/templates/jobs/powershell_wrapper.ps1 b/app/templates/jobs/powershell_wrapper.ps1 old mode 100644 new mode 100755 diff --git a/app/templates/jobs/python_test.txt b/app/templates/jobs/python_test.txt old mode 100644 new mode 100755 diff --git a/app/templates/jobs/python_wrapper.py.txt b/app/templates/jobs/python_wrapper.py.txt old mode 100644 new mode 100755 diff --git a/app/templates/ldap_search.html b/app/templates/ldap_search.html old mode 100644 new mode 100755 diff --git a/app/templates/login.html b/app/templates/login.html old mode 100644 new mode 100755 diff --git a/app/templates/meshcentral_sync.html b/app/templates/meshcentral_sync.html old mode 100644 new mode 100755 diff --git a/app/templates/privacy_report_export.html b/app/templates/privacy_report_export.html old mode 100644 new mode 100755 diff --git a/app/templates/profile.html b/app/templates/profile.html old mode 100644 new mode 100755 diff --git a/app/templates/settings.html b/app/templates/settings.html old mode 100644 new mode 100755 diff --git a/app/templates/settings_backup.html b/app/templates/settings_backup.html old mode 100644 new mode 100755 diff --git a/app/templates/settings_backup_placeholder.html b/app/templates/settings_backup_placeholder.html old mode 100644 new mode 100755 diff --git a/app/templates/settings_import_export.html b/app/templates/settings_import_export.html old mode 100644 new mode 100755 diff --git a/app/templates/settings_privacy.html b/app/templates/settings_privacy.html old mode 100644 new mode 100755 diff --git a/app/templates/settings_software.html b/app/templates/settings_software.html old mode 100644 new mode 100755 diff --git a/app/templates/software.html b/app/templates/software.html old mode 100644 new mode 100755 diff --git a/app/templates/software_compare.html b/app/templates/software_compare.html old mode 100644 new mode 100755 diff --git a/app/templates/software_installations.html b/app/templates/software_installations.html old mode 100644 new mode 100755 diff --git a/app/templates/software_job.html b/app/templates/software_job.html old mode 100644 new mode 100755 diff --git a/app/templates/software_job_states.html b/app/templates/software_job_states.html old mode 100644 new mode 100755 diff --git a/app/templates/software_summary.html b/app/templates/software_summary.html old mode 100644 new mode 100755 diff --git a/app/templates/status_options.html b/app/templates/status_options.html old mode 100644 new mode 100755 diff --git a/app/templates/translations.html b/app/templates/translations.html old mode 100644 new mode 100755 diff --git a/app/templates/users.html b/app/templates/users.html old mode 100644 new mode 100755 diff --git a/app/version.py b/app/version.py old mode 100644 new mode 100755 index 34fca34..dccd9c1 --- a/app/version.py +++ b/app/version.py @@ -1,2 +1,2 @@ -APP_VERSION = "0.5.5.65" +APP_VERSION = "0.5.5.68" __version__ = APP_VERSION diff --git a/data/image-library/RFID-chip.png b/data/image-library/RFID-chip.png new file mode 100644 index 0000000..1151d8f Binary files /dev/null and b/data/image-library/RFID-chip.png differ diff --git a/data/image-library/accesspoint.png b/data/image-library/accesspoint.png new file mode 100644 index 0000000..5b52e9e Binary files /dev/null and b/data/image-library/accesspoint.png differ diff --git a/data/image-library/beamer.png b/data/image-library/beamer.png new file mode 100644 index 0000000..809ab67 Binary files /dev/null and b/data/image-library/beamer.png differ diff --git a/data/image-library/computer2.png b/data/image-library/computer2.png new file mode 100644 index 0000000..53ad645 Binary files /dev/null and b/data/image-library/computer2.png differ diff --git a/data/image-library/converter.png b/data/image-library/converter.png new file mode 100644 index 0000000..265d447 Binary files /dev/null and b/data/image-library/converter.png differ diff --git a/data/image-library/dect_phone.png b/data/image-library/dect_phone.png new file mode 100644 index 0000000..2a92197 Binary files /dev/null and b/data/image-library/dect_phone.png differ diff --git a/data/image-library/digi_usb_5-converter.png b/data/image-library/digi_usb_5-converter.png new file mode 100644 index 0000000..882ab79 Binary files /dev/null and b/data/image-library/digi_usb_5-converter.png differ diff --git a/data/image-library/firewall.png b/data/image-library/firewall.png new file mode 100644 index 0000000..e0ca752 Binary files /dev/null and b/data/image-library/firewall.png differ diff --git a/data/image-library/hand_scanner.png b/data/image-library/hand_scanner.png new file mode 100644 index 0000000..4001f10 Binary files /dev/null and b/data/image-library/hand_scanner.png differ diff --git a/data/image-library/handheld.png b/data/image-library/handheld.png new file mode 100644 index 0000000..1c43b8e Binary files /dev/null and b/data/image-library/handheld.png differ diff --git a/data/image-library/headset.png b/data/image-library/headset.png new file mode 100644 index 0000000..df16ca2 Binary files /dev/null and b/data/image-library/headset.png differ diff --git a/data/image-library/ip-com.png b/data/image-library/ip-com.png new file mode 100644 index 0000000..83a645a Binary files /dev/null and b/data/image-library/ip-com.png differ diff --git a/data/image-library/key.png b/data/image-library/key.png new file mode 100644 index 0000000..7df4fe8 Binary files /dev/null and b/data/image-library/key.png differ diff --git a/data/image-library/keyboard.png b/data/image-library/keyboard.png new file mode 100644 index 0000000..3f91db2 Binary files /dev/null and b/data/image-library/keyboard.png differ diff --git a/data/image-library/lto_drive.png b/data/image-library/lto_drive.png new file mode 100644 index 0000000..9292db6 Binary files /dev/null and b/data/image-library/lto_drive.png differ diff --git a/data/image-library/media_player.png b/data/image-library/media_player.png new file mode 100644 index 0000000..744e146 Binary files /dev/null and b/data/image-library/media_player.png differ diff --git a/data/image-library/mini-pc.png b/data/image-library/mini-pc.png new file mode 100644 index 0000000..1334071 Binary files /dev/null and b/data/image-library/mini-pc.png differ diff --git a/data/image-library/mobile_phone.png b/data/image-library/mobile_phone.png new file mode 100644 index 0000000..3a1fee0 Binary files /dev/null and b/data/image-library/mobile_phone.png differ diff --git a/data/image-library/modem.png b/data/image-library/modem.png new file mode 100644 index 0000000..2c025e3 Binary files /dev/null and b/data/image-library/modem.png differ diff --git a/data/image-library/monitor.png b/data/image-library/monitor.png new file mode 100644 index 0000000..d61babe Binary files /dev/null and b/data/image-library/monitor.png differ diff --git a/data/image-library/mouse.png b/data/image-library/mouse.png new file mode 100644 index 0000000..78a03bf Binary files /dev/null and b/data/image-library/mouse.png differ diff --git a/data/image-library/moxa_converter.png b/data/image-library/moxa_converter.png new file mode 100644 index 0000000..01e832a Binary files /dev/null and b/data/image-library/moxa_converter.png differ diff --git a/data/image-library/nas.png b/data/image-library/nas.png new file mode 100644 index 0000000..47366d8 Binary files /dev/null and b/data/image-library/nas.png differ diff --git a/data/image-library/notebook.png b/data/image-library/notebook.png new file mode 100644 index 0000000..9a62924 Binary files /dev/null and b/data/image-library/notebook.png differ diff --git a/data/image-library/others.png b/data/image-library/others.png new file mode 100644 index 0000000..73c3544 Binary files /dev/null and b/data/image-library/others.png differ diff --git a/data/image-library/phone.png b/data/image-library/phone.png new file mode 100644 index 0000000..254fad7 Binary files /dev/null and b/data/image-library/phone.png differ diff --git a/data/image-library/printer.png b/data/image-library/printer.png new file mode 100644 index 0000000..13937d3 Binary files /dev/null and b/data/image-library/printer.png differ diff --git a/data/image-library/router.png b/data/image-library/router.png new file mode 100644 index 0000000..391801c Binary files /dev/null and b/data/image-library/router.png differ diff --git a/data/image-library/scanner.png b/data/image-library/scanner.png new file mode 100644 index 0000000..a8bff79 Binary files /dev/null and b/data/image-library/scanner.png differ diff --git a/data/image-library/server.png b/data/image-library/server.png new file mode 100644 index 0000000..c21914d Binary files /dev/null and b/data/image-library/server.png differ diff --git a/data/image-library/server2.png b/data/image-library/server2.png new file mode 100644 index 0000000..a728265 Binary files /dev/null and b/data/image-library/server2.png differ diff --git a/data/image-library/soundbar.png b/data/image-library/soundbar.png new file mode 100644 index 0000000..b7c1c96 Binary files /dev/null and b/data/image-library/soundbar.png differ diff --git a/data/image-library/switch.png b/data/image-library/switch.png new file mode 100644 index 0000000..f766ac4 Binary files /dev/null and b/data/image-library/switch.png differ diff --git a/data/image-library/tablet.png b/data/image-library/tablet.png new file mode 100644 index 0000000..ccbe608 Binary files /dev/null and b/data/image-library/tablet.png differ diff --git a/data/image-library/test_picture.jpg b/data/image-library/test_picture.jpg new file mode 100644 index 0000000..1398ffd Binary files /dev/null and b/data/image-library/test_picture.jpg differ diff --git a/data/image-library/usv.png b/data/image-library/usv.png new file mode 100644 index 0000000..11b7c6f Binary files /dev/null and b/data/image-library/usv.png differ diff --git a/data/image-library/video_recorder.png b/data/image-library/video_recorder.png new file mode 100644 index 0000000..90f04e2 Binary files /dev/null and b/data/image-library/video_recorder.png differ diff --git a/data/image-library/video_surveillance.png b/data/image-library/video_surveillance.png new file mode 100644 index 0000000..7725bf6 Binary files /dev/null and b/data/image-library/video_surveillance.png differ diff --git a/data/image-library/videoconference.png b/data/image-library/videoconference.png new file mode 100644 index 0000000..820672b Binary files /dev/null and b/data/image-library/videoconference.png differ diff --git a/data/image-library/virtual_machine.png b/data/image-library/virtual_machine.png new file mode 100644 index 0000000..a68c815 Binary files /dev/null and b/data/image-library/virtual_machine.png differ diff --git a/docker-compose.yml b/docker-compose.yml old mode 100644 new mode 100755 diff --git a/docker-compose_old.yml b/docker-compose_old.yml new file mode 100644 index 0000000..6087ccc --- /dev/null +++ b/docker-compose_old.yml @@ -0,0 +1,51 @@ +name: assetmanager + +services: + db: + image: postgres:16-alpine + container_name: assetmanager-db + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:-assetmanager} + POSTGRES_USER: ${POSTGRES_USER:-assetmanager} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-change-me} + volumes: + - ./data/postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-assetmanager} -d ${POSTGRES_DB:-assetmanager}"] + interval: 5s + timeout: 5s + retries: 10 + + app: + build: . + container_name: assetmanager-app + restart: unless-stopped + depends_on: + db: + condition: service_healthy + environment: + DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-assetmanager}:${POSTGRES_PASSWORD:-change-me}@db:5432/${POSTGRES_DB:-assetmanager} + APP_TITLE: AssetManager + APP_CONFIG: /app/config/config.json + APPINFO_PATH: /app/config/APPINFO.json + BACKUP_DIR: /data/backups + BACKUP_INTERVAL_HOURS: ${BACKUP_INTERVAL_HOURS:-8} + BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-3} + MESHCENTRAL_PASSWORD: ${MESHCENTRAL_PASSWORD:-} + SYNC_LOG_DIR: /app/data/logs/sync + DIAGNOSTIC_DIR: /app/data/logs/diagnostics + LDAP_BIND_PASSWORD: ${LDAP_BIND_PASSWORD:-} + SESSION_SECRET: ${SESSION_SECRET:-} + LOCAL_ADMIN_USERNAME: ${LOCAL_ADMIN_USERNAME:-} + LOCAL_ADMIN_PASSWORD: ${LOCAL_ADMIN_PASSWORD:-} + + ports: + - "${APP_PORT:-8088}:8000" + volumes: + - ./data/config:/app/config + - ./data/uploads:/app/app/static/uploads + - ./data/logs:/app/data/logs + - ./data/backups:/data/backups + - ./data/scripts:/scripts + diff --git a/docs/ASSET-VISUALS.md b/docs/ASSET-VISUALS.md old mode 100644 new mode 100755 index 546e5fd..b2d6b01 --- a/docs/ASSET-VISUALS.md +++ b/docs/ASSET-VISUALS.md @@ -42,6 +42,7 @@ Version 0.5.5.60 adds a reusable standard image library for assets and categorie - Library images are referenced, not copied per asset/category. - Removing or replacing an association never deletes the physical library file. - Uploads never overwrite an existing file. If a name already exists, a numeric suffix is added (`switch-1.png`, `switch-2.png`, ...). -- Administrators may also place supported image files directly into the persistent host directory. +- Administrators may also place supported image files directly into the persistent host directory. The directory is read from the filesystem whenever an asset or category form is opened. +- Subdirectories below `./data/uploads/library` are scanned recursively and their relative path is shown in the selector. Orphaned or unused library images are intentionally retained. A later maintenance function can safely identify and remove unreferenced files. diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md old mode 100644 new mode 100755 index 043c3c5..f25e88b --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -15,7 +15,7 @@ mkdir -p data/config data/uploads data/logs data/backups data/scripts data/postg Create a `.env` file. Use strong, unique values for all secrets: ```env -ASSETMANAGER_VERSION=0.5.5.65 +ASSETMANAGER_VERSION=0.5.5.68 APP_PORT=8088 POSTGRES_DB=assetmanager POSTGRES_USER=assetmanager @@ -58,7 +58,7 @@ services: retries: 10 app: - image: git.jusaro.de/roland/assetmanager:${ASSETMANAGER_VERSION:-0.5.5.65} + image: git.jusaro.de/roland/assetmanager:${ASSETMANAGER_VERSION:-0.5.5.68} container_name: assetmanager-app restart: unless-stopped depends_on: @@ -91,7 +91,7 @@ services: - ./data/scripts:/scripts callback: - image: git.jusaro.de/roland/assetmanager:${ASSETMANAGER_VERSION:-0.5.5.65} + image: git.jusaro.de/roland/assetmanager:${ASSETMANAGER_VERSION:-0.5.5.68} container_name: assetmanager-callback restart: unless-stopped depends_on: @@ -172,7 +172,7 @@ Examples: ```text https://assetmanager.example.org https://assetmanager.example.org:8443 -http://192.168.1.40:8088 +http://192.0.2.10:8088 ``` For Internet-facing use, HTTPS through a properly configured reverse proxy is strongly recommended. The callback endpoint must be reachable from the managed client. @@ -218,7 +218,7 @@ Back up `.env` and the complete `data/` directory before significant updates. Set the desired fixed image version in `.env`, for example: ```env -ASSETMANAGER_VERSION=0.5.5.65 +ASSETMANAGER_VERSION=0.5.5.68 ``` Then update: @@ -312,7 +312,7 @@ docker compose pull docker compose up -d ``` -Images copied directly into `./data/uploads/library` are available in the AssetManager image library after reloading the page. +Images copied directly into `./data/uploads/library` are read from the filesystem and are available in the AssetManager image library after reloading the page. Subdirectories are scanned recursively. ## Dedicated callback port / DMZ listener diff --git a/docs/version-history/README.md b/docs/version-history/README.md old mode 100644 new mode 100755 index 94de587..ba73b39 --- a/docs/version-history/README.md +++ b/docs/version-history/README.md @@ -5,12 +5,15 @@ Release notes are stored outside the project root to keep the repository overview compact. -The current release is **0.5.5.65**. +The current release is **0.5.5.68**. Older notes are concise English summaries migrated from the original release documents. Git history remains authoritative for exact implementation details. ## Releases +- [0.5.5.68](UPDATE-0.5.5.68.md) +- [0.5.5.67](UPDATE-0.5.5.67.md) +- [0.5.5.66](UPDATE-0.5.5.66.md) - [0.5.5.65](UPDATE-0.5.5.65.md) - [0.5.5.47](UPDATE-0.5.5.47.md) diff --git a/docs/version-history/UPDATE-0.2.6.1.md b/docs/version-history/UPDATE-0.2.6.1.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.2.7.1.md b/docs/version-history/UPDATE-0.2.7.1.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.2.7.2.md b/docs/version-history/UPDATE-0.2.7.2.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.2.7.md b/docs/version-history/UPDATE-0.2.7.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.2.8.md b/docs/version-history/UPDATE-0.2.8.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.2.9.1.md b/docs/version-history/UPDATE-0.2.9.1.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.2.9.md b/docs/version-history/UPDATE-0.2.9.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.0.md b/docs/version-history/UPDATE-0.3.0.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.1.md b/docs/version-history/UPDATE-0.3.1.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.10.0.md b/docs/version-history/UPDATE-0.3.10.0.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.11.0.md b/docs/version-history/UPDATE-0.3.11.0.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.12.0.md b/docs/version-history/UPDATE-0.3.12.0.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.12.1.md b/docs/version-history/UPDATE-0.3.12.1.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.13.0.md b/docs/version-history/UPDATE-0.3.13.0.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.13.1.md b/docs/version-history/UPDATE-0.3.13.1.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.14.0.md b/docs/version-history/UPDATE-0.3.14.0.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.14.1.md b/docs/version-history/UPDATE-0.3.14.1.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.14.2.md b/docs/version-history/UPDATE-0.3.14.2.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.14.3.md b/docs/version-history/UPDATE-0.3.14.3.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.14.4.md b/docs/version-history/UPDATE-0.3.14.4.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.14.5.md b/docs/version-history/UPDATE-0.3.14.5.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.14.6.md b/docs/version-history/UPDATE-0.3.14.6.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.14.7.md b/docs/version-history/UPDATE-0.3.14.7.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.14.8.md b/docs/version-history/UPDATE-0.3.14.8.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.15.0.md b/docs/version-history/UPDATE-0.3.15.0.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.15.1.md b/docs/version-history/UPDATE-0.3.15.1.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.15.2.md b/docs/version-history/UPDATE-0.3.15.2.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.3.md b/docs/version-history/UPDATE-0.3.3.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.4.7.md b/docs/version-history/UPDATE-0.3.4.7.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.4.md b/docs/version-history/UPDATE-0.3.4.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.5.1.md b/docs/version-history/UPDATE-0.3.5.1.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.5.md b/docs/version-history/UPDATE-0.3.5.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.6.2.md b/docs/version-history/UPDATE-0.3.6.2.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.6.3.md b/docs/version-history/UPDATE-0.3.6.3.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.6.md b/docs/version-history/UPDATE-0.3.6.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.7.10.md b/docs/version-history/UPDATE-0.3.7.10.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.7.3.md b/docs/version-history/UPDATE-0.3.7.3.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.7.4.md b/docs/version-history/UPDATE-0.3.7.4.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.7.5.md b/docs/version-history/UPDATE-0.3.7.5.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.7.6.md b/docs/version-history/UPDATE-0.3.7.6.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.7.7.md b/docs/version-history/UPDATE-0.3.7.7.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.7.8.md b/docs/version-history/UPDATE-0.3.7.8.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.7.md b/docs/version-history/UPDATE-0.3.7.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.8.0.md b/docs/version-history/UPDATE-0.3.8.0.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.8.1.md b/docs/version-history/UPDATE-0.3.8.1.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.8.2.md b/docs/version-history/UPDATE-0.3.8.2.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.8.3.md b/docs/version-history/UPDATE-0.3.8.3.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.8.4.md b/docs/version-history/UPDATE-0.3.8.4.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.8.5.md b/docs/version-history/UPDATE-0.3.8.5.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.8.6.md b/docs/version-history/UPDATE-0.3.8.6.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.9.0.md b/docs/version-history/UPDATE-0.3.9.0.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.9.1.md b/docs/version-history/UPDATE-0.3.9.1.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.9.2.md b/docs/version-history/UPDATE-0.3.9.2.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.9.3.md b/docs/version-history/UPDATE-0.3.9.3.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.9.4.md b/docs/version-history/UPDATE-0.3.9.4.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.9.5.md b/docs/version-history/UPDATE-0.3.9.5.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.9.6.1.md b/docs/version-history/UPDATE-0.3.9.6.1.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.9.6.2.md b/docs/version-history/UPDATE-0.3.9.6.2.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.9.6.3.md b/docs/version-history/UPDATE-0.3.9.6.3.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.9.6.md b/docs/version-history/UPDATE-0.3.9.6.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.9.7.md b/docs/version-history/UPDATE-0.3.9.7.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.9.8.md b/docs/version-history/UPDATE-0.3.9.8.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.3.9.9.md b/docs/version-history/UPDATE-0.3.9.9.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.4.0.0.md b/docs/version-history/UPDATE-0.4.0.0.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.4.0.1.md b/docs/version-history/UPDATE-0.4.0.1.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.4.0.10.md b/docs/version-history/UPDATE-0.4.0.10.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.4.0.11.md b/docs/version-history/UPDATE-0.4.0.11.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.4.0.12.md b/docs/version-history/UPDATE-0.4.0.12.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.4.0.13.md b/docs/version-history/UPDATE-0.4.0.13.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.4.0.14.md b/docs/version-history/UPDATE-0.4.0.14.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.4.0.2.md b/docs/version-history/UPDATE-0.4.0.2.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.4.0.3.md b/docs/version-history/UPDATE-0.4.0.3.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.4.0.4.md b/docs/version-history/UPDATE-0.4.0.4.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.4.0.5.md b/docs/version-history/UPDATE-0.4.0.5.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.4.0.6.md b/docs/version-history/UPDATE-0.4.0.6.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.4.0.7.md b/docs/version-history/UPDATE-0.4.0.7.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.4.0.8.md b/docs/version-history/UPDATE-0.4.0.8.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.4.0.9.md b/docs/version-history/UPDATE-0.4.0.9.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.0.0.md b/docs/version-history/UPDATE-0.5.0.0.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.0.1.md b/docs/version-history/UPDATE-0.5.0.1.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.0.2.md b/docs/version-history/UPDATE-0.5.0.2.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.0.3.md b/docs/version-history/UPDATE-0.5.0.3.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.0.4.md b/docs/version-history/UPDATE-0.5.0.4.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.0.5.md b/docs/version-history/UPDATE-0.5.0.5.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.0.6.md b/docs/version-history/UPDATE-0.5.0.6.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.0.7.md b/docs/version-history/UPDATE-0.5.0.7.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.0.8.md b/docs/version-history/UPDATE-0.5.0.8.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.0.9.md b/docs/version-history/UPDATE-0.5.0.9.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.1.0.md b/docs/version-history/UPDATE-0.5.1.0.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.1.1.md b/docs/version-history/UPDATE-0.5.1.1.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.1.2.md b/docs/version-history/UPDATE-0.5.1.2.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.1.3.md b/docs/version-history/UPDATE-0.5.1.3.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.1.4.md b/docs/version-history/UPDATE-0.5.1.4.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.1.5.md b/docs/version-history/UPDATE-0.5.1.5.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.1.6.md b/docs/version-history/UPDATE-0.5.1.6.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.1.7.md b/docs/version-history/UPDATE-0.5.1.7.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.2.0.md b/docs/version-history/UPDATE-0.5.2.0.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.2.1.md b/docs/version-history/UPDATE-0.5.2.1.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.2.2.md b/docs/version-history/UPDATE-0.5.2.2.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.3.0.md b/docs/version-history/UPDATE-0.5.3.0.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.3.1.md b/docs/version-history/UPDATE-0.5.3.1.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.3.2.md b/docs/version-history/UPDATE-0.5.3.2.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.3.3.md b/docs/version-history/UPDATE-0.5.3.3.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.3.4.md b/docs/version-history/UPDATE-0.5.3.4.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.3.5.md b/docs/version-history/UPDATE-0.5.3.5.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.3.6.md b/docs/version-history/UPDATE-0.5.3.6.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.3.7.md b/docs/version-history/UPDATE-0.5.3.7.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.3.8.md b/docs/version-history/UPDATE-0.5.3.8.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.3.9.md b/docs/version-history/UPDATE-0.5.3.9.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.4.0.md b/docs/version-history/UPDATE-0.5.4.0.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.4.1.md b/docs/version-history/UPDATE-0.5.4.1.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.4.2.md b/docs/version-history/UPDATE-0.5.4.2.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.4.3.md b/docs/version-history/UPDATE-0.5.4.3.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.4.4.md b/docs/version-history/UPDATE-0.5.4.4.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.4.5.md b/docs/version-history/UPDATE-0.5.4.5.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.4.6.md b/docs/version-history/UPDATE-0.5.4.6.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.4.7.md b/docs/version-history/UPDATE-0.5.4.7.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.4.8.md b/docs/version-history/UPDATE-0.5.4.8.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.0.md b/docs/version-history/UPDATE-0.5.5.0.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.10.md b/docs/version-history/UPDATE-0.5.5.10.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.11.md b/docs/version-history/UPDATE-0.5.5.11.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.12.md b/docs/version-history/UPDATE-0.5.5.12.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.13.md b/docs/version-history/UPDATE-0.5.5.13.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.14.md b/docs/version-history/UPDATE-0.5.5.14.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.15.md b/docs/version-history/UPDATE-0.5.5.15.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.16.md b/docs/version-history/UPDATE-0.5.5.16.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.17.md b/docs/version-history/UPDATE-0.5.5.17.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.18.md b/docs/version-history/UPDATE-0.5.5.18.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.19.md b/docs/version-history/UPDATE-0.5.5.19.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.20.md b/docs/version-history/UPDATE-0.5.5.20.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.21.md b/docs/version-history/UPDATE-0.5.5.21.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.22.md b/docs/version-history/UPDATE-0.5.5.22.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.23.md b/docs/version-history/UPDATE-0.5.5.23.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.24.md b/docs/version-history/UPDATE-0.5.5.24.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.25.md b/docs/version-history/UPDATE-0.5.5.25.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.26.md b/docs/version-history/UPDATE-0.5.5.26.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.27.md b/docs/version-history/UPDATE-0.5.5.27.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.28.md b/docs/version-history/UPDATE-0.5.5.28.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.29.md b/docs/version-history/UPDATE-0.5.5.29.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.30.md b/docs/version-history/UPDATE-0.5.5.30.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.31.md b/docs/version-history/UPDATE-0.5.5.31.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.32.md b/docs/version-history/UPDATE-0.5.5.32.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.33.md b/docs/version-history/UPDATE-0.5.5.33.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.34.md b/docs/version-history/UPDATE-0.5.5.34.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.35.md b/docs/version-history/UPDATE-0.5.5.35.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.36.md b/docs/version-history/UPDATE-0.5.5.36.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.37.md b/docs/version-history/UPDATE-0.5.5.37.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.38.md b/docs/version-history/UPDATE-0.5.5.38.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.39.md b/docs/version-history/UPDATE-0.5.5.39.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.40.md b/docs/version-history/UPDATE-0.5.5.40.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.41.md b/docs/version-history/UPDATE-0.5.5.41.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.42.md b/docs/version-history/UPDATE-0.5.5.42.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.43.md b/docs/version-history/UPDATE-0.5.5.43.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.44.md b/docs/version-history/UPDATE-0.5.5.44.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.45.md b/docs/version-history/UPDATE-0.5.5.45.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.46.md b/docs/version-history/UPDATE-0.5.5.46.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.47.md b/docs/version-history/UPDATE-0.5.5.47.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.48.md b/docs/version-history/UPDATE-0.5.5.48.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.49.md b/docs/version-history/UPDATE-0.5.5.49.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.5.md b/docs/version-history/UPDATE-0.5.5.5.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.50.md b/docs/version-history/UPDATE-0.5.5.50.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.51.md b/docs/version-history/UPDATE-0.5.5.51.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.52.md b/docs/version-history/UPDATE-0.5.5.52.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.53.md b/docs/version-history/UPDATE-0.5.5.53.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.54.md b/docs/version-history/UPDATE-0.5.5.54.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.55.md b/docs/version-history/UPDATE-0.5.5.55.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.56.md b/docs/version-history/UPDATE-0.5.5.56.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.57.md b/docs/version-history/UPDATE-0.5.5.57.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.58.md b/docs/version-history/UPDATE-0.5.5.58.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.59.md b/docs/version-history/UPDATE-0.5.5.59.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.6.md b/docs/version-history/UPDATE-0.5.5.6.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.60.md b/docs/version-history/UPDATE-0.5.5.60.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.61.md b/docs/version-history/UPDATE-0.5.5.61.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.62.md b/docs/version-history/UPDATE-0.5.5.62.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.64.md b/docs/version-history/UPDATE-0.5.5.64.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.65.md b/docs/version-history/UPDATE-0.5.5.65.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.66.md b/docs/version-history/UPDATE-0.5.5.66.md new file mode 100755 index 0000000..dd059ff --- /dev/null +++ b/docs/version-history/UPDATE-0.5.5.66.md @@ -0,0 +1,9 @@ +# Update 0.5.5.66 + +## Consistent alphabetical category ordering + +- Category tiles on the dashboard are now sorted alphabetically by category name. +- Category dropdowns use the same language-aware alphabetical ordering across asset forms and administration pages. +- The asset list and asset tree category selectors use the same ordering. +- The category management table defaults to category name ascending when no user-specific sort order has been stored yet. +- Existing stored table sort preferences continue to take precedence over the default. diff --git a/docs/version-history/UPDATE-0.5.5.67.md b/docs/version-history/UPDATE-0.5.5.67.md new file mode 100755 index 0000000..a8271d7 --- /dev/null +++ b/docs/version-history/UPDATE-0.5.5.67.md @@ -0,0 +1,22 @@ +# AssetManager 0.5.5.67 + +## Changes + +- Added line-based multi-value filters to the shared table filtering component. + - Paste values copied from Excel or a text file directly into a column filter. + - One value per line. + - Values inside the same column are OR-linked; filters across different columns remain AND-linked. + - Existing single-value filtering, sorting, persisted browser state and table resizing remain unchanged. + - The server-side recent-jobs table supports the same multi-value behavior. +- Improved software comparison from the asset list. + - Two selected assets continue to open the comparison directly. + - With exactly one selected asset, a dialog now asks for the second asset. + - The selector defaults to the asset name field. + - Users can choose another filterable scalar asset field; only values actually present on accessible assets are offered. + - Empty values and the already selected asset are excluded. + - Duplicate field values are disambiguated with the asset name. +- Added German and English translations for the new controls. + +## Compatibility + +No database migration is required. Existing job execution, callback handling and software comparison logic are unchanged. diff --git a/docs/version-history/UPDATE-0.5.5.68.md b/docs/version-history/UPDATE-0.5.5.68.md new file mode 100755 index 0000000..bb8b8ed --- /dev/null +++ b/docs/version-history/UPDATE-0.5.5.68.md @@ -0,0 +1,16 @@ +# AssetManager 0.5.5.68 + +## Changes + +- Improved the reusable asset/category image library. + - The persistent `data/uploads/library` directory is scanned directly whenever an asset or category form is opened. + - Supported image files copied manually to the Docker host are available after reloading the form. + - Subdirectories are scanned recursively and displayed using their relative path. + - Symbolic links are ignored so the configured library root cannot be escaped. +- Improved duplicate highlighting in dark mode. + - Duplicate rows keep the existing yellow background. + - Text, links and secondary text use a dark foreground color for sufficient contrast. + +## Compatibility + +No database migration is required. Existing asset/category image references and duplicate-search logic remain unchanged. diff --git a/docs/version-history/UPDATE-0.5.5.7.md b/docs/version-history/UPDATE-0.5.5.7.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.8.md b/docs/version-history/UPDATE-0.5.5.8.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-0.5.5.9.md b/docs/version-history/UPDATE-0.5.5.9.md old mode 100644 new mode 100755 diff --git a/docs/version-history/UPDATE-NOTE.md b/docs/version-history/UPDATE-NOTE.md old mode 100644 new mode 100755 diff --git a/requirements.txt b/requirements.txt old mode 100644 new mode 100755 diff --git a/update_from_git.sh b/update_from_git.sh new file mode 100755 index 0000000..6d1b79c --- /dev/null +++ b/update_from_git.sh @@ -0,0 +1,73 @@ +#!/bin/bash +set -Eeuo pipefail + +APP_DIR="/volume2/docker_nvme/assetmanager" +REMOTE_URL="https://git.jusaro.de/Roland/Assetmanager.git" +BRANCH="main" + +cd "$APP_DIR" + +command -v git >/dev/null || { + echo "Fehler: Git ist nicht installiert." + exit 1 +} + +command -v docker >/dev/null || { + echo "Fehler: Docker wurde nicht gefunden." + exit 1 +} + +[ -d ".git" ] || { + echo "Fehler: $APP_DIR ist kein Git-Repository." + exit 1 +} + +# Prüfen, ob wichtige lokale Daten tatsächlich ignoriert werden +for path in \ + ".env" \ + "data/config/config.json" \ + "data/postgres/__check__" \ + "data/uploads/__check__" \ + "data/logs/__check__" +do + if ! git check-ignore -q --no-index "$path"; then + echo "Sicherheitsabbruch: $path wird nicht durch .gitignore geschützt." + exit 1 + fi +done + +# Remote-Adresse korrigieren +git remote set-url origin "$REMOTE_URL" + +echo "AssetManager wird gestoppt ..." +docker compose down + +SUCCESS=0 + +restart_on_error() { + if [ "$SUCCESS" -eq 0 ]; then + echo "Update fehlgeschlagen – Container werden wieder gestartet." + docker compose up -d || true + fi +} + +trap restart_on_error EXIT + +echo "Aktueller Stand wird von Gitea geladen ..." +git fetch --prune origin + +echo "Lokaler Programmstand wird ersetzt ..." +git reset --hard "origin/$BRANCH" + +# Entfernt unversionierte, aber nicht ignorierte Dateien. +# .env, Datenbank, Uploads und Konfiguration bleiben erhalten. +git clean -fd + +echo "Container werden neu gebaut und gestartet ..." +docker compose up -d --build + +SUCCESS=1 +trap - EXIT + +echo "Update abgeschlossen." +docker compose logs --tail=100 -f