0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-06-22 04:58:40 +00:00

Merge branch 'master' of https://github.com/naturalcrit/homebrewery into add-cm-features

This commit is contained in:
Víctor Losada Hernández
2026-06-17 18:57:58 +02:00
4 changed files with 196 additions and 49 deletions
+4
View File
@@ -111,6 +111,10 @@ body {
vertical-align : middle; vertical-align : middle;
text-align : center; text-align : center;
border-right : 1px solid; border-right : 1px solid;
max-width:50ch;
overflow:hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:last-child { border-right : none; } &:last-child { border-right : none; }
} }
@@ -1,70 +1,174 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import request from 'superagent'; import request from 'superagent';
import Moment from 'moment';
const BrewCleanup = ({})=>{ const BrewCleanup = ({})=>{
const [count, setCount] = useState(0); const [junkBrewCollection, setJunkBrewCollection] = useState([]);
const [pending, setPending] = useState(false); const [lostBrewCollection, setLostBrewCollection] = useState([]);
const [primed, setPrimed] = useState(false); const [pendingJunk, setPendingJunk] = useState(false);
const [pendingLost, setPendingLost] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const prime = async ()=>{ const find = async (type)=>{
setPending(true);
try {
const res = await request.get('/admin/cleanup');
setCount(res.body.count); if(type === 'junk') try {
setPrimed(true); setPendingJunk(true);
const res = await request.get('/admin/cleanupJunk');
setJunkBrewCollection(res.body.brewCollection);
} catch (err) { } catch (err) {
setError(err); setError(err);
} finally { } finally {
setPending(false); setPendingJunk(false);
} }
};
const cleanup = async ()=>{ if(type === 'lost') try {
setPending(true); setPendingLost(true);
const res = await request.get('/admin/cleanupLost');
try { setLostBrewCollection(res.body.brewCollection);
const res = await request.post('/admin/cleanup');
setCount(res.body.count);
} catch (err) { } catch (err) {
setError(err); setError(err);
} finally { } finally {
setPending(false); setPendingLost(false);
setPrimed(false);
} }
}; };
const renderPrimed = ()=>{
if(!primed) return;
if(!count) return <div className='result noBrews'>No Matching Brews found.</div>; const cleanup = async (type)=>{
if(type === 'junk') try {
setPendingJunk(true);
console.log('deleting junk');
const res = await request.post('/admin/cleanupJunk');
} catch (err) {
setError(err);
} finally {
setPendingJunk(false);
setJunkBrewCollection([]);
}
if(type === 'lost') try {
setPendingLost(true);
const res = await request.post('/admin/cleanupLost');
} catch (err) {
setError(err);
} finally {
setPendingLost(false);
setLostBrewCollection([]);
}
};
const renderBrewList = (type)=>{
const brewList = type === 'lost' ? lostBrewCollection : junkBrewCollection;
if(!brewList || brewList.length === 0) {
return <>
<h3>{`Results - No brews found` }</h3>
<table className='resultsTable'>
<thead>
<tr>
<th>Title</th>
<th>Last Update</th>
<th>last viewed</th>
<th>Storage</th>
</tr>
</thead>
<tbody>
<tr>
<td colSpan={4}><strong>"No brews found"</strong></td>
</tr>
</tbody>
</table>
</>;
}
console.log(type);
console.log(brewList);
return <>
<h3>{`Results - ${brewList.length} brews` }</h3>
<table className='resultsTable'>
<thead>
<tr>
<th>Title</th>
<th>Last Update</th>
<th>last viewed</th>
<th>Storage</th>
</tr>
</thead>
<tbody>
{brewList
.sort((a, b)=>{ // Sort brews from most recently updated
if(a.lastViewed > b.lastViewed) return -1;
return 1;
})
.map((brew, idx)=>{
return <tr key={idx}>
<td><strong>{brew.title || 'No Title'}</strong></td>
<td style={{ width: '200px' }}>{Moment(brew.updatedAt).fromNow()}</td>
<td>{brew.lastViewed ? Moment(brew.lastViewed).fromNow() : 'No last viewed date'}</td>
<td>{brew.googleId ? 'Google' : 'Homebrewery'}</td>
</tr>
})}
</tbody>
</table>
</>;
};
const renderFound = (type)=>{
const deleteButton = !(type === 'junk' && junkBrewCollection.length === 0 || type === 'lost' && lostBrewCollection.length === 0);
return <div className='result'> return <div className='result'>
<button onClick={()=>cleanup()} className='remove'> {deleteButton && <button onClick={()=>cleanup(type)} className='remove'>
{pending {pendingLost && type === "lost" || pendingJunk && type === "junk"
? <i className='fas fa-spin fa-spinner' /> ? <i className='fas fa-spin fa-spinner' />
: <span><i className='fas fa-times' /> Remove</span> : <span><i className='fas fa-times' /> Remove</span>
} }
</button> </button>
<span>Found {count} Brews that could be removed. </span> }
{renderBrewList(type)}
</div>;
};
const renderJunkBrewCleanup = ()=>{
return <div className='junk'>
<h3> Junk brews</h3>
<p>Queries unauthored brews that have not been viewed or <br/>updated in 30 days and are shorter than 140 bytes (up to 300)</p>
<button onClick={()=>find('junk')} className='query'>
{pendingJunk
? <i className='fas fa-spin fa-spinner' />
: 'Query Brews'
}
</button>
{renderFound('junk')}
{error && <div className='error noBrews'>{error.toString()}</div>}
</div>;
};
const renderLostBrewCleanup = ()=>{
return <div className='lost'>
<h3> Lost brews</h3>
<p>Queries unauthored brews that have not been <br/>updated or viewed for 2 years (up to 500)</p>
<button onClick={()=>find('lost')} className='query'>
{pendingLost
? <i className='fas fa-spin fa-spinner' />
: 'Query Brews'
}
</button>
{renderFound('lost')}
{error && <div className='error noBrews'>{error.toString()}</div>}
</div>; </div>;
}; };
return <div className='brewUtil brewCleanup'> return <div className='brewUtil brewCleanup'>
<h2> Brew Cleanup </h2> <h2> Brew Cleanup </h2>
<p>Removes very short brews to tidy up the database</p> {renderJunkBrewCleanup()}
<br/>
<br/>
{renderLostBrewCleanup()}
<button onClick={()=>prime()} className='query'>
{pending
? <i className='fas fa-spin fa-spinner' />
: 'Query Brews'
}
</button>
{renderPrimed()}
{error && <div className='error noBrews'>{error.toString()}</div>}
</div>; </div>;
}; };
+5 -3
View File
@@ -18,8 +18,7 @@ const SplitPane = (props)=>{
const [liveScroll, setLiveScroll] = useState(false); const [liveScroll, setLiveScroll] = useState(false);
useEffect(()=>{ useEffect(()=>{
const savedPos = window.localStorage.getItem(PANE_WIDTH_KEY); handleResize();
setDividerPos(savedPos ? limitPosition(savedPos, 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)) : window.innerWidth / 2);
setLiveScroll(window.localStorage.getItem(LIVE_SCROLL_KEY) === 'true'); setLiveScroll(window.localStorage.getItem(LIVE_SCROLL_KEY) === 'true');
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
@@ -29,7 +28,10 @@ const SplitPane = (props)=>{
const limitPosition = (x, min = 1, max = window.innerWidth - 13)=>Math.round(Math.min(max, Math.max(min, x))); const limitPosition = (x, min = 1, max = window.innerWidth - 13)=>Math.round(Math.min(max, Math.max(min, x)));
//when resizing, the divider should grow smaller if less space is given, then grow back if the space is restored, to the original position //when resizing, the divider should grow smaller if less space is given, then grow back if the space is restored, to the original position
const handleResize = ()=>setDividerPos(limitPosition(window.localStorage.getItem(PANE_WIDTH_KEY), 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13))); const handleResize = ()=>{
const savedPos = window.localStorage.getItem(PANE_WIDTH_KEY);
setDividerPos(savedPos ? limitPosition(savedPos, 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)) : window.innerWidth / 2);
};
const handleUp =(e)=>{ const handleUp =(e)=>{
e.preventDefault(); e.preventDefault();
+47 -10
View File
@@ -39,14 +39,29 @@ export default function createAdminApi(vite) {
} }
}; };
const junkBrewPipeline = [ // Search for up to 300 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
const junkBrewsPipeline = [
{ $match : { { $match : {
updatedAt : { $lt: Moment().subtract(30, 'days').toDate() }, updatedAt : { $lt: Moment().subtract(30, 'days').toDate() },
lastViewed : { $lt: Moment().subtract(30, 'days').toDate() } lastViewed : { $lt: Moment().subtract(30, 'days').toDate() }
} }, } },
{ $project: { textBinSize: { $binarySize: '$textBin' } } }, { $project: { _id: 1, textBinSize: { $binarySize: '$textBin' }, updatedAt: 1, lastViewed: 1} },
{ $match: { textBinSize: { $lt: 140 } } }, { $match: { textBinSize: { $lt: 140 } } },
{ $limit: 100 } { $limit: 300 }
];
// Search for up to 500 unauthored brews that have not been viewed or updated in two years
const lostBrewsPipeline = [
{
$match: {
authors: [],
updatedAt: { $lt: Moment().subtract(2, 'years').toDate() },
lastViewed: { $lt: Moment().subtract(2, 'years').toDate() }
}
},
{
$limit: 500
}
]; ];
/* Search for brews that aren't compressed (missing the compressed text field) */ /* Search for brews that aren't compressed (missing the compressed text field) */
@@ -54,19 +69,41 @@ export default function createAdminApi(vite) {
'text' : { '$exists': true } 'text' : { '$exists': true }
}).lean().limit(10000).select('_id'); }).lean().limit(10000).select('_id');
// Search for up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes router.get('/admin/cleanupJunk', mw.adminOnly, (req, res)=>{
router.get('/admin/cleanup', mw.adminOnly, (req, res)=>{ HomebrewModel.aggregate(junkBrewsPipeline).option({ maxTimeMS: 60000 })
HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 }) .then((objs)=>res.json({ count: objs.length, brewCollection : objs }))
.then((objs)=>res.json({ count: objs.length }))
.catch((error)=>{ .catch((error)=>{
console.error(error); console.error(error);
res.status(500).json({ error: 'Internal Server Error' }); res.status(500).json({ error: 'Internal Server Error' });
}); });
}); });
// Delete up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes // Delete result of junkBrewsPipeline
router.post('/admin/cleanup', mw.adminOnly, (req, res)=>{ router.post('/admin/cleanupJunk', mw.adminOnly, (req, res)=>{
HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 }) HomebrewModel.aggregate(junkBrewsPipeline).option({ maxTimeMS: 60000 })
.then((docs)=>{
const ids = docs.map((doc)=>doc._id);
return HomebrewModel.deleteMany({ _id: { $in: ids } });
}).then((result)=>{
res.json({ count: result.deletedCount });
}).catch((error)=>{
console.error(error);
res.status(500).json({ error: 'Internal Server Error' });
});
});
router.get('/admin/cleanupLost', mw.adminOnly, (req, res)=>{
HomebrewModel.aggregate(lostBrewsPipeline).option({ maxTimeMS: 60000 })
.then((objs)=>res.json({ count: objs.length, brewCollection : objs }))
.catch((error)=>{
console.error(error);
res.status(500).json({ error: 'Internal Server Error' });
});
});
// Delete result of lostBrewsPipeline
router.post('/admin/cleanupLost', mw.adminOnly, (req, res)=>{
HomebrewModel.aggregate(lostBrewsPipeline).option({ maxTimeMS: 60000 })
.then((docs)=>{ .then((docs)=>{
const ids = docs.map((doc)=>doc._id); const ids = docs.map((doc)=>doc._id);
return HomebrewModel.deleteMany({ _id: { $in: ids } }); return HomebrewModel.deleteMany({ _id: { $in: ids } });