Fix operations flag 3 link counting and persist training UI state via localStorage#183
Fix operations flag 3 link counting and persist training UI state via localStorage#183
Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses reported training-plugin usability and flag-verification issues by fixing operations flag #3 link counting, adjusting flag-evaluation flow in the training API, and persisting the user’s training UI selection state across restarts.
Changes:
- Persist
selectedCert/selectedBadgeUI state vialocalStoragein the training Vue view. - Update training flag polling to evaluate flags without stopping after the first incomplete one.
- Expand Operations flag #3 verification to count multiple link sources and add diagnostic logging.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 9 comments.
| File | Description |
|---|---|
gui/views/training.vue |
Restores and persists training UI selection state via localStorage; adjusts polling interval call. |
app/training_api.py |
Alters flag iteration behavior (removes early break), but currently introduces an indentation/syntax risk. |
app/flags/operations/flag_3.py |
Updates verification logic to count additional link types and adds diagnostic logging. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
app/training_api.py
Outdated
| # Process all flags independently — don't stop after first incomplete one | ||
| # if not hasattr(cert, 'cert_type'): | ||
| # break |
app/training_api.py
Outdated
| # Process all flags independently — don't stop after first incomplete one | ||
| # if not hasattr(cert, 'cert_type'): | ||
| # break |
| watch( | ||
| [selectedCert, selectedBadge], | ||
| () => { | ||
| const state = { | ||
| selectedCert: selectedCert.value, | ||
| selectedBadge: selectedBadge.value, | ||
| }; | ||
| localStorage.setItem("trainingState", JSON.stringify(state)); | ||
| }, |
gui/views/training.vue
Outdated
| updateInterval = setInterval(async () => { | ||
| getTraining(); | ||
| }, "3000"); | ||
| }, 3000); // <-- you can also remove the quotes, should be a number not a string |
app/flags/operations/flag_3.py
Outdated
|
|
||
| class OperationsFlag3(Flag): | ||
| name = 'Empty operation' | ||
| name = 'Empty Op' |
| async def verify(self, services): | ||
| for op in await services.get('data_svc').locate('operations'): | ||
| if op.finish and op.adversary.adversary_id == 'ad-hoc' and len(op.chain) >= 5 and not op.group: | ||
| data_svc = services.get('data_svc') | ||
| if not data_svc: | ||
| logging.error("[training.flag3] data_svc is None!") | ||
| return False | ||
|
|
||
| operations = await data_svc.locate('operations') | ||
| for op in operations: | ||
| logging.info(f"[training.flag3] Checking operation '{op.name}' | state={op.state}, group={op.group}, adversary_id={getattr(op.adversary, 'adversary_id', None)}") | ||
|
|
||
| # Gather all link lists that might exist | ||
| chain_links = getattr(op, 'chain', []) or [] | ||
| potential_links = getattr(op, 'potential_links', []) or [] | ||
| manual_links = getattr(op, 'links', []) or [] | ||
|
|
||
| total_links = len(chain_links) + len(potential_links) + len(manual_links) | ||
|
|
||
| logging.info(f"[training.flag3] Found {len(chain_links)} chain links, {len(potential_links)} potential links, {len(manual_links)} manual links (total={total_links})") | ||
|
|
||
| # Verify completion conditions | ||
| if ( | ||
| op.finish | ||
| and getattr(op.adversary, 'adversary_id', None) == 'ad-hoc' | ||
| and total_links >= 5 | ||
| and not op.group | ||
| ): |
gui/views/training.vue
Outdated
| if (updateInterval) clearInterval(updateInterval); | ||
| updateInterval = setInterval(async () => { | ||
| getTraining(); | ||
| }, "3000"); | ||
| }, 3000); // <-- you can also remove the quotes, should be a number not a string |
app/flags/operations/flag_3.py
Outdated
| logging.info(f"[training.flag3] Checking operation '{op.name}' | state={op.state}, group={op.group}, adversary_id={getattr(op.adversary, 'adversary_id', None)}") | ||
|
|
||
| # Gather all link lists that might exist | ||
| chain_links = getattr(op, 'chain', []) or [] | ||
| potential_links = getattr(op, 'potential_links', []) or [] | ||
| manual_links = getattr(op, 'links', []) or [] | ||
|
|
||
| total_links = len(chain_links) + len(potential_links) + len(manual_links) | ||
|
|
||
| logging.info(f"[training.flag3] Found {len(chain_links)} chain links, {len(potential_links)} potential links, {len(manual_links)} manual links (total={total_links})") | ||
|
|
||
| # Verify completion conditions | ||
| if ( | ||
| op.finish | ||
| and getattr(op.adversary, 'adversary_id', None) == 'ad-hoc' | ||
| and total_links >= 5 | ||
| and not op.group | ||
| ): | ||
| logging.info(f"[training.flag3] ✅ Operation '{op.name}' meets all criteria!") | ||
| return True | ||
| return False | ||
|
|
||
| logging.info("[training.flag3] ❌ No operation met the criteria.") |
| @@ -1,21 +1,47 @@ | |||
| import logging | |||
| from plugins.training.app.c_flag import Flag | |||
|
|
|||
There was a problem hiding this comment.
Pull request overview
Fixes training flag evaluation and improves UX by persisting the training UI’s selected cert/badge across restarts, while correcting Operations Flag 3’s link-counting logic so completed challenges reliably grant the flag.
Changes:
- Update Operations Flag 3 to count chain + potential + manual links and add diagnostics.
- Ensure flag verification doesn’t stop early when iterating flags in
training_api. - Persist/restore
selectedCertandselectedBadgein the training UI usinglocalStorage.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 9 comments.
| File | Description |
|---|---|
| gui/views/training.vue | Restores and persists training UI selection state via localStorage; minor interval fix. |
| app/training_api.py | Removes early loop termination so all flags are evaluated each poll. |
| app/flags/operations/flag_3.py | Fixes Empty Op completion logic by counting all link types; adds logging. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
gui/views/training.vue
Outdated
| const savedState = localStorage.getItem("trainingState"); | ||
| if (savedState) { | ||
| try { | ||
| const parsed = JSON.parse(savedState); | ||
| selectedCert.value = parsed.selectedCert || ""; | ||
| selectedBadge.value = parsed.selectedBadgeName | ||
| ? { name: parsed.selectedBadgeName } | ||
| : ""; | ||
| } catch (err) { | ||
| console.warn("Failed to parse saved training state:", err); | ||
| } |
gui/views/training.vue
Outdated
| selectedCert: selectedCert.value, | ||
| selectedBadgeName: selectedBadge.value?.name || "", | ||
| }; | ||
| localStorage.setItem("trainingState", JSON.stringify(state)); |
gui/views/training.vue
Outdated
| updateInterval = setInterval(async () => { | ||
| getTraining(); | ||
| }, "3000"); | ||
| }, 3000); // <-- you can also remove the quotes, should be a number not a string |
app/training_api.py
Outdated
| # if not hasattr(cert, 'cert_type'): | ||
| # break |
app/flags/operations/flag_3.py
Outdated
|
|
||
| operations = await data_svc.locate('operations') | ||
| for op in operations: | ||
| logging.info(f"[training.flag3] Checking operation '{op.name}' | state={op.state}, group={op.group}, adversary_id={getattr(op.adversary, 'adversary_id', None)}") |
app/flags/operations/flag_3.py
Outdated
| # Verify completion conditions | ||
| if ( | ||
| op.finish | ||
| and getattr(op.adversary, 'adversary_id', None) == 'ad-hoc' |
app/flags/operations/flag_3.py
Outdated
|
|
||
| operations = await data_svc.locate('operations') | ||
| for op in operations: | ||
| logging.info(f"[training.flag3] Checking operation '{op.name}' | state={op.state}, group={op.group}, adversary_id={getattr(op.adversary, 'adversary_id', None)}") |
app/flags/operations/flag_3.py
Outdated
|
|
||
| total_links = len(chain_links) + len(potential_links) + len(manual_links) | ||
|
|
||
| logging.info(f"[training.flag3] Found {len(chain_links)} chain links, {len(potential_links)} potential links, {len(manual_links)} manual links (total={total_links})") |
app/flags/operations/flag_3.py
Outdated
| logging.info(f"[training.flag3] ✅ Operation '{op.name}' meets all criteria!") | ||
| return True | ||
| return False | ||
|
|
||
| logging.info("[training.flag3] ❌ No operation met the criteria.") |
…l object
- Fix indentation of comment block in retrieve_flags so it sits inside
the try block rather than at column 0 (avoids misleading dead-code look)
- Store only selectedBadge.name in localStorage instead of the full badge
object; restore as a minimal {name} stub so existing .name accesses work
51bc111 to
80a46dd
Compare
There was a problem hiding this comment.
Pull request overview
This PR focuses on fixing a training flag verification bug in Operations flag 3 and improving the Training UI experience by persisting the user’s current cert/badge selection in localStorage.
Changes:
- Update Operations Flag 3 verification to count multiple link types when determining completion.
- Add
localStoragepersistence for selected certificate/badge in the Training UI (store badge by name, restore on mount).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
app/flags/operations/flag_3.py |
Adjusts verification logic for “Empty operation” style challenge and adds diagnostic logging. |
gui/views/training.vue |
Changes persisted training UI state schema to store/restore selected badge by name. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
app/flags/operations/flag_3.py
Outdated
|
|
||
| class OperationsFlag3(Flag): | ||
| name = 'Empty operation' | ||
| name = 'Empty Op' |
app/flags/operations/flag_3.py
Outdated
| logging.info(f"[training.flag3] Checking operation '{op.name}' | state={op.state}, group={op.group}, adversary_id={getattr(op.adversary, 'adversary_id', None)}") | ||
|
|
||
| # Gather all link lists that might exist | ||
| chain_links = getattr(op, 'chain', []) or [] | ||
| potential_links = getattr(op, 'potential_links', []) or [] | ||
| manual_links = getattr(op, 'links', []) or [] | ||
|
|
||
| total_links = len(chain_links) + len(potential_links) + len(manual_links) | ||
|
|
||
| logging.info(f"[training.flag3] Found {len(chain_links)} chain links, {len(potential_links)} potential links, {len(manual_links)} manual links (total={total_links})") | ||
|
|
||
| # Verify completion conditions | ||
| if ( | ||
| op.finish | ||
| and getattr(op.adversary, 'adversary_id', None) == 'ad-hoc' | ||
| and total_links >= 5 | ||
| and not op.group | ||
| ): | ||
| logging.info(f"[training.flag3] ✅ Operation '{op.name}' meets all criteria!") | ||
| return True | ||
| return False | ||
|
|
||
| logging.info("[training.flag3] ❌ No operation met the criteria.") |
gui/views/training.vue
Outdated
| try { | ||
| const parsed = JSON.parse(savedState); | ||
| selectedCert.value = parsed.selectedCert || ""; | ||
| selectedBadge.value = parsed.selectedBadge || ""; | ||
| selectedBadge.value = parsed.selectedBadgeName | ||
| ? { name: parsed.selectedBadgeName } | ||
| : ""; |
| # Gather all link lists that might exist | ||
| chain_links = getattr(op, 'chain', []) or [] | ||
| potential_links = getattr(op, 'potential_links', []) or [] | ||
| manual_links = getattr(op, 'links', []) or [] | ||
|
|
||
| total_links = len(chain_links) + len(potential_links) + len(manual_links) | ||
|
|
||
| logging.info(f"[training.flag3] Found {len(chain_links)} chain links, {len(potential_links)} potential links, {len(manual_links)} manual links (total={total_links})") | ||
|
|
||
| # Verify completion conditions | ||
| if ( | ||
| op.finish | ||
| and getattr(op.adversary, 'adversary_id', None) == 'ad-hoc' | ||
| and total_links >= 5 | ||
| and not op.group |
- flag_3.py: add module logger, use log.debug instead of logging.info, two-step getattr for adversary, two blank lines before class, remove emoji from log messages, revert flag name to 'Empty operation' - training.vue: wrap all localStorage accesses in try/catch, add schema migration to support old selectedBadge key alongside new selectedBadgeName
|
@github-copilot review |
Description
This branch contains bug fixes from
operationsflagbugfix(previously PR #177, closed without merge). The changes address real functional issues reported by users:Changes
1.
app/flags/operations/flag_3.py— Fix link counting logicchainlinks were counted2.
app/training_api.py— Remove early break that skips flag verificationbreakstatement that stopped processing flags after the first incomplete flag in non-cert-type badges3.
gui/views/training.vue— Persist selected cert/badge across restarts via localStorageselectedCertandselectedBadgetolocalStoragewhenever they changeType of change
Related Issues
Partially addresses #182 (training flags reset after restart — preserves UI selection state)
How Has This Been Tested?
Manual testing by original author (ahenao).
Checklist