Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Freezeman #1

Open
wants to merge 111 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
111 commits
Select commit Hold shift + click to select a range
2466897
First draft for freezeman integration to the monitor
Feb 21, 2023
e22f272
Fixing the email tempaltes
Mar 22, 2023
e60c2ae
Merge branch 'main' of github.com:c3g/monitor into freezeman
Mar 22, 2023
2b5acd7
Adding path for freezeman run info files to watch
Mar 22, 2023
d8d3786
Updating outdir for debug profile
Mar 22, 2023
468237f
Fixed typo in config
Mar 22, 2023
8aa2115
updating path for testing assets
Mar 22, 2023
dbd10a6
Setting monitor for Freezeman LIMS !
Mar 27, 2023
755013d
Freezeman monitor - Illumina runs and MGI runs are now all watched
May 15, 2023
d6a96ef
garantjm dev branch, child process Mem issue
garantjm May 24, 2023
1a10400
Updating the email start groovy template
May 24, 2023
b36951b
Email templates fixes and updates
May 25, 2023
e97df46
MGI T7 - fixed used of flag files path from config
May 29, 2023
904f7f0
Clarity - fixing use of config for MGI T7 flag files
May 29, 2023
233e8bb
Start working on freezeman ingestion
May 29, 2023
9d8c01d
Genpipes as submodules
garantjm May 31, 2023
76438d6
Merge branch 'fzmn-child-process' into freezeman
garantjm May 31, 2023
7926728
Updating the freezemannIngestor script and its call bty the momitor
May 31, 2023
c07ff80
Merge branch 'freezeman' of https://github.com/c3g/monitor into freez…
May 31, 2023
498c368
fix config
May 31, 2023
4ff597e
Fix run dir for illumina runs
Jun 1, 2023
e8bbc34
Readme, .py arguments format
garantjm Jun 1, 2023
d9200e5
Merge branch 'freezeman' into fzmn-child-process
garantjm Jun 1, 2023
d87eca0
Fix debug.db path in config
Jun 1, 2023
37cc733
Revision of argparser, include password prompt
garantjm Jun 1, 2023
db3b7f9
Changed GenPipes submodule URL to a public accessible https
garantjm Jun 1, 2023
ead990b
Fixing ini for genpipes as well as email start template
Jun 2, 2023
5f3f032
Changing definition of custom_ini
Jun 2, 2023
aa87985
Fixes on emails & BeginRun
Jun 4, 2023
b53fc23
Resetting 'onstart' emails for debug profile
Jun 4, 2023
89d669d
Merge branch 'main' into freezeman
Jun 4, 2023
eca0e7a
Updated .gitmodules with specific 'run_processing' branch from genpipes
Jun 4, 2023
2439862
Add correct imports in clarity/launch.nf
Jun 4, 2023
f74e2d5
Update doc information
garantjm Jun 7, 2023
3d1ec86
GetGenpipes process removed
garantjm Jun 7, 2023
919cbf1
few adkjustmens in config + other minor changes
Jun 8, 2023
7e7e45e
Merge branch 'freezeman' into fzmn-child-process
garantjm Jun 8, 2023
3450580
Strengthen the parsing of the MultiQC json
Jun 13, 2023
5015b75
Corrected `outdir` in BeginRun
Jun 15, 2023
955c4ad
Merge branch 'freezeman' into fzmn-child-process
garantjm Jun 15, 2023
3df7551
Add fzmn-child-process configs & ini
garantjm Jun 16, 2023
0c2322d
GenPipes submodules updated
garantjm Jun 21, 2023
86adf51
GetGenpipes leftovers removed, validation report ssh key, more logs
garantjm Jul 19, 2023
2d191ed
Merge branch 'fzmn-child-process' into freezeman
garantjm Jul 19, 2023
3a35094
Add key arg to summary upload & errorStrategy ignore for emails
garantjm Jul 20, 2023
448f6b8
FreezemanIngestor connect & auth fixed, new arguments
garantjm Aug 3, 2023
c1e2877
Freezeman runinfo file modifications
garantjm Aug 15, 2023
3f1f8b3
GenPipes update for run validation json, error strategy handling, doc…
garantjm Aug 31, 2023
708f3a5
Clarity BeginRun fixed with new GenPipes submodule
garantjm Sep 7, 2023
31b5e5a
Path changes towards dedicated freezeman-processing
garantjm Oct 27, 2023
720cc53
GenPipes RunProcessing fixes for T7
garantjm Jan 18, 2024
a99ee50
Update MultiQC module
garantjm Jan 24, 2024
2cf0b37
Genome null length issue and tweaks for freezeman-qc
garantjm Jan 24, 2024
8798783
Genomic database copy paste fix in genpipes
garantjm Jan 29, 2024
72376b8
Duplicate genomic database fix in genpipes readset
garantjm Feb 5, 2024
03d6ae1
Ingestion in Freezeman
garantjm Feb 8, 2024
f933d17
FreezemanIngest running, freezemanIngestor sending multiple Validatio…
garantjm Feb 19, 2024
1cb85a3
Commenting out support for HiseqX, Iseq1 & Iseq2
garantjm Feb 21, 2024
11f064b
Add project_name to validation report for Freezeman ingestion and exi…
garantjm Feb 26, 2024
a6d6dcf
NovaseqX paths & channels added, genpipes supported
garantjm Mar 18, 2024
0a11418
New MultiQC run validation reports directory on genap's datahub
garantjm Mar 18, 2024
8cf5aea
Reorder to have NovaseqX ready up before Novaseq
garantjm Mar 18, 2024
bbb3afd
NovaseqX flowcell string has a different position in path
garantjm Mar 20, 2024
bd866bf
Run_processing relies on GenPipes version of adapter_settings_format.txt
garantjm Mar 27, 2024
0de8621
Repair profiles string in config
garantjm Mar 27, 2024
5f3973d
Update MulitQC, add year to outdir path & clean logs
garantjm Mar 27, 2024
104b779
NovaseqX index job, WGBS not aligned, summaryReport T7
garantjm Mar 27, 2024
1f9c54c
Add year to monitor.nf watchPath
garantjm Apr 1, 2024
15478a9
HotFix: params.outdir set instead of .mgi.outdir
garantjm Apr 4, 2024
50265e3
Genpipes run_validation report with url towards datahub Freezeman_val…
garantjm Apr 18, 2024
1584bd3
Fix validation report URL in finish email template, update onfinish e…
garantjm Apr 22, 2024
cfe5661
GenPipes copy step includes a compression job of logs.
garantjm Apr 25, 2024
8ef1d85
Update mugqic_tools to fix types in alignment metrics for run validation
garantjm May 16, 2024
15dfcd4
Folder name added to finish email
garantjm Jul 4, 2024
6de729b
Replace run value from MultiQC with run_name from run info file, add …
garantjm Jul 11, 2024
2afac82
Simple copy of each MultiQC run to the run processing folder
garantjm Jul 15, 2024
cc2a78e
Includes QoL changes requested by the lab
garantjm Aug 29, 2024
a6089ab
Sex concordance fix and new certificates
garantjm Oct 24, 2024
8a184fc
Remove retries for BeginRun to make sure there are no double launches…
garantjm Oct 28, 2024
d63efb3
Update MultiQC_C3G to v1.23
garantjm Nov 4, 2024
0059dea
Update indexes, barcodes, adapters in GenPipes
garantjm Nov 14, 2024
f4b3322
Add sortmerna for RNAseq reports
garantjm Dec 3, 2024
8ad8536
Removing retries to reduce the potential issues associated
garantjm Dec 5, 2024
8f79f81
Merge sortmerna & copy step PR in GenPipes
garantjm Dec 5, 2024
84aa070
Genpipes fix for Danielle missing mkdir in fastq move
garantjm Dec 11, 2024
e34b5e2
Change the glob expression that ignores dnbseqt7 for a glob restricte…
garantjm Dec 11, 2024
2cb2f87
Adding Ioannis Ragoussis to emailonfinish list
garantjm Dec 16, 2024
23ec915
MultiQC reports rsynced towards /lb/robot/ as well as /nb/Research
garantjm Dec 18, 2024
3976ff7
update genpipes version within monitor
MareikeJaniak Dec 18, 2024
592dfc1
add curl command to send data to dashrunr after multiqc report is gen…
MareikeJaniak Dec 18, 2024
233a033
update genpipes version within monitor
MareikeJaniak Dec 19, 2024
99513ed
update multiqc version, move multiqc report sync and dashrunnr update…
MareikeJaniak Jan 3, 2025
aadeaf6
debug FinalSync
MareikeJaniak Jan 3, 2025
fe3a13d
debug final sync
MareikeJaniak Jan 3, 2025
a766951
debug final sync
MareikeJaniak Jan 3, 2025
830f4d5
revert FinalSync for now, not working
MareikeJaniak Jan 6, 2025
24bbb45
add FinalSync to sync nb and lb, push to run dashboard
MareikeJaniak Jan 6, 2025
760832c
moving FinalSync to happen after RunMultiQC
MareikeJaniak Jan 6, 2025
4644c5d
update multiqc module version
MareikeJaniak Jan 6, 2025
a16792e
debug final sync for monitor, fix genap link in finish email template
MareikeJaniak Jan 6, 2025
54bf812
debug final sync for monitor
MareikeJaniak Jan 6, 2025
f550794
debug final sync, format analysis dir
MareikeJaniak Jan 7, 2025
9662e8b
debug final sync, escaping
MareikeJaniak Jan 7, 2025
2b3aede
debug final sync, remove curl to dashrunnr for now
MareikeJaniak Jan 7, 2025
6be71e8
Merge pull request #3 from c3g/freezeman_finalSync
MareikeJaniak Jan 7, 2025
6aa10cc
access year from runinfo, instead of using wild card for FinalSync
MareikeJaniak Jan 7, 2025
d74f77a
import analysis dir as element of list instead of whole list, update …
MareikeJaniak Jan 7, 2025
f128b64
clean up FinalSync for PR
MareikeJaniak Jan 7, 2025
325f853
Merge pull request #4 from c3g/freezeman_finalSync
MareikeJaniak Jan 7, 2025
684ae5f
update genpipes version within monitor
MareikeJaniak Jan 9, 2025
34c7513
update genpipes version within monitor - copy step fix
MareikeJaniak Jan 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
*.swp
*.swo
*.swn

.nextflow*
work
outputs
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[submodule "genpipes"]
path = genpipes
url = https://bitbucket.org/mugqic/genpipes.git
branch = run_processing
161 changes: 161 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
Run Processing Monitor
======================

This nextflow workflow manages the monitoring and automates launch of run
processing jobs following the completion of a run provided by the sequencing
laboratory at McGill Genome Center. Launches rely on Freezeman for their inputs
and outputs. There is also a some legacy code that managed the same but from
runs that were managed through Clarity (Illumina's proprietary Freezeman
equivalent).

Install
-------

Make sure to clone the repository along with its submodule.

```
git clone --recurse-submodules git@github.com:c3g/monitor.git

or

git clone --recurse-submodules https://github.com/c3g/monitor.git
```

Usage
-----

The monitor is dependant on the local McGill University cluster, Abacus. Make
sure to load the required modules before launching it.

```
module purge && module load mugqic/java/openjdk-jdk-17.0.1 mugqic/nextflow/22.10.6 mugqic/python/3.10.2
```

The simplest usage of the monitor is to run `main.nf` along with an entrypoint.
To reduce the ressources usage, limiting the entrypoint to Freezeman monitoring
is recommended, particularly knowing that Clarity support is deprecated.

Profiles are used to set configurations, they change behaviors such as emailing
notifications, filepath globs to watch for sequencing files, remote access
details and more.

The production profile should not be changed lightly and is only meant to be
run by the freezeman-lims user on Abacus.

```
nextflow run main.nf -profile [production,debug,dev] -entry [Freezeman][Monitor,Launch,MonitorAndLaunch]
```

Redirecting the logs is recommended.

```
nextflow -log [filepath].log run main.nf -profile [production,debug,dev] -entry [Freezeman][Monitor,Launch,MonitorAndLaunch]
```

Remember to keep the nextflow logs tidy with

```
nextflow log

and

nexflow clean
```

Notes
-----

### Launch delays

The Launch part of the monitor is particularly slow to be ready to receive
NovaSeq runs runinfofile. Even with the fzmn-child-process child config file
that reduce the glob pattern to check for `RTAComplete.txt` files, the launch
for Illumina takes at least 25 mins.

*The current version in production requires ~10 mins to be ready with MGI files
and a good 8 hours for Illumina files.*

Not only is the repo relying on Abacus' system and filesystem, the trigger that
launches the run processing relies on files dropped in the
`freezeman-lims-run-info` folder. In the current case, that is performed by a
5min cron job under freezeman-[lims,qc,dev] users that drops them in a folder
under freezeman-[lims,qc,dev] access.

I put some efforts on the bloating of the monitor and I will not put anymore.
Removing useless steps had an underwhelming impact on the overall monitor both
on time to run and memory usage. Reduced from ~40GB RAM to ~30GB in production
and similar impact on the launch of nextflow until all channels are up to
monitor. The only impactful changes would be to reduce the number of open
channels or limit the size of glob patterns and wildcards to match fewer
filepaths in the filesystem. Doing so would require to revisit the way
runinfofiles, RTAcomplete.txt and checkpoint files are monitored at the
filesystem level by the watchPatch channels.

**Most of the time lost is spent managing the database** which is used to match
runs to their metadata. Their are columns that are used to get filepaths,
timestamps and such, but unfortunately **the entire run info json file is store
in a cell for each run**. Parsing these is the time consuming step. Therefore
the minimal launch time can be obtained with the smallest database, I recommend
to move/remove the previous database before launch. Be weary that the monitor
won't monitor runs that were process prior to this move/remove.

Typical "short" launch:

```
module load mugqic/java/openjdk-jdk-17.0.1 mugqic/nextflow/22.10.6 mugqic/python/3.10.2 && mv /nb/Research/freezeman-processing/monitor.db /nb/Research/freezeman-processing/db.bu/monitor.db.240704 && nextflow -log log/log run main.nf -profile production -entry FreezemanMonitorAndLaunch -bg
```

### Particular set-up

The monitor is highly dependant on a number of softwares, env variables, ssh
keys, paths and network access that are only avaible on Abacus. It was not
designed to be run outside of the freezeman-[lims,qc,dev] users environment and
relies on some of their specific set-up. Notable examples of required set-up:

1. The runinfofiles are dumped by the Freezeman interface when an experiment is
"Launched" to be processed and reingested to be added as a "Dataset" for run
validation. They are copied via an rsync 5min cron job that relies on a
unique ssh-key for each freezeman-[lims,qc,dev] to access the virtual
machine under the "intermediary" user. A different user will have to move or
copy these files themselves inside the directories set in `nextflow.config`
under `neweventpath` & `newruninfopath`

2. Genpipes run_processing.py is using some software that are part of
$MUGQIC_INSTALL_HOME_PRIVATE, such as bcl2fastq. To access these, one must
set their environment using:

```
export MUGQIC_INSTALL_HOME_PRIVATE=/lb/project/mugqic/analyste_private
module use $MUGQIC_INSTALL_HOME_PRIVATE/modulefiles
```

User freezeman-lims have $MUGQIC_INSTALL_HOME_PRIVATE set-up in its
`~.bash_profile`.

3. The user running the monitor should also have access to the run_processing
directories: `/nb/Research/<platform>/<run_folder>`

4. Review the content of nextflow.config before launch. Paths listed in the
configs should exist since the monitor will try to read from them in the
early steps. There is also a final copy of the run_processing output files
that is targeting a directory listed in the config parameter `custom_ini`
files that should target one of the provided `.ini` files in the `assets`
folder. Make sure that this folder exists before launching. In the `.ini`,
search for

```
[copy]
destination_folder=/lb/project/mugqic/projects/[...]
```

### What is dependent on the monitor

Even though this monitor needs to be able to take care of Illumina & MGI
run_processing, at the moment, most run_processing is managed by Haig
Djambazian's pipeline which I believe is a set of bash scripts. However, it
seems that some of his processing relies on this monitor for complementary
steps, notably MultiQC and reporting emails. They are part of WatchCheckpoints
workflow in monitor.nf that manages the interface between Haig's stuff and this
monitor. It used to run from the main branch of the repo, under the bravolims
user, but it hasn't been restarted since the last Abacus outages (2023/07/08 &
2023/08/05).
8 changes: 8 additions & 0 deletions assets/debug.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[DEFAULT]
cluster_other_arg = -W umask=0002 -l qos=f2

[basecall]
cluster_other_arg=-m a -M $JOB_MAIL -W umask=0002 -l nodes=1:gpus=1

[fastq_t7]
tmp_dir=/lb/scratch/$USER/tmp.$PBS_JOBID
12 changes: 0 additions & 12 deletions assets/debug.t7.ini

This file was deleted.

8 changes: 8 additions & 0 deletions assets/dev.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[DEFAULT]
cluster_other_arg = -W umask=0002 -l qos=normal

[basecall]
cluster_other_arg=-m a -M $JOB_MAIL -W umask=0002 -l nodes=1:gpus=1

[fastq_t7]
tmp_dir=/lb/scratch/$USER/tmp.$PBS_JOBID
4 changes: 0 additions & 4 deletions assets/email_T7_run_start.groovy

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,40 @@ yieldUnescaped '<!DOCTYPE html>'
html(lang:'en') {
head {
meta('http-equiv':'"Content-Type" content="text/html; charset=utf-8"')
title("MGI Run finished: ${run.run}")
title("${platform} Run finished: ${run.run}")
}
body {
div(style:"font-family: Helvetica, Arial, sans-serif; padding: 30px; max-width: 900px; margin: 0 auto;") {
h3 "Run: ${run.run} (${run.flowcell})"
h3 "Run: ${event.data.run_name} (${run.flowcell})"
h3 "Folder: [${run.analysis_dir}]"
p {
span "Run processing finished. Full report attached to this email, but also available "
a ( href:"https://datahub-297-p25.p.genap.ca/MGI_validation/2023/${run.run}.report.html", "on GenAP" )
a ( href:"https://datahub-297-p25.p.genap.ca/Freezeman_validation/${event.year}/${event.data.run_name}.report.html", "on GenAP" )
span "."
}
ul {
li "Spread: ${run.headerInfo.Spreads}"
li "Yield assigned to samples: ${nfGeneral.format(yield)} bp"
li "Instrument: ${run.headerInfo.Instrument}"
}
table(style:"box-shadow: 0 0 30px rgba(0, 0, 0, 0.05);margin: 25px 0;font-size: 0.9em;border-collapse: collapse;") {
tbody {
tr(style:"background-color: #009879;color: #ffffff;text-align: left;") {
th(style:'padding: 5px 10px', "Project Name")
th(style:'padding: 5px 10px', "Samples")
}
samples.countBy{it.ProjectName ? it.ProjectName : it.project_name}.each { projectname, count ->
tr(style:'border: 2px solid #dddddd;') {
td style:"padding: 5px 10px; font-weight: bold", projectname
td style:"padding: 5px 10px;", count == 1 ? "${count} sample" : "${count} samples"
}
}
tr(style:'background-color: #666666;color: #ffffff;text-align:left;') {
td "Total"
td samples.size() == 1 ? "${samples.size()} sample" : "${samples.size()} samples"
}
}
}
table(style:"box-shadow: 0 0 30px rgba(0, 0, 0, 0.05);margin: 25px 0;font-size: 0.9em;border-collapse: collapse;") {
thead {
tr(style:"background-color: #009879;color: #ffffff;text-align: left;") {
Expand Down Expand Up @@ -83,6 +102,11 @@ html(lang:'en') {
span workflow.commitId ? "Email generated at ${dateFormat(now)} using monitor at commit ${workflow.commitId}." : "Email generated at ${dateFormat(now)}."
}
p(style:"color: #999999; font-size: 12px", "C3G Run Processing.")
// p {
// span(class:"apple-link", style:"color: #999999; font-size: 12px; text-align: center;") {
// a(href:"https://c3g.ca/", style:"text-decoration: none; color: #999999; font-size: 12px; text-align: center;", "C3G Run Processing")
// }
// }
}
}
}
126 changes: 126 additions & 0 deletions assets/email_run_start.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// == Introduction == //
// This template expects three variables to be defined - `flowcell`, `samples` and `platofrm`.
// The template is instantiated (in the `EmailAlertStart` process) like so:
// email_fields = [flowcell: eventfile.flowcell, samples: rows, platform: platform]
// def html_template = engine.createTemplate(html).make(email_fields)

// == Testing == //
// To test modifications to this template, there is the `Debug` workflow, which looks for changes to
// this file and upon detecting a change, regenerates the template using an example multiqc json file.
// The demo HTML is written to outputs/testing/email/email_MGI_run_finish.html. I'd recommend setting up
// a live watcher so that you can just save the template and the page gets updated in-browser instantly.

// == Helpers == //
// Here we define a couple of helpful variables/function to make the rest of the
// template a little cleaner.
Date now = new Date()

def dateFormat(date) {
date.format('yyyy-MM-dd HH:mm:ss z')
}

// == HTML Template == //
// The actual temlate code.
yieldUnescaped '<!DOCTYPE html>'
html(lang:'en') {
head {
meta('http-equiv':'"Content-Type" content="text/html; charset=utf-8"')
title("${platform} Run started - Flowcell ${flowcell}")
}
body {
div(style:"font-family: Helvetica, Arial, sans-serif; padding: 30px; max-width: 900px; margin: 0 auto;") {
h2 "New run processing started : ${flowcell}"
p {
span "This is an automated message sent from the run processing event monitor."
}
p {
span "Run processing has started for new ${platform} run on flowcell: ${flowcell}. "
}
p {
span "Freezeman Run Info file used is attached."
}
table(style:"box-shadow: 0 0 30px rgba(0, 0, 0, 0.05);margin: 25px 0;font-size: 0.9em;border-collapse: collapse;") {
tbody {
tr(style:"background-color: #009879;color: #ffffff;text-align: left;") {
th(style:'padding: 5px 10px', "Project Name")
th(style:'padding: 5px 10px', "Samples")
}
samples.countBy{it.ProjectName ? it.ProjectName : it.project_name}.each { projectname, count ->
tr(style:'border: 2px solid #dddddd;') {
td style:"padding: 5px 10px; font-weight: bold", projectname
td style:"padding: 5px 10px;", count == 1 ? "${count} sample" : "${count} samples"
}
}
tr(style:'background-color: #666666;color: #ffffff;text-align:left;') {
td "Total"
td samples.size() == 1 ? "${samples.size()} sample" : "${samples.size()} samples"
}
}
}
p(style:"color: #999999; font-size: 12px") {
span workflow.commitId ? "Email generated at ${dateFormat(now)} using monitor at commit ${workflow.commitId}." : "Email generated at ${dateFormat(now)}."
}
p {
span(class:"apple-link", style:"color: #999999; font-size: 12px; text-align: center;") {
a(href:"https://c3g.ca/", style:"text-decoration: none; color: #999999; font-size: 12px; text-align: center;", "C3G Run Processing")
}
}
}
}
}


// <html>

// <head>
// <meta charset="utf-8">
// <meta http-equiv="X-UA-Compatible" content="IE=edge">
// <meta name="viewport" content="width=device-width, initial-scale=1">

// <meta name="description" content="New run notification.">
// <title>T7 Flowcell ${flowcell}</title>
// </head>

// <body>
// <div style="font-family: Helvetica, Arial, sans-serif; padding: 30px; max-width: 800px; margin: 0 auto;">

// <h2>New run processing started</h2>
// <h3>Flowcell: <span style='font-weight: bold'>$flowcell</span></h3>

// <table
// style="width:100%; max-width:100%; border-spacing: 0; border-collapse: collapse; border:0; margin-bottom: 30px;">
// <tbody style="border-bottom: 1px solid #ddd;">
// <tr>
// <th
// style='text-align:left; padding: 8px 0; line-height: 1.42857143; vertical-align: top; border-top: 1px solid #ddd; border-bottom: 1px solid #ddd;'>
// Project Name</th>
// <th
// style='text-align:left; padding: 8px; line-height: 1.42857143; vertical-align: top; border-top: 1px solid #ddd; border-bottom: 1px solid #ddd;'>
// Samples</th>
// </tr>
// <% samples.countBy{it.ProjectName}.each { projectname, count -> %>
// <tr style='line-height: 1.42857143; vertical-align: top; '>
// <td>$projectname</td>
// <td>$count ${count == 1 ? "sample" : "samples"}</td>
// </tr>
// <% } %>
// <tr
// style='text-align:left; padding: 8px; line-height: 1.42857143; vertical-align: top; border-top: 1px solid #ddd; border-bottom: 1px solid #ddd;'>
// <td>Total</td>
// <td>${samples.size()} ${samples.size() == 1 ? "sample" : "samples"}</td>
// </tr>
// </tbody>
// </table>

// <p>
// <span class="apple-link" style="color: #999999; font-size: 12px; text-align: center;"><a
// href="https://c3g.ca/"
// style="text-decoration: none; color: #999999; font-size: 12px; text-align: center;">C3G
// Run Processing</a>
// </p>

// </div>

// </body>

// </html>
Loading