Skip to content

Fix operations flag 3 link counting and persist training UI state via localStorage#183

Merged
deacon-mp merged 3 commits intomasterfrom
operationsflagbugfix
Mar 18, 2026
Merged

Fix operations flag 3 link counting and persist training UI state via localStorage#183
deacon-mp merged 3 commits intomasterfrom
operationsflagbugfix

Conversation

@deacon-mp
Copy link
Copy Markdown
Contributor

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 logic

  • Counts all link types (chain links, potential links, AND manual links) when verifying the Empty Op flag
  • Adds diagnostic logging to help debug future flag verification issues
  • Fixes cases where users complete the challenge but the flag is never granted because only chain links were counted

2. app/training_api.py — Remove early break that skips flag verification

  • Removes the break statement that stopped processing flags after the first incomplete flag in non-cert-type badges
  • Allows all flags to be evaluated independently on each polling interval

3. gui/views/training.vue — Persist selected cert/badge across restarts via localStorage

  • Saves selectedCert and selectedBadge to localStorage whenever they change
  • Restores these selections on mount before fetching data
  • Addresses issue Training flags resets after a shutdown or restart. #182: training state (selected cert/badge UI position) is now preserved across server restarts

Note: Flag completion state (whether a flag was checked off) is still server-side RAM only and will reset on restart — that is a deeper architectural issue. This PR at minimum preserves which cert/badge the user was viewing.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

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

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 / selectedBadge UI state via localStorage in 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.

Comment on lines +78 to +80
# Process all flags independently — don't stop after first incomplete one
# if not hasattr(cert, 'cert_type'):
# break
Comment on lines +78 to +80
# Process all flags independently — don't stop after first incomplete one
# if not hasattr(cert, 'cert_type'):
# break
Comment on lines +58 to +66
watch(
[selectedCert, selectedBadge],
() => {
const state = {
selectedCert: selectedCert.value,
selectedBadge: selectedBadge.value,
};
localStorage.setItem("trainingState", JSON.stringify(state));
},
updateInterval = setInterval(async () => {
getTraining();
}, "3000");
}, 3000); // <-- you can also remove the quotes, should be a number not a string

class OperationsFlag3(Flag):
name = 'Empty operation'
name = 'Empty Op'
Comment on lines 17 to +42
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
):
Comment on lines +51 to +54
if (updateInterval) clearInterval(updateInterval);
updateInterval = setInterval(async () => {
getTraining();
}, "3000");
}, 3000); // <-- you can also remove the quotes, should be a number not a string
Comment on lines +25 to +46
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

@deacon-mp deacon-mp requested a review from Copilot March 16, 2026 04:30
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 selectedCert and selectedBadge in the training UI using localStorage.

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.

Comment on lines +29 to +39
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);
}
selectedCert: selectedCert.value,
selectedBadgeName: selectedBadge.value?.name || "",
};
localStorage.setItem("trainingState", JSON.stringify(state));
updateInterval = setInterval(async () => {
getTraining();
}, "3000");
}, 3000); // <-- you can also remove the quotes, should be a number not a string
Comment on lines +79 to +80
# if not hasattr(cert, 'cert_type'):
# break

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)}")
# Verify completion conditions
if (
op.finish
and getattr(op.adversary, 'adversary_id', None) == 'ad-hoc'

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)}")

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})")
Comment on lines +43 to +46
logging.info(f"[training.flag3] ✅ Operation '{op.name}' meets all criteria!")
return True
return False

logging.info("[training.flag3] ❌ No operation met the criteria.")
ahenao4 and others added 2 commits March 16, 2026 09:29
…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
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 localStorage persistence 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.


class OperationsFlag3(Flag):
name = 'Empty operation'
name = 'Empty Op'
Comment on lines +25 to +46
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.")
Comment on lines +122 to +127
try {
const parsed = JSON.parse(savedState);
selectedCert.value = parsed.selectedCert || "";
selectedBadge.value = parsed.selectedBadge || "";
selectedBadge.value = parsed.selectedBadgeName
? { name: parsed.selectedBadgeName }
: "";
Comment on lines +27 to +41
# 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
@deacon-mp
Copy link
Copy Markdown
Contributor Author

@github-copilot review

@deacon-mp deacon-mp merged commit 39195f7 into master Mar 18, 2026
2 checks passed
@deacon-mp deacon-mp deleted the operationsflagbugfix branch March 18, 2026 13:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants