diff --git a/CHANGELOG.md b/CHANGELOG.md
index 58b4146..4ae0678 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
---
-## [1.6] - 2026-04-09
+## [1.6.1] - 2026-04-08
+
+### Added
+
+- `--version` now includes Python version, Python executable path, and OS/platform alongside the Atlas version string
+- `session prune --older-than DAYS` bulk-deletes uncaptured sessions (created but never captured) older than the specified number of days; supports `--dry-run` to preview and `--force` to skip confirmation
+
+### Changed
+
+- Fixed the licensing issues in a few files to reflect the GPL 3.0 license properly
+
+## [1.6] - 2026-04-08
### Added
diff --git a/README.md b/README.md
index 1cafbe4..06d3b5d 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,7 @@
# Platform Atlas
-

-
-
+
> Enterprise configuration auditing and compliance reporting for Itential Automation Platform
@@ -738,20 +736,10 @@ For issues, questions, or feature requests, please contact:
## License
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
+This project is licensed under the GNU General Public License v3.0. See the [LICENSE](LICENSE) file for details.
---
-**Version:** 1.5
+**Version:** 1.6.1
**Author:** Cody Rester
**Last Updated:** April 2026
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 6199f02..5511f03 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,8 +1,8 @@
[project]
name = "platform-atlas"
-version = "1.6"
+version = "1.6.1"
description = "Health Analysis for Platform"
-license = "Apache-2.0"
+license = "GPL-3.0-or-later"
authors = [{ name = "Cody Rester", email = "cody.rester@itential.com" }]
maintainers = [{ name = "Cody Rester", email = "cody.rester@itential.com" }]
readme = "README.md"
diff --git a/src/platform_atlas/core/_version.py b/src/platform_atlas/core/_version.py
index c1f4473..eb5c305 100644
--- a/src/platform_atlas/core/_version.py
+++ b/src/platform_atlas/core/_version.py
@@ -1,2 +1,2 @@
"""Platform Atlas Version"""
-__version__ = "1.6"
+__version__ = "1.6.1"
diff --git a/src/platform_atlas/core/cli.py b/src/platform_atlas/core/cli.py
index 0eab537..a776750 100644
--- a/src/platform_atlas/core/cli.py
+++ b/src/platform_atlas/core/cli.py
@@ -13,8 +13,10 @@
- preflight: Run preflight connectivity checks
"""
+import sys
import json
import argparse
+import platform as _platform
from pathlib import Path
from rich_argparse import RichHelpFormatter
@@ -23,6 +25,28 @@
theme = ui.theme
+# =================================================
+# Version Action
+# =================================================
+
+class _VersionAction(argparse.Action):
+ """Custom --version action that includes system info."""
+
+ def __init__(self, option_strings, dest=argparse.SUPPRESS, default=argparse.SUPPRESS, help=None):
+ super().__init__(option_strings=option_strings, dest=dest, default=default, nargs=0, help=help)
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ py_version = sys.version.split()[0]
+ py_path = sys.executable
+ os_name = _platform.system()
+ os_release = _platform.release()
+ machine = _platform.machine()
+ print(f"platform-atlas {__version__}")
+ print(f"Python {py_version} ({py_path})")
+ print(f"{os_name} {os_release} ({machine})")
+ parser.exit()
+
+
# =================================================
# Custom Help Formatter
# =================================================
@@ -59,8 +83,8 @@ def create_parser() -> argparse.ArgumentParser:
parser.add_argument(
'--version',
- action='version',
- version=f'%(prog)s {__version__}'
+ action=_VersionAction,
+ help='Show version and system information'
)
parser.add_argument(
@@ -434,6 +458,33 @@ def _add_session_commands(subparsers):
help='Show what would change without writing anything'
)
+ # session prune
+ prune = session_subparsers.add_parser(
+ 'prune',
+ help='Delete uncaptured sessions older than N days',
+ formatter_class=AtlasHelpFormatter,
+ description='Bulk-delete sessions that were created but never captured, '
+ 'older than the specified number of days.'
+ )
+ prune.add_argument(
+ '--older-than',
+ dest='older_than',
+ type=int,
+ required=True,
+ metavar='DAYS',
+ help='Prune sessions created more than DAYS days ago (e.g. --older-than 90)'
+ )
+ prune.add_argument(
+ '--dry-run',
+ action='store_true',
+ help='Show what would be deleted without removing anything'
+ )
+ prune.add_argument(
+ '--force',
+ action='store_true',
+ help='Skip confirmation prompt'
+ )
+
# =================================================
# RULESET Command Group
# =================================================
diff --git a/src/platform_atlas/core/handlers/session.py b/src/platform_atlas/core/handlers/session.py
index 82770c1..b12764a 100644
--- a/src/platform_atlas/core/handlers/session.py
+++ b/src/platform_atlas/core/handlers/session.py
@@ -1762,6 +1762,130 @@ def handle_session_repair(args: Namespace) -> int:
return 1
+@registry.register("session", "prune", description="Delete uncaptured sessions older than N days")
+def handle_session_prune(args: Namespace) -> int:
+ """
+ Bulk-delete sessions that were created but never captured,
+ older than --older-than DAYS days.
+
+ The active session is always skipped even if it qualifies.
+ Use --dry-run to preview without deleting. Use --force to skip confirmation.
+ """
+ from datetime import datetime, timezone, timedelta
+ from rich.table import Table
+ from rich import box
+
+ try:
+ manager = get_session_manager()
+ older_than: int = args.older_than
+ dry_run: bool = getattr(args, "dry_run", False)
+ force: bool = getattr(args, "force", False)
+
+ cutoff = datetime.now(tz=timezone.utc) - timedelta(days=older_than)
+ active_name = manager.get_active_session_name()
+
+ all_sessions = manager.list()
+ candidates = []
+ skipped_active = False
+
+ for session in all_sessions:
+ # Only sessions that were never captured
+ if session.metadata.capture_completed:
+ continue
+ # Age check against created_at
+ created = session.metadata.created_at
+ if created.tzinfo is None:
+ created = created.replace(tzinfo=timezone.utc)
+ if created >= cutoff:
+ continue
+ # Never touch the active session
+ if session.name == active_name:
+ skipped_active = True
+ continue
+ candidates.append(session)
+
+ if not candidates:
+ console.print(f"\n [{theme.text_dim}]No uncaptured sessions older than {older_than} days.[/{theme.text_dim}]\n")
+ if skipped_active:
+ console.print(
+ f" [{theme.warning}]Note: active session '{active_name}' was skipped "
+ f"(deactivate it first to include it).[/{theme.warning}]\n"
+ )
+ return 0
+
+ # Build preview table
+ table = Table(
+ title=f"{'[dim]Dry run — [/dim]' if dry_run else ''}Uncaptured sessions to prune ({len(candidates)})",
+ box=box.ROUNDED,
+ )
+ table.add_column("Name", style="cyan")
+ table.add_column("Environment", style=theme.accent)
+ table.add_column("Created", style="dim")
+ table.add_column("Age (days)", justify="right", style=theme.warning)
+
+ today = datetime.now(tz=timezone.utc)
+ for session in candidates:
+ created = session.metadata.created_at
+ if created.tzinfo is None:
+ created = created.replace(tzinfo=timezone.utc)
+ age_days = (today - created).days
+ env_display = session.metadata.environment or f"[{theme.text_ghost}]—[/{theme.text_ghost}]"
+ table.add_row(
+ session.name,
+ env_display,
+ created.strftime("%Y-%m-%d"),
+ str(age_days),
+ )
+
+ console.print()
+ console.print(table)
+
+ if skipped_active:
+ console.print(
+ f"\n [{theme.warning}]Note: active session '{active_name}' was skipped.[/{theme.warning}]"
+ )
+
+ if dry_run:
+ console.print(
+ f"\n [{theme.text_dim}]Dry run — nothing deleted. "
+ f"Remove --dry-run to prune these {len(candidates)} session(s).[/{theme.text_dim}]\n"
+ )
+ return 0
+
+ # Confirm unless --force
+ if not force:
+ console.print()
+ if not Confirm.ask(
+ f"Permanently delete {len(candidates)} session(s)?", default=False
+ ):
+ console.print(f" [{theme.text_dim}]Cancelled[/{theme.text_dim}]")
+ return 0
+
+ # Delete
+ deleted = 0
+ failed = 0
+ for session in candidates:
+ try:
+ manager.delete(session.name, force=True)
+ console.print(f" [{theme.success}]✓[/{theme.success}] Deleted: {session.name}")
+ deleted += 1
+ except SessionError as e:
+ console.print(f" [red]✗[/red] {session.name}: {e.message}")
+ failed += 1
+
+ console.print()
+ console.print(
+ f" [{theme.success}]✓[/{theme.success}] {deleted} session(s) deleted"
+ + (f", {failed} failed" if failed else "")
+ )
+ console.print()
+ return 0 if not failed else 1
+
+ except SessionError as e:
+ console.print(f"[red]✗[/red] {e.message}")
+ return 1
+
+
# =================================================
# Shared Helpers
# =================================================
diff --git a/src/platform_atlas/core/whats_new.py b/src/platform_atlas/core/whats_new.py
index 0fe5003..d24669b 100644
--- a/src/platform_atlas/core/whats_new.py
+++ b/src/platform_atlas/core/whats_new.py
@@ -5,12 +5,16 @@
1. Prints a brief CLI summary with bullet points (always visible)
2. Opens a detailed HTML page in the default browser (if available)
-The HTML template lives in reporting/assets/templates/whats-new-{version}.html.
+The HTML template lives in reporting/assets/templates/whats-new-{series}.html,
+where series is the major.minor version (e.g. "1.6" for 1.6, 1.6.1, 1.6.2, …).
+
Brand assets (logos, images) live in reporting/assets/images/ and are
base64-encoded into the HTML at runtime so the output is fully self-contained.
Tracking:
- ~/.atlas/.seen_version stores the last version whose update was shown.
+ ~/.atlas/.seen_version stores the last minor series whose update was shown
+ (e.g. "1.6"). Any patch release in the same series won't re-show the page.
+ A user who jumps straight from 1.5 to 1.6.2 will still see the 1.6 page.
"""
from __future__ import annotations
@@ -34,11 +38,12 @@
SEEN_VERSION_FILE = ATLAS_HOME / ".seen_version"
ASSETS_IMAGES_DIR = PROJECT_TEMPLATES.parent / "images"
-# Versions that have a What's New page
-WHATS_NEW_VERSIONS = {"1.5"}
+# Minor version series that have a What's New page.
+# Key is "major.minor" — covers all patch releases in that series.
+WHATS_NEW_VERSIONS = {"1.5", "1.6"}
-# ── Version comparison ────────────────────────────────────────────
+# ── Version helpers ───────────────────────────────────────────────
def _parse_version(v: str) -> tuple[int, ...]:
try:
@@ -47,6 +52,14 @@ def _parse_version(v: str) -> tuple[int, ...]:
return (0,)
+def _get_minor_series(version: str) -> str:
+ """Return 'major.minor' from any version string (e.g. '1.6.1' → '1.6')."""
+ parts = version.strip().split(".")
+ if len(parts) >= 2:
+ return f"{parts[0]}.{parts[1]}"
+ return version.strip()
+
+
def _get_seen_version() -> str | None:
if not SEEN_VERSION_FILE.is_file():
return None
@@ -57,8 +70,10 @@ def _get_seen_version() -> str | None:
def _mark_seen(version: str) -> None:
+ """Store the minor series (e.g. '1.6') so any patch release is covered."""
+ series = _get_minor_series(version)
try:
- SEEN_VERSION_FILE.write_text(version, encoding="utf-8")
+ SEEN_VERSION_FILE.write_text(series, encoding="utf-8")
except OSError as e:
logger.debug("Could not write seen version file: %s", e)
@@ -66,12 +81,15 @@ def _mark_seen(version: str) -> None:
def _should_show() -> bool:
if not ATLAS_HOME.is_dir():
return False
- if __version__ not in WHATS_NEW_VERSIONS:
+ current_series = _get_minor_series(__version__)
+ if current_series not in WHATS_NEW_VERSIONS:
return False
seen = _get_seen_version()
if seen is None:
return True
- return _parse_version(__version__) > _parse_version(seen)
+ # Normalize seen value — old installs may have stored an exact version
+ seen_series = _get_minor_series(seen)
+ return seen_series != current_series
# ── Asset helpers ─────────────────────────────────────────────────
@@ -106,12 +124,13 @@ def _load_image_data_uri(filename: str) -> str:
def _build_html(version: str) -> str | None:
"""
- Read the HTML template for a version and inject any asset placeholders.
+ Read the HTML template for the version's minor series and inject asset placeholders.
Supported placeholders:
{{ITENTIAL_LOGO}} — base64 data URI for itential-logo-dark.svg
"""
- template_path = PROJECT_TEMPLATES / f"whats-new-{version}.html"
+ series = _get_minor_series(version)
+ template_path = PROJECT_TEMPLATES / f"whats-new-{series}.html"
if not template_path.is_file():
logger.debug("What's New template not found: %s", template_path)
return None
@@ -138,6 +157,13 @@ def _build_html(version: str) -> str | None:
"Knowledge Base remediation steps shown by default in reports",
"Run [bold]session repair[/bold] to backfill pre-1.5 session metadata",
],
+ "1.6": [
+ "MongoDB logs now collected and analyzed alongside IAP logs",
+ "Kubernetes environment support — values.yaml + kubectl, no SSH required",
+ "[bold]--version[/bold] now shows Python version, path, and OS info",
+ "[bold]session prune --older-than DAYS[/bold] — bulk-delete uncaptured sessions",
+ "Use [bold]--dry-run[/bold] to preview which sessions would be pruned",
+ ],
}
@@ -146,7 +172,8 @@ def _show_cli_summary(version: str) -> None:
theme = ui.theme
console = Console()
- bullets = _CLI_BULLETS.get(version, [])
+ series = _get_minor_series(version)
+ bullets = _CLI_BULLETS.get(series, [])
if not bullets:
return
@@ -156,7 +183,7 @@ def _show_cli_summary(version: str) -> None:
console.print(Panel(
"\n".join(lines),
- title=f"[bold {theme.primary}]🎉 What's New in v{version}[/bold {theme.primary}]",
+ title=f"[bold {theme.primary}]🎉 What's New in v{series}[/bold {theme.primary}]",
title_align="left",
border_style=theme.primary,
box=box.ROUNDED,
@@ -194,13 +221,13 @@ def _open_html_page(version: str) -> None:
if html is None:
return
- # Write to ~/.atlas so the browser can read it reliably
- page_path = ATLAS_HOME / f"whats-new-v{version}.html"
+ series = _get_minor_series(version)
+ page_path = ATLAS_HOME / f"whats-new-v{series}.html"
try:
page_path.write_text(html, encoding="utf-8")
except OSError:
tmp = tempfile.NamedTemporaryFile(
- suffix=".html", prefix=f"atlas-whats-new-{version}-",
+ suffix=".html", prefix=f"atlas-whats-new-{series}-",
delete=False,
)
tmp.write(html.encode("utf-8"))
@@ -221,11 +248,15 @@ def maybe_show_whats_new(*, force: bool = False) -> None:
Prints CLI bullet points to the terminal (always visible, even over SSH),
then opens the detailed HTML page in the browser (if available).
+
+ The notice is keyed to the minor version series (major.minor). Any patch
+ release in the same series uses the same page and is only shown once.
"""
version = __version__
+ series = _get_minor_series(version)
if force:
- if version in WHATS_NEW_VERSIONS:
+ if series in WHATS_NEW_VERSIONS:
_show_cli_summary(version)
_open_html_page(version)
_mark_seen(version)
diff --git a/src/platform_atlas/main.py b/src/platform_atlas/main.py
index 11620e9..69cfb8b 100644
--- a/src/platform_atlas/main.py
+++ b/src/platform_atlas/main.py
@@ -33,7 +33,7 @@
__author__ = "Cody Rester"
__contact__ = "cody.rester@itential.com"
-__license__ = "Apache-2.0"
+__license__ = "GPL-3.0-or-later"
#----############## MAIN ##############----#
@handle_errors(exit_on_error=True, show_traceback=False)
diff --git a/src/platform_atlas/reporting/assets/templates/whats-new-1.6.html b/src/platform_atlas/reporting/assets/templates/whats-new-1.6.html
new file mode 100644
index 0000000..ddaaa37
--- /dev/null
+++ b/src/platform_atlas/reporting/assets/templates/whats-new-1.6.html
@@ -0,0 +1,550 @@
+
+
+
+
+
+What's New in Platform Atlas v1.6
+
+
+
+
+
+
+
+
+
+
+
+ Platform Atlas
+
+
What's New
+
Version 1.6 Series · April 2026
+
+
+
+
+
+
+
+
+ Log Analysis
+
MongoDB logs in the mix
+
+ Atlas now captures and analyzes MongoDB logs alongside IAP and webserver logs.
+ Run top-N frequency ranking to surface the most repeated messages, or switch
+ to heuristic keyword matching to catch errors and warnings that matter most.
+
+
+ Control log collection with
+ --log-mode top or
+ --log-mode heuristics,
+ tune depth with --log-top-n,
+ and filter by level with --log-levels error warn.
+ Skip log collection entirely with --skip-logs.
+
+
+
+
+
+
+
+
+
+
+ Environments
+
Kubernetes environments
+
+ When creating an environment, you can now select Kubernetes as the deployment type.
+ Atlas collects configuration by parsing values.yaml
+ and running kubectl commands — no SSH access required.
+
+
+ IAP deployments on Kubernetes have different paths and configuration patterns.
+ Atlas handles them natively so you don't have to adapt your workflow.
+
+
+ env create — select Kubernetes when prompted for deployment type.
+
+
+
+
+
+
+
+
+
+
+ Session Management
+
Bulk session cleanup
+
+ Accumulate unused sessions over time? session prune
+ deletes sessions that were created but never captured, older than a threshold you set.
+
+
+ Run with --dry-run first to see exactly what would
+ be removed — a table shows each session's name, environment, creation date, and age in days.
+ The active session is always protected even if it qualifies.
+
+
+ session prune --older-than 90 --dry-run
+
+
+
+
+
+
+
+
+
+
+
🖥️
+
--version system info 1.6.1
+
+ platform-atlas --version now shows your Python version and
+ executable path alongside the Atlas version, and the OS and architecture.
+ Useful for diagnosing environment issues without hunting through system settings.
+
+
+
+
📋
+
Log collection flags 1.6
+
+ Fine-tune log collection per capture run:
+ --log-mode,
+ --log-top-n,
+ --log-levels, and
+ --skip-logs.
+ Apply to session run capture or
+ session run all.
+