Compare commits

..

62 Commits

Author SHA1 Message Date
D. Berge
673c60a359 Add error handling 2025-08-22 16:40:06 +02:00
D. Berge
99e425270c getGroup() returns all comparisons.
Just like saveGroup() saves all.
2025-08-22 16:39:22 +02:00
D. Berge
63633715e2 Guard against underpopulated groups 2025-08-22 16:16:10 +02:00
D. Berge
8afac5c150 Fix indentation 2025-08-22 16:01:20 +02:00
D. Berge
11168def68 Fix typos 2025-08-22 16:01:20 +02:00
D. Berge
0f477b8e65 Replace tilt icons 2025-08-22 16:01:20 +02:00
D. Berge
03b00a4ea7 Remove dead code 2025-08-22 16:01:20 +02:00
D. Berge
c5faa53bee Add more view controls to group map 2025-08-22 16:01:20 +02:00
D. Berge
46b2512530 Add more view controls to map 2025-08-22 16:01:20 +02:00
D. Berge
db4c9a0235 Add script to update comparison groups.
This should be run at regular intervals (via cron or so) to keep
the comparisons up to date.

It is not necessarily a good idea to run this as part of the
runner.sh script as it will delay other tasks trying to
update the active project every time.

Probably OK to put it on a cronjbo every 2‒24 hours. If two
copies are running concurrently that should not break anything
but it will increase the server load.
2025-08-22 16:01:20 +02:00
D. Berge
1a12ea13ed Return project timestamps 2025-08-22 16:01:20 +02:00
D. Berge
81717c37f1 Add option to return project timestamp 2025-08-22 16:01:20 +02:00
D. Berge
6377e8854c Updated wanted db schema 2025-08-22 16:01:20 +02:00
D. Berge
d3446d03bd Add database upgrade file 44 2025-08-22 16:01:20 +02:00
D. Berge
a52f7811f2 Clean up dead code 2025-08-22 16:01:20 +02:00
D. Berge
ef2bd4888e Update the required schema version.
This is necessary for the comparisons code to work.
2025-08-22 16:01:20 +02:00
D. Berge
8801442c92 Don't show monitor lines by default 2025-08-22 16:01:20 +02:00
D. Berge
30f65dbeaa Make loading indicator spin when 0% 2025-08-22 16:01:20 +02:00
D. Berge
c2f53ac150 Remove unneded dependency 2025-08-22 16:01:20 +02:00
D. Berge
4328fc4d2a Fix typo 2025-08-22 16:01:20 +02:00
D. Berge
2c2eb8fceb Add group map view 2025-08-22 16:01:20 +02:00
D. Berge
767c2f2cb1 Add support for type 4 decoding 2025-08-22 16:01:20 +02:00
D. Berge
57a73f7d1c Fix component paths 2025-08-22 16:01:20 +02:00
D. Berge
9f299056d8 Move components to subdirectory 2025-08-22 16:01:20 +02:00
D. Berge
5d3c59867c Return type 4 sequence data 2025-08-22 16:01:20 +02:00
D. Berge
76b8355ede Add encoding type 4 to bundle 2025-08-22 16:01:20 +02:00
D. Berge
76b55f514d Link from group summary to individual projects 2025-08-22 16:01:20 +02:00
D. Berge
4e1d3209df Don't request summaries in ProjectList.
Those will be populated directly by Vuex.
2025-08-22 16:01:20 +02:00
D. Berge
f21ff7ee38 Try to improve responsiveness when refreshing project list 2025-08-22 16:01:20 +02:00
D. Berge
2446b42785 Expand groups router definition 2025-08-22 16:01:20 +02:00
D. Berge
196e772004 Make event handler more specific 2025-08-22 16:01:20 +02:00
D. Berge
674d818fee Rework comparison components.
More focused on error ellipses.
2025-08-22 16:01:20 +02:00
D. Berge
5527576679 Refresh comparisons when notified of changes 2025-08-22 16:01:20 +02:00
D. Berge
fe7c016dea Add control to reset comparisons view 2025-08-22 16:01:20 +02:00
D. Berge
b7543aa6c4 Add overlays when loading / data error 2025-08-22 16:01:20 +02:00
D. Berge
b48a060dc0 Don't cache comparisons in the API 2025-08-22 16:01:20 +02:00
D. Berge
c0f9a2de5a Don't save comparison samples 2025-08-22 16:01:20 +02:00
D. Berge
32a9c7a5f2 Add comparisons channel to notifications 2025-08-22 16:01:20 +02:00
D. Berge
f1f74080f6 Add database upgrade file 43 2025-08-22 16:01:20 +02:00
D. Berge
c5eb8e45f1 Add database upgrade file 42 2025-08-22 16:01:20 +02:00
D. Berge
caab968fd6 Add database upgrade file 41 2025-08-22 16:01:20 +02:00
D. Berge
5f28d1be7b Don't overwrite existing comparisons unless forced.
opts.overwrite = true will cause existing comparisons to be
recomputed.
2025-08-22 16:01:20 +02:00
D. Berge
22c9537889 Fix non-existent method 2025-08-22 16:01:20 +02:00
D. Berge
e95aaa7de7 Add link to group comparison from project list 2025-08-22 16:01:20 +02:00
D. Berge
4f44f5a10c Add frontend route for 4D comparisons 2025-08-22 16:01:20 +02:00
D. Berge
0ba467d34c Add 4D comparisons list Vue component 2025-08-22 16:01:20 +02:00
D. Berge
2b5b302e54 Add 4D comparisons Vue component 2025-08-22 16:01:20 +02:00
D. Berge
28938e27a9 Add utilities for transforming duration objects 2025-08-22 16:01:20 +02:00
D. Berge
97f96fdc1e Add Vue components for 4D comparisons 2025-08-22 16:01:20 +02:00
D. Berge
1e3ce35f76 Add set operations utilities 2025-08-22 16:01:20 +02:00
D. Berge
619a886781 Add comparison API endpoints 2025-08-22 16:01:20 +02:00
D. Berge
c054e63325 Add two new bundle types.
Of which 0xa is not actually used and 0xc is used for geometric
comparison data ([ line, point, δi, δj ]).
2025-08-22 16:01:20 +02:00
D. Berge
fd94b3b6f4 Add comparison functions to server/lib 2025-08-22 16:01:20 +02:00
D. Berge
7b67b4afc9 Fix bug trying to get project info for undefined 2025-08-22 16:01:20 +02:00
D. Berge
7c52ada922 Add project group info to Vuex 2025-08-22 16:01:20 +02:00
D. Berge
9072bbe389 Add iterators 2025-08-22 16:01:20 +02:00
D. Berge
6639b7110b Add sequence navigation controls to log.
Closes #135
2025-08-22 15:57:49 +02:00
D. Berge
be6652b539 Name Shotlog route 2025-08-22 15:56:59 +02:00
D. Berge
bf054d3902 Persist event log user preferences 2025-08-22 15:56:12 +02:00
D. Berge
2734870871 Fix errors when loading graphs.
Errors due to the parent element having zero width / height or
rendering too early.
2025-08-22 15:54:17 +02:00
D. Berge
52f49e6799 Fix log entries pagination.
Fixes #340
2025-08-22 12:31:19 +02:00
D. Berge
30150a8728 Kill runner if it hangs around for too long.
This gives the import processes a chance to run.
2025-08-21 15:33:05 +02:00
9 changed files with 314 additions and 81 deletions

View File

@@ -1,5 +1,7 @@
#!/bin/bash #!/bin/bash
# Maximum runtime in seconds before killing an overdue instance (e.g., 10 minutes)
MAX_RUNTIME_SECONDS=$((15 * 60))
DOUGAL_ROOT=${DOUGAL_ROOT:-$(dirname "$0")/..} DOUGAL_ROOT=${DOUGAL_ROOT:-$(dirname "$0")/..}
@@ -80,8 +82,9 @@ function run () {
# DESCRIPTION="" # DESCRIPTION=""
SERVICE="deferred_imports" SERVICE="deferred_imports"
$BINDIR/send_alert.py -t "$TITLE" -s "$SERVICE" -l "critical" \ # Disable GitLab alerts. They're just not very practical
-O "$(cat $STDOUTLOG)" -E "$(cat $STDERRLOG)" # $BINDIR/send_alert.py -t "$TITLE" -s "$SERVICE" -l "critical" \
# -O "$(cat $STDOUTLOG)" -E "$(cat $STDERRLOG)"
exit 2 exit 2
} }
@@ -97,14 +100,37 @@ function cleanup () {
} }
if [[ -f $LOCKFILE ]]; then if [[ -f $LOCKFILE ]]; then
PID=$(cat "$LOCKFILE") PID=$(cat "$LOCKFILE")
if pgrep -F "$LOCKFILE"; then if kill -0 "$PID" 2>/dev/null; then # Check if process is running
print_warning $(printf "The previous process is still running (%d)" $PID) # Get elapsed time in D-HH:MM:SS format and convert to seconds
exit 1 ELAPSED_STR=$(ps -p "$PID" -o etime= | tr -d '[:space:]')
else if [ -n "$ELAPSED_STR" ]; then
rm "$LOCKFILE" # Convert D-HH:MM:SS to seconds
print_warning $(printf "Previous process (%d) not found. Must have died unexpectedly" $PID) ELAPSED_SECONDS=$(echo "$ELAPSED_STR" | awk -F'[-:]' '{
fi seconds = 0
if (NF == 4) { seconds += $1 * 86400 } # Days
if (NF >= 3) { seconds += $NF-2 * 3600 } # Hours
if (NF >= 2) { seconds += $NF-1 * 60 } # Minutes
seconds += $NF # Seconds
print seconds
}')
if [ "$ELAPSED_SECONDS" -gt "$MAX_RUNTIME_SECONDS" ]; then
# Kill the overdue process (SIGTERM; use -9 for SIGKILL if needed)
kill "$PID" 2>/dev/null
print_warning $(printf "Killed overdue process (%d) that ran for %s (%d seconds)" "$PID" "$ELAPSED_STR" "$ELAPSED_SECONDS")
rm "$LOCKFILE"
else
print_warning $(printf "Previous process is still running (%d) for %s (%d seconds)" "$PID" "$ELAPSED_STR" "$ELAPSED_SECONDS")
exit 1
fi
else
print_warning $(printf "Could not retrieve elapsed time for process (%d)" "$PID")
exit 1
fi
else
rm "$LOCKFILE"
print_warning $(printf "Previous process (%d) not found. Must have died unexpectedly" "$PID")
fi
fi fi
echo "$$" > "$LOCKFILE" || { echo "$$" > "$LOCKFILE" || {

View File

@@ -2,8 +2,32 @@
const cmp = require('../lib/www/server/lib/comparisons'); const cmp = require('../lib/www/server/lib/comparisons');
async function purgeComparisons () {
const groups = await cmp.groups();
const comparisons = await cmp.getGroup();
const pids = new Set(Object.values(groups).flat().map( p => p.pid ));
const comparison_pids = new Set(comparisons.map( c => [ c.baseline_pid, c.monitor_pid ] ).flat());
for (const pid of comparison_pids) {
if (!pids.has(pid)) {
console.log(`${pid} no longer par of a group. Deleting comparisons`);
staleComps = comparisons.filter( c => c.baseline_pid == pid || c.monitor_pid == pid );
for (c of staleComps) {
console.log(`Deleting comparison ${c.baseline_pid}${c.monitor_pid}`);
await cmp.remove(c.baseline_pid, c.monitor_pid);
}
}
}
}
async function main () { async function main () {
console.log("Looking for unreferenced comparisons to purge");
await purgeComparisons();
console.log("Retrieving project groups"); console.log("Retrieving project groups");
const groups = await cmp.groups(); const groups = await cmp.groups();
@@ -12,7 +36,7 @@ async function main () {
return 0; return 0;
} }
console.log(`Found ${groups.length} groups: ${Object.keys(groups).join(", ")}`); console.log(`Found ${Object.keys(groups)?.length} groups: ${Object.keys(groups).join(", ")}`);
for (const groupName of Object.keys(groups)) { for (const groupName of Object.keys(groups)) {
const projects = groups[groupName]; const projects = groups[groupName];
@@ -21,6 +45,11 @@ async function main () {
const comparisons = await cmp.getGroup(groupName); const comparisons = await cmp.getGroup(groupName);
if (!comparisons || !comparisons.length) {
console.log(`No comparisons found for ${groupName}`);
continue;
}
// Check if there are any projects that have been modified since last comparison // Check if there are any projects that have been modified since last comparison
// or if there are any pairs that are no longer part of the group // or if there are any pairs that are no longer part of the group

View File

@@ -39,7 +39,8 @@ export default {
default: default:
return { return {
editable: false, editable: false,
displaylogo: false displaylogo: false,
responsive: true
}; };
} }
}, },
@@ -48,7 +49,8 @@ export default {
const base = { const base = {
font: { font: {
color: this.$vuetify.theme.isDark ? "#fff" : undefined color: this.$vuetify.theme.isDark ? "#fff" : undefined
} },
autosize: true
}; };
switch (this.facet) { switch (this.facet) {
@@ -274,18 +276,25 @@ export default {
replot () { replot () {
if (this.plotted) { if (this.plotted) {
const ref = this.$refs.graph; const ref = this.$refs.graph;
Plotly.relayout(ref, { if (ref && ref.clientWidth > 0 && ref.clientHeight > 0) {
width: ref.clientWidth, Plotly.relayout(ref, {
height: ref.clientHeight width: ref.clientWidth,
}); height: ref.clientHeight
});
}
} }
} }
}, },
mounted () { mounted () {
this.resizeObserver = new ResizeObserver(this.replot) this.$nextTick( () => {
this.resizeObserver.observe(this.$refs.graph); if (this.items?.length) {
this.plot();
}
this.resizeObserver = new ResizeObserver(this.replot)
this.resizeObserver.observe(this.$refs.graph);
});
}, },
beforeDestroy () { beforeDestroy () {

View File

@@ -36,7 +36,8 @@ export default {
config () { config () {
return { return {
editable: false, editable: false,
displaylogo: false displaylogo: false,
responsive: true
}; };
}, },
@@ -53,7 +54,8 @@ export default {
title: "Time (s)" title: "Time (s)"
}, },
plot_bgcolor:"rgba(0,0,0,0)", plot_bgcolor:"rgba(0,0,0,0)",
paper_bgcolor:"rgba(0,0,0,0)" paper_bgcolor:"rgba(0,0,0,0)",
autosize: true
}; };
}, },
@@ -154,10 +156,12 @@ export default {
replot () { replot () {
if (this.plotted) { if (this.plotted) {
const ref = this.$refs.graph; const ref = this.$refs.graph;
Plotly.relayout(ref, { if (ref && ref.clientWidth > 0 && ref.clientHeight > 0) {
width: ref.clientWidth, Plotly.relayout(ref, {
height: ref.clientHeight width: ref.clientWidth,
}); height: ref.clientHeight
});
}
} }
}, },
@@ -190,8 +194,13 @@ export default {
}, },
mounted () { mounted () {
this.resizeObserver = new ResizeObserver(this.replot) this.$nextTick( () => {
this.resizeObserver.observe(this.$refs.graph); if (this.items?.length) {
this.plot();
}
this.resizeObserver = new ResizeObserver(this.replot)
this.resizeObserver.observe(this.$refs.graph);
});
}, },
beforeDestroy () { beforeDestroy () {

View File

@@ -158,6 +158,7 @@ Vue.use(VueRouter)
component: SequenceList component: SequenceList
}, },
{ {
name: "shotlog",
path: "sequences/:sequence", path: "sequences/:sequence",
component: SequenceSummary component: SequenceSummary
}, },

View File

@@ -36,7 +36,7 @@ async function refreshEvents ({commit, dispatch, state, rootState}, [modifiedAft
/** Return a subset of events from state.events /** Return a subset of events from state.events
*/ */
async function getEvents ({commit, dispatch, state}, [projectId, {sequence, date0, date1, sortBy, sortDesc, itemsPerPage, page, text, label}]) { async function getEvents ({commit, dispatch, state}, [projectId, {sequence, date0, date1, sortBy, sortDesc, itemsPerPage, page, text, label, excludeLabels}]) {
let filteredEvents = [...state.events]; let filteredEvents = [...state.events];
if (sortBy) { if (sortBy) {
@@ -114,6 +114,10 @@ async function getEvents ({commit, dispatch, state}, [projectId, {sequence, date
filteredEvents = filteredEvents.filter( event => event.labels?.includes(label) ); filteredEvents = filteredEvents.filter( event => event.labels?.includes(label) );
} }
if (excludeLabels) {
filteredEvents = filteredEvents.filter( event => !excludeLabels?.some( label => event.labels?.includes(label) ) );
}
const count = filteredEvents.length; const count = filteredEvents.length;
if (itemsPerPage && itemsPerPage > 0) { if (itemsPerPage && itemsPerPage > 0) {

View File

@@ -5,6 +5,22 @@
<v-card-title> <v-card-title>
<v-toolbar flat> <v-toolbar flat>
<v-toolbar-title> <v-toolbar-title>
<template v-if="$route.params.sequence">
<v-btn icon small
:disabled="sequenceIndex >= (sequences.length - 1)"
:to="{name: 'logBySequence', params: { sequence: (sequences[sequences.length-1]||{}).sequence }}"
title="Go to the first sequence"
>
<v-icon dense>mdi-chevron-double-left</v-icon>
</v-btn>
<v-btn icon small
:disabled="sequenceIndex >= (sequences.length - 1)"
:to="{name: 'logBySequence', params: { sequence: (sequences[sequenceIndex+1]||{}).sequence }}"
title="Go to the previous sequence"
>
<v-icon dense>mdi-chevron-left</v-icon>
</v-btn>
</template>
<span class="d-none d-lg-inline"> <span class="d-none d-lg-inline">
{{ {{
$route.params.sequence $route.params.sequence
@@ -31,18 +47,38 @@
: "" : ""
}} }}
</span> </span>
<template v-if="$route.params.sequence">
<v-btn icon small
:disabled="sequenceIndex==0"
:to="{name: 'logBySequence', params: { sequence: (sequences[sequenceIndex-1]||{}).sequence }}"
title="Go to the next sequence"
>
<v-icon dense>mdi-chevron-right</v-icon>
</v-btn>
<v-btn icon small class="mr-1"
:disabled="sequenceIndex==0"
:to="{name: 'logBySequence', params: { sequence: (sequences[0]||{}).sequence }}"
title="Go to the last sequence"
>
<v-icon dense>mdi-chevron-double-right</v-icon>
</v-btn>
</template>
<a v-if="$route.params.sequence"
class="mr-3"
:href="`/projects/${$route.params.project}/sequences/${$route.params.sequence}`"
title="View the shotlog for this sequence"
>
<v-icon
right
color="teal"
>mdi-format-list-numbered</v-icon>
</a>
</v-toolbar-title> </v-toolbar-title>
<a v-if="$route.params.sequence"
class="mr-3"
:href="`/projects/${$route.params.project}/sequences/${$route.params.sequence}`"
title="View the shotlog for this sequence"
>
<v-icon
right
color="teal"
>mdi-format-list-numbered</v-icon>
</a>
<dougal-event-edit v-if="$parent.writeaccess()" <dougal-event-edit v-if="$parent.writeaccess()"
v-model="eventDialog" v-model="eventDialog"
@@ -494,17 +530,6 @@ export default {
rows () { rows () {
const rows = {}; const rows = {};
this.items this.items
.filter(i => {
return !this.$route.params.sequence || (this.$route.params.sequence == i.sequence)
})
.filter(i => {
for (const label of this.filterableLabels) {
if (!this.shownLabels.includes(label) && i.labels.includes(label)) {
return false;
}
}
return true;
})
.forEach(i => { .forEach(i => {
const key = (i.sequence && i.point) ? (i.sequence+"@"+i.point) : i.tstamp; const key = (i.sequence && i.point) ? (i.sequence+"@"+i.point) : i.tstamp;
if (!rows[key]) { if (!rows[key]) {
@@ -535,6 +560,10 @@ export default {
.sort( (a, b) => b[1]-a[1] ); .sort( (a, b) => b[1]-a[1] );
}, },
filteredLabels () {
return this.filterableLabels.filter( label => !this.shownLabels.includes(label) );
},
presetRemarks () { presetRemarks () {
return this.projectConfiguration?.events?.presetRemarks ?? []; return this.projectConfiguration?.events?.presetRemarks ?? [];
}, },
@@ -547,7 +576,17 @@ export default {
} }
}, },
...mapGetters(['user', 'eventsLoading', 'online', 'sequence', 'line', 'point', 'position', 'timestamp', 'lineName', 'events', 'labels', 'userLabels', 'projectConfiguration']), sequenceIndex () {
if ("sequence" in this.$route.params) {
const index = this.sequences.findIndex( i => i.sequence == this.$route.params.sequence );
if (index != -1) {
return index;
}
}
// return undefined
},
...mapGetters(['user', 'eventsLoading', 'online', 'sequence', 'sequences', 'line', 'point', 'position', 'timestamp', 'lineName', 'events', 'labels', 'userLabels', 'projectConfiguration']),
...mapState({projectSchema: state => state.project.projectSchema}) ...mapState({projectSchema: state => state.project.projectSchema})
}, },
@@ -555,6 +594,7 @@ export default {
watch: { watch: {
options: { options: {
async handler () { async handler () {
this.savePrefs(),
await this.fetchEvents(); await this.fetchEvents();
}, },
deep: true deep: true
@@ -573,12 +613,19 @@ export default {
}, },
filter (newVal, oldVal) { filter (newVal, oldVal) {
this.savePrefs();
if (newVal?.toLowerCase() != oldVal?.toLowerCase()) { if (newVal?.toLowerCase() != oldVal?.toLowerCase()) {
this.fetchEvents(); this.fetchEvents();
} }
}, },
labelSearch () { labelSearch () {
this.savePrefs();
this.fetchEvents();
},
filteredLabels () {
this.savePrefs()
this.fetchEvents(); this.fetchEvents();
}, },
@@ -587,7 +634,7 @@ export default {
}, },
user (newVal, oldVal) { user (newVal, oldVal) {
this.itemsPerPage = Number(localStorage.getItem(`dougal/prefs/${this.user?.name}/${this.$route.params.project}/${this.$options.name}/items-per-page`)) || 25; this.loadPrefs();
} }
}, },
@@ -638,8 +685,10 @@ export default {
async fetchEvents (opts = {}) { async fetchEvents (opts = {}) {
const options = { const options = {
sequence: this.$route.params.sequence,
text: this.filter, text: this.filter,
label: this.labelSearch, label: this.labelSearch,
excludeLabels: this.filteredLabels,
...this.options ...this.options
}; };
const res = await this.getEvents([this.$route.params.project, options]); const res = await this.getEvents([this.$route.params.project, options]);
@@ -877,10 +926,36 @@ export default {
*/ */
}, },
getPrefsKey () {
return `dougal/prefs/${this.user?.name}/${this.$route.params.project}/Log/v1`;
},
savePrefs () {
const prefs = {
shownLabels: this.shownLabels,
labelSearch: this.labelSearch,
filter: this.filter,
options: this.options
};
localStorage.setItem(this.getPrefsKey(), JSON.stringify(prefs));
},
loadPrefs () {
const stored = localStorage.getItem(this.getPrefsKey());
if (stored) {
const prefs = JSON.parse(stored);
if (prefs.shownLabels !== undefined) this.shownLabels = prefs.shownLabels;
if (prefs.labelSearch !== undefined) this.labelSearch = prefs.labelSearch;
if (prefs.filter !== undefined) this.filter = prefs.filter;
if (prefs.options !== undefined) this.options = prefs.options;
}
},
...mapActions(["api", "showSnack", "refreshEvents", "getEvents"]) ...mapActions(["api", "showSnack", "refreshEvents", "getEvents"])
}, },
async mounted () { async mounted () {
this.loadPrefs();
this.fetchEvents(); this.fetchEvents();
window.addEventListener('keyup', this.handleKeyboardEvent); window.addEventListener('keyup', this.handleKeyboardEvent);

View File

@@ -6,8 +6,42 @@
<v-progress-linear indeterminate v-if="loading"></v-progress-linear> <v-progress-linear indeterminate v-if="loading"></v-progress-linear>
<v-toolbar flat> <v-toolbar flat>
<v-toolbar-title> <v-toolbar-title>
<template v-if="$route.params.sequence">
<v-btn icon small
:disabled="sequenceIndex >= (sequences.length - 1)"
:to="{name: 'shotlog', params: { sequence: (sequences[sequences.length-1]||{}).sequence }}"
title="Go to the first sequence"
>
<v-icon dense>mdi-chevron-double-left</v-icon>
</v-btn>
<v-btn icon small
:disabled="sequenceIndex >= (sequences.length - 1)"
:to="{name: 'shotlog', params: { sequence: (sequences[sequenceIndex+1]||{}).sequence }}"
title="Go to the previous sequence"
>
<v-icon dense>mdi-chevron-left</v-icon>
</v-btn>
</template>
Sequence {{sequenceNumber}} Sequence {{sequenceNumber}}
<small :class="statusColour" v-if="sequence">({{sequence.status}})</small> <small :class="statusColour" v-if="sequence">({{sequence.status}})</small>
<template v-if="$route.params.sequence">
<v-btn icon small
:disabled="sequenceIndex==0"
:to="{name: 'shotlog', params: { sequence: (sequences[sequenceIndex-1]||{}).sequence }}"
title="Go to the next sequence"
>
<v-icon dense>mdi-chevron-right</v-icon>
</v-btn>
<v-btn icon small class="mr-1"
:disabled="sequenceIndex==0"
:to="{name: 'shotlog', params: { sequence: (sequences[0]||{}).sequence }}"
title="Go to the last sequence"
>
<v-icon dense>mdi-chevron-double-right</v-icon>
</v-btn>
</template>
</v-toolbar-title> </v-toolbar-title>
<a v-if="$route.params.sequence" <a v-if="$route.params.sequence"
@@ -352,6 +386,16 @@ export default {
return this.sequences.find(i => i.sequence == this.sequenceNumber); return this.sequences.find(i => i.sequence == this.sequenceNumber);
}, },
sequenceIndex () {
if ("sequence" in this.$route.params) {
const index = this.sequences.findIndex( i => i.sequence == this.$route.params.sequence );
if (index != -1) {
return index;
}
}
// return undefined
},
remarks () { remarks () {
return this.sequence?.remarks || "Nil."; return this.sequence?.remarks || "Nil.";
}, },

View File

@@ -330,44 +330,80 @@ async function saveGroup (group, opts = {}) {
async function getGroup (groupName, opts = {}) { async function getGroup (groupName, opts = {}) {
const group = (await groups())?.[groupName]?.map( i => i.pid)?.sort();
if (!group?.length) return;
const client = await pool.connect(); const client = await pool.connect();
try { try {
const pairs = combinations(group, 2); if (groupName) {
const flatValues = pairs.flat();
const placeholders = [];
for (let i = 0; i < pairs.length; i++) {
placeholders.push(`($${i * 2 + 1}, $${i * 2 + 2})`);
}
const inClause = placeholders.join(',');
const selectFields = opts.returnData ? 'data, meta' : 'meta';
const text = ` const group = (await groups())?.[groupName]?.map( i => i.pid)?.sort();
SELECT baseline_pid, monitor_pid, ${selectFields}
FROM comparisons.comparisons
WHERE type = 'geometric_difference'
AND (baseline_pid, monitor_pid) IN (VALUES ${inClause})
ORDER BY baseline_pid, monitor_pid
`;
const res = await client.query(text, flatValues); if (!group?.length || group?.length < 2) return;
if (!res.rows.length) {
console.log("Comparison not found in database");
return; const pairs = combinations(group, 2);
} const flatValues = pairs.flat();
const placeholders = [];
for (let i = 0; i < pairs.length; i++) {
placeholders.push(`($${i * 2 + 1}, $${i * 2 + 2})`);
}
const inClause = placeholders.join(',');
const selectFields = opts.returnData ? 'data, meta' : 'meta';
const text = `
SELECT baseline_pid, monitor_pid, ${selectFields}
FROM comparisons.comparisons
WHERE type = 'geometric_difference'
AND (baseline_pid, monitor_pid) IN (VALUES ${inClause})
ORDER BY baseline_pid, monitor_pid
`;
if (!placeholders) {
console.log("No pairs found in group");
return [];
}
const res = await client.query(text, flatValues);
if (!res.rows.length) {
console.log("Comparison not found in database");
return;
}
if (opts.returnData) {
return res.rows.map( row => ({
...row,
data: DougalBinaryBundle.clone(row.data),
}));
} else {
return res.rows;
}
if (opts.returnData) {
return res.rows.map( row => ({
...row,
data: DougalBinaryBundle.clone(row.data),
}));
} else { } else {
return res.rows;
const selectFields = opts.returnData ? 'data, meta' : 'meta';
const text = `
SELECT baseline_pid, monitor_pid, ${selectFields}
FROM comparisons.comparisons
WHERE type = 'geometric_difference'
ORDER BY baseline_pid, monitor_pid
`;
const res = await client.query(text);
if (!res.rows.length) {
console.log("Comparison not found in database");
return;
}
if (opts.returnData) {
return res.rows.map( row => ({
...row,
data: DougalBinaryBundle.clone(row.data),
}));
} else {
return res.rows;
}
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);