PHP Labware source code viewer / Internal utilities | 22 Sep, 2025
Root | Help
./other/CrossrefAuthorCheck_v1.1.1.htm
<!doctype html>

<!--
	DOI-based authorship assessment tool using Crossref API.
	Single HTML file, to be opened in a modern browser.
	By Santosh Patnaik, MD, PhD.
	CC0, GPL3
-->

<html lang="en">

<head>
	<meta content="same-origin">
	<title>Crossref Author Check</title>

	<style>
		body {
			-webkit-font-smoothing: antialiased;
			-moz-osx-font-smoothing: grayscale;
		}
		body, div, input, textarea {
			font-size: 0.9em;
			font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
		}
		table { 
			border-spacing: 0;
			border-collapse: collapse;
		}

		button.disabled {
			font-size: 1em;
			font-weight: bold;
			color: gray;
			border: 1px solid orange;
		}
		button.enabled {
			font-size: 1em;
			font-weight: bold;
			border: 1px solid blue;
			background-color: whitesmoke;
		}
		button.minor {
			padding: 0.05em 0.3em 0.1em;
			color: gray;
			background-color: whitesmoke;
			border: none;
			border-radius: 10%;
			font-size: 0.8em;
		}
		#container {
			margin: auto;
			width: 800px;
		}
		.formTr {
			background-color: lightyellow;
		}
		.formTd {
			vertical-align: top;
			padding: 1em;
			border: none;
		}
		#helpBtn, #helpCloseBtn {
			float: right;
		}
		.headTr {
			background-color: white;
			vertical-align: top;
			padding: 1em;
			border: none;
		}
		.helpTr, .helpTd {
			background-color: honeydew;
			vertical-align: top;
			padding: 1em;
			border: none;
		}
		.input {
			color: blue;
		}
		input:invalid:required, textarea:invalid:required {
			border: 1px solid orange;
		}
		input:required, textarea:required {
			border: 1px solid blue;
		}
		input.minor {
			color: gray;
		}
		.itemTr, .itemTr td {
			vertical-align: top;
			padding: 0.5em;
			border: none;
		}
		.resultTr, .resultTd {
			background-color: aliceblue;
			vertical-align: top;
			padding: 1em;
			border: none;
		}
		span.header {
			color: gray;
			font-variant: small-caps;
			font-weight: bold;
		}
		span.minor {
			color: gray;
			font-size: 0.8em;
			font-weight: normal;
			font-variant: normal;
		}
		span.inform {
			color: green;
			font-weight: normal;
			font-variant: normal;
		}
		span.warning {
			color: red;
			font-weight: normal;
			font-variant: normal;
		}
		.tallyTdh, .tallyTr, .tallyTrh, .tallyTr td, .tallyTrh td {
			vertical-align: top;
			padding: 0.1em;
			border: none;
		}
		.tallyTdh, .tallyTr td, .tallyTrh td  {
			padding-right: 3em;
		}

	</style>

</head>

<body>
	<div id="container">
	<table><tbody>

		<!-- Heading -->

		<tr id="head" class="headTr"><td colspan="2">

		<span class="header">Crossref Author Check</span>
		 &nbsp; &nbsp; <button id="helpBtn" class="minor" title="click for help" onclick="document.getElementById('help').style.display = ''; document.getElementById('helpBtn').style.display = 'none';">Help</button>

		</td></tr>

		<!-- Help -->

		<tr id="help" class="helpTr" style="display: none"><td class="helpTd" colspan="2">

		<strong>Help</strong>  <button id="helpCloseBtn" class="minor" title="click to close Help" onclick="document.getElementById('help').style.display = 'none'; document.getElementById('helpBtn').style.display = '';"> x </button>
		<br />
		This tool retrieves CrossRef information on article type, authorship, print date, etc. for publications specified by DOI and/or PubMed IDs (max. 100). The tool also tallies the order of authorship for a specified author. For a PubMed ID, the tool first gets its DOI from NCBI, USA, which takes about 500 ms per ID. Hover over form fields for additional help.
		<br /><br />
		<span class="minor">single HTML file webpage application requiring a recent browser; save webpage as HTML file to distribute; CC0, GPL3; v1.1.1, 19 Mar 2025; <a href="https://sf.net/p/crossrefauthorcheck">update?</span>

		</td></tr>

		<!-- Form section -->

		<tr class="formTr"><td class="formTd" width="45%">

		<label for="idList">*<strong>DOI or PubMed IDs</strong>:<br />separated by space, comma, or line</label>
		<br />
		<textarea id="idList" rows="10" cols="40" class="input" required="required" minlength="5" title="max. 100"></textarea>
		
		</td><td class="formTd" width="55%">

		<span class="minor">* required</span> &nbsp; &nbsp; 
		<button id="exampleBtn" class="minor" title="click to fill with example values">Example</button> &nbsp; &nbsp; 
		<button id="clearBtn" class="minor" title="click to clear values">Clear form</button>
		<br /><br />

		<strong>Author</strong>
		<br />
		<label for="nameLast">*Last name:</label>
		<input type="text" id="nameLast" required="required" size="12" class="input" title="required; can be just the first letter; can be any letter if you don't care; can be a regex pattern like Singh|Verma" />
		<label for="nameFirst"> &nbsp; &nbsp; First name:</label>
		<input type="text" id="nameFirst" size="12" class="input" title="optional; can be just the first letter, or a regex pattern like Akshay|AK or ak*; don't use period/dot unless as a regex character" />
		<br /><br />

		<strong>Publications since</strong>
		<br />
		<label for="year">Year:</label>
		<input type="text" id="year" size="4" class="input" placeholder="yyyy" title="optional; 4-digit format"/>
		<label for="month"> &nbsp; &nbsp; Month:</label>
		<input type="text" id="month" size="2" class="input" placeholder="mm" title="optional; 2-digit format" />		
		&nbsp; &nbsp; <input type="checkbox" id="epubTimeUse" title="if checked, online/epub instead of print publishing time will be considered" />
		<label for="epubTimeUse">E-printing time</label>
		<br /><br />

		<!--
		<label for="ncbiApiKey"><span class="minor">NCBI API key:</span></label>
		<input type="text" id="ncbiApiKey" size="32" class="minor" title="optional; to possibly reduce the chance for any PubMed query issue"/>
		<br />
		-->

		<br /><br />

		<button id="submitBtn" disabled="disabled" class="disabled" title="provide at least one ID and at least the initial of a last name">Submit</button>

		&nbsp; &nbsp; <span id="submitNotice" class="inform"></span>

		</td></tr>

		<!-- Result section -->

		<tr id="result" class="resultTr" style="display: none"><td class="resultTd" colspan="2">

		<strong>Result</strong>
		<br /><br /><span class="inform">Publication tally by authorship order</span><br /><br />
		<div id="talliedResult"></div>
		<br /><span class="inform">Publications</span><br /><br />
		<div id="itemizedResult"></div>
		</td></tr>

	</tbody></table></div>

	<!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script> -->
	<script type="text/javascript">
		//<![CDATA[

		//// General support functionalities

		// Polyfill for Promise.allSettled

		if (!Promise.allSettled) {
			Promise.allSettled = function (promises) {
				return Promise.all(promises.map(function (promise) {
					return promise.then(function (value) {
						return { status: 'fulfilled', value: value };
					}).catch(function (reason) {
						return { status: 'rejected', reason: reason };
					});
				}));
			};
		}

		// Remove duplicate array elements

		let unique = (a,t={}) => a.filter(
			e=>!(t[e]=e in t)
		);

		// Timed promise

		function withTimeout(msecs, promise) {
			const timeout = new Promise((resolve, reject) =>
				setTimeout(() => reject(`Timeout after ${msecs} ms.`), msecs));
			return Promise.race([promise, timeout]);
		}

		const result = document.getElementById('result');
		const itemizedResult = document.getElementById('itemizedResult');
		const submitBtn = document.getElementById('submitBtn');
		const submitNotice = document.getElementById('submitNotice');

		//// Form interface functionalities

		// Clear form

		function clearFill() {
			document.getElementById('idList').value = '';
			document.getElementById('nameFirst').value = '';
			document.getElementById('nameLast').value = '';
			document.getElementById('month').value = '';
			document.getElementById('year').value = '';
			document.getElementById('epubTimeUse').removeAttribute('checked');
			// document.getElementById('ncbiApiKey').value = '';
			submitNotice.textContent = '';
			itemizedResult.innerHTML = '';
			result.setAttribute('style', 'display:none');
			handleSubmitBtn();
		}

		// Example fill

		function exampleFill() {
			// Example PubMed IDs with no DOIs: 123456, 123567, 345671
			//document.getElementById('idList').value = "34247166\n24247166\n345671";
			document.getElementById('idList').value = "10.1186/s12919-020-00195-z\n10.1016/j.vaccine.2014.05.047\n10.1016/j.biologicals.2019.10.001\n10.1073/pnas.2005857117\n31200881";
			document.getElementById('nameFirst').value = 'M';
			document.getElementById('nameLast').value = 'Bhan';
			document.getElementById('month').value = '06';
			document.getElementById('year').value = '2015';
			// document.getElementById('ncbiApiKey').value = '';
			submitNotice.textContent = '';
			itemizedResult.innerHTML = '';
			result.setAttribute('style', 'display:none');
			handleSubmitBtn();
		}

		// Disable submit if required fields not filled or result awaited

		function handleSubmitBtn() {
			if(document.getElementById('nameLast').value.length < 1 || document.getElementById('idList').value.length < 5) {
				submitBtn.setAttribute('disabled', 'disabled');
				submitBtn.setAttribute('class', 'disabled');
				submitBtn.setAttribute('title', 'provide at least one ID and at least the initial of last name');
			} else { 
				submitBtn.removeAttribute('disabled');
				submitBtn.removeAttribute('title');
				submitBtn.setAttribute('class', 'enabled');
			}
		}

		//// Main

		// Get DOI if PubMed ID; NCBI permits max. 3 requests/sec without API key (with key: 10)

		// To do: reduce restriction if NCBI API key available

		const pmDelay = (msec = 500) => new Promise(resolve => setTimeout(resolve, msec));
		let pmQueryNum = 0;

		async function getDoi(id) {
			let doi = 0;
			pmQueryNum += 1;
			try {
				await pmDelay(pmQueryNum * 500);
				const pmResp = await withTimeout(10000, fetch(
					// 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&retmode=json&api_key=' + document.getElementById('ncbiApiKey').value + '&id=' + id, {
					'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&retmode=json&id=' + id, {
					method: 'GET',
					mode: 'cors',

				}));
				if(pmResp.ok) {
					const pmJson = await pmResp.json();
					if(pmJson !== 0 && !pmJson.hasOwnProperty('error') && !pmJson.result[`${id}`].hasOwnProperty('error')) {
						doi = pmJson.result[`${id}`].articleids.filter(function(v){return v.idtype == 'doi';}).pop().value;
					}
				}
			} catch (err) {
				console.log(`DOI could not be obtained for ID ${id} (${err})`);
			}
			return doi;
		}

		// Get data for DOI

		async function getData(doi, id) {
			let data = 0;
			if(doi) {
				try {
					const crResp = await withTimeout(10000, fetch(
						'https://api.crossref.org/works/query=' + doi, {
						method: "GET",
					}));
					if(crResp.ok) {
						const crJson = await crResp.json();
						data = crJson.message;
					}
				} catch (err) {
					console.log(`Data could not be obtained for ID ${id} (${err})`);
				}
			}
			return {'id': id, 'doi': doi, 'data': data};
		}

		// Parse DOI data

		function getDataValues(id, doi, data) {

			// Authors
			let authors = data.author;
			let all_authors = [];
			for(let x in authors) {
				if(!('given' in authors[x])) {
					all_authors.push(authors[x]['family']).trim().replace(',', ' ').replace('\s{2,}', ' ');
				} else if(authors[x]['sequence'] == 'first') {
					all_authors.push(authors[x]['given'].trim().replace(',', ' ').replace('\s{2,}', ' ') + ' ' + authors[x]['family'].trim().replace(',', ' ').replace('\s{2,}', ' '));
				} else if(authors[x]['sequence'] == 'additional') {
					all_authors.push(authors[x]['given'].trim().replace(',', ' ').replace('\s{2,}', ' ') + ' ' + authors[x]['family'].trim().replace(',', ' ').replace('\s{2,}', ' '));
				}
			}

			// Dates
			let print_dateY = '?';
			if(data['published-print'] && (print_dateY = data['published-print']['date-parts'][0][0])) {
			} else if(data['journal-issue'] && (print_dateY = data['journal-issue']['published-print']['date-parts'][0][0])) {
			}
			let print_dateM = '?';
			if(data['published-print'] && (print_dateM = data['published-print']['date-parts'][0][1])) {
			} else if(data['journal-issue'] && (print_dateM = data['journal-issue']['published-print']['date-parts'][0][1])) {
			}
			print_date = print_dateY + '.' + (('0' + print_dateM).slice(-2));

			let eprint_date = '?/?';
			if(data['published-online'] && (edateInfo = data['published-online']['date-parts'][0])) {
				eprint_date = (edateInfo[0]?  edateInfo[0] : '?') + '.' + (edateInfo[1]?  (('0' + edateInfo[1]).slice(-2)) : '?');
			}

			// Publication
			let publication = '';
			if(publicationAr = data['short-container-title']) {
				publication = publicationAr.pop();
			} else if(publicationAr = data['container-title']) {
				publication = publicationAr.pop();
			}
			publication = publication?.length == 0 ? 'Unknown publication' : publication.replace('/journal/gi', 'J.');

			/* Being deprecated in API
			// Subject
			let subject = [];
			if(subjectAr = data.subject) {
				subject = unique(subjectAr).filter(a=>a);
			}
			subject = subject?.length == 0 ? ['Unknown subject'] : subject;
			*/

			// Title
			let title = '';
			if(titleAr = data.title) {
				title = titleAr.pop();
			} else if(titleAr = data['short-title']) {
				title = titleAr.pop();
			}
			title = title?.length == 0 ? 'Unknown title' : title;

			// Type
			let type = '';
			type = data?.type;
			type = type?.length == 0 ? 'unknown-type' : type;

			// URL
			let url = '';
			if(url = data.resource?.primary?.URL) {
			} else if(urlAr = data.link) {
				url = urlAr.pop().URL;
			}
			url = url?.length !== 0 ? url : 'https://dx.doi.org/' + doi;

			return {'id': id, 'doi': doi, 'all_authors': all_authors, 'eprint_date': eprint_date, 'print_date': print_date, 'publication': publication, 'title': title, 'type': type, 'url': url};
		}

		// Process input IDs, converting to DOIs as needed, and get Crossref data for the DOIs

		async function processIds() {
			submitNotice.textContent = '';
			itemizedResult.innerHTML = '';
			result.setAttribute('style', 'display:none');

			// Get ID values from HTML textarea named idList;
			// input values may be separated by space, comma, or line.

			let idAr = document.getElementById('idList').value;
			idAr = idAr.replace(/[\r\n\t,]/g, ' ').replace(/ +/g, ' ').split(' ');

			// Sanitize IDs (e.g., remove duplicate, null, and 'doi:') and keep max. 100.

			idAr = idAr.map(function(id) {
				id = String(id).replace(/[^a-zA-Z0-9\-\._;()\/#%<>&;:]/g, '').replace(/^[^0-9]+/, '');
				return (id.length < 5 || id.length > 500) ? null : id;
			});
			idAr = unique(idAr).filter(a=>a).slice(0, 100);

			submitBtn.setAttribute('disabled', 'disabled');
			submitBtn.setAttribute('class', 'disabled');

			if(document.getElementById('nameLast').value.length < 1 || idAr.length < 1) {
				submitNotice.setAttribute('class', 'warning');
				submitNotice.textContent = 'Provide at least one ID and a last name!';
				return 0;
			} else {
				submitNotice.setAttribute('class', 'inform');
				submitNotice.textContent = 'Processing ' + idAr.length + ' unique input ID' + (idAr.length > 1 ? 's' : '') + '...';
			}

			// Get Crossref data

			let todoAr = [];
			let dataAr = [];
			for(const id of idAr){
				if(id.match(/^[0-9]+$/)) {
					todo = getDoi(id)
						.then(doi => {
							return getData(doi, id)})
						.then(data => {
							dataAr.push(data);
							return data;});
				} else {
					todo = getData(id, id)
						.then(data => {
							dataAr.push(data);
							return data;});
				}
				todoAr.push(todo);
			}
			let results = await Promise.allSettled(todoAr);

			// Initiate Crossref data display

			showResult(dataAr);
		}

		async function showResult(dataAr) {
			result.setAttribute('style', 'display:');
			submitBtn.removeAttribute('disabled');
			submitBtn.setAttribute('class', 'enabled');

			let emptyDataAr = await dataAr.filter((v) => v.data === 0);
			let itemAr = await dataAr.filter((v) => v.data !== 0);

			if(!emptyDataAr.length && itemAr.length) {
				submitNotice.setAttribute('class', 'inform');
				submitNotice.textContent = 'Retrieved DOI info. for ' + itemAr.length + ' unique input ID' + (itemAr.length > 1 ? 's' : '');
			} else if(emptyDataAr.length) {
				submitNotice.setAttribute('class', 'warning');
				submitNotice.textContent = 'DOI info. retrieval failed for ' + emptyDataAr.length + ' ID' + (emptyDataAr.length > 1 ? 's' : '') + '.';
			}

			let tallyAr = Array.apply(null, Array(16)).map(function(){return 0});
			let uniqueItemAr = [];
			let duplicatedItemAr = [];

			let nameFirst = document.getElementById('nameFirst').value.trim().replace(',', '');
			if(!nameFirst.length) {
				nameFirst = '[^, ]*';
			}
			let nameLast = document.getElementById('nameLast').value.trim().replace(',', '');
			let nameRegex = new RegExp('(( $))', 'g');
			if(nameLast.length) {
				nameRegex = new RegExp('(^|, )(' + nameFirst + ')([^,]*?)(' + nameLast + ')(,|$)', 'gi');
			}

			let testPubDate = 0;
			let timepoint = 0;
			let timepointYear = document.getElementById('year').value.trim();
			if(/^[0-9]{4}$/.test(timepointYear)) {
				let timepointMonth = document.getElementById('month').value.trim();
				if(/^[0-1]*[0-9]$/.test(timepointMonth) == false) {
					timepointMonth = '1';
				}
				timepoint = timepointYear + '.' + ('0' + timepointMonth).slice(-2);
				timepoint = +timepoint;
				testPubDate = document.getElementById('epubTimeUse').checked ? 2 : 1;
			}

			// Itemized result table

			let itemizedResultTable = '<table>';

			itemAr.forEach((item) => {
				let values = getDataValues(item.id, item.doi, item.data);
				if(uniqueItemAr.includes(item.doi)) {
					duplicatedItemAr.push(item.id);
				} else {
					uniqueItemAr.push(item.doi);
					itemizedResultTable += '<tr class="itemTr"><td><span class="inform">' + uniqueItemAr.length + '</span></td><td>';
					if(values.id.match(/^[0-9]+$/)) {
						itemizedResultTable += 'PMID <a href="https://pubmed.ncbi.nlm.nih.gov/' + values.id + '" onclick="window.open(\'https://pubmed.ncbi.nlm.nih.gov/' + values.id + '\'); return false">' + values.id + '</a><br />';
					}
					itemizedResultTable += '<a href="' + values.url + '" onclick="window.open(\'' + values.url + '\'); return false">' + values.doi + '</a><br />';
					itemizedResultTable += '<em>' + values.publication + '</em><br />';
					itemizedResultTable += values.type + '<br />';

					let printDate = values.print_date;
					let eprintDate = values.eprint_date;
					let pubDate = testPubDate > 1 ? values.eprint_date : values.print_date;
					let pubDateKnown = /^[0-9]{4}[.][0-9]{2}$/.test(pubDate) ? 1 : 0;
					let pubDateOk = 0;
					if(testPubDate && pubDateKnown && timepoint <= pubDate) {
						pubDateOk = 1;
					}
					itemizedResultTable += (pubDateOk && testPubDate == 1 ? '<strong>' : '') + printDate.replace('.', '/') + (pubDateOk && testPubDate == 1 ? '</strong>' : '') + ' (e-pub ' + (pubDateOk && testPubDate == 2 ? '<strong>' : '') + eprintDate.replace('.', '/') + (pubDateOk && testPubDate == 2 ? '</strong>' : '') + ')<br />';

					let authorOrder = values.all_authors.findIndex(value => nameRegex.test(value)) + 1;
					let totalAuthors = values.all_authors.length;
					let authorType = 'Not author';
					if(authorOrder == 1) {
						authorType = 'First';
						tallyAr[1]++;
						if(pubDateOk) tallyAr[6]++;
						else tallyAr[11]++;
					}
					else if(authorOrder == totalAuthors) {
						authorType = 'Last';
						tallyAr[2]++;
						if(pubDateOk) tallyAr[7]++;
						else tallyAr[12]++;
					}
					else if(authorOrder == 2) {
						authorType = 'Second';
						tallyAr[3]++;
						if(pubDateOk) tallyAr[8]++;
						else tallyAr[13]++;
					}
					else if(authorOrder > 2) {
						authorType = 'Other';
						tallyAr[4]++;
						if(pubDateOk) tallyAr[9]++;
						else tallyAr[14]++;
					}
					else {
						tallyAr[5]++;
						if(pubDateOk) tallyAr[10]++;
						else tallyAr[15]++;
					}
					
					itemizedResultTable += '</td><td>' + values.all_authors.join(', ').trim().replace(nameRegex, '$1<strong>$2$3$4 [' + authorOrder + ']</strong>');
					itemizedResultTable += '</td><td>' + values.title + '</td></tr>';
				}
			});

			itemizedResultTable += '</table>';

			if(duplicatedItemAr.length != 0) {
				itemizedResultTable += '<br />' + duplicatedItemAr.length + ' input ID' + (duplicatedItemAr.length > 1 ? 's appear' : ' appears') + ' to be duplicate, already covered in above result: ' + duplicatedItemAr.join(', ') + '.';
			}

			if(emptyDataAr.length != 0) {
				itemizedResultTable += '<br /><span class="warning">Retrieval failed for ' + emptyDataAr.length + ' input ID' + (emptyDataAr.length > 1 ? 's' : '') + ': ' + emptyDataAr.map(i => {return i.id}).join(', ') + '. Check that the ID' + (emptyDataAr.length > 1 ? 's are' : ' is') + ' correct and retry.';
			}

			itemizedResult.innerHTML = itemizedResultTable;

			// Tallied result table

			let talliedResultTable = '<table>';

			talliedResultTable += '<tr class="tallyTrh">' + (timepoint ? '<td class="tallyTdh"></td>' : '') + '<td>First</td><td>Last</td><td>Second</td><td>Other</td><td>Not author</td></tr>';

			talliedResultTable += '<tr class="tallyTr">' + (timepoint ? '<td class="tallyThr">Any time</td>' : '') + '<td>' + tallyAr[1] + '</td><td>' + tallyAr[2] + '</td><td>' + tallyAr[3] + '</td><td>' + tallyAr[4] + '</td><td>' + tallyAr[5] + '</td></tr>';
			talliedResultTable += timepoint ? '<tr class="tallyTr"><td class="tallyTdh">On/after ' + ('' + timepoint).replace('.', '/') + '</td><td>' + tallyAr[6] + '</td><td>' + tallyAr[7] + '</td><td>' + tallyAr[8] + '</td><td>' + tallyAr[9] + '</td><td>' + tallyAr[10] + '</td></tr>' : '';
			talliedResultTable += timepoint ? '<tr class="tallyTr"><td class="tallyTdh">Time unknown</td><td>' + tallyAr[11] + '</td><td>' + tallyAr[12] + '</td><td>' + tallyAr[13] + '</td><td>' + tallyAr[14] + '</td><td>' + tallyAr[15] + '</td></tr>' : '';

			talliedResult.innerHTML = talliedResultTable + '</table>';
		}

		// In case of reload with filled values
		handleSubmitBtn();

		document.getElementById('clearBtn').addEventListener('click', clearFill, false);
		document.getElementById('exampleBtn').addEventListener('click', exampleFill, false);
		submitBtn.addEventListener('click', processIds, false);
		document.getElementById('idList').addEventListener('keyup', handleSubmitBtn, false);
		document.getElementById('nameLast').addEventListener('keyup', handleSubmitBtn, false);

		//]]>
	</script>
	</body>
</html>
Presented with Sourceer
PHP Labware home | visitors since Sept 2017