Turn the crank on add/edit/delete of issues.

Turn deletion of an issue into a GET rather than a DELETE. This breaks
HTTP idioms, but I have not found a way to perform a DELETE (via
fetch() in the browser) and properly handle a 303 redirect (it can
work, but does a double-fetch of the Location target)

Iterate the add/edit endpoints to fetch the form data.

do_delete_issue_endpoint() now calls .delete_issue() to actually
delete the issue from the Election.

add flash messages to the issue-handling endpoints.

fix the JS in manage.ezt for the dynamic add/edit modal, and add an
id= to the delete buttons for deleteIssue() to find.
diff --git a/v3/server/pages.py b/v3/server/pages.py
index 557101b..cd83822 100644
--- a/v3/server/pages.py
+++ b/v3/server/pages.py
@@ -317,11 +317,21 @@
 
     ### check authz
 
-    ### do stuff
+    data = edict(await quart.request.get_json())
+    print('FORM:', data)
 
-    _LOGGER.info(f'User[U:{result.uid}] added issue[I:{iid}]'
+    ### do stuff
+    ### add_issue(iid, title, description, vtype, kv)
+    ### the IID should be created by add_issue. Do this for now.
+    issue = edict(iid=steve.crypto.create_id(),
+                  title='<placeholder>')
+
+    _LOGGER.info(f'User[U:{result.uid}] added issue[I:{issue.iid}]'
                  f' to election[E:{election.eid}]')
 
+    ### fill in the real title from the form data
+    await flash_success(f'Issue "{issue.title}" has been added.')
+
     # Return to the management page for this Election.
     return quart.redirect(f'/manage/{election.eid}', code=303)
 
@@ -334,16 +344,23 @@
 
     ### check authz
 
+    data = edict(await quart.request.get_json())
+    print('FORM:', data)
+
     ### do stuff
+    ### add_issue(iid, title, description, vtype, kv)
 
     _LOGGER.info(f'User[U:{result.uid}] edited issue[I:{issue.iid}]'
                  f' in election[E:{election.eid}]')
 
+    ### this is old title. switch to new title.
+    await flash_success(f'Issue "{issue.title}" has been updated.')
+
     # Return to the management page for this Election.
     return quart.redirect(f'/manage/{election.eid}', code=303)
 
 
-@APP.delete('/do-delete-issue/<eid>/<iid>')
+@APP.get('/do-delete-issue/<eid>/<iid>')
 @asfquart.auth.require({R.committer})  ### need general solution
 @load_election_issue
 async def do_delete_issue_endpoint(election, issue):
@@ -351,11 +368,14 @@
 
     ### check authz
 
-    ### do stuff
+    # Issue exists, and was loaded. No errors to handle?
+    election.delete_issue(issue.iid)
 
     _LOGGER.info(f'User[U:{result.uid}] deleted issue[I:{issue.iid}]'
                  f' from election[E:{election.eid}]')
 
+    await flash_success(f'Issue "{issue.title}" has been deleted.')
+
     # Return to the management page for this Election.
     return quart.redirect(f'/manage/{election.eid}', code=303)
 
diff --git a/v3/server/templates/manage.ezt b/v3/server/templates/manage.ezt
index de0768f..9a1cd6f 100644
--- a/v3/server/templates/manage.ezt
+++ b/v3/server/templates/manage.ezt
@@ -127,6 +127,7 @@
                             <i class="bi bi-pencil"></i>
                         </button>
                         <button type="button" class="btn btn-outline-danger btn-sm"
+                              id="delete-[issues.iid]"
                               onclick="deleteIssue('[issues.iid]')" aria-label="Delete Issue">
                             <i class="bi bi-trash"></i>
                         </button>
@@ -238,15 +239,17 @@
         body: JSON.stringify({ title, description }),
       })
         .then(response => {
-          if (!response.ok) throw new Error('Network response was not ok');
-          return response.json();
-        })
-        .then(data => {
-          bootstrap.Modal.getInstance(document.getElementById('issueModal')).hide();
-          // Server will refresh page with flash messages
+            if (response.status === 303) {
+                const redirectUrl = response.headers.get('Location');
+                window.location.href = redirectUrl; // Navigate to server’s Location
+            } else {
+                // Fallback for any non-303 response (success or error)
+                window.location.reload(); // Let server render flash messages or error page
+            }
         })
         .catch(error => {
-          // Server will handle error and flash message
+            console.error('Network error:', error);
+            window.location.reload(); // Reload to show server’s error page or flash message
         });
     }
 
@@ -254,19 +257,10 @@
     function deleteIssue(issueId) {
       if (!confirm('Are you sure you want to delete this issue?')) return;
 
-      fetch(`/do-delete-issue/[eid]/${issueId}`, {
-        method: 'DELETE',
-      })
-        .then(response => {
-          if (!response.ok) throw new Error('Network response was not ok');
-          return response.json();
-        })
-        .then(data => {
-          // Server will refresh page with flash messages
-        })
-        .catch(error => {
-          // Server will handle error and flash message
-        });
+      const button = document.querySelector(`#delete-${issueId}`);
+      button.disabled = true; // Prevent multiple clicks
+
+      window.location.href = `/do-delete-issue/[eid]/${issueId}`;
     }
 
 </script>