diff --git a/reporting/compensation-bot/README.md b/reporting/compensation-bot/README.md new file mode 100644 index 0000000..8b53d8f --- /dev/null +++ b/reporting/compensation-bot/README.md @@ -0,0 +1,39 @@ +# compensation-bot +a bot to parse bisq compensation requests + +Specification is here: https://github.com/bisq-network/projects/issues/32 + + +## Configure settings +follow instructions for the `.env` file at https://probot.github.io/docs/development/#manually-configuring-a-github-app + + +## Setup + +```sh +# Install dependencies +npm install + +# Run the bot +npm start +``` + +## Tools + +`linter_tool.html` is a thin test harness for the linter. It can be used for + +- testing an individual compensation request +- running the linter test suite +- loading an existing compensation request from github to examine & test it + + +`reporting_tool.html` allows the end of cycle data to be analysed. It works from a JSON data blob which can be obtained from a file, or generated by re-parsing all GitHub compensation reports. +The cycle report outputs the following CSV reports: +- cycle issuance by team (in BSQ) +- cycle issuance by team (in USD) +- cycle issuance by team/user (in BSQ) +- cycle issuance by team/user (in USD) + + +`issuance.json` is the JSON blob used to store a record of parsed compensation reports. Ideally it should be updated by a maintainer at each cycle end. This is something that `reporting_tool` can help with. + diff --git a/reporting/compensation-bot/crParser.js b/reporting/compensation-bot/crParser.js new file mode 100644 index 0000000..cec9d41 --- /dev/null +++ b/reporting/compensation-bot/crParser.js @@ -0,0 +1,401 @@ + + // =================================================================== + // bisq-bot compensation request parser + // this code is shared between linter_tool.html and the bot (index.js) + + var crParserNS = { + + BSQ_precision: 2, // decimal places + USD_precision: 2, // decimal places + compRequest: null, + bsqRate: 0.99, // will be read from issues list + cycle: 99, // will be read from issues list + strict: true, + validTeams: [ "dev", "growth", "ops", "support", "security", "admin" ], + + writeLinterSummary: function() { + var results = ""; + try { + if (this.compRequest.infoList.length > 0) { + results += "## Info\n"; + for (const i of this.compRequest.infoList) { + results += i + "\n"; + } + } + if (this.compRequest.errorList.length > 0) { + results += "\n\n"; + results += "## Errors\n"; + for (const i of this.compRequest.errorList) { + results += i + "\n"; + } + results += "\n"; + return results; // stop here when we've encountered errors + } + results += "\n\n"; + results += "### NO ERRORS\n"; + results += "\n\n"; + } + catch(e) { + console.log(e); + results += "[data error]: " + e.message; + } + return results; + }, + + writeIssuance: function() { + var results = ""; + try { + results += "\n"; + if (this.compRequest.errorList.length > 0) { + results += "Issuance cannot be displayed as there were errors in the comp request\n"; + return results; + } + results += "## Issuance by Team:\n"; + results += "|team|amount BSQ|amount USD|\n"; + results += "|---|---|---|\n"; + var all_info = this.compRequest.issuance.byTeam; + for (const res of this.compRequest.issuance.byTeam) { + results += "|" + res.team + "|" + + res.bsq.toFixed(this.BSQ_precision) + "|" + + res.usd.toFixed(this.USD_precision) + "|\n"; + } + + results += "\n"; + results += "Total Issuance: " + this.compRequest.issuance.total_BSQ.toFixed(this.BSQ_precision) + " BSQ"; + results += " (equivalent to: " + this.compRequest.issuance.total_USD.toFixed(this.USD_precision) + " USD)\n"; + results += "\n"; + } + catch(e) { + console.log(e); + results += "[data error]: " + e.message; + } + return results; + }, + + // check the line items sum up to the summary requested amt + verifyItemsMatchSummaryTotal : function() { + var retVal = true; + var tolerance = 1.00; + if (this.compRequest.summary.usdRequested != null) { + var usdDifference = Math.abs(this.compRequest.summary.usdRequested - this.compRequest.issuance.total_USD); + if (usdDifference >= tolerance) { + this.compRequest.errorList.push("ERROR: Total USD does not match the sum of line items:"); + this.compRequest.errorList.push(" - Summary total: " + this.compRequest.summary.usdRequested.toFixed(this.USD_precision) + " USD"); + this.compRequest.errorList.push(" - Calculated total: " + this.compRequest.issuance.total_USD.toFixed(this.USD_precision) + " USD\n"); + retVal = false; + } + } + if (this.compRequest.summary.bsqRequested != null) { + var bsqDerivedFromUsd = this.compRequest.summary.usdRequested / this.bsqRate; + var bsqDifference = Math.abs(this.compRequest.summary.bsqRequested - bsqDerivedFromUsd); + if (bsqDifference >= tolerance) { + this.compRequest.errorList.push("ERROR: Total BSQ does not match the sum of line items:"); + this.compRequest.errorList.push(" - Summary total: " + this.compRequest.summary.bsqRequested.toFixed(this.BSQ_precision) + " BSQ"); + this.compRequest.errorList.push(" - Calculated total: " + bsqDerivedFromUsd.toFixed(this.BSQ_precision) + " BSQ\n"); + retVal = false; + } + } + return retVal; + }, + + getTeamLabels : function() { + var retVal = []; + var all_info = this.compRequest.issuance.byTeam; + for (const i of all_info) { + retVal.push("team:"+i.team.toLowerCase()); + } + return retVal; + }, + + validateTeam : function(team) { + var cleanTeam = team.replace(/\*/g, ''); + if (this.validTeams.includes(cleanTeam.toLowerCase())) { + return cleanTeam.toLowerCase(); + } + this.compRequest.errorList.push("Unknown team specified: " + cleanTeam); + return null; + }, + + validateAmount : function(amount) { + try { + var sanitizedNumber = Number(amount.replace(/[,]/g, '').replace(/USD/g, '')); + if (typeof sanitizedNumber == "number" && !isNaN(sanitizedNumber)) { + return sanitizedNumber; + } + } + catch(e) { + console.log(e.message); + } + this.compRequest.errorList.push("Invalid amount specified: " + amount); + return 0; + }, + + removeMultilineComments(lines) { + var linesToRemove = []; + var inComment = false; + for (i=0; i < lines.length; i++) { + var x = lines[i].toUpperCase().replace(/[ \t\r`*]/g, ''); + x = x.replace(//g, ''); // remove HTML comments + if (x.match(//g)) { linesToRemove.push(i); inComment = false; } + else if (inComment == true) linesToRemove.push(i); + } + for (i=linesToRemove.length-1; i>=0; i--) { + lines.splice(linesToRemove[i],1); + } + return lines; + }, + + // we need to parse summary info from comp request + findCrSummaryInText : function(lines) { + var retVal = true; + var inSummary = false; + for (i=0; i < lines.length; i++) { + var x = lines[i].toUpperCase().replace(/[ \t\r`*-]/g, ''); + x = x.replace(//g, ''); // remove HTML comments + if (x.match(/^##SUMMARY/g)) { + // found the summary section + this.compRequest.summary.startLine = i; + inSummary = true; + continue; + } + else if (x.match(/^##/g)) { + // we've found the next section + if (inSummary == true) { + this.compRequest.summary.endLine = i-1; + } + inSummary = false; + continue; + } + if (inSummary == true) { + // processing the summary section + if (x.match(/^USDREQUESTED:/g)) { + var y = x.replace(/^USDREQUESTED:/g, ''); + var z = y.replace(/[^\d.]/g, ''); + if (z.length > 0) { + this.compRequest.summary.usdRequested = Number(z); + this.compRequest.infoList.push("Read USD amount from summary: " + this.compRequest.summary.usdRequested); + } + } + if (x.match(/^BSQREQUESTED:/g)) { + var y = x.replace(/^BSQREQUESTED:/g, '').replace(/BSQ$/g, '').split("="); + var z = y[y.length-1].replace(/[^\d.]/g, ''); + if (z.length > 0) { + this.compRequest.summary.bsqRequested = Number(z); + this.compRequest.infoList.push("Read BSQ amount from summary: " + this.compRequest.summary.bsqRequested); + } + } + if (x.match(/^BSQRATE:/g)) { + var y = x.replace(/^BSQRATE:|\(.*\.*.\)/g, '').split("USD"); + var z = y[0].replace(/[^\d.]/g, ''); + if (z.length > 0) { + var specifiedBsqRate = Number(z); + if (this.strict) { + var precision = 0.001; + if (Math.abs(this.bsqRate - specifiedBsqRate) > precision) { + this.compRequest.errorList.push("Incorrect BSQ rate specified: " + z + ", expected: " + this.bsqRate); + } + } else { + this.bsqRate = specifiedBsqRate; + } + this.compRequest.infoList.push("Read BSQ rate from summary: " + specifiedBsqRate); + } + } + } + } + if (this.compRequest.summary.usdRequested == null) { + this.compRequest.errorList.push("USD amount not specified in summary section"); + retVal = false; + } + if (this.compRequest.summary.bsqRequested == null) { + this.compRequest.errorList.push("BSQ amount not specified in summary section"); + retVal = false; + } + return retVal; + }, + + // we need to parse data from compensation requests + // the data is formatted in markdown tables + // this routine will search the text for a table + findNextTableInText : function(lines, tableInfo) { + tableInfo.foundStart = -1; + tableInfo.foundEnd = -1; + var inProgress = false; + for (i=tableInfo.beginAtLine; i < lines.length; i++) { + var x = lines[i].replace(/[ \t\r]/g, ''); + x = x.replace(//g, ''); // remove HTML comments + x = x.replace(//g, ''); // remove HTML comments + if (x.match(/^##.*INPROGRESS/gi)) { + // found the in progress section - we'll have to skip this + inProgress = true; + continue; + } + else if (x.match(/^##/g)) { + // we've found the next section + inProgress = false; + continue; + } + if (inProgress == true) { + continue; // skipping the inprogress section + } + if (tableInfo.foundStart < 0 && x.match(/TITLE\|TEAM\|USD\|LINK\|NOTES/gi)) { + // found the start of the markdown table. + tableInfo.foundStart = i; + continue; + } + if (tableInfo.foundStart >= 0 && tableInfo.foundEnd < 0 && x == '') { + // found the end of the markdown table. + tableInfo.foundEnd = i; + break; + } + } + if (tableInfo.foundStart < 0 || tableInfo.foundEnd < 0) { + // error table not found + return false; + } + // copy the table data into tableInfo.tableLines + // skip the header line + for (i=tableInfo.foundStart+1; i < tableInfo.foundEnd; i++) { + var x = lines[i].replace(/[ \t\r]/g, ''); + if (!x.match(/^\|/gi)) x = "|"+x; // some people do not specify first bar + if (x.match(/\|(-)\1{1,}\|(-)\1{1,}\|(-)\1{1,}\|(-)\1{1,}\|(-)\1{1,}\|/gi)) { continue; } // skip blank table rows + if (x == "||||||") { continue; } // skip blank table rows + tableInfo.tableLines.push(x); + } + return true; + }, + + // parse the contents of the table, ignore the header row + parseRequestsFromTable : function(lines) { + var recordsParsed = 0; + var teams = new Set(); + for (i=0; i < lines.length; i++) { + // each line should split into 5 fields: title,team,USD,link,notes + var x = lines[i].replace(/[, \t\r]/g, ''); + x = x.replace(//g, ''); // remove HTML comments + var fields = x.split('|'); + if (fields.length < 5) { + this.compRequest.errorList.push("Tableformat: request lineitem #"+(i+1)+" does not have the requisite number of fields."); + console.log("WRONG#FIELDS:"+x); + continue; + } + // ignore blank entries + // - this allows the user to detail a list of work items and claim a total amount on the last line + if ((fields[2] == "") && (fields[3] == "")) { + continue; + } + var requestLineItem = { team: this.validateTeam(fields[2]), amount: this.validateAmount(fields[3]) }; + if (requestLineItem.team) { // only process known valid teams + teams.add(requestLineItem.team); + this.compRequest.requests.push(requestLineItem); + this.compRequest.infoList.push("Parsed lineitem: " + JSON.stringify(requestLineItem)); + recordsParsed += 1; + } else { + this.compRequest.errorList.push("Tableformat: request lineitem #"+(i+1)+" did not pass validation."); + } + } + this.compRequest.teams = Array.from(teams); + if (recordsParsed < 1) { + this.compRequest.errorList.push("No compensation lineitems found."); + } + return recordsParsed; + }, + + // we're given the complete issue text: + // grab request items from one or more markdown tables + // collate the amounts by team + // validate against the summary total entered by users + parseContributionRequest : function(crBodyText) { + this.compRequest = { + errorList: [], + infoList: [], + summary: { bsqRequested: null, usdRequested: null, startLine: 0, endLine: 0 }, + requests: [], + issuance: { + total_USD: null, + total_BSQ: null, + byTeam: [] + } + }; + + var lines = crBodyText.split('\n'); + lines = this.removeMultilineComments(lines); + this.findCrSummaryInText(lines); + // grab the markdown table(s), ignore the rest + // start after the summary section + var tableInfo = { beginAtLine: this.compRequest.summary.endLine+1, foundStart: -1, foundEnd: this.compRequest.summary.endLine, tableLines: [ ] } + do { + tableInfo.beginAtLine = tableInfo.foundEnd+1; + } while (this.findNextTableInText(lines, tableInfo)); + + if (this.parseRequestsFromTable(tableInfo.tableLines) > 0) { + // now sum up the USD amount by team + // and calculate the equivalent BSQ amount based on proportion of total BSQ requested + var issuancePerTeam = [ ]; + var compRequestTotalUsd = 0; + var compRequestTotalBsq = 0; + for (const currentTeam of this.compRequest.teams) { + var usdPerTeam = 0; + var all_info = this.compRequest.requests; + for (const i of all_info) { + if (i.team == currentTeam) { + usdPerTeam += i.amount; + } + } + var bsqPerTeam = this.compRequest.summary.bsqRequested*(usdPerTeam/this.compRequest.summary.usdRequested); + // only track the team bucket if amount claimed is greater than zero + if (bsqPerTeam > 0 || usdPerTeam > 0) { + issuancePerTeam.push({ team: currentTeam, bsq: Number(bsqPerTeam.toFixed(this.BSQ_precision)), usd: Number(usdPerTeam.toFixed(this.USD_precision)) }); + compRequestTotalUsd += usdPerTeam; + compRequestTotalBsq += bsqPerTeam; + } + } + // set the team totals in the compRequest object + this.compRequest.issuance.total_USD = Number(compRequestTotalUsd); + this.compRequest.issuance.total_BSQ = Number(compRequestTotalBsq); + this.compRequest.issuance.byTeam = issuancePerTeam; + if (this.verifyItemsMatchSummaryTotal()) { + // after validating the amounts match (within tolerance), set the issuance equal to what was requested + // this is because sometimes users round their amounts up or down to the nearest whole number (which we consider acceptable) + this.compRequest.issuance.total_USD = Number(this.compRequest.summary.usdRequested); + this.compRequest.issuance.total_BSQ = Number(this.compRequest.summary.bsqRequested); + } + } + return this.writeLinterSummary(); + }, + + configLoadBsqRate : function(html) { + var lines = html.split(','); + for (i=0; i < lines.length; i++) { + var x = lines[i].toUpperCase().replace(/[ \n\t\r`*-]/g, ''); + x = x.replace(//g, ''); // remove HTML comments + if (x.match(/^"TITLE":/g)) { + // found line containing the rate + x = x.replace(/"TITLE":/g, ''); + var cycleTxt = x.replace(/^"BSQRATEFORCYCLE/g, ''); + var fields2 = cycleTxt.split("IS"); + var y = fields2[0].replace(/[^\d.]/g, ''); + if (y.length > 0) { + var specifiedBsqCycle = Number(y); + this.cycle = specifiedBsqCycle; + } + var rateTxt = x.replace(/^"BSQRATEFORCYCLE.*IS/g, ''); + var fields = rateTxt.split("USD"); + var z = fields[0].replace(/[^\d.]/g, ''); + if (z.length > 0) { + var specifiedBsqRate = Number(z); + this.bsqRate = specifiedBsqRate; + return "Read from Github: BSQ rate=" + this.bsqRate + " cycle=" + this.cycle; + } + } + } + return "failed to read BSQ rate/cycle from Github: " + lines.length; + }, + + + }; // end of crParserNS + +module.exports = crParserNS + + diff --git a/reporting/compensation-bot/index.js b/reporting/compensation-bot/index.js new file mode 100644 index 0000000..f10b5cc --- /dev/null +++ b/reporting/compensation-bot/index.js @@ -0,0 +1,136 @@ +var XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest; +var crParser = require('./crParser') + +/** + * This is the main entrypoint to your Probot app + * @param {import('probot').Application} app + */ +module.exports = app => { + app.log('app loaded!') + // grab the current cycle BSQ rate issue posted by mwithm + var html = httpGet('https://api.github.com/repos/bisq-network/compensation/issues?creator=mwithm&sort=created-asc'); + app.log(crParser.configLoadBsqRate(html)); + + // bot workflow: + // if [WIP] in title then don't do anything + // if safeMode then don't do anything + // issue created or edited => run the parser, write error rpt if applicable, apply labels + // issue labeled or milestoned => update labelling, write issuance rpt if applicable + // issue comment created or edited => nothing + // + var safeMode = false; + + app.on(['issues.opened','issues.edited'], async context => { + if (!checkContext(context)) { + return; + } + + if (crParser.compRequest.errorList.length > 0) { + applyLabels(context, ["parsed:invalid"], /^parsed:/g); + app.log("I think we should write the error report"); + var errRpt = crParser.writeLinterSummary(); + const newComment = context.issue({ body: errRpt }); + // ONLY WRITE THE ERROR REPORT WHEN REALLY SURE + if (!safeMode) { + context.github.issues.createComment(newComment); + } + } + else { + applyLabels(context, ["parsed:valid"], /^parsed:/g); + } + // manage the team labels + applyLabels(context, crParser.getTeamLabels(), /^team:/g); + }) + + app.on(['issues.labeled', 'issues.milestoned'], async context => { + if (!checkContext(context)) { + return; + } + + // issuance should be written after the comp request has been accepted + if (isIssueAccepted(context)) { + app.log("issueWasAccepted! I think we should write the issuance report"); + var issuance = crParser.writeIssuance(); + const newComment = context.issue({ body: issuance }); + if (!safeMode) { + context.github.issues.createComment(newComment); + } + return; + } + }) + + function checkContext(context) { + if (context.isBot) { // update was from a bot, so we don't process it + app.log("isBot=true, ignoring"); + return false; + } + + app.log("event: " + context.event + " action: " + context.payload.action); + app.log("title: " + context.payload.issue.title); + app.log("safeMode: " + safeMode); + app.log("================ ISSUE # " + context.payload.issue.number + " ====================="); + + // parse and log the CR so we can see in the logs what's going on + crParser.parseContributionRequest(context.payload.issue.body); + app.log(crParser.writeLinterSummary()); + app.log(crParser.writeIssuance()); + + if (context.payload.issue.state != 'open') { + app.log("issue is not open, therefore ignoring"); + return false; + } + var isWIP = /\[WIP\]/gi.test(context.payload.issue.title); + if (isWIP) { + app.log("issue is marked [WIP], therefore not making any changes"); + return false; + } + return true; + } + + function isIssueAccepted(context) { + let existingLabels = []; + var labelObjs = context.payload.issue.labels; + labelObjs.map(label => existingLabels.push(label.name)); + return (existingLabels.indexOf("was:accepted") >= 0); + } + + function applyLabels(context, labelsRequired, regexFilter) { + let existingLabels = []; + var labelObjs = context.payload.issue.labels; + labelObjs.map(label => existingLabels.push(label.name)); + existingLabels = existingLabels.filter(label => label.match(regexFilter)); + + app.log("existing labels: " + existingLabels); + app.log("labels required: " + labelsRequired); + var labelsToAdd = labelsRequired.filter(label => existingLabels.indexOf(label) < 0); + var labelsToRemove = existingLabels.filter(label => labelsRequired.indexOf(label) < 0); + for (const lbl of labelsToRemove) { + app.log("removing: "+lbl); + if (!safeMode) { + context.github.issues.removeLabel(context.issue({name: lbl})); + } + } + for (const lbl of labelsToAdd) { + app.log("adding: "+lbl); + if (!safeMode) { + context.github.issues.addLabels(context.issue({labels: lbl.split()})); + } + } + } + + function httpGet(theUrl) { + var xmlHttp = new XMLHttpRequest(); + xmlHttp.open( "GET", theUrl, false ); // false for synchronous request + xmlHttp.send( null ); + return xmlHttp.responseText; + } + + + + + // For more information on building apps: + // https://probot.github.io/docs/ + + // To get your app running against GitHub, see: + // https://probot.github.io/docs/development/ +} diff --git a/reporting/compensation-bot/issuance.json b/reporting/compensation-bot/issuance.json new file mode 100644 index 0000000..4364189 --- /dev/null +++ b/reporting/compensation-bot/issuance.json @@ -0,0 +1,51 @@ +[ + {"cycle":14,"entries":[ + {"crNbr":606,"issuance":{"userName":"robkaandorp","buckets":[{"team":"ops","bsq":79.37,"usd":50}]}}, + {"crNbr":604,"issuance":{"userName":"m52go","buckets":[{"team":"support","bsq":4007.51,"usd":2525},{"team":"growth","bsq":2539.42,"usd":1600},{"team":"ops","bsq":238.07,"usd":150}]}}, + {"crNbr":603,"issuance":{"userName":"Bayernatoor","buckets":[{"team":"support","bsq":793.65,"usd":500}]}}, + {"crNbr":602,"issuance":{"userName":"wiz","buckets":[{"team":"ops","bsq":3094.88,"usd":1950},{"team":"support","bsq":1587.12,"usd":1000}]}}, + {"crNbr":601,"issuance":{"userName":"mrosseel","buckets":[{"team":"ops","bsq":476,"usd":300}]}}, + {"crNbr":600,"issuance":{"userName":"cbeams","buckets":[{"team":"admin","bsq":329.37,"usd":207.5}]}}, + {"crNbr":597,"issuance":{"userName":"sqrrm","buckets":[{"team":"support","bsq":71.43,"usd":45},{"team":"dev","bsq":1484.13,"usd":935},{"team":"ops","bsq":444.44,"usd":280}]}}, + {"crNbr":596,"issuance":{"userName":"freimair","buckets":[{"team":"dev","bsq":238.2,"usd":150},{"team":"ops","bsq":158.8,"usd":100}]}}, + {"crNbr":594,"issuance":{"userName":"Bisq-knight","buckets":[{"team":"support","bsq":3174,"usd":2000}]}}, + {"crNbr":593,"issuance":{"userName":"devinbileck","buckets":[{"team":"ops","bsq":952.38,"usd":600}]}}, + {"crNbr":592,"issuance":{"userName":"leo816","buckets":[{"team":"support","bsq":2857,"usd":1800}]}}, + {"crNbr":591,"issuance":{"userName":"MwithM","buckets":[{"team":"growth","bsq":674.61,"usd":425},{"team":"support","bsq":793.66,"usd":500},{"team":"admin","bsq":158.73,"usd":100}]}}, + {"crNbr":590,"issuance":{"userName":"RefundAgent","buckets":[{"team":"support","bsq":3000,"usd":1890}]}}, + {"crNbr":589,"issuance":{"userName":"RefundAgent","buckets":[{"team":"support","bsq":91387.6,"usd":57574.19}]}}, + {"crNbr":588,"issuance":{"userName":"dmos62","buckets":[{"team":"dev","bsq":428,"usd":270}]}}, + {"crNbr":586,"issuance":{"userName":"ghubstan","buckets":[{"team":"dev","bsq":1984.13,"usd":1250}]}}, + {"crNbr":584,"issuance":{"userName":"petrhejna","buckets":[{"team":"dev","bsq":48,"usd":30}]}}, + {"crNbr":583,"issuance":{"userName":"jmacxx","buckets":[{"team":"dev","bsq":158.73,"usd":100}]}}, + {"crNbr":582,"issuance":{"userName":"luisantoniocrag","buckets":[{"team":"growth","bsq":507,"usd":320}]}} + ]}, + {"cycle":15,"entries":[ + {"crNbr":632,"issuance":{"userName":"wiz","buckets":[{"team":"support","bsq":1538.36,"usd":1000},{"team":"ops","bsq":1615.28,"usd":1050},{"team":"dev","bsq":1538.36,"usd":1000}]}}, + {"crNbr":631,"issuance":{"userName":"m52go","buckets":[{"team":"growth","bsq":5652.99,"usd":3675},{"team":"support","bsq":192.28,"usd":125},{"team":"ops","bsq":230.73,"usd":150}]}}, + {"crNbr":630,"issuance":{"userName":"Bisq-knight","buckets":[{"team":"support","bsq":1538,"usd":1000}]}}, + {"crNbr":629,"issuance":{"userName":"burningman3","buckets":[{"team":"support","bsq":1000,"usd":650}]}}, + {"crNbr":628,"issuance":{"userName":"leo816","buckets":[{"team":"support","bsq":4340,"usd":2821}]}}, + {"crNbr":627,"issuance":{"userName":"elkimek","buckets":[{"team":"growth","bsq":76.92,"usd":50}]}}, + {"crNbr":626,"issuance":{"userName":"bounhun","buckets":[{"team":"growth","bsq":750,"usd":487.5}]}}, + {"crNbr":625,"issuance":{"userName":"softsimon","buckets":[{"team":"ops","bsq":6769,"usd":4400}]}}, + {"crNbr":624,"issuance":{"userName":"ripcurlx","buckets":[{"team":"dev","bsq":6384.33,"usd":4150},{"team":"ops","bsq":507.67,"usd":330}]}}, + {"crNbr":623,"issuance":{"userName":"sqrrm","buckets":[{"team":"dev","bsq":7907.25,"usd":5140},{"team":"ops","bsq":430.75,"usd":280}]}}, + {"crNbr":622,"issuance":{"userName":"robkaandorp","buckets":[{"team":"ops","bsq":76.92,"usd":50}]}}, + {"crNbr":621,"issuance":{"userName":"devinbileck","buckets":[{"team":"ops","bsq":1076.92,"usd":700}]}}, + {"crNbr":620,"issuance":{"userName":"ghubstan","buckets":[{"team":"dev","bsq":9230.77,"usd":6000}]}}, + {"crNbr":619,"issuance":{"userName":"MwithM","buckets":[{"team":"growth","bsq":735.38,"usd":478},{"team":"support","bsq":769.23,"usd":500},{"team":"admin","bsq":153.85,"usd":100}]}}, + {"crNbr":618,"issuance":{"userName":"Bayernatoor","buckets":[{"team":"support","bsq":769.23,"usd":500}]}}, + {"crNbr":617,"issuance":{"userName":"Emzy","buckets":[{"team":"ops","bsq":1538,"usd":1000}]}}, + {"crNbr":616,"issuance":{"userName":"dmos62","buckets":[{"team":"dev","bsq":1446,"usd":940}]}}, + {"crNbr":615,"issuance":{"userName":"mrosseel","buckets":[{"team":"ops","bsq":461,"usd":300}]}}, + {"crNbr":614,"issuance":{"userName":"RefundAgent","buckets":[{"team":"support","bsq":1855,"usd":1205.8}]}}, + {"crNbr":613,"issuance":{"userName":"RefundAgent","buckets":[{"team":"support","bsq":46142.7,"usd":29992.8}]}}, + {"crNbr":612,"issuance":{"userName":"cbeams","buckets":[{"team":"admin","bsq":153.85,"usd":100}]}}, + {"crNbr":611,"issuance":{"userName":"freimair","buckets":[{"team":"dev","bsq":1923.15,"usd":1250},{"team":"ops","bsq":153.85,"usd":100}]}}, + {"crNbr":610,"issuance":{"userName":"doitsu232","buckets":[{"team":"growth","bsq":59.55,"usd":38.71}]}}, + {"crNbr":609,"issuance":{"userName":"alexej996","buckets":[{"team":"ops","bsq":307.2,"usd":200},{"team":"growth","bsq":76.8,"usd":50}]}}, + {"crNbr":608,"issuance":{"userName":"jmacxx","buckets":[{"team":"dev","bsq":307.69,"usd":200}]}} + ]} +] + diff --git a/reporting/compensation-bot/linter_tool.html b/reporting/compensation-bot/linter_tool.html new file mode 100644 index 0000000..ba34eee --- /dev/null +++ b/reporting/compensation-bot/linter_tool.html @@ -0,0 +1,544 @@ + + + + Linter for Compensation request + + + +
+

Linter for Compensation request

+ Select from these example issues, or paste your own markdown into the edit box. +
+ + + + + + +
+ +
+
+ + + + +
+ BSQ rate: +
+ Issue: +
+ +
+ + + + + + + diff --git a/reporting/compensation-bot/package.json b/reporting/compensation-bot/package.json new file mode 100644 index 0000000..109bcaa --- /dev/null +++ b/reporting/compensation-bot/package.json @@ -0,0 +1,52 @@ +{ + "name": "jmacxx-app", + "version": "1.0.0", + "private": true, + "description": "A Probot app", + "author": "jmacxx ", + "license": "ISC", + "repository": "https://github.com/jmacxx/dogecoin-nodes.net.git", + "homepage": "https://github.com/jmacxx/dogecoin-nodes.net", + "bugs": "https://github.com/jmacxx/dogecoin-nodes.net/issues", + "keywords": [ + "probot", + "github", + "probot-app" + ], + "scripts": { + "dev": "nodemon", + "start": "probot run ./index.js", + "lint": "standard --fix", + "test": "jest && standard", + "test:watch": "jest --watch --notify --notifyMode=change --coverage" + }, + "dependencies": { + "probot": "^9.5.3", + "xmlhttprequest": "^1.8.0" + }, + "devDependencies": { + "jest": "^24.9.0", + "nock": "^12.0.0", + "nodemon": "^2.0.0", + "smee-client": "^1.1.0", + "standard": "^14.3.1" + }, + "engines": { + "node": ">= 8.3.0" + }, + "standard": { + "env": [ + "jest" + ] + }, + "nodemonConfig": { + "exec": "npm start", + "watch": [ + ".env", + "." + ] + }, + "jest": { + "testEnvironment": "node" + } +} diff --git a/reporting/compensation-bot/reporting_tool.html b/reporting/compensation-bot/reporting_tool.html new file mode 100644 index 0000000..937c9dc --- /dev/null +++ b/reporting/compensation-bot/reporting_tool.html @@ -0,0 +1,299 @@ + + + + Compensation Reporting + + + +
+

Compensation Reporting

+ +
+
+Data Source + +
+
+
+ Cycle: + + + +
+ +
+ + + + + + +