This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-atr-experiments.git


The following commit(s) were added to refs/heads/main by this push:
     new b8b196a  Improve the release candidate workflow
b8b196a is described below

commit b8b196a76bf66edab2d72eca1c6b306f950fa3e3
Author: Sean B. Palmer <s...@miscoranda.com>
AuthorDate: Wed Feb 19 19:24:54 2025 +0200

    Improve the release candidate workflow
---
 atr/config.py                                      |   2 -
 atr/db/__init__.py                                 |   4 +-
 atr/db/models.py                                   |  15 +-
 atr/routes.py                                      | 143 +++++++-----
 atr/static/css/atr.css                             |   6 +
 .../{release-attach.html => candidate-attach.html} |  10 +-
 .../{release-create.html => candidate-create.html} |   0
 atr/templates/candidate-review.html                | 163 ++++++++++++++
 ...verify.html => candidate-signature-verify.html} |  86 +++++---
 atr/templates/includes/sidebar.html                |  22 +-
 atr/templates/index.html                           |  11 +-
 .../{user-keys-add.html => keys-add.html}          | 134 +-----------
 atr/templates/keys-review.html                     | 141 ++++++++++++
 atr/templates/pages.html                           | 240 ---------------------
 atr/templates/user-uploads.html                    | 103 ---------
 ...al_schema.py => b561e6142755_initial_schema.py} |   6 +-
 16 files changed, 503 insertions(+), 583 deletions(-)

diff --git a/atr/config.py b/atr/config.py
index 7a3c11d..8da79a7 100644
--- a/atr/config.py
+++ b/atr/config.py
@@ -23,14 +23,12 @@ from atr.db.models import __file__ as data_models_file
 
 
 class AppConfig:
-    # Get the project root directory (where alembic.ini is)
     PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
     STATE_DIR = os.path.join(PROJECT_ROOT, "state")
 
     RELEASE_STORAGE_DIR = os.path.join(STATE_DIR, "releases")
     DATA_MODELS_FILE = data_models_file
 
-    # Use aiosqlite for async SQLite access
     SQLITE_URL = config("SQLITE_URL", default="sqlite+aiosqlite:///./atr.db")
 
     ADMIN_USERS = frozenset(
diff --git a/atr/db/__init__.py b/atr/db/__init__.py
index 21e200e..003b436 100644
--- a/atr/db/__init__.py
+++ b/atr/db/__init__.py
@@ -17,7 +17,7 @@
 
 import os
 
-from alembic import command
+# from alembic import command
 from alembic.config import Config
 from quart import current_app
 from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, 
create_async_engine
@@ -66,7 +66,7 @@ def create_database(app: QuartApp) -> None:
         alembic_cfg.set_main_option("script_location", 
os.path.join(project_root, "migrations"))
         # Set the database URL in the config
         alembic_cfg.set_main_option("sqlalchemy.url", sqlite_url)
-        command.upgrade(alembic_cfg, "head")
+        # command.upgrade(alembic_cfg, "head")
 
         # Create any tables that might be missing
         async with engine.begin() as conn:
diff --git a/atr/db/models.py b/atr/db/models.py
index 6b27006..639f509 100644
--- a/atr/db/models.py
+++ b/atr/db/models.py
@@ -134,10 +134,16 @@ class DistributionChannel(SQLModel, table=True):
 
 
 class Package(SQLModel, table=True):
-    id: int | None = Field(default=None, primary_key=True)
-    file: str
-    signature: str
-    checksum: str
+    # The SHA3-256 hash of the file, used as filename in storage
+    id_sha3: str = Field(primary_key=True)
+    # Original filename from uploader
+    filename: str
+    # SHA-512 hash of the file
+    sha512: str
+    # The signature file
+    signature_sha3: str
+    # Uploaded timestamp
+    uploaded: datetime.datetime
 
     # Many-to-one: A package belongs to one release
     release_key: str | None = Field(default=None, 
foreign_key="release.storage_key")
@@ -179,6 +185,7 @@ class Release(SQLModel, table=True):
     storage_key: str = Field(primary_key=True)
     stage: ReleaseStage
     phase: ReleasePhase
+    created: datetime.datetime
 
     # Many-to-one: A release belongs to one PMC, a PMC can have multiple 
releases
     pmc_id: int | None = Field(default=None, foreign_key="pmc.id")
diff --git a/atr/routes.py b/atr/routes.py
index c37c9c3..bb4fc12 100644
--- a/atr/routes.py
+++ b/atr/routes.py
@@ -125,28 +125,44 @@ async def release_attach_post(session: ClientSession, 
request: Request) -> Respo
 
     # Save files using their hashes as filenames
     uploads_path = Path(get_release_storage_dir())
-    artifact_hash = await save_file_by_hash(uploads_path, artifact_file)
-    # TODO: Do we need to do anything with the signature hash?
-    # These should be identical, but path might be absolute?
-    # TODO: Need to check, ideally. Could have a data browser
-    signature_hash = await save_file_by_hash(uploads_path, signature_file)
+    artifact_sha3 = await save_file_by_hash(uploads_path, artifact_file)
+    signature_sha3 = await save_file_by_hash(uploads_path, signature_file)
 
-    # Compute SHA-512 checksum of the artifact for the package record
-    checksum_512 = compute_sha512(uploads_path / artifact_hash)
+    # Check if these files are already attached to this release
+    async with get_session() as db_session:
+        # Check for duplicate artifact or signature in a single query
+        statement = select(Package).where(
+            Package.release_key == release_key,
+            (Package.id_sha3 == artifact_sha3) | (Package.signature_sha3 == 
signature_sha3),
+        )
+        duplicate = (await db_session.execute(statement)).first()
+
+        if duplicate:
+            package = duplicate[0]
+            # TODO: Perhaps we should call the id_sha3 field artifact_sha3 
instead
+            if package.id_sha3 == artifact_sha3:
+                raise ASFQuartException("This release artifact has already 
been uploaded", errorcode=400)
+            else:
+                raise ASFQuartException("This signature file has already been 
uploaded", errorcode=400)
+
+        # Compute SHA-512 of the artifact for the package record
+        sha512 = compute_sha512(uploads_path / artifact_sha3)
 
     # Create the package record in the database
     async with get_session() as db_session:
         async with db_session.begin():
             package = Package(
-                file=artifact_hash,
-                signature=signature_hash,
-                checksum=checksum_512,
+                id_sha3=artifact_sha3,
+                filename=artifact_file.filename,
+                signature_sha3=signature_sha3,
+                sha512=sha512,
                 release_key=release_key,
+                uploaded=datetime.datetime.now(datetime.UTC),
             )
             db_session.add(package)
 
-    # Redirect to the user's uploads page
-    return redirect(url_for("root_user_uploads"))
+    # Redirect to the release candidate review page
+    return redirect(url_for("root_candidate_review"))
 
 
 async def release_create_post(session: ClientSession, request: Request) -> 
Response:
@@ -193,6 +209,7 @@ async def release_create_post(session: ClientSession, 
request: Request) -> Respo
                 phase=ReleasePhase.RELEASE_CANDIDATE,
                 pmc_id=pmc.id,
                 version=version,
+                created=datetime.datetime.now(datetime.UTC),
             )
             db_session.add(release)
 
@@ -202,7 +219,7 @@ async def release_create_post(session: ClientSession, 
request: Request) -> Respo
 
     # Redirect to the attach artifacts page with the storage token
     # We should possibly have a results, or list of releases, page instead
-    return redirect(url_for("root_release_attach", storage_key=storage_token))
+    return redirect(url_for("root_candidate_attach", 
storage_key=storage_token))
 
 
 @APP.route("/")
@@ -211,15 +228,9 @@ async def root() -> str:
     return await render_template("index.html")
 
 
-@APP.route("/pages")
-async def root_pages() -> str:
-    """List all pages on the website."""
-    return await render_template("pages.html")
-
-
-@APP.route("/release/attach", methods=["GET", "POST"])
+@APP.route("/candidate/attach", methods=["GET", "POST"])
 @require(Requirements.committer)
-async def root_release_attach() -> Response | str:
+async def root_candidate_attach() -> Response | str:
     """Attach package artifacts to an existing release."""
     session = await session_read()
     if session is None:
@@ -249,16 +260,16 @@ async def root_release_attach() -> Response | str:
 
     # For GET requests, show the form
     return await render_template(
-        "release-attach.html",
+        "candidate-attach.html",
         asf_id=session.uid,
         releases=user_releases,
         selected_release=storage_key,
     )
 
 
-@APP.route("/release/create", methods=["GET", "POST"])
+@APP.route("/candidate/create", methods=["GET", "POST"])
 @require(Requirements.committer)
-async def root_release_create() -> Response | str:
+async def root_candidate_create() -> Response | str:
     """Create a new release in the database."""
     session = await session_read()
     if session is None:
@@ -270,16 +281,16 @@ async def root_release_create() -> Response | str:
 
     # For GET requests, show the form
     return await render_template(
-        "release-create.html",
+        "candidate-create.html",
         asf_id=session.uid,
         pmc_memberships=session.committees,
         committer_projects=session.projects,
     )
 
 
-@APP.route("/release/signatures/verify/<release_key>")
+@APP.route("/candidate/signatures/verify/<release_key>")
 @require(Requirements.committer)
-async def root_release_signatures_verify(release_key: str) -> str:
+async def root_candidate_signatures_verify(release_key: str) -> str:
     """Verify the GPG signatures for all packages in a release candidate."""
     session = await session_read()
     if session is None:
@@ -314,10 +325,10 @@ async def root_release_signatures_verify(release_key: 
str) -> str:
         storage_dir = Path(get_release_storage_dir())
 
         for package in release.packages:
-            result = {"file": package.file}
+            result = {"file": package.id_sha3}
 
-            artifact_path = storage_dir / package.file
-            signature_path = storage_dir / package.signature
+            artifact_path = storage_dir / package.id_sha3
+            signature_path = storage_dir / package.signature_sha3
 
             if not artifact_path.exists():
                 result["error"] = "Package artifact file not found"
@@ -326,12 +337,12 @@ async def root_release_signatures_verify(release_key: 
str) -> str:
             else:
                 # Verify the signature
                 result = await verify_gpg_signature(artifact_path, 
signature_path, ascii_armored_keys)
-                result["file"] = package.file
+                result["file"] = package.id_sha3
 
             verification_results.append(result)
 
         return await render_template(
-            "release-signature-verify.html", release=release, 
verification_results=verification_results
+            "candidate-signature-verify.html", release=release, 
verification_results=verification_results
         )
 
 
@@ -405,24 +416,39 @@ async def root_pmc_list() -> list[dict]:
     ]
 
 
-@APP.route("/user/keys/add", methods=["GET", "POST"])
+@APP.route("/keys/review")
 @require(Requirements.committer)
-async def root_user_keys_add() -> str:
-    """Add a new GPG key to the user's account."""
+async def root_keys_review() -> str:
+    """Show all GPG keys associated with the user's account."""
     session = await session_read()
     if session is None:
         raise ASFQuartException("Not authenticated", errorcode=401)
 
-    error = None
-    key_info = None
-    user_keys = []
-
     # Get all existing keys for the user
     async with get_session() as db_session:
         pmcs_loader = selectinload(cast(InstrumentedAttribute[list[PMC]], 
PublicSigningKey.pmcs))
         statement = 
select(PublicSigningKey).options(pmcs_loader).where(PublicSigningKey.apache_uid 
== session.uid)
         user_keys = (await db_session.execute(statement)).scalars().all()
 
+    return await render_template(
+        "keys-review.html",
+        asf_id=session.uid,
+        user_keys=user_keys,
+        algorithms=algorithms,
+    )
+
+
+@APP.route("/keys/add", methods=["GET", "POST"])
+@require(Requirements.committer)
+async def root_keys_add() -> str:
+    """Add a new GPG key to the user's account."""
+    session = await session_read()
+    if session is None:
+        raise ASFQuartException("Not authenticated", errorcode=401)
+
+    error = None
+    key_info = None
+
     if request.method == "POST":
         form = await request.form
         public_key = form.get("public_key")
@@ -434,26 +460,26 @@ async def root_user_keys_add() -> str:
         selected_pmcs = form.getlist("selected_pmcs")
         if not selected_pmcs:
             return await render_template(
-                "user-keys-add.html",
+                "keys-add.html",
                 asf_id=session.uid,
                 pmc_memberships=session.committees,
                 error="You must select at least one PMC",
                 key_info=None,
-                user_keys=user_keys,
                 algorithms=algorithms,
                 committer_projects=session.projects,
             )
 
         # Ensure that the selected PMCs are ones of which the user is actually 
a member
-        invalid_pmcs = [pmc for pmc in selected_pmcs if pmc not in 
session.committees]
+        invalid_pmcs = [
+            pmc for pmc in selected_pmcs if (pmc not in session.committees) 
and (pmc not in session.projects)
+        ]
         if invalid_pmcs:
             return await render_template(
-                "user-keys-add.html",
+                "keys-add.html",
                 asf_id=session.uid,
                 pmc_memberships=session.committees,
                 error=f"Invalid PMC selection: {', '.join(invalid_pmcs)}",
                 key_info=None,
-                user_keys=user_keys,
                 algorithms=algorithms,
                 committer_projects=session.projects,
             )
@@ -461,12 +487,11 @@ async def root_user_keys_add() -> str:
         error, key_info = await user_keys_add(session, public_key, 
selected_pmcs)
 
     return await render_template(
-        "user-keys-add.html",
+        "keys-add.html",
         asf_id=session.uid,
         pmc_memberships=session.committees,
         error=error,
         key_info=key_info,
-        user_keys=user_keys,
         algorithms=algorithms,
         committer_projects=session.projects,
     )
@@ -495,10 +520,10 @@ async def root_user_keys_delete() -> str:
         return f"Deleted {count} keys"
 
 
-@APP.route("/user/uploads")
+@APP.route("/candidate/review")
 @require(Requirements.committer)
-async def root_user_uploads() -> str:
-    """Show all release candidates uploaded by the current user."""
+async def root_candidate_review() -> str:
+    """Show all release candidates to which the user has access."""
     session = await session_read()
     if session is None:
         raise ASFQuartException("Not authenticated", errorcode=401)
@@ -525,7 +550,7 @@ async def root_user_uploads() -> str:
             if session.uid in r.pmc.pmc_members:
                 user_releases.append(r)
 
-        return await render_template("user-uploads.html", 
releases=user_releases)
+        return await render_template("candidate-review.html", 
releases=user_releases)
 
 
 async def save_file_by_hash(base_dir: Path, file: FileStorage) -> str:
@@ -579,7 +604,9 @@ async def user_keys_add(session: ClientSession, public_key: 
str, selected_pmcs:
         if not import_result.fingerprints:
             return ("Invalid public key format", None)
 
-        fingerprint = import_result.fingerprints[0].lower()
+        fingerprint = import_result.fingerprints[0]
+        if fingerprint is not None:
+            fingerprint = fingerprint.lower()
         # APP.logger.info("Import result: %s", vars(import_result))
         # Get key details
         # We could probably use import_result instead
@@ -589,7 +616,7 @@ async def user_keys_add(session: ClientSession, public_key: 
str, selected_pmcs:
     # Then we have the properties listed here:
     # https://gnupg.readthedocs.io/en/latest/#listing-keys
     # Note that "fingerprint" is not listed there, but we have it anyway...
-    key = next((k for k in keys if k["fingerprint"].lower() == fingerprint), 
None)
+    key = next((k for k in keys if (k["fingerprint"] is not None) and 
(k["fingerprint"].lower() == fingerprint)), None)
     if not key:
         return ("Failed to import key", None)
     if (key.get("algo") == "1") and (int(key.get("length", "0")) < 2048):
@@ -620,11 +647,15 @@ async def user_keys_add_session(
     if not session.uid:
         return ("You must be signed in to add a key", None)
 
+    fingerprint = key.get("fingerprint")
+    if not isinstance(fingerprint, str):
+        return ("Invalid key fingerprint", None)
+    fingerprint = fingerprint.lower()
     uids = key.get("uids")
     async with db_session.begin():
         # Create new key record
         key_record = PublicSigningKey(
-            fingerprint=key["fingerprint"].lower(),
+            fingerprint=fingerprint,
             algorithm=int(key["algo"]),
             length=int(key.get("length", "0")),
             created=datetime.datetime.fromtimestamp(int(key["date"])),
@@ -650,7 +681,7 @@ async def user_keys_add_session(
         "",
         {
             "key_id": key["keyid"],
-            "fingerprint": key["fingerprint"].lower(),
+            "fingerprint": key["fingerprint"].lower() if 
key.get("fingerprint") else "Unknown",
             "user_id": key["uids"][0] if key.get("uids") else "Unknown",
             "creation_date": datetime.datetime.fromtimestamp(int(key["date"])),
             "expiration_date": 
datetime.datetime.fromtimestamp(int(key["expires"])) if key.get("expires") else 
None,
@@ -694,8 +725,8 @@ async def verify_gpg_signature_file(
     # Collect all available information for debugging
     debug_info = {
         "key_id": verified.key_id or "Not available",
-        "fingerprint": verified.fingerprint.lower() or "Not available",
-        "pubkey_fingerprint": verified.pubkey_fingerprint.lower() or "Not 
available",
+        "fingerprint": verified.fingerprint.lower() if verified.fingerprint 
else "Not available",
+        "pubkey_fingerprint": verified.pubkey_fingerprint.lower() if 
verified.pubkey_fingerprint else "Not available",
         "creation_date": verified.creation_date or "Not available",
         "timestamp": verified.timestamp or "Not available",
         "username": verified.username or "Not available",
diff --git a/atr/static/css/atr.css b/atr/static/css/atr.css
index 5aca184..6625b04 100644
--- a/atr/static/css/atr.css
+++ b/atr/static/css/atr.css
@@ -81,9 +81,15 @@ table td {
 
     /* Not sure if we should keep it this way, but it seems pretty good */
     font-family: ui-monospace, "SFMono-Regular", "Menlo", "Monaco", 
"Consolas", monospace;
+    word-break: break-all;
     font-size: 0.9em;
 }
 
+table td.prose {
+    font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe 
UI", "Oxygen", "Ubuntu", "Cantarell", "Open Sans", "Helvetica Neue", sans-serif;
+    font-size: 1em;
+}
+
 table tr {
     /* This doesn't always work; not clear why */
     border-bottom: 1px solid #c1c2c3;
diff --git a/atr/templates/release-attach.html 
b/atr/templates/candidate-attach.html
similarity index 93%
rename from atr/templates/release-attach.html
rename to atr/templates/candidate-attach.html
index 082a784..b304560 100644
--- a/atr/templates/release-attach.html
+++ b/atr/templates/candidate-attach.html
@@ -64,7 +64,8 @@
 {% block content %}
   <h1>Attach package artifacts</h1>
   <p class="intro">
-    Welcome, <strong>{{ asf_id }}</strong>! Use this form to attach package 
artifacts to an existing release candidate.
+    Welcome, <strong>{{ asf_id }}</strong>! Use this form to attach package 
artifacts
+    to an existing release candidate.
   </p>
 
   <form method="post" enctype="multipart/form-data" class="striking">
@@ -84,7 +85,12 @@
                 </option>
               {% endfor %}
             </select>
-            {% if not releases %}<p class="error-message">No releases found 
that you can attach artifacts to.</p>{% endif %}
+            {% if not releases %}
+              <p class="error-message">
+                No releases found that you can
+                attach artifacts to.
+              </p>
+            {% endif %}
           </td>
         </tr>
 
diff --git a/atr/templates/release-create.html 
b/atr/templates/candidate-create.html
similarity index 100%
rename from atr/templates/release-create.html
rename to atr/templates/candidate-create.html
diff --git a/atr/templates/candidate-review.html 
b/atr/templates/candidate-review.html
new file mode 100644
index 0000000..40f3e5a
--- /dev/null
+++ b/atr/templates/candidate-review.html
@@ -0,0 +1,163 @@
+{% extends "layouts/base.html" %}
+
+{% block title %}
+  Release candidates ~ ATR
+{% endblock title %}
+
+{% block description %}
+  Release candidates to which you have access.
+{% endblock description %}
+
+{% block stylesheets %}
+  {{ super() }}
+  <style>
+      .candidate-table {
+          width: 100%;
+          border-collapse: collapse;
+          margin: 1rem 0;
+      }
+
+      .candidate-table th,
+      .candidate-table td {
+          padding: 0.75rem;
+          text-align: left;
+          border: 1px solid #ddd;
+      }
+
+      .candidate-table th {
+          background-color: #f5f5f5;
+          font-weight: 600;
+          width: 200px;
+      }
+
+      .candidate-table tr:hover {
+          background-color: #f8f8f8;
+      }
+
+      .candidate-meta {
+          color: #666;
+          font-size: 0.9em;
+      }
+
+      .no-releases {
+          color: #666;
+          font-style: italic;
+      }
+
+      .verify-link {
+          display: inline-block;
+          padding: 0.5rem 1rem;
+          background: #003366;
+          color: white;
+          border: none;
+          border-radius: 4px;
+          cursor: pointer;
+          font-weight: 500;
+          text-decoration: none;
+      }
+
+      .verify-link:hover {
+          background: #004477;
+          color: white;
+      }
+
+      .package-separator {
+          height: 2rem;
+          background-color: #f5f5f5;
+      }
+
+      .candidate-header {
+          border: 1px solid #ddd;
+          border-radius: 4px;
+          padding: 1rem;
+          margin-bottom: 1rem;
+          background-color: #f8f8f8;
+      }
+
+      .candidate-header h3 {
+          margin: 0 0 0.5rem 0;
+      }
+
+      .candidate-meta {
+          color: #666;
+          font-size: 0.9em;
+          display: flex;
+          flex-wrap: wrap;
+          gap: 1rem;
+      }
+
+      .candidate-meta-item::after {
+          content: "•";
+          margin-left: 1rem;
+          color: #ccc;
+      }
+
+      .candidate-meta-item:last-child::after {
+          content: none;
+      }
+  </style>
+{% endblock stylesheets %}
+
+{% block content %}
+  <h1>Release candidates</h1>
+  <p class="intro">Here are all the release candidates to which you have 
access.</p>
+
+  {% if releases %}
+    {% for release in releases %}
+      <div class="candidate-header">
+        <h3>{{ release.pmc.project_name }}</h3>
+        <div class="candidate-meta">
+          <span class="candidate-meta-item">Version: {{ release.version 
}}</span>
+          <span class="candidate-meta-item">Stage: {{ release.stage.value 
}}</span>
+          <span class="candidate-meta-item">Phase: {{ release.phase.value 
}}</span>
+          <span class="candidate-meta-item">Created: {{ 
release.created.strftime("%Y-%m-%d %H:%M UTC") }}</span>
+        </div>
+      </div>
+
+      <table class="candidate-table">
+        {% for package in release.packages %}
+          {% if not loop.first %}
+            <tr class="package-separator">
+              <td colspan="2"></td>
+            </tr>
+          {% endif %}
+          <tr>
+            <th>Original Filename</th>
+            <td>{{ package.filename }}</td>
+          </tr>
+          <tr>
+            <th>File Hash (SHA3)</th>
+            <td>{{ package.id_sha3 }}</td>
+          </tr>
+          <tr>
+            <th>Signature Hash (SHA3)</th>
+            <td>{{ package.signature_sha3 }}</td>
+          </tr>
+          <tr>
+            <th>SHA-512</th>
+            <td>{{ package.sha512 }}</td>
+          </tr>
+          <tr>
+            <th>Uploaded</th>
+            <td>{{ package.uploaded.strftime("%Y-%m-%d %H:%M UTC") }}</td>
+          </tr>
+          <tr>
+            <th>Actions</th>
+            <td class="prose">
+              <a class="verify-link"
+                 href="{{ url_for('root_candidate_signatures_verify', 
release_key=release.storage_key) }}">
+                Verify Signatures
+              </a>
+            </td>
+          </tr>
+        {% endfor %}
+      </table>
+    {% endfor %}
+  {% else %}
+    <p class="no-releases">You haven't created any releases yet.</p>
+  {% endif %}
+
+  <p>
+    <a href="{{ url_for('root_candidate_create') }}">Create a release 
candidate</a>
+  </p>
+{% endblock content %}
diff --git a/atr/templates/release-signature-verify.html 
b/atr/templates/candidate-signature-verify.html
similarity index 67%
rename from atr/templates/release-signature-verify.html
rename to atr/templates/candidate-signature-verify.html
index 0d4c697..f9be33f 100644
--- a/atr/templates/release-signature-verify.html
+++ b/atr/templates/candidate-signature-verify.html
@@ -11,7 +11,7 @@
 {% block stylesheets %}
   {{ super() }}
   <style>
-      .release-info {
+      .candidate-info {
           margin-bottom: 2rem;
       }
 
@@ -37,6 +37,10 @@
           background: #f5f5f5;
       }
 
+      .verification-status .status {
+          font-weight: bold;
+      }
+
       .navigation {
           margin-top: 2rem;
       }
@@ -51,7 +55,7 @@
       }
 
       .status.success {
-          color: #28a745;
+          color: #219f3f;
       }
 
       .status.failure {
@@ -73,9 +77,20 @@
           border: 1px solid #dee2e6;
       }
 
-      .debug-info h3 {
-          margin-top: 0;
+      .debug-info summary {
           color: #666;
+          font-weight: bold;
+          cursor: pointer;
+          padding-bottom: 0.5rem;
+      }
+
+      .debug-info summary:hover {
+          color: #333;
+      }
+
+      .debug-info[open] summary {
+          border-bottom: 1px solid #dee2e6;
+          margin-bottom: 1rem;
       }
 
       .debug-info dl {
@@ -95,9 +110,38 @@
           word-break: break-all;
       }
 
+      .candidate-header {
+          border: 1px solid #ddd;
+          border-radius: 4px;
+          padding: 1rem;
+          margin-bottom: 1rem;
+          background-color: #f8f8f8;
+      }
+
+      .candidate-header h3 {
+          margin: 0 0 0.5rem 0;
+      }
+
+      .candidate-meta {
+          color: #666;
+          font-size: 0.9em;
+          display: flex;
+          flex-wrap: wrap;
+          gap: 1rem;
+      }
+
+      .candidate-meta-item::after {
+          content: "•";
+          margin-left: 1rem;
+          color: #ccc;
+      }
+
+      .candidate-meta-item:last-child::after {
+          content: none;
+      }
+
       pre.stderr {
           background: #f8f9fa;
-          padding: 0.5rem;
           border-radius: 2px;
           overflow-x: auto;
           margin: 0.5rem 0;
@@ -109,13 +153,14 @@
 {% block content %}
   <h1>Verify release signatures</h1>
 
-  <div class="release-info">
-    <h2>{{ release.pmc.project_name }}</h2>
-    <p>
-      Stage: {{ release.stage.value }}
-      •
-      Phase: {{ release.phase.value }}
-    </p>
+  <div class="candidate-header">
+    <h3>{{ release.pmc.project_name }}</h3>
+    <div class="candidate-meta">
+      <span class="candidate-meta-item">Version: {{ release.version }}</span>
+      <span class="candidate-meta-item">Stage: {{ release.stage.value }}</span>
+      <span class="candidate-meta-item">Phase: {{ release.phase.value }}</span>
+      <span class="candidate-meta-item">Created: {{ 
release.created.strftime("%Y-%m-%d %H:%M UTC") }}</span>
+    </div>
   </div>
 
   <div class="package-list">
@@ -140,8 +185,8 @@
           {% endif %}
 
           {% if result.debug_info %}
-            <div class="debug-info">
-              <h3>Debug Information</h3>
+            <details class="debug-info">
+              <summary>Debug Information</summary>
               <dl>
                 {% for key, value in result.debug_info.items() %}
                   <dt>{{ key }}</dt>
@@ -154,21 +199,10 @@
                   </dd>
                 {% endfor %}
               </dl>
-            </div>
+            </details>
           {% endif %}
         </div>
       </div>
     {% endfor %}
   </div>
-
-  <h2>Navigation</h2>
-
-  <div class="navigation">
-    <p>
-      <a href="{{ url_for('root_user_uploads') }}">Back to Your Uploads</a>
-    </p>
-    <p>
-      <a href="{{ url_for('root_pages') }}">Return to Main Page</a>
-    </p>
-  </div>
 {% endblock content %}
diff --git a/atr/templates/includes/sidebar.html 
b/atr/templates/includes/sidebar.html
index 46c8ce9..65ad794 100644
--- a/atr/templates/includes/sidebar.html
+++ b/atr/templates/includes/sidebar.html
@@ -39,28 +39,32 @@
     </ul>
 
     {% if current_user %}
-      <h3>Release management</h3>
+      <h3>Candidate management</h3>
       <ul>
         <li>
-          <a href="{{ url_for('root_release_create') }}"
-             {% if request.endpoint == 'root_release_create' 
%}class="active"{% endif %}>Create release candidate</a>
+          <a href="{{ url_for('root_candidate_create') }}"
+             {% if request.endpoint == 'root_candidate_create' 
%}class="active"{% endif %}>Create candidate</a>
         </li>
         <!-- TODO: Don't show this if the user doesn't have any release 
candidates? -->
         <li>
-          <a href="{{ url_for('root_release_attach') }}"
-             {% if request.endpoint == 'root_release_attach' 
%}class="active"{% endif %}>Attach package artifacts</a>
+          <a href="{{ url_for('root_candidate_attach') }}"
+             {% if request.endpoint == 'root_candidate_attach' 
%}class="active"{% endif %}>Attach artifacts</a>
         </li>
         <li>
-          <a href="{{ url_for('root_user_uploads') }}"
-             {% if request.endpoint == 'root_user_uploads' %}class="active"{% 
endif %}>Your release candidates</a>
+          <a href="{{ url_for('root_candidate_review') }}"
+             {% if request.endpoint == 'root_candidate_review' 
%}class="active"{% endif %}>Review candidates</a>
         </li>
       </ul>
 
       <h3>User management</h3>
       <ul>
         <li>
-          <a href="{{ url_for('root_user_keys_add') }}"
-             {% if request.endpoint == 'root_user_keys_add' %}class="active"{% 
endif %}>Add signing key</a>
+          <a href="{{ url_for('root_keys_review') }}"
+             {% if request.endpoint == 'root_keys_review' %}class="active"{% 
endif %}>Your signing keys</a>
+        </li>
+        <li>
+          <a href="{{ url_for('root_keys_add') }}"
+             {% if request.endpoint == 'root_keys_add' %}class="active"{% 
endif %}>Add signing key</a>
         </li>
         <li>
           <a href="{{ url_for('root_user_keys_delete') }}"
diff --git a/atr/templates/index.html b/atr/templates/index.html
index 66bcd9e..06bf967 100644
--- a/atr/templates/index.html
+++ b/atr/templates/index.html
@@ -8,7 +8,9 @@
   <h1>Apache Trusted Release</h1>
 
   <p>
-    ATR is a release management platform for <a 
href="https://www.apache.org";>Apache Software Foundation</a> projects. It 
provides a standardized workflow for PMC members to submit, verify, and track 
release candidates.
+    ATR is a release management platform for <a 
href="https://www.apache.org";>Apache Software
+    Foundation</a> projects. It provides a standardized workflow for PMC 
members to submit,
+    verify, and track release candidates.
   </p>
 
   <h2>Key Features</h2>
@@ -21,20 +23,17 @@
 
   <h2>Getting Started</h2>
   <p>
-    To submit a release candidate, you must be a PMC member of the target 
project. First, <a href="{{ url_for('root_user_keys_add') }}">add your signing 
key</a>, then <a href="{{ url_for('root_release_create') }}">create a release 
candidate</a>.
+    To submit a release candidate, you must be a PMC member of the target 
project. First, <a href="{{ url_for('root_keys_add') }}">add your signing 
key</a>, then <a href="{{ url_for('root_candidate_create') }}">create a release 
candidate</a>.
   </p>
 
   <h2>Documentation</h2>
   <ul>
-    <li>
-      <a href="{{ url_for('root_pages') }}">Available endpoints and access 
requirements</a>
-    </li>
     <li>
       <a href="{{ url_for('root_pmc_directory') }}">PMC directory and release 
manager assignments</a>
     </li>
     {% if current_user %}
       <li>
-        <a href="{{ url_for('root_user_uploads') }}">Track your uploaded 
release candidates</a>
+        <a href="{{ url_for('root_candidate_review') }}">Track your release 
candidates</a>
       </li>
     {% endif %}
   </ul>
diff --git a/atr/templates/user-keys-add.html b/atr/templates/keys-add.html
similarity index 53%
rename from atr/templates/user-keys-add.html
rename to atr/templates/keys-add.html
index d5e7c13..274c840 100644
--- a/atr/templates/user-keys-add.html
+++ b/atr/templates/keys-add.html
@@ -5,7 +5,7 @@
 {% endblock title %}
 
 {% block description %}
-  Add a GPG public key to your account.
+  Add a public signing key to your account.
 {% endblock description %}
 
 {% block stylesheets %}
@@ -15,7 +15,6 @@
           margin-bottom: 1rem;
       }
 
-      /* TODO: Consider moving this to atr.css */
       .form-group label {
           display: inline-block;
           margin-bottom: 1rem;
@@ -57,66 +56,6 @@
           margin-top: 2rem;
       }
 
-      .success-message {
-          color: #28a745;
-          margin: 1rem 0;
-          padding: 1rem;
-          background: #d4edda;
-          border-radius: 4px;
-      }
-
-      .existing-keys {
-          margin-bottom: 2rem;
-          padding: 1rem 2rem 2rem 2rem;
-          background: #f8f9fa;
-          border-radius: 4px;
-      }
-
-      .keys-grid {
-          display: grid;
-          /* This just messes up resizing */
-          /* grid-template-columns: repeat(auto-fill, minmax(800px, 1fr)); */
-          gap: 1.5rem;
-      }
-
-      .key-card {
-          background: white;
-          border: 1px solid #d1d2d3;
-          border-radius: 4px;
-          overflow: hidden;
-          padding: 1rem;
-      }
-
-      .key-card table {
-          margin: 0;
-      }
-
-      .key-card td {
-          word-break: break-all;
-      }
-
-      .key-card h3 {
-          margin-top: 0;
-          margin-bottom: 1rem;
-      }
-
-      .delete-key-form {
-          margin-top: 1rem;
-      }
-
-      .delete-button {
-          background: #dc3545;
-          color: white;
-          border: none;
-          padding: 0.5rem 1rem;
-          border-radius: 4px;
-          cursor: pointer;
-      }
-
-      .delete-button:hover {
-          background: #c82333;
-      }
-
       .pmc-checkboxes {
           display: grid;
           grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
@@ -146,7 +85,7 @@
 
 {% block content %}
   <h1>Add signing key</h1>
-  <p class="intro">Add your GPG public key to use for signing release 
artifacts.</p>
+  <p class="intro">Add your public key to use for signing release 
artifacts.</p>
 
   <div class="user-info">
     <p>
@@ -194,66 +133,13 @@
     </div>
   {% endif %}
 
-  {% if success %}
-    <div class="success-message">
-      <h2>Success</h2>
-      <p>{{ success }}</p>
-    </div>
-  {% endif %}
-
-  {% if user_keys %}
-    <h2>Your Existing Keys</h2>
-    <div class="existing-keys">
-      <div class="keys-grid">
-        {% for key in user_keys %}
-          <div class="key-card">
-            <table>
-              <tbody>
-                <tr>
-                  <th>Fingerprint</th>
-                  <td>{{ key.fingerprint }}</td>
-                </tr>
-                <tr>
-                  <th>Key Type</th>
-                  <td>{{ algorithms[key.algorithm] }} ({{ key.length }} 
bits)</td>
-                </tr>
-                <tr>
-                  <th>Created</th>
-                  <td>{{ key.created.strftime("%Y-%m-%d %H:%M:%S") }}</td>
-                </tr>
-                <tr>
-                  <th>Expires</th>
-                  <td>{{ key.expires.strftime("%Y-%m-%d %H:%M:%S") if 
key.expires else 'Never' }}</td>
-                </tr>
-                <tr>
-                  <th>User ID</th>
-                  <td>{{ key.declared_uid or 'Not specified' }}</td>
-                </tr>
-                <tr>
-                  <th>Associated projects</th>
-                  <td>
-                    {% if key.pmcs %}
-                      {{ key.pmcs|map(attribute='project_name') |join(', ') }}
-                    {% else %}
-                      No projects associated
-                    {% endif %}
-                  </td>
-                </tr>
-              </tbody>
-            </table>
-          </div>
-        {% endfor %}
-      </div>
-    </div>
-  {% endif %}
-
   <form method="post" class="striking">
     <div class="form-group">
       <label for="public_key">Public Key:</label>
       <textarea id="public_key"
                 name="public_key"
                 required
-                placeholder="Paste your GPG public key here (in ASCII-armored 
format)"
+                placeholder="Paste your public key here (in ASCII-armored 
format)"
                 aria-describedby="key-help"></textarea>
       <small id="key-help">
         Your public key should be in ASCII-armored format, starting with 
"-----BEGIN PGP PUBLIC KEY BLOCK-----"
@@ -269,8 +155,7 @@
               <input type="checkbox"
                      id="pmc_{{ pmc }}"
                      name="selected_pmcs"
-                     value="{{ pmc }}"
-                     required />
+                     value="{{ pmc }}" />
               <label for="pmc_{{ pmc }}">{{ pmc }}</label>
             </div>
           {% endfor %}
@@ -285,15 +170,4 @@
 
     <button type="submit">Add Key</button>
   </form>
-
-  <h2>Navigation</h2>
-
-  <div class="navigation">
-    <p>
-      <a href="{{ url_for('root_user_uploads') }}">Back to Your Uploads</a>
-    </p>
-    <p>
-      <a href="{{ url_for('root_pages') }}">Return to Main Page</a>
-    </p>
-  </div>
 {% endblock content %}
diff --git a/atr/templates/keys-review.html b/atr/templates/keys-review.html
new file mode 100644
index 0000000..6455709
--- /dev/null
+++ b/atr/templates/keys-review.html
@@ -0,0 +1,141 @@
+{% extends "layouts/base.html" %}
+
+{% block title %}
+  Your signing keys ~ ATR
+{% endblock title %}
+
+{% block description %}
+  Review your signing keys.
+{% endblock description %}
+
+{% block stylesheets %}
+  {{ super() }}
+  <style>
+      .existing-keys {
+          margin-bottom: 2rem;
+          padding: 1rem 2rem 2rem 2rem;
+          background: #f8f9fa;
+          border-radius: 4px;
+      }
+
+      .keys-grid {
+          display: grid;
+          gap: 1.5rem;
+      }
+
+      .key-card {
+          background: white;
+          border: 1px solid #d1d2d3;
+          border-radius: 4px;
+          overflow: hidden;
+          padding: 1rem;
+      }
+
+      .key-card table {
+          margin: 0;
+      }
+
+      .key-card td {
+          word-break: break-all;
+      }
+
+      .key-card h3 {
+          margin-top: 0;
+          margin-bottom: 1rem;
+      }
+
+      .delete-key-form {
+          margin-top: 1rem;
+      }
+
+      .delete-button {
+          background: #dc3545;
+          color: white;
+          border: none;
+          padding: 0.5rem 1rem;
+          border-radius: 4px;
+          cursor: pointer;
+      }
+
+      .delete-button:hover {
+          background: #c82333;
+      }
+
+      .navigation {
+          margin-top: 2rem;
+      }
+
+      .success-message {
+          color: #28a745;
+          margin: 1rem 0;
+          padding: 1rem;
+          background: #d4edda;
+          border-radius: 4px;
+      }
+  </style>
+{% endblock stylesheets %}
+
+{% block content %}
+  <h1>Your signing keys</h1>
+  <p class="intro">Review your public keys used for signing release 
artifacts.</p>
+
+  <div class="user-info">
+    <p>
+      Welcome, <strong>{{ asf_id }}</strong>! You are authenticated as an ASF 
committer.
+    </p>
+  </div>
+
+  {% if success %}
+    <div class="success-message">
+      <h2>Success</h2>
+      <p>{{ success }}</p>
+    </div>
+  {% endif %}
+
+  {% if user_keys %}
+    <div class="existing-keys">
+      <div class="keys-grid">
+        {% for key in user_keys %}
+          <div class="key-card">
+            <table>
+              <tbody>
+                <tr>
+                  <th>Fingerprint</th>
+                  <td>{{ key.fingerprint }}</td>
+                </tr>
+                <tr>
+                  <th>Key Type</th>
+                  <td>{{ algorithms[key.algorithm] }} ({{ key.length }} 
bits)</td>
+                </tr>
+                <tr>
+                  <th>Created</th>
+                  <td>{{ key.created.strftime("%Y-%m-%d %H:%M:%S") }}</td>
+                </tr>
+                <tr>
+                  <th>Expires</th>
+                  <td>{{ key.expires.strftime("%Y-%m-%d %H:%M:%S") if 
key.expires else 'Never' }}</td>
+                </tr>
+                <tr>
+                  <th>User ID</th>
+                  <td>{{ key.declared_uid or 'Not specified' }}</td>
+                </tr>
+                <tr>
+                  <th>Associated projects</th>
+                  <td>
+                    {% if key.pmcs %}
+                      {{ key.pmcs|map(attribute='project_name') |join(', ') }}
+                    {% else %}
+                      No projects associated
+                    {% endif %}
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+        {% endfor %}
+      </div>
+    </div>
+  {% else %}
+    <p>You haven't added any signing keys yet.</p>
+  {% endif %}
+{% endblock content %}
diff --git a/atr/templates/pages.html b/atr/templates/pages.html
deleted file mode 100644
index de9db30..0000000
--- a/atr/templates/pages.html
+++ /dev/null
@@ -1,240 +0,0 @@
-{% extends "layouts/base.html" %}
-
-{% block title %}
-  Pages ~ ATR
-{% endblock title %}
-{% block description %}
-  List of all pages and endpoints in ATR.
-{% endblock description %}
-
-{% block stylesheets %}
-  {{ super() }}
-  <style>
-      .endpoint-list {
-          margin: 2rem 0;
-      }
-
-      .endpoint-group {
-          margin-bottom: 2rem;
-      }
-
-      .endpoint {
-          border: 1px solid #ddd;
-          padding: 1rem;
-          margin-bottom: 1rem;
-          border-radius: 4px;
-      }
-
-      .endpoint h3 {
-          margin: 0 0 0.5rem 0;
-      }
-
-      .endpoint-meta {
-          color: #666;
-          font-size: 0.9em;
-          margin-bottom: 0.5rem;
-      }
-
-      .endpoint-description {
-          margin-bottom: 0.5rem;
-      }
-
-      .access-requirement {
-          display: inline-block;
-          padding: 0.25rem 0.5rem;
-          border-radius: 2px;
-          font-size: 0.8em;
-          background: #f5f5f5;
-      }
-
-      .access-requirement.committer {
-          background: #e6f3ff;
-          border: 1px solid #cce5ff;
-      }
-
-      .access-requirement.admin {
-          background: #ffeeba;
-          border: 1px solid #f5d88c;
-      }
-
-      .access-requirement.public {
-          background: #e6ffe6;
-          border: 1px solid #ccebcc;
-      }
-
-      .access-requirement.warning {
-          background: #ffe6e6;
-          border: 1px solid #ffcccc;
-          color: #cc0000;
-          font-weight: bold;
-      }
-  </style>
-{% endblock stylesheets %}
-
-{% block content %}
-  <h1>Pages</h1>
-  <p class="intro">A complete list of all pages and endpoints available in 
ATR.</p>
-
-  <div class="endpoint-list">
-    <div class="endpoint-group">
-      <h2>Main Pages</h2>
-
-      <div class="endpoint">
-        <h3>
-          <a href="{{ url_for('root') }}">/</a>
-        </h3>
-        <div class="endpoint-description">Main welcome page.</div>
-        <div class="endpoint-meta">
-          Access: <span class="access-requirement public">Public</span>
-        </div>
-      </div>
-
-      <div class="endpoint">
-        <h3>
-          <a href="{{ url_for('root_pages') }}">/pages</a>
-        </h3>
-        <div class="endpoint-description">List of all pages on the website 
(this page).</div>
-        <div class="endpoint-meta">
-          Access: <span class="access-requirement public">Public</span>
-        </div>
-      </div>
-    </div>
-
-    <div class="endpoint-group">
-      <h2>PMC Management</h2>
-
-      <div class="endpoint">
-        <h3>
-          <a href="{{ url_for('root_pmc_directory') }}">/pmc/directory</a>
-        </h3>
-        <div class="endpoint-description">Main PMC directory page with all 
PMCs and their latest releases.</div>
-        <div class="endpoint-meta">
-          Access: <span class="access-requirement public">Public</span>
-        </div>
-      </div>
-
-      <div class="endpoint">
-        <h3>
-          <a href="{{ url_for('root_pmc_list') }}">/pmc/list</a>
-        </h3>
-        <div class="endpoint-description">List all PMCs in the database (JSON 
format).</div>
-        <div class="endpoint-meta">
-          Access: <span class="access-requirement public">Public</span>
-        </div>
-      </div>
-
-      <div class="endpoint">
-        <h3>/pmc/&lt;project_name&gt;</h3>
-        <div class="endpoint-description">Get details for a specific PMC (JSON 
format).</div>
-        <div class="endpoint-meta">
-          Access: <span class="access-requirement public">Public</span>
-        </div>
-      </div>
-    </div>
-
-    <div class="endpoint-group">
-      <h2>Release Management</h2>
-
-      <div class="endpoint">
-        <h3>
-          <a href="{{ url_for('root_release_create') }}">/release/create</a>
-        </h3>
-        <div class="endpoint-description">Add a release candidate to the 
database.</div>
-        <div class="endpoint-meta">
-          Access: <span class="access-requirement committer">Committer</span>
-          <br />
-          Additional requirement: Must be PMC member of the target project
-        </div>
-      </div>
-
-      <div class="endpoint">
-        <h3>
-          <a href="{{ url_for('root_user_uploads') }}">/user/uploads</a>
-        </h3>
-        <div class="endpoint-description">Show all release candidates uploaded 
by the current user.</div>
-        <div class="endpoint-meta">
-          Access: <span class="access-requirement committer">Committer</span>
-        </div>
-      </div>
-
-      <div class="endpoint">
-        <h3>/release/signatures/verify/&lt;release_key&gt;</h3>
-        <div class="endpoint-description">Verify GPG signatures for all 
packages in a release candidate.</div>
-        <div class="endpoint-meta">
-          Access: <span class="access-requirement committer">Committer</span>
-        </div>
-      </div>
-    </div>
-
-    <div class="endpoint-group">
-      <h2>User Management</h2>
-
-      <div class="endpoint">
-        <h3>
-          <a href="{{ url_for('root_user_keys_add') }}">/user/keys/add</a>
-        </h3>
-        <div class="endpoint-description">Add a GPG public key to your account 
for signing releases.</div>
-        <div class="endpoint-meta">
-          Access: <span class="access-requirement committer">Committer</span>
-        </div>
-      </div>
-
-      <div class="endpoint">
-        <h3>
-          <a href="{{ url_for('root_user_keys_delete') 
}}">/user/keys/delete</a>
-        </h3>
-        <div class="endpoint-description">Delete all GPG keys associated with 
your account.</div>
-        <div class="endpoint-meta">
-          Access: <span class="access-requirement committer">Committer</span>
-          <br />
-          <span class="access-requirement warning">Warning: This will delete 
all your keys without confirmation!</span>
-        </div>
-      </div>
-    </div>
-
-    <div class="endpoint-group">
-      <h2>Administration</h2>
-
-      <div class="endpoint">
-        <h3>
-          <a href="{{ url_for('secret_blueprint.secret_data') 
}}">/secret/data</a>
-        </h3>
-        <div class="endpoint-description">Browse all records in the 
database.</div>
-        <div class="endpoint-meta">
-          Access: <span class="access-requirement committer">Committer</span>
-          <span class="access-requirement admin">Admin</span>
-          <br />
-          Additional requirement: Must be in ALLOWED_USERS list
-        </div>
-      </div>
-
-      <div class="endpoint">
-        <h3>
-          <a href="{{ url_for('secret_blueprint.secret_pmcs_update') 
}}">/secret/pmcs/update</a>
-        </h3>
-        <div class="endpoint-description">Update PMCs from remote, 
authoritative committee-info.json.</div>
-        <div class="endpoint-meta">
-          Access: <span class="access-requirement committer">Committer</span>
-          <span class="access-requirement admin">Admin</span>
-          <br />
-          Additional requirement: Must be in ALLOWED_USERS list
-        </div>
-      </div>
-    </div>
-
-    <div class="endpoint-group">
-      <h2>API</h2>
-
-      <div class="endpoint">
-        <h3>
-          <a href="{{ url_for('swagger_ui') }}">/api/docs</a>
-        </h3>
-        <div class="endpoint-description">Swagger UI.</div>
-        <div class="endpoint-meta">
-          Access: <span class="access-requirement public">Public</span>
-        </div>
-      </div>
-    </div>
-  </div>
-
-{% endblock content %}
diff --git a/atr/templates/user-uploads.html b/atr/templates/user-uploads.html
deleted file mode 100644
index 48df508..0000000
--- a/atr/templates/user-uploads.html
+++ /dev/null
@@ -1,103 +0,0 @@
-{% extends "layouts/base.html" %}
-
-{% block title %}
-  Release candidates ~ ATR
-{% endblock title %}
-
-{% block description %}
-  Release candidates to which you have access.
-{% endblock description %}
-
-{% block stylesheets %}
-  {{ super() }}
-  <style>
-      .release-list {
-          margin: 1rem 0;
-      }
-
-      div.release {
-          border: 1px solid #ddd;
-          padding: 1rem;
-          margin-bottom: 1rem;
-          border-radius: 4px;
-      }
-
-      .release-header {
-          display: flex;
-          justify-content: space-between;
-          align-items: center;
-          margin-bottom: 0.5rem;
-      }
-
-      .release-meta {
-          color: #666;
-          font-size: 0.9em;
-      }
-
-      .package-list {
-          margin-top: 0.5rem;
-      }
-
-      .package {
-          background: #f5f5f5;
-          padding: 0.5rem;
-          margin: 0.25rem 0;
-          border-radius: 2px;
-      }
-
-      .package div {
-          margin-bottom: 0.25rem;
-      }
-
-      .no-releases {
-          color: #666;
-          font-style: italic;
-      }
-  </style>
-{% endblock stylesheets %}
-
-{% block content %}
-  <h1>Release candidates</h1>
-  <p class="intro">Here are all the release candidates to which you have 
access.</p>
-
-  {% if releases %}
-    <div class="release-list">
-      {% for release in releases %}
-        <div class="release">
-          <div class="release-header">
-            <h3>{{ release.pmc.project_name }}</h3>
-            <span class="release-meta">
-              Stage: {{ release.stage.value }}
-              •
-              Phase: {{ release.phase.value }}
-            </span>
-          </div>
-          <div class="package-list">
-            {% for package in release.packages %}
-              <div class="package">
-                <div>
-                  File: <span class="hex">{{ package.file }}</span>
-                </div>
-                <div>
-                  Signature: <span class="hex">{{ package.signature }}</span>
-                </div>
-                <div>
-                  Checksum (SHA-512): <span class="hex">{{ package.checksum 
}}</span>
-                </div>
-                <p class="package-actions">
-                  <a href="{{ url_for('root_release_signatures_verify', 
release_key=release.storage_key) }}">Verify Signatures</a>
-                </p>
-              </div>
-            {% endfor %}
-          </div>
-        </div>
-      {% endfor %}
-    </div>
-  {% else %}
-    <p class="no-releases">You haven't created any releases yet.</p>
-  {% endif %}
-
-  <p>
-    <a href="{{ url_for('root_release_create') }}">Create a release 
candidate</a>
-  </p>
-{% endblock content %}
diff --git a/migrations/versions/512e973a9ce4_initial_schema.py 
b/migrations/versions/b561e6142755_initial_schema.py
similarity index 84%
rename from migrations/versions/512e973a9ce4_initial_schema.py
rename to migrations/versions/b561e6142755_initial_schema.py
index c9f6768..991d9e4 100644
--- a/migrations/versions/512e973a9ce4_initial_schema.py
+++ b/migrations/versions/b561e6142755_initial_schema.py
@@ -1,15 +1,15 @@
 """initial_schema
 
-Revision ID: 512e973a9ce4
+Revision ID: b561e6142755
 Revises:
-Create Date: 2025-02-18 16:37:04.346002
+Create Date: 2025-02-19 18:52:21.878941
 
 """
 
 from collections.abc import Sequence
 
 # revision identifiers, used by Alembic.
-revision: str = "512e973a9ce4"
+revision: str = "b561e6142755"
 down_revision: str | None = None
 branch_labels: str | Sequence[str] | None = None
 depends_on: str | Sequence[str] | None = None


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tooling.apache.org
For additional commands, e-mail: dev-h...@tooling.apache.org

Reply via email to